Files
minstrel/flutter_client
bvandeusen d67c0de596 refactor(playlists): #411 R2 — generic registry-driven system endpoints
Replaces the per-kind refresh/shuffle handlers with one generic
pair driven off the kind registry, in lockstep across both clients.

Server:
- systemPlaylistKind gains Singleton; RefreshableSystemKind(key)
  exported. for_you/discover singleton; songs_like_artist not.
- New generic POST /api/playlists/system/{kind}/refresh and
  GET /api/playlists/system/{kind}/shuffle ({kind} = raw
  system_variant). Non-singleton/unknown kind → 404. Deleted
  playlists_{foryou,discover}_refresh.go and the per-kind shuffle
  wrappers; serveSystemPlaylistShuffle core kept.
- playlistRowView.refreshable: server-derived flag so clients show
  the refresh affordance generically without hardcoding kinds.

Web (not drift-cached → uses the server flag):
- refreshSystem(variant) replaces refreshForYou/refreshDiscover;
  systemShuffle drops the for_you→for-you mapping (raw variant).
- PlaylistCard + detail page gate the kebab/Refresh button on
  playlist.refreshable; label is "Refresh {name}". Tests reworked;
  obsolete refresh-foryou/discover api tests deleted.

Flutter (list tiles are drift-cache-sourced → derive, no migration):
- Playlist.refreshable getter = isSystem && variant !=
  songs_like_artist (holds for all current + planned kinds).
- refreshSystem/systemShuffle use the raw variant; PlaylistCard +
  detail screen gate kebab/Regenerate/source-tagging on refreshable
  so songs_like_artist plays via get() (no by-kind endpoint).

Pure-plumbing refactor; CI verifies parity. Next (R3): the five
discovery mixes — each a candidate query + one registry entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:21:09 -04:00
..

Minstrel mobile client

Flutter (iOS + Android) sibling of the SvelteKit web SPA. v1 first slice ships: scaffold, auth, library browse (home / artist / album), likes, player with background audio + lock-screen controls.

Setup

cd flutter_client
flutter pub get
./tool/sync_shared.sh         # copy tokens / error-copy / placeholders from ../web
dart run tool/gen_tokens.dart # regen lib/theme/tokens.dart from shared/fabledsword.tokens.json
flutter run

sync_shared.sh is idempotent. CI runs it on every build; rerun locally whenever web/src/lib/styles/tokens.json, web/src/lib/styles/error-copy.json, or web/static/placeholders/album-fallback.svg changes.

Project layout

See docs/superpowers/specs/2026-05-02-flutter-mobile-foundation-design.md section 3. Briefly:

  • lib/api/ — dio client, ApiError, error-copy loader, per-surface endpoints.
  • lib/auth/ — server URL screen, login screen, AuthController + secure storage.
  • lib/library/ — home, artist detail, album detail, providers, widgets.
  • lib/likes/ — LikeButton + optimistic toggle controller.
  • lib/player/ — audio handler, player provider, mini PlayerBar, NowPlaying.
  • lib/theme/ — FabledSword token Dart class (generated), ThemeExtension, ThemeData.
  • lib/shared/ — routing (go_router shell), version gate, connection error banner.

shared/fabledsword.tokens.json and assets/error-copy.json are synced from web/. Don't edit them directly — edit web/src/lib/styles/ and re-run ./tool/sync_shared.sh.

Architecture

  • State: Riverpod 2 (AsyncValue, AsyncNotifier, family providers).
  • HTTP: dio 5 with a Bearer-auth interceptor; 401 clears the session and the router redirects to login.
  • Audio: just_audio wrapped by audio_service for background playback + lock-screen / notification controls.
  • Auth: Bearer tokens in flutter_secure_storage. Server already supports both cookie (web SPA) and Bearer (internal/auth/session.go).
  • Routing: go_router with a ShellRoute so the PlayerBar persists across navigation. Cold launch flow: no server-url → /server-url, url set / no token → /login, token present → /home.

CI

.forgejo/workflows/flutter.yml runs on push to dev/main, tag pushes, PR to main, and manual dispatch — path-filtered to flutter_client/** plus the shared web inputs. Steps: sync, analyze, test, build APK. Debug APKs upload as artifacts; release APKs attach to the Forgejo release on tag.

Versioning

pubspec.yaml version mirrors the server tag (e.g. server v0.1.0 → Flutter 0.1.0+1). The server's /healthz returns min_client_version; old clients show an Update Required modal and refuse to operate. Bump internal/server/version.go MinClientVersion when a server-side change requires a paired client update.

Distribution

  • Android: APK on the Forgejo release page. No Play Store for v1.
  • iOS: TestFlight on tag (manual upload v1, automate later).

Out-of-scope for slice 1

Search, discover, requests, settings beyond server URL, admin, listening history, playlists, offline cache (#357). Those land in subsequent slices with their own brainstorm/spec/plan cycles. Per the operator's M7 ordering decision (2026-05-02), slice 2+ pauses to ship the missing web features first (#352 playlists, #362 theme toggle, #363 queue UI, #364 listening history, etc.) before mirroring on Flutter.