90d8aae51a
Slice 4 — completes the #392 hybrid live-refresh loop. live_events_provider.dart subscribes to /api/events/stream via dio's streaming response mode, parses SSE frames (kind + JSON data + UserID scope), and exposes them as a Riverpod StreamProvider. Heartbeat comments are silently dropped; malformed JSON frames are skipped. The provider auto-rebuilds when auth state changes (token rotation, sign-out → sign-in), so reconnect is implicit. live_events_dispatcher.dart listens to the stream and invalidates the small set of publicly-importable providers we know about: - myQuarantineProvider + homeProvider on any quarantine.* event - homeProvider on any playlist.* event (Home renders the Playlists row) Screen-private providers (library_screen.dart's _liked* / _libraryAlbums / _history, admin screens, etc.) opt in to live-refresh by themselves listening to liveEventsProvider in follow-up commits; the dispatcher stays small and avoids back-edge dependencies on every feature folder. The dispatcher also installs an AppLifecycleState observer for resume-time defensive invalidation. SSE will catch up on its own when the app returns from background, but the invalidate flushes any stale data immediately so the first frame back is fresh. app.dart wires the dispatcher into the post-first-frame callback alongside the other startup activations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
58 lines
2.0 KiB
Dart
58 lines
2.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.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);
|
|
});
|
|
}
|
|
|
|
@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,
|
|
);
|
|
}
|
|
}
|