Two unrelated wins as a single batch.
Flutter — lazy source building in setQueueFromTracks:
Today: Future.wait builds all N AudioSource objects (drift queries +
LockCaching ctor) before the player can call setAudioSources →
play(). Measured at 83ms for a 25-track playlist, on top of the
~285ms initial-source preload.
New flow: build only the initial source, hand it to setAudioSources
([initial], initialIndex: 0) so play() can start, then background-
fill the rest. Forward direction (skipNext targets) added via
addAudioSource. Backward direction (skipPrev) inserted at index 0..
initialIndex-1 with _suppressIndexUpdates true so the unavoidable
currentIndex shifts don't push the wrong MediaItem onto the stream.
Saves the up-front source-build wait — tap-to-audio for long queues
should drop by ~80-100ms even on cache hits.
Server — Cache-Control on the three byte-serving endpoints:
- /api/albums/{id}/cover: max-age=86400, must-revalidate. Covers
change rarely (re-scan, MBID enrichment); a day of cache is safe
and skips conditional GETs for the bulk of a session.
- /api/playlists/{id}/cover: max-age=300, must-revalidate. Collages
recompute when contents change; short enough for edits to feel
fresh, long enough to skip repeat fetches during a session.
- /api/tracks/{id}/stream: max-age=31536000, immutable. Track bytes
are immutable for a given id (scanner re-indexes by file_path; new
files get new ids). LockCachingAudioSource on the Flutter side
already disk-caches, but proper headers let it skip even the
conditional 304 on repeat plays.
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.