diff --git a/flutter_client/lib/admin/admin_quarantine_screen.dart b/flutter_client/lib/admin/admin_quarantine_screen.dart index d9c55a9d..be77580a 100644 --- a/flutter_client/lib/admin/admin_quarantine_screen.dart +++ b/flutter_client/lib/admin/admin_quarantine_screen.dart @@ -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()!; + // #402 wire-up: any quarantine event (flag from a user / admin + // resolve / file delete / lidarr delete) refreshes the admin queue. + ref.listen>(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, diff --git a/flutter_client/lib/admin/admin_requests_screen.dart b/flutter_client/lib/admin/admin_requests_screen.dart index b0faecf6..5063265e 100644 --- a/flutter_client/lib/admin/admin_requests_screen.dart +++ b/flutter_client/lib/admin/admin_requests_screen.dart @@ -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()!; + // #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>(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 diff --git a/flutter_client/lib/library/library_screen.dart b/flutter_client/lib/library/library_screen.dart index ebe6ea27..159160f0 100644 --- a/flutter_client/lib/library/library_screen.dart +++ b/flutter_client/lib/library/library_screen.dart @@ -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()!; + // #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>(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); diff --git a/flutter_client/lib/playlists/playlist_detail_screen.dart b/flutter_client/lib/playlists/playlist_detail_screen.dart index 5e8c07e7..1e0b0fbb 100644 --- a/flutter_client/lib/playlists/playlist_detail_screen.dart +++ b/flutter_client/lib/playlists/playlist_detail_screen.dart @@ -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()!; + // #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>(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. diff --git a/flutter_client/lib/requests/requests_screen.dart b/flutter_client/lib/requests/requests_screen.dart index 0eb8f19a..2cf98fdd 100644 --- a/flutter_client/lib/requests/requests_screen.dart +++ b/flutter_client/lib/requests/requests_screen.dart @@ -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()!; + // #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>(liveEventsProvider, (_, next) { + final e = next.asData?.value; + if (e?.kind == 'request.status_changed') { + ref.invalidate(myRequestsProvider); + } + }); final requests = ref.watch(myRequestsProvider); return Scaffold(