Commit Graph

14 Commits

Author SHA1 Message Date
bvandeusen 2499449c0b feat(flutter/player): playNext + enqueue + PlaylistsApi.appendTracks
Three small data-layer additions for the upcoming track-actions menu:

- PlaylistsApi.appendTracks(playlistId, trackIds) wraps
  POST /api/playlists/{id}/tracks for the "Add to playlist" action.
- audio_handler gains playNext (insertAudioSource at currentIndex+1)
  and enqueue (addAudioSource) — both also push the audio_service
  queue notifier so the queue-screen UI stays in sync.
- The AudioSource construction was extracted into a private
  _buildAudioSource helper so setQueueFromTracks / playNext / enqueue
  share one source-building path.
- PlayerActions exposes playNext / enqueue for menu use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:40:17 -04:00
bvandeusen fdaa1a0472 feat(flutter/api): QuarantineApi for flag/unflag
Dio client for /api/quarantine — flag a track with a reason + optional
notes, unflag by track id. Mirrors the server's user-scoped quarantine
endpoints (separate from /admin/quarantine).

Used by the track-actions menu's Hide/Unhide action in the next slice
of commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:38:52 -04:00
bvandeusen d872a30506 feat(flutter): SystemPlaylistsStatus model + endpoint method
Adds the model + MeApi.systemPlaylistsStatus() for the home Playlists
row's placeholder-variant logic (building / failed / pending /
seed-needed). Mirrors GET /api/me/system-playlists-status which
returns the caller's most recent system_playlist_runs row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:36:15 -04:00
bvandeusen 5613fec5ef fix(flutter): PlaylistsApi.list parses {owned, public} envelope
The server has always returned an enveloped response from GET
/api/playlists; the existing client decoded it as a flat List which
fails at runtime against the actual Map shape. Existing playlists
list screen would have been broken on any production instance.

list() now returns PlaylistsList { owned, public }. The existing
list screen consumes lists.all (owned + public flattened) — same
visible output as the previous flat-list assumption.

This unblocks the home parity slice which needs the same endpoint
for the new Playlists row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:35:34 -04:00
bvandeusen 79d00ba001 feat(flutter/api): admin endpoint clients
Four small Dio-backed API classes mirroring the server's actual
contract (verified against internal/api/admin_*.go):

- Requests/Quarantine return flat lists; Users/Invites are enveloped
  ({"users": [...]}, {"invites": [...]}) and unwrap here
- setAutoApprove sends {auto_approve: bool} — different from the
  response field name auto_approve_requests
- resetPassword takes admin-supplied {password: string}, returns 204
- Invite create takes optional {note: string}; expiry is server-fixed
  at 24h

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:52:00 -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 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 44e2623b3c feat(flutter/likes): LikeButton with optimistic toggle + rollback
LikedIdsController loads /api/likes/ids once and mutates the local set
on toggle. Failed mutations roll the set back so the icon never lies.
Wired into ArtistDetail header, AlbumDetail header, and per-track in
the album track list.
2026-05-02 17:34:59 -04:00
bvandeusen 3e267918b7 feat(flutter/library): API endpoints + Riverpod providers
LibraryApi wraps GET /api/home, /api/artists/{id}(/tracks),
/api/albums/{id}. dioProvider builds an authenticated dio (token
resolver reads session_token from secure storage on every request).
homeProvider, artistProvider(id), albumProvider(id) sit on top.
2026-05-02 17:26:57 -04:00
bvandeusen 9cbbfcff6c feat(flutter/auth): login screen + AuthApi.login posting to /api/auth/login
Login uses a non-authenticated dio (token resolver returns null) since
we don't have one yet. On success, setSession persists token + user
into secure storage and the AuthController state flips, which the
router watches to navigate.
2026-05-02 17:21:42 -04:00
bvandeusen 3cf874ab69 feat(flutter/auth): server URL screen + auth provider + secure storage
AuthController is an AsyncNotifier holding the currently-logged-in
User. server_url, session_token, current_user all live in
flutter_secure_storage. ServerUrlScreen probes /healthz before saving
the URL so we don't store a bogus base.
2026-05-02 17:20:26 -04:00