60085b1368
Periodic worker that walks cached_artists for missing album lists and cached_albums for missing track lists, then fills them via /api/artists/:id + /api/albums/:id. Newly-discovered album covers are pre-warmed into flutter_cache_manager's disk cache too. Solves the "tap an artist → empty album area → pop in" experience: artistAlbumsProvider was drift-first but the cache only got populated when the user navigated TO an artist. SyncController's /api/library/sync delta doesn't carry per-artist album lists (those are query-time derived). Now the filler pre-populates them in the background so the drift hit is real on first tap. Pacing (intentionally conservative): * 10-second initial delay so SyncController has time to land its first sync — the WHERE NOT EXISTS query has nothing to do until cached_artists is populated. * 5-minute interval thereafter. Once steady state is reached the sweep is a cheap drift query + early exit. * 200ms throttle between per-entity REST requests — never competes with active playback. * 200 entities per sweep cap so a fresh install with thousands of artists doesn't tie up the network for one continuous run. Next sweep picks up where this one left off (NOT EXISTS naturally skips already-filled rows). Wall time estimate for a 1000-artist library: ~3-4 minutes spread over multiple sweeps. Single round-trip per artist (new getArtistDetail API method returns artist + albums in one shot). Activated from app.dart's postFrameCallback alongside the existing SyncController / Prefetcher / MetadataPrefetcher / LiveEvents hooks. Disposed via ref.onDispose when the provider scope tears down (effectively process death in practice).
67 lines
2.5 KiB
Dart
67 lines
2.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import 'cache/cache_filler.dart';
|
|
import 'cache/metadata_prefetcher.dart';
|
|
import 'cache/prefetcher.dart';
|
|
import 'cache/sync_controller.dart';
|
|
import 'shared/live_events_dispatcher.dart';
|
|
import 'shared/routing.dart';
|
|
import 'theme/theme_data.dart';
|
|
import 'theme/theme_mode_provider.dart';
|
|
|
|
class MinstrelApp extends ConsumerStatefulWidget {
|
|
const MinstrelApp({super.key});
|
|
|
|
@override
|
|
ConsumerState<MinstrelApp> createState() => _MinstrelAppState();
|
|
}
|
|
|
|
class _MinstrelAppState extends ConsumerState<MinstrelApp> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Activate offline-mode infrastructure once the first frame ships.
|
|
// SyncController.sync() is connectivity-aware (no-ops on no auth /
|
|
// no server URL), so calling it unconditionally is safe.
|
|
// Reading prefetcherProvider runs its constructor, which wires the
|
|
// queue + settings listeners.
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
// ignore: unawaited_futures
|
|
ref.read(syncControllerProvider.notifier).sync();
|
|
ref.read(prefetcherProvider);
|
|
// Metadata prefetcher: when /api/home returns, fire background
|
|
// albumProvider/artistProvider reads for the top-N items in
|
|
// each section so subsequent taps are drift hits, not network
|
|
// round trips.
|
|
ref.read(metadataPrefetcherProvider);
|
|
// Live events (#392): subscribes to /api/events/stream and
|
|
// invalidates publicly-scoped providers when relevant events
|
|
// arrive. Also installs an AppLifecycleState observer for
|
|
// resume-time defensive invalidation.
|
|
ref.read(liveEventsDispatcherProvider);
|
|
// Cache filler: background sweeper that walks cached_artists
|
|
// / cached_albums for missing relations and fetches the
|
|
// per-entity detail so tapping an artist surfaces albums
|
|
// immediately (drift hit, no /api/artists/:id round-trip at
|
|
// tap time). First sweep 10s after launch; every 5 minutes
|
|
// thereafter. Throttled 200ms between requests so it never
|
|
// competes with user activity.
|
|
ref.read(cacheFillerProvider);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final router = ref.watch(routerProvider);
|
|
final mode = ref.watch(themeModeProvider).value ?? AppThemeMode.system;
|
|
return MaterialApp.router(
|
|
title: 'Minstrel',
|
|
theme: buildLightTheme(),
|
|
darkTheme: buildDarkTheme(),
|
|
themeMode: mode.materialMode,
|
|
routerConfig: router,
|
|
);
|
|
}
|
|
}
|