Mirrors web's compact PlayerBar both visually (3-column top + full- width seek bottom) and functionally — closes parity gaps that had been hidden because the prior mini-bar surfaced none of these controls. ### audio_handler.dart - Override `setShuffleMode` → delegates to just_audio's `setShuffleModeEnabled`. - Override `setRepeatMode` → maps audio_service modes (none/one/all/ group) to just_audio's LoopMode (off/one/all). - New `setVolume(double)` + `volumeStream` getter (audio_service's PlaybackState doesn't carry volume — surface it separately). - `_broadcastState` now populates PlaybackState's shuffleMode + repeatMode fields, and re-fires on shuffle/repeat/loop stream changes so UI subscribers stay in sync. ### player_provider.dart - New `volumeProvider` (StreamProvider<double>). - `PlayerActions` gains `toggleShuffle()`, `cycleRepeat()` (none → all → one → none), and `setVolume(double)`. ### player_bar.dart Complete rebuild matching the operator-designed layout: ┌──────────┬───────────┬──────────────────┐ │ art │ ♥ ⋮ │ 🔀 🔁 ☰ │ ← short row │ title │ │ │ │ artist │ ⏮ ⏯ ⏭ │ volume slider │ ← play row, full size ├──────────┴───────────┴──────────────────┤ │ 0:42 ━━━━━●━━━━━━━━━━━━━━━━━━━━━ 3:21 │ └─────────────────────────────────────────┘ - Column 1 (35%): cover + title + artist. Tap → /now-playing. - Column 2 (intrinsic): like + kebab on top, play controls (40/48/40) on bottom. - Column 3 (30%): shuffle/repeat/queue on top, volume slider on bottom. - Bottom: full-width seek slider with mm:ss labels. `TrackActionsButton` reused for the kebab; reconstructs a minimal TrackRef from MediaItem extras (same pattern NowPlayingScreen already uses). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.