import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:permission_handler/permission_handler.dart'; import 'cache/cache_filler.dart'; import 'cache/metadata_prefetcher.dart'; import 'cache/offline_provider.dart'; import 'cache/mutation_queue.dart'; import 'cache/prefetcher.dart'; import 'cache/resume_controller.dart'; import 'cache/sync_controller.dart'; import 'player/play_events_reporter.dart'; import 'player/playback_error_reporter.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 createState() => _MinstrelAppState(); } class _MinstrelAppState extends ConsumerState { @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); // Mutation replayer: drains the cached_mutations queue when // connectivity comes back. Controllers (LikesController, // MyQuarantineController, the add-to-playlist + request flows) // enqueue on REST failure so user intent persists across // network loss instead of getting rolled back. ref.read(mutationReplayerProvider); // Play-events reporter (#415): the Flutter client otherwise // reports no plays at all — this feeds history, recommendation // scoring, scrobbles, and system-playlist rotation, and is the // path that carries the `source` tag for #415. ref.read(playEventsReporterProvider); // Resume-on-launch (#54): restores the last persisted session // (paused) and persists queue/index/position on track change, // pause, and app teardown. Pairs with the #52 idle teardown so a // torn-down session is recoverable instead of lost. ref.read(resumeControllerProvider); // Playback-error reporter (#58): turns the handler's silent // dead-track skips into a debounced/coalesced SnackBar so a // vanished track isn't mysterious (and aids server/cache debug). ref.read(playbackErrorReporterProvider); // Offline marker (#427 S1): periodic /healthz reachability // probe → offlineProvider. Read here to start the poller; S4 // gates system-playlist play + Shuffle-all on it. ref.read(offlineProvider); // POST_NOTIFICATIONS (Android 13+) is denied-by-default until // requested; without it the media notification is silently // suppressed on physical devices. One-shot, post-first-frame so // it never blocks launch; no-op on <13 / once already decided. if (Platform.isAndroid) { // ignore: unawaited_futures Permission.notification.request(); } }); } @override Widget build(BuildContext context) { final router = ref.watch(routerProvider); final mode = ref.watch(themeModeProvider).value ?? AppThemeMode.system; return MaterialApp.router( title: 'Minstrel', scaffoldMessengerKey: scaffoldMessengerKey, theme: buildLightTheme(), darkTheme: buildDarkTheme(), themeMode: mode.materialMode, routerConfig: router, ); } }