From 3372ce77fa7b0e0c8e2faf3b9f77f852e77618a6 Mon Sep 17 00:00:00 2001 From: cottongin Date: Sat, 28 Mar 2026 12:53:33 -0400 Subject: [PATCH] Move card action buttons to top-right corner and add Mark Played to alternates Reposition approve/reject/played buttons as absolute top-right overlay on each card. Add Mark Played buttons to alternate match cards with a new play-alt API endpoint, and right-align all alternate action buttons. Made-with: Cursor --- SongRequest/templates/index.html | 13 ++++++- SongRequest/web.py | 58 +++++++++++++++++++++++++++++--- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/SongRequest/templates/index.html b/SongRequest/templates/index.html index 8b44a17..f8fd2ed 100644 --- a/SongRequest/templates/index.html +++ b/SongRequest/templates/index.html @@ -338,6 +338,7 @@ border: 1px solid var(--border); border-radius: var(--radius); transition: all 0.2s ease; + position: relative; } .request-card:hover { border-color: #3a3a3a; } @@ -422,7 +423,9 @@ .card-actions { display: flex; gap: 0.5rem; - margin-top: 0.5rem; + position: absolute; + top: 0.625rem; + right: 0.625rem; } .btn { @@ -541,6 +544,14 @@ display: flex; flex-direction: column; min-width: 0; + flex: 1; + } + + .alt-actions { + display: flex; + gap: 0.375rem; + flex-shrink: 0; + margin-left: auto; } .alt-title { diff --git a/SongRequest/web.py b/SongRequest/web.py index 81d7615..e2bdaee 100644 --- a/SongRequest/web.py +++ b/SongRequest/web.py @@ -32,26 +32,35 @@ def _render_alternates(request_id: int, alternates_json: str, status: str) -> st esc = html.escape show_approve = status == "pending" + show_play = status in ("pending", "approved") items = [] for idx, alt in enumerate(alts): artwork = esc(alt.get("artwork_url", "").replace("600x600", "100x100")) title = esc(alt.get("title", "")) artist = esc(alt.get("artist", "")) url = esc(alt.get("apple_music_url", "")) - approve_btn = "" + buttons = [] if show_approve: - approve_btn = ( + buttons.append( f'' ) + if show_play: + buttons.append( + f'' + ) + actions_html = f'
{"".join(buttons)}
' if buttons else "" items.append( f'
' f'' f'
{title}' f'{artist}
' - f'{approve_btn}
' + f'{actions_html}' ) return ( '
' @@ -104,8 +113,8 @@ def render_request_card(req: SongRequestModel) -> str:
{esc(req.album)}
Requested by {esc(req.requester_nick)} in {esc(req.channel)} at {created}
{esc(req.status)} - {actions} + {actions} {alternates_html} """ @@ -181,6 +190,7 @@ class WebServer: app.router.add_get("/api/channels", self._handle_channels) app.router.add_get("/api/requests", self._handle_api_get) app.router.add_post("/api/requests/{request_id}/approve-alt/{alt_idx}", self._handle_approve_alt) + app.router.add_post("/api/requests/{request_id}/play-alt/{alt_idx}", self._handle_play_alt) app.router.add_post("/api/requests/{request_id}/{action}", self._handle_api_action) app.router.add_get("/api/export/markdown", self._handle_export_markdown) app.router.add_get("/api/status", self._handle_get_status) @@ -446,6 +456,46 @@ class WebServer: return web.Response(text=card_html, content_type="text/html") + async def _handle_play_alt(self, request: web.Request) -> web.Response: + denied = self._require_auth(request) + if denied: + return denied + + try: + request_id = int(request.match_info["request_id"]) + alt_idx = int(request.match_info["alt_idx"]) + except (ValueError, KeyError): + return web.Response(text="Bad request", status=400) + + req = self._store.get(request_id) + if not req: + return web.Response(text="Request not found", status=404) + + try: + alts = json.loads(req.alternates_json) if req.alternates_json else [] + except (json.JSONDecodeError, TypeError): + alts = [] + + if alt_idx < 0 or alt_idx >= len(alts): + return web.Response(text="Invalid alternate index", status=400) + + alt = alts[alt_idx] + self._store.swap_alternate(request_id, alt) + req = self._store.update_status(request_id, "played") + if not req: + return web.Response(text="Request not found", status=404) + + card_html = render_request_card(req) + await self._broadcast("request-update", card_html) + + if self._on_status_change: + try: + self._on_status_change(req) + except Exception: + log.exception("SongRequest: on_status_change callback failed") + + return web.Response(text=card_html, content_type="text/html") + async def _handle_export_markdown(self, request: web.Request) -> web.Response: denied = self._require_auth(request) if denied: