Files
minstrel/flutter_client
bvandeusen 2ebe6229b7 fix(player): smooth full-player cover + backdrop on track change
Two related "snap in" effects on the now-playing screen:

1. **Album art snapped in after the fade.** AnimatedSwitcher cross-
   fades the new _AlbumArt over 300ms, but FileImage's bytes weren't
   decoded yet — the widget was visually empty during the fade and
   the cover landed abruptly after. precacheImage on the new file://
   artUri pre-decodes the bytes so by the time AnimatedSwitcher
   mounts the new tile, the cover paints synchronously inside it.
   The cross-fade now carries real content end-to-end.

2. **Backdrop color snapped in.** albumColorProvider.family is
   loading for the new id during the track-change moment, so
   dominant fell back to fs.obsidian; AnimatedContainer tweened to
   obsidian and then snapped to the resolved color a beat later.
   _NowPlayingScreenState now holds _lastDominant across builds:
   while extraction for the new id is loading, the gradient stays
   on the previous album's color, then animates straight to the
   new one once it resolves. No intermediate obsidian stop.

Net effect: track changes feel like a single smooth transition
instead of fade-out → blank → snap.
2026-05-14 07:41:27 -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.