Three changes addressing the cold-start spinner + stale-on-revisit pain. A. SWR on remaining cacheFirst providers - artistProvider, artistAlbumsProvider, artistTracksProvider all gain alwaysRefresh: true. Cache hit still renders instantly; one background refresh per subscription keeps the row from going stale forever. - albumProvider (inline async*) and playlistDetailProvider (inline async*) now keep a `revalidated` flag and kick a one-shot background fetch on the first complete cache hit. Same effect as cacheFirst's alwaysRefresh, just inline. - Three providers gained the connectivity timeout that album/artist/playlist already had. B. Navigation hydration - AlbumDetailScreen accepts `AlbumRef? seed`; ArtistDetailScreen accepts `ArtistRef? seed`. When the live provider is still loading, the seed populates cover/title/artist immediately so the page isn't blank. - Routing wires `extra: AlbumRef|ArtistRef` from go_router into the seed parameter. - Call sites updated: home (Recently added, Rediscover albums + artists, Last played), artist detail album grid. Where a ref isn't available (track actions sheet), the screen falls back to the spinner — no regression. C. Cold-start home skeleton - Replace the full-screen CircularProgressIndicator on /home with a layout-preserving skeleton: 5 section titles + 6 grey card-shaped placeholders per row. The page feels populated immediately; sections fill in independently as data arrives via the per-section providers. - Drops the unused DelayedLoading import. Net effect: re-visits to detail screens render instantly (cache hit + silent refresh); first visit from a tile shows the seed header immediately while tracks load; cold-start home shows a layout skeleton instead of a 30s blank spinner.
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.