feat(#402): wire screen-scoped providers to liveEventsProvider
#392's dispatcher only invalidates publicly-importable providers (myQuarantine + home). Screen-scoped providers (file-private in their feature folders) get their own ref.listen(liveEventsProvider, ...) so they go live without needing back-edge dependencies from /shared. Five screens wired: - library_screen.dart _LikedTab — invalidates _likedTracksProvider / _likedAlbumsProvider / _likedArtistsProvider on any of the six track/album/artist like/unlike kinds. - playlist_detail_screen.dart — invalidates playlistDetailProvider(id) on playlist.updated / playlist.tracks_changed matching the visible playlist_id. On playlist.deleted matching the visible id, pops back so the user isn't left staring at a gone playlist. - admin_requests_screen.dart — invalidates adminRequestsProvider on request.status_changed (covers user create/cancel + admin approve/reject + reconciler complete). - admin_quarantine_screen.dart — invalidates adminQuarantineProvider on any quarantine.* event (flag from a user / admin resolve / file delete / lidarr delete). - requests_screen.dart (own requests) — invalidates myRequestsProvider on request.status_changed. Server-side events are user-scoped via publishRequestStatusChanged's row.UserID, so admin actions on someone else's request route to the right stream. History tab is NOT wired (no server-side play.scrobbled event yet — documented in #402 body as deferred until that event ships). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../shared/live_events_provider.dart';
|
||||
import '../shared/widgets/main_app_bar_actions.dart';
|
||||
import '../theme/theme_extension.dart';
|
||||
import 'admin_providers.dart';
|
||||
@@ -12,6 +13,15 @@ class AdminQuarantineScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
||||
// #402 wire-up: any quarantine event (flag from a user / admin
|
||||
// resolve / file delete / lidarr delete) refreshes the admin queue.
|
||||
ref.listen<AsyncValue<LiveEvent>>(liveEventsProvider, (_, next) {
|
||||
final e = next.asData?.value;
|
||||
if (e == null) return;
|
||||
if (e.kind.startsWith('quarantine.')) {
|
||||
ref.invalidate(adminQuarantineProvider);
|
||||
}
|
||||
});
|
||||
final items = ref.watch(adminQuarantineProvider);
|
||||
return Scaffold(
|
||||
backgroundColor: fs.obsidian,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../models/admin_user.dart';
|
||||
import '../shared/live_events_provider.dart';
|
||||
import '../shared/widgets/main_app_bar_actions.dart';
|
||||
import '../theme/theme_extension.dart';
|
||||
import 'admin_providers.dart';
|
||||
@@ -13,6 +14,15 @@ class AdminRequestsScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
||||
// #402 wire-up: invalidate the admin requests list when a user
|
||||
// creates/cancels/admin approves/rejects/reconciler completes —
|
||||
// request.status_changed covers all of them.
|
||||
ref.listen<AsyncValue<LiveEvent>>(liveEventsProvider, (_, next) {
|
||||
final e = next.asData?.value;
|
||||
if (e?.kind == 'request.status_changed') {
|
||||
ref.invalidate(adminRequestsProvider);
|
||||
}
|
||||
});
|
||||
final requests = ref.watch(adminRequestsProvider);
|
||||
// Best-effort lookup for requester usernames. If the users provider
|
||||
// hasn't loaded yet, valueOrNull is null and rows fall back to the
|
||||
|
||||
@@ -17,6 +17,7 @@ import '../models/quarantine_mine.dart';
|
||||
import '../models/track.dart';
|
||||
import '../player/player_provider.dart';
|
||||
import '../quarantine/quarantine_provider.dart';
|
||||
import '../shared/live_events_provider.dart';
|
||||
import '../shared/widgets/main_app_bar_actions.dart';
|
||||
import '../shared/widgets/server_image.dart';
|
||||
import '../theme/theme_extension.dart';
|
||||
@@ -375,6 +376,25 @@ class _LikedTab extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
||||
// #402 wire-up: when any like/unlike event arrives via SSE,
|
||||
// invalidate all three Liked sub-lists. Server-side events are
|
||||
// already user-scoped (publishLikeEvent attaches user_id), so the
|
||||
// dispatcher filters out other users' events upstream.
|
||||
ref.listen<AsyncValue<LiveEvent>>(liveEventsProvider, (_, next) {
|
||||
final e = next.asData?.value;
|
||||
if (e == null) return;
|
||||
switch (e.kind) {
|
||||
case 'track.liked':
|
||||
case 'track.unliked':
|
||||
case 'album.liked':
|
||||
case 'album.unliked':
|
||||
case 'artist.liked':
|
||||
case 'artist.unliked':
|
||||
ref.invalidate(_likedTracksProvider);
|
||||
ref.invalidate(_likedAlbumsProvider);
|
||||
ref.invalidate(_likedArtistsProvider);
|
||||
}
|
||||
});
|
||||
final tracksA = ref.watch(_likedTracksProvider);
|
||||
final albumsA = ref.watch(_likedAlbumsProvider);
|
||||
final artistsA = ref.watch(_likedArtistsProvider);
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../cache/db.dart';
|
||||
import '../models/playlist.dart';
|
||||
import '../models/track.dart';
|
||||
import '../player/player_provider.dart';
|
||||
import '../shared/live_events_provider.dart';
|
||||
import '../shared/widgets/track_actions/track_actions_button.dart';
|
||||
import '../theme/theme_extension.dart';
|
||||
import 'playlists_provider.dart';
|
||||
@@ -24,6 +25,23 @@ class PlaylistDetailScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
||||
// #402 wire-up: invalidate the detail provider on playlist.updated /
|
||||
// .tracks_changed events whose payload matches the visible id. On
|
||||
// .deleted matching this id, navigate back so the user isn't left
|
||||
// staring at a gone-from-server playlist.
|
||||
ref.listen<AsyncValue<LiveEvent>>(liveEventsProvider, (_, next) {
|
||||
final e = next.asData?.value;
|
||||
if (e == null) return;
|
||||
final eventPlaylistId = e.data['playlist_id'] as String?;
|
||||
if (eventPlaylistId != id) return;
|
||||
switch (e.kind) {
|
||||
case 'playlist.updated':
|
||||
case 'playlist.tracks_changed':
|
||||
ref.invalidate(playlistDetailProvider(id));
|
||||
case 'playlist.deleted':
|
||||
if (context.mounted) context.pop();
|
||||
}
|
||||
});
|
||||
final detail = ref.watch(playlistDetailProvider(id));
|
||||
|
||||
// Resolve the best playlist info available for the AppBar title.
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../models/admin_request.dart';
|
||||
import '../shared/live_events_provider.dart';
|
||||
import '../shared/widgets/main_app_bar_actions.dart';
|
||||
import '../theme/theme_extension.dart';
|
||||
import 'requests_provider.dart';
|
||||
@@ -13,6 +14,16 @@ class RequestsScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
||||
// #402 wire-up: refresh the user's own requests list on
|
||||
// request.status_changed. Server-side events are user-scoped
|
||||
// (admin actions on someone else's request route to that other
|
||||
// user's stream); the dispatcher's filter already drops mismatches.
|
||||
ref.listen<AsyncValue<LiveEvent>>(liveEventsProvider, (_, next) {
|
||||
final e = next.asData?.value;
|
||||
if (e?.kind == 'request.status_changed') {
|
||||
ref.invalidate(myRequestsProvider);
|
||||
}
|
||||
});
|
||||
final requests = ref.watch(myRequestsProvider);
|
||||
|
||||
return Scaffold(
|
||||
|
||||
Reference in New Issue
Block a user