Files
minstrel/flutter_client/lib/shared/live_events_dispatcher.dart
T
bvandeusen 1646b02ca2 refactor(flutter): drop legacy home snapshot path (#406)
The home screen renders solely from the per-item cached_home_index
path (proven on devices for many releases); the legacy snapshot stack
was dead weight.

- library_providers: remove homeProvider + _encodeHomeData +
  _albumToJson/_artistToJson/_trackToJson; drop now-unused dart:convert
  and models/home_data.dart imports
- metadata_prefetcher: re-point off homeIndexProvider/HomeIndex —
  pre-warm artistProvider from the rediscover/last-played artist
  sections (album/track tiles hydrate their own artist on render)
- live_events_dispatcher: homeProvider -> homeIndexProvider so live
  events still refresh the home screen
- db.dart: drop CachedHomeSnapshot (table class + @DriftDatabase
  entry); schemaVersion 10->11; from<3 createTable -> raw
  customStatement so the historical step compiles without the
  generated symbol; from<11 DROP TABLE cached_home_snapshot

No test references the removed symbols. db.g.dart regenerated by CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:04:43 -04:00

105 lines
4.1 KiB
Dart

// Maps incoming LiveEvent kinds to provider invalidations.
//
// Activated by ref.read(liveEventsDispatcherProvider) in app.dart; once
// activated, the dispatcher listens to liveEventsProvider for the
// lifetime of the ProviderScope and invalidates the small set of
// public-scoped providers we know about.
//
// Screen-scoped providers (file-private providers in library_screen.dart,
// admin handlers, etc.) opt in to live-refresh by themselves listening
// to liveEventsProvider — the dispatcher only handles cross-screen,
// publicly-importable providers. This keeps the dispatcher small and
// avoids a back-edge dependency from /shared onto every feature folder.
//
// Includes an AppLifecycleState resume handler that defensively
// invalidates the same set on app foreground. SSE will catch up on its
// own, but on cold-start or after a long background period the
// re-invalidate is fast and avoids stale data flashing before the
// stream reconnects.
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../library/library_providers.dart' show homeIndexProvider;
import '../quarantine/quarantine_provider.dart';
import 'live_events_provider.dart';
/// Activates the SSE → invalidation dispatcher for the lifetime of the
/// containing ProviderScope. Read this once at startup (from app.dart);
/// the provider's body runs once, sets up listeners, and returns a
/// sentinel value.
final liveEventsDispatcherProvider = Provider<_LiveEventsDispatcher>((ref) {
final d = _LiveEventsDispatcher(ref);
ref.onDispose(d.dispose);
return d;
});
class _LiveEventsDispatcher with WidgetsBindingObserver {
_LiveEventsDispatcher(this._ref) {
// Subscribe to the event stream. Each emitted event invokes
// _handle. Errors / disconnects auto-rebuild the underlying
// provider; we don't need to react to them here.
_sub = _ref.listen<AsyncValue<LiveEvent>>(
liveEventsProvider,
(_, next) => next.whenData(_handle),
fireImmediately: false,
);
WidgetsBinding.instance.addObserver(this);
}
final Ref _ref;
late final ProviderSubscription<AsyncValue<LiveEvent>> _sub;
void _handle(LiveEvent e) {
// Map event kinds to provider invalidations. Keep this list short:
// only providers reachable from /shared. Screen-private providers
// listen to liveEventsProvider themselves.
switch (e.kind) {
case 'quarantine.flagged':
case 'quarantine.unflagged':
case 'quarantine.resolved':
case 'quarantine.file_deleted':
case 'quarantine.deleted_via_lidarr':
_ref.invalidate(myQuarantineProvider);
// Hidden / liked / album rows referencing a deleted track may
// now be stale — homeIndexProvider re-fetch refreshes the cards.
_ref.invalidate(homeIndexProvider);
case 'playlist.created':
case 'playlist.updated':
case 'playlist.deleted':
case 'playlist.tracks_changed':
// Home renders a Playlists row; refresh it so a delete or add
// is reflected immediately. Detail screens that need the
// mutated row will get a separate invalidate from their own
// listener (filed as a follow-up).
_ref.invalidate(homeIndexProvider);
case 'scan.run_started':
case 'scan.run_finished':
// Admin scan card provider lives in the admin feature folder
// and is file-private today; will be invalidated by the admin
// dashboard's own listener once that exists.
break;
default:
// track.liked / track.unliked / request.status_changed reach
// screen-private providers; their screens listen directly.
break;
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// Defensive cold-start invalidation. SSE will catch up on its
// own, but the re-invalidate flushes any stale data that
// landed while the app was backgrounded.
_ref.invalidate(myQuarantineProvider);
_ref.invalidate(homeIndexProvider);
}
}
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_sub.close();
}
}