Files
minstrel/flutter_client
bvandeusen a09b636e1a fix(flutter): full player kebab Go to album/artist navigates cleanly
Same root cause as the /queue duplicate-page-key crash: /now-playing
is a top-level route (lives outside the ShellRoute), but
/artists/:id and /albums/:id are shell-children. Pushing a shell-
child from a top-level route makes go_router attempt to mount a
second ShellRoute on top of the active one, leaving navigation in a
broken state. The mini player works because it's already inside the
shell.

Add an optional onBeforeNavigate callback to TrackActionsSheet
(forwarded through TrackActionsButton). When set, fires after
sheet.pop() and before context.push() of the destination route.

Wire the full player's TrackActionsButton with onBeforeNavigate:
() => Navigator.of(context).maybePop() so /now-playing dismisses
itself before the detail route is pushed. Result: clean navigation
into the destination, mini player visible underneath as expected.

Mini player keeps the default (no callback) since it's already in
the shell.
2026-05-11 23:50:42 -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.