# 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 ```bash 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 `.gitea/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 Gitea 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 Gitea 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.