fix(propagation): scope events by server

This commit is contained in:
edde746
2026-02-06 09:58:57 +01:00
parent f6aa8e51e0
commit bbee74ef9d
9 changed files with 152 additions and 2 deletions
+24 -2
View File
@@ -27,6 +27,17 @@ import '../utils/deletion_notifier.dart';
mixin DeletionAware<T extends StatefulWidget> on State<T> {
StreamSubscription<DeletionEvent>? _deletionSubscription;
/// Override to scope events to a specific server.
///
/// Return null to receive events from all servers.
String? get deletionServerId => null;
/// Override to specify which global keys this screen cares about.
///
/// Use format `serverId:ratingKey`.
/// Return null to fall back to [deletionRatingKeys] matching.
Set<String>? get deletionGlobalKeys => null;
/// Override to specify which ratingKeys this screen cares about.
///
/// Return null to receive ALL events (not recommended for performance).
@@ -52,10 +63,21 @@ mixin DeletionAware<T extends StatefulWidget> on State<T> {
_deletionSubscription = DeletionNotifier().stream.listen((event) {
if (!mounted) return;
final keys = deletionRatingKeys;
final serverId = deletionServerId;
if (serverId != null && event.serverId != serverId) return;
final globalKeys = deletionGlobalKeys;
if (globalKeys != null) {
if (event.affectsAnyGlobalKey(globalKeys)) {
onDeletionEvent(event);
}
return;
}
final ratingKeys = deletionRatingKeys;
// If keys is null, receive all events
// Otherwise, filter to events that affect our keys
if (keys == null || event.affectsAnyOf(keys)) {
if (ratingKeys == null || event.affectsAnyOf(ratingKeys)) {
onDeletionEvent(event);
}
});
+22
View File
@@ -26,6 +26,17 @@ import '../utils/watch_state_notifier.dart';
mixin WatchStateAware<T extends StatefulWidget> on State<T> {
StreamSubscription<WatchStateEvent>? _watchStateSubscription;
/// Override to scope events to a specific server.
///
/// Return null to receive events from all servers.
String? get watchStateServerId => null;
/// Override to specify which global keys this screen cares about.
///
/// Use format `serverId:ratingKey`.
/// Return null to fall back to [watchedRatingKeys] matching.
Set<String>? get watchedGlobalKeys => null;
/// Override to specify which ratingKeys this screen cares about.
///
/// Return null to receive ALL events (not recommended for performance).
@@ -51,6 +62,17 @@ mixin WatchStateAware<T extends StatefulWidget> on State<T> {
_watchStateSubscription = WatchStateNotifier().stream.listen((event) {
if (!mounted) return;
final serverId = watchStateServerId;
if (serverId != null && event.serverId != serverId) return;
final globalKeys = watchedGlobalKeys;
if (globalKeys != null) {
if (event.affectsAnyGlobalKey(globalKeys)) {
onWatchStateChanged(event);
}
return;
}
final keys = watchedRatingKeys;
// If keys is null, receive all events
// Otherwise, filter to events that affect our keys
+20
View File
@@ -75,6 +75,8 @@ class _DiscoverScreenState extends State<DiscoverScreen>
final ValueNotifier<double> _indicatorProgress = ValueNotifier(0.0);
bool _isAutoScrollPaused = false;
String _toGlobalKey(String ratingKey, String serverId) => '$serverId:$ratingKey';
// WatchStateAware: watch on-deck items and their parent shows/seasons
@override
Set<String>? get watchedRatingKeys {
@@ -91,6 +93,24 @@ class _DiscoverScreenState extends State<DiscoverScreen>
return keys;
}
@override
Set<String>? get watchedGlobalKeys {
final keys = <String>{};
for (final item in _onDeck) {
final serverId = item.serverId;
if (serverId == null) return null;
keys.add(_toGlobalKey(item.ratingKey, serverId));
if (item.parentRatingKey != null) {
keys.add(_toGlobalKey(item.parentRatingKey!, serverId));
}
if (item.grandparentRatingKey != null) {
keys.add(_toGlobalKey(item.grandparentRatingKey!, serverId));
}
}
return keys;
}
@override
void onWatchStateChanged(WatchStateEvent event) {
// Refresh continue watching when any relevant item changes
@@ -59,9 +59,28 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<PlexMetadata, LibraryBr
@override
PlexClient get client => getClientForLibrary();
String _toGlobalKey(String ratingKey, {String? serverId}) =>
'${serverId ?? widget.library.serverId ?? ''}:$ratingKey';
@override
String? get deletionServerId => widget.library.serverId;
@override
Set<String>? get deletionRatingKeys => items.map((e) => e.ratingKey).toSet();
@override
Set<String>? get deletionGlobalKeys {
if (items.isEmpty) return <String>{};
final keys = <String>{};
for (final item in items) {
final serverId = item.serverId ?? widget.library.serverId;
if (serverId == null) return null;
keys.add(_toGlobalKey(item.ratingKey, serverId: serverId));
}
return keys;
}
@override
void onDeletionEvent(DeletionEvent event) {
// If we have an item that matches the rating key exactly, then remove it from our list
+33
View File
@@ -76,6 +76,9 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
// GlobalKeys for season cards to access their context menu
final Map<int, GlobalKey<MediaCardState>> _seasonCardKeys = {};
String _toGlobalKey(String ratingKey, {String? serverId}) =>
'${serverId ?? widget.metadata.serverId ?? ''}:$ratingKey';
// WatchStateAware: watch the show/movie and all season ratingKeys
@override
Set<String>? get watchedRatingKeys {
@@ -86,6 +89,21 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
return keys;
}
@override
String? get watchStateServerId => widget.metadata.serverId;
@override
Set<String>? get watchedGlobalKeys {
final serverId = widget.metadata.serverId;
if (serverId == null) return null;
final keys = <String>{_toGlobalKey(widget.metadata.ratingKey, serverId: serverId)};
for (final season in _seasons) {
keys.add(_toGlobalKey(season.ratingKey, serverId: season.serverId ?? serverId));
}
return keys;
}
@override
void onWatchStateChanged(WatchStateEvent event) {
// Lightweight refresh - no loader, preserves scroll position
@@ -103,6 +121,21 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
return keys;
}
@override
String? get deletionServerId => widget.metadata.serverId;
@override
Set<String>? get deletionGlobalKeys {
final serverId = widget.metadata.serverId;
if (serverId == null) return null;
final keys = <String>{_toGlobalKey(widget.metadata.ratingKey, serverId: serverId)};
for (final season in _seasons) {
keys.add(_toGlobalKey(season.ratingKey, serverId: season.serverId ?? serverId));
}
return keys;
}
@override
void onDeletionEvent(DeletionEvent event) {
if (widget.isOffline) return;
+26
View File
@@ -55,10 +55,23 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen>
bool _suppressNextBackKeyUp = false;
bool _routeSubscribed = false;
String _toGlobalKey(String ratingKey, {String? serverId}) => '${serverId ?? widget.season.serverId ?? ''}:$ratingKey';
// WatchStateAware: watch all episode ratingKeys
@override
Set<String>? get watchedRatingKeys => _episodes.map((e) => e.ratingKey).toSet();
@override
String? get watchStateServerId => widget.season.serverId;
@override
Set<String>? get watchedGlobalKeys {
final serverId = widget.season.serverId;
if (serverId == null) return null;
return _episodes.map((e) => _toGlobalKey(e.ratingKey, serverId: e.serverId ?? serverId)).toSet();
}
@override
void onWatchStateChanged(WatchStateEvent event) {
// Update the affected episode
@@ -74,6 +87,19 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen>
return keys;
}
@override
String? get deletionServerId => widget.season.serverId;
@override
Set<String>? get deletionGlobalKeys {
final serverId = widget.season.serverId;
if (serverId == null) return null;
final keys = _episodes.map((e) => _toGlobalKey(e.ratingKey, serverId: e.serverId ?? serverId)).toSet();
keys.add(_toGlobalKey(widget.season.ratingKey, serverId: serverId));
return keys;
}
@override
void onDeletionEvent(DeletionEvent event) {
// If we have an episode that matches the rating key exactly, then remove it from our list
+3
View File
@@ -44,6 +44,9 @@ class DeletionEvent {
/// Check if this event affects any item in a collection
bool affectsAnyOf(Iterable<String> ratingKeys) => ratingKeys.any(affectsItem);
/// Check if this event affects any item in a global-key collection
bool affectsAnyGlobalKey(Iterable<String> globalKeys) => globalKeys.any(affectsGlobalKey);
@override
String toString() => 'DeletionEvent(deleted: $globalKey, type: $mediaType, parents: $parentChain)';
}
+3
View File
@@ -54,6 +54,9 @@ class WatchStateEvent {
/// Check if this event affects any item in a collection
bool affectsAnyOf(Iterable<String> ratingKeys) => ratingKeys.any(affectsItem);
/// Check if this event affects any item in a global-key collection
bool affectsAnyGlobalKey(Iterable<String> globalKeys) => globalKeys.any(affectsGlobalKey);
@override
String toString() => 'WatchStateEvent($changeType, $globalKey, parents: $parentChain)';
}
+2
View File
@@ -1073,6 +1073,8 @@ class MediaContextMenuState extends State<MediaContextMenu> {
showSuccessSnackBar(context, t.mediaMenu.mediaDeletedSuccessfully);
// Broadcast deletion event for cross-screen propagation
DeletionNotifier().notifyDeleted(metadata: metadata);
// Backward-compatible list refresh for screens that are not DeletionAware yet
widget.onListRefresh?.call();
} else {
showErrorSnackBar(context, t.mediaMenu.mediaFailedToDelete);
}