Commit Graph

6 Commits

Author SHA1 Message Date
bvandeusen 25ff7afc33 feat(flutter): TrackRow renders TrackActionsButton by default
Adds optional actions: bool = true param. When true (default), renders
the 3-dot menu trigger at the end of the row. Album detail / Search /
History / Liked tabs all consume TrackRow so they pick up the menu
transitively.

Pair with c8baaa5 which wired the button into the other 3 surfaces;
this commit completes the wire-up by including TrackRow itself (its
write was missed in the previous commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:43:40 -04:00
bvandeusen c8baaa5329 feat(flutter): wire TrackActionsButton into 4 surfaces
- TrackRow gains an actions: bool = true param; renders the button
  after duration. Album detail + Search + History/Liked tabs all
  consume TrackRow so they pick up the menu transitively.
- CompactTrackCard (home Most-played) gets the button at the end of
  its inner row.
- _PlaylistTrackRow in playlist_detail_screen gets the button next
  to duration; skipped for unavailable rows (no track id).
- NowPlayingScreen renders the button next to the title with
  hideQueueActions: true (Play next / Add to queue suppressed since
  the menu's track IS the playing one). artistId is left empty since
  it's not stashed in MediaItem.extras — Go-to-artist will route to
  /artists/ which is benign for v1; a follow-up could stash it.

Closes Tier 1 follow-up #2 (track-level actions menu).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:43:12 -04:00
bvandeusen 8fb82d473d feat(flutter/widgets): HorizontalScrollRow skips header when title empty
Lets multiple stacked HorizontalScrollRow instances render under a
single visible heading by passing title: "" to the second-row-onward
instances. Combined with the existing shared-controller support, this
gives us multi-row scrollers without a new widget.

Used in the next commit's home rewrite for Recently added (2 rows)
and Most played (3 rows).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:38:57 -04:00
bvandeusen dccdb00bce feat(flutter/widgets): CompactTrackCard for dense home Most-played row
Mirrors the web CompactTrackCard — small horizontal cell with cover
art (48dp), title, artist. Tap plays from this track in the section.
Width 176dp matches web's compact density so 25 tracks per row scroll
naturally on phone widths.

Cover URL is derived from track.albumId (/api/albums/<id>/cover) since
TrackRef itself doesn't carry a cover field — same pattern as web's
coverUrl() helper.

Used in the next commit's home Most-played section (3 rows × 25);
otherwise unused this slice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:37:40 -04:00
bvandeusen 602ef3bfdf fix(flutter): cover URLs resolve against server baseUrl + audio stream URL guard
Cover-art Image.network calls were passing server-relative URLs
(/api/albums/<id>/cover) straight to NetworkImage, which interprets
"no scheme" as file:/// and crashes with "No host specified in URI".
Same root cause regardless of HTTPS or HTTP server.

ServerImage wraps Image.network with a Riverpod read of
serverUrlProvider and prefixes the configured base URL. Absolute URLs
(e.g. discover screen's Lidarr image_urls) pass through unchanged.

Three call sites updated: album_card, artist_card, playlists_list_screen.
discover_screen left as-is — its row.imageUrl is already absolute (Lidarr
returns full URLs from MusicBrainz / Spotify metadata) and it has a
meaningful errorBuilder that ServerImage doesn't expose.

Also adds a defensive check in audio_handler.setQueueFromTracks: if
the constructed stream URL ends up scheme-less, throw a StateError
naming baseUrl + track.streamUrl + track.id instead of letting it
fall through to ExoPlayer which surfaces a confusing "Cleartext HTTP
traffic to 127.0.0.1 not permitted" error (Android's URL parser
defaults a scheme-less URI to localhost). User reported this exact
confusing error against an HTTPS prod server; the better message
will pinpoint where the empty baseUrl comes from on next reproduction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:47:25 -04:00
bvandeusen c47deb3c87 feat(flutter/library): card + row widgets (artist/album/track)
HorizontalScrollRow takes an optional shared ScrollController so
multi-row sections (Most played: 3 rows) couple their scroll like the
web HorizontalScrollRow.svelte. Cards fall back to the synced
album-fallback.svg when coverUrl is empty.
2026-05-02 17:29:03 -04:00