Commit Graph

18 Commits

Author SHA1 Message Date
bvandeusen 83df3773ae fix(flutter): cover URLs send Bearer auth + audio handler diag logging
Two issues from on-device testing against prod HTTPS:

1. ServerImage was resolving cover URLs correctly
   (https://minstrel.fabledsword.com/api/albums/.../cover) but the server
   returned 401 because Image.network doesn't carry the session token
   automatically the way the browser sends cookies. Forwards the stored
   session token as an Authorization: Bearer header. No-op when no token
   is present.

2. The "Cleartext HTTP traffic to 127.0.0.1" audio error reproduced even
   after the previous defensive check landed, which means the URL handed
   to ExoPlayer has a valid scheme+host (just the wrong host). The check
   only catches scheme-less URLs, so it didn't fire. Added debugPrint
   logging at configure() and setQueueFromTracks() time to show the
   actual baseUrl + per-track resolved URL on the next reproduction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:15:56 -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 d5c8d316c5 fix(flutter): clear analyze --fatal-infos errors from admin parity slice
CI caught six issues against the new admin slice:

- AsyncValue<T> in this Riverpod 3.3.1 codebase exposes `.value`
  (returns T?), not `.valueOrNull`. Switched the three admin readers
  to match the established convention (player_bar, queue_screen,
  now_playing_screen all use `.value`).
- home_screen.dart still has ctx.push calls in _albumsRow / _artistsRow
  (carousel tap handlers) — restored the go_router import that
  Task 2 over-eagerly removed.
- admin_user_edit_sheet.dart had a single-line `if (!await ...) return;`
  that violated curly_braces_in_flow_control_structures. Wrapped in
  braces.
- admin_quarantine_item.dart doc comment had `<top>` placeholders that
  the analyzer flagged as unintended HTML. Rephrased without angle
  brackets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:02:14 -04:00
bvandeusen dfc08650e7 feat(flutter): wire admin routes + isAdmin gate
Adds /admin, /admin/requests, /admin/quarantine, /admin/users routes to
the ShellRoute (so PlayerBar persists). Redirect closure refuses any
/admin path when user.isAdmin is false. Server-side RequireAdmin
middleware remains the actual authority — this is UX.

Screens land in subsequent commits; this intermediate state won't build
until task 8/9 lands the AdminLandingScreen and the missing siblings
are stubbed out by task 11/13/16.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:50:53 -04:00
bvandeusen 6564d37a2a feat(flutter): MainAppBarActions widget with admin-gated overflow
Shared AppBar actions for top-level screens — three primary icons
(Home/Library/Search, current screen suppressed) plus a kebab containing
Playlists/Discover/Settings and (only when isAdmin) Admin. Replaces the
ad-hoc per-screen IconButton lists; centralises admin-gating logic in
one place so /admin entry can't accidentally leak to non-admins.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:48:52 -04:00
bvandeusen 3f2822dfc6 feat(flutter): discover screen — search Lidarr + create requests
- models/lidarr.dart (LidarrSearchResult + LidarrRequestKind)
- api/endpoints/discover.dart (search + createRequest)
- discover/discover_screen.dart with TextField, kind toggle (Artists/Albums),
  results list with in_library / requested pills
- /discover route + explore icon on home AppBar (5 actions now: Library,
  Playlists, Search, Discover, Settings)
2026-05-08 14:53:06 -04:00
bvandeusen dfbeed01a6 feat(flutter): settings screen + 4-icon home AppBar
- models/my_profile.dart (MyProfile + ListenBrainzStatus)
- api/endpoints/settings.dart (profile / password / listenbrainz CRUD)
- settings/settings_screen.dart with three sections:
  - Profile: display_name + email, Save profile
  - Password: current + new + confirm, validates >=8 + match
  - ListenBrainz: token paste + scrobble-enabled toggle
- /settings route + Settings icon on home AppBar
- Home AppBar now: Library + Playlists + Search + Settings (was just Search)
2026-05-08 14:51:04 -04:00
bvandeusen da1cb7b590 feat(flutter): queue screen + skipToQueueItem support
- audio_handler: override skipToQueueItem(index) -> _player.seek(zero, index: i)
- player/queue_screen.dart: list of MediaItems with current track highlighted
  (left accent border + Now Playing badge), tap-to-jump on non-current rows
- /queue route + queue_music icon on /now-playing AppBar

Reorder + remove deferred for v1.
2026-05-08 14:42:10 -04:00
bvandeusen 66545bf8c3 feat(flutter): library screen with 5 tabs (Artists/Albums/History/Liked/Hidden)
Models:
- history_event.dart (HistoryEvent + HistoryPage envelope)
- quarantine_mine.dart (QuarantineMineRow for /api/quarantine/mine)
- Renamed Page<T> -> Paged<T> to avoid collision with Material's Page

Endpoints:
- library_lists.dart: listArtists / listAlbums (paged)
- me.dart: history + quarantineMine
- likes.dart: extended with listTracks/Albums/Artists (paged)

UI:
- library_screen.dart: TabBar over 5 tabs
- Artists tab: 3-col grid
- Albums tab: 2-col grid
- History tab: list with relative-time formatting (matches HistoryRow.svelte)
- Liked tab: 3 stacked sections (artists scroll-row, albums scroll-row, tracks list)
- Hidden tab: list of quarantined tracks with reason pill
- /library route + library_music icon on home AppBar

First page only for v1 (limit 50). Infinite scroll deferred.
2026-05-08 14:40:26 -04:00
bvandeusen 5541171e94 feat(flutter): playlists list + detail screens
- models/playlist.dart (Playlist, PlaylistTrack, PlaylistDetail)
- api/endpoints/playlists.dart (list with kind=user|system|all, get detail)
- playlists/playlists_provider.dart (Riverpod family providers)
- playlists/playlists_list_screen.dart (with system-variant pill)
- playlists/playlist_detail_screen.dart (header + Play button + rows)
- /playlists + /playlists/:id routes wired
- Home AppBar: queue_music icon next to Search

Tracks with track_id=null render greyed-out + struck-through (matches
web's PlaylistTrackRow behavior). Tap a row plays from that position;
top-level Play button plays the full playable subset from track 0.
2026-05-08 14:33:53 -04:00
bvandeusen 147d6e280e feat(flutter): bump deps to latest + Search screen slice
Major version bumps (riverpod 2->3, go_router 14->17, just_audio 0.9->0.10,
flutter_secure_storage 9->10, google_fonts 6->8, flutter_lints 4->6).
package_info_plus held at 8.x due to win32 conflict with secure_storage 10.x.

Riverpod 3 breaks: AsyncValue.valueOrNull -> .value, StateProvider replaced
with Notifier subclass. just_audio 0.10: ConcatenatingAudioSource -> setAudioSources.

Search slice:
- models/page.dart (generic Page<T> envelope)
- models/search_response.dart (3-facet wrapper)
- api/endpoints/search.dart
- search/search_provider.dart (debounced 250ms)
- search/search_screen.dart (TextField + 3 horizontal/vertical sections)
- Wired /search route + search button on home AppBar
2026-05-08 14:30:01 -04:00
bvandeusen d86c694cde fix(flutter): five flutter analyze --fatal-infos issues from first CI run
1. lib/app.dart + lib/shared/routing.dart — buildRouter takes a Ref but was
   being called from a ConsumerWidget's build() with a WidgetRef. Add a
   routerProvider; the widget watches it instead of constructing the
   router from its own ref. Real bug — would have crashed compile in
   slice 1, just never compiled until CI ran.

2. lib/player/audio_handler.dart — override of AudioHandler.seek used
   `p` for the Duration; rename to `position` to match the base class
   (avoid_renaming_method_parameters lint).

3. lib/theme/theme_extension.dart — fromTokens() returned a non-const
   constructor; all inputs are const so make the call const too
   (prefer_const_constructors).

4. test/library/home_screen_test.dart — same const-constructor lint on
   the HomeData test fixture.

5. test/smoke_test.dart — drop the unused
   `import 'package:flutter/material.dart'`.

These are exactly the kind of issues the no-in-task-tests rule shifted
to CI; this is the first run that actually exercises the analyzer
against the slice 1 code.
2026-05-02 19:01:54 -04:00
bvandeusen ec0c10f312 feat(flutter): connection-error banner + 401-clears-session interceptor
ApiClient.buildDio takes on401; the library's dioProvider wires it to
the auth controller's clearSession. The router redirect already handles
navigation away from authed screens once the session goes null.
HomeScreen surfaces the banner on connection_refused with Retry +
Change URL.
2026-05-02 17:41:46 -04:00
bvandeusen f675782bf5 feat(flutter/player): NowPlaying full-screen view with scrubber
Reached by tapping the PlayerBar. Reads media + playback streams; seek
dispatches through the audio handler. Swipe-down dismiss is a stand-in
chevron-down AppBar leading icon for v1.
2026-05-02 17:40:13 -04:00
bvandeusen bbfeb5d3ff feat(flutter/player): mini PlayerBar in shell + tap-to-play wiring
PlayerBar reads playbackState + mediaItem streams; hidden when no
queue. Home track-row tap, album play button, album track tap, artist
play button (shuffle) all route to playerActions.playTracks. Token +
baseUrl forwarded to the audio handler before each new queue.
2026-05-02 17:39:09 -04:00
bvandeusen 6ec7cea362 feat(flutter/library): artist + album detail screens
Headers carry placeholder play buttons (wired in Task 18) + room for
LikeButton (Task 16). Artist screen has a 2-column album grid; album
screen has a track list with mm:ss durations. Routes :id-parameterized
under the shell so the PlayerBar persists.
2026-05-02 17:32:34 -04:00
bvandeusen 5ef782905e feat(flutter/home): home screen with five sections
Sections mirror the web home: Recently added · Rediscover (albums +
artists) · Most played · Last played artists. Tap routes pushed for
albums/artists; track-tap is wired in the player task. Pull-to-refresh
re-fetches /api/home.
2026-05-02 17:30:42 -04:00
bvandeusen d115c267f4 feat(flutter): GoRouter shell + version gate + auth-aware redirects
Cold launch flow: no server-url -> /server-url. URL set, no token ->
/login. Token present -> /home with shell. VersionGate runs once when
the URL changes, hits /healthz, blocks if min_client_version > package
version. /home is a placeholder until Task 14.

Replaces Navigator.pushReplacementNamed in server_url_screen +
login_screen with context.go now that go_router owns the routes.
2026-05-02 17:24:08 -04:00