Merge branch 'feature/improve-deletion'
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../utils/deletion_notifier.dart';
|
||||
|
||||
/// Mixin for screens that need to react to deletion events.
|
||||
///
|
||||
/// Provides automatic subscription management and filtering based on
|
||||
/// which items the screen cares about.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// class _MyScreenState extends State<MyScreen> with DeletionAware {
|
||||
/// List<PlexMetadata> _items = [];
|
||||
///
|
||||
/// @override
|
||||
/// Set<String>? get deletionRatingKeys =>
|
||||
/// _items.map((e) => e.ratingKey).toSet();
|
||||
///
|
||||
/// @override
|
||||
/// void onDeletionEvent(DeletionEvent event) {
|
||||
/// setState(() {
|
||||
/// _items.removeWhere((e) => e.ratingKey == event.ratingKey);
|
||||
/// });
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
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).
|
||||
/// Return an empty set to receive no events.
|
||||
///
|
||||
/// The set should include:
|
||||
/// - Direct items displayed (e.g., episode ratingKeys in a season view)
|
||||
/// - Parent items that affect display (e.g., show ratingKey for seasons)
|
||||
Set<String>? get deletionRatingKeys;
|
||||
|
||||
/// Called when a relevant deletion event occurs.
|
||||
///
|
||||
/// Only called if [deletionRatingKeys] is null or contains an affected key.
|
||||
void onDeletionEvent(DeletionEvent event);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_subscribeToDeletions();
|
||||
}
|
||||
|
||||
void _subscribeToDeletions() {
|
||||
_deletionSubscription = DeletionNotifier().stream.listen((event) {
|
||||
if (!mounted) return;
|
||||
|
||||
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 (ratingKeys == null || event.affectsAnyOf(ratingKeys)) {
|
||||
onDeletionEvent(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_deletionSubscription?.cancel();
|
||||
_deletionSubscription = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -29,6 +29,8 @@ import '../../../services/storage_service.dart';
|
||||
import '../../../services/settings_service.dart' show ViewMode, EpisodePosterMode;
|
||||
import '../../../mixins/grid_focus_node_mixin.dart';
|
||||
import '../../../mixins/item_updatable.dart';
|
||||
import '../../../mixins/deletion_aware.dart';
|
||||
import '../../../utils/deletion_notifier.dart';
|
||||
import '../../../utils/platform_detector.dart';
|
||||
import '../../../i18n/strings.g.dart';
|
||||
import '../../main_screen.dart';
|
||||
@@ -53,10 +55,65 @@ class LibraryBrowseTab extends BaseLibraryTab<PlexMetadata> {
|
||||
}
|
||||
|
||||
class _LibraryBrowseTabState extends BaseLibraryTabState<PlexMetadata, LibraryBrowseTab>
|
||||
with ItemUpdatable, LibraryTabFocusMixin, GridFocusNodeMixin {
|
||||
with ItemUpdatable, LibraryTabFocusMixin, GridFocusNodeMixin, DeletionAware {
|
||||
@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
|
||||
final index = items.indexWhere((e) => e.ratingKey == event.ratingKey);
|
||||
if (index != -1) {
|
||||
setState(() {
|
||||
items.removeAt(index);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If a child item was delete, then update our list to reflect that.
|
||||
// If all children were deleted, remove our item.
|
||||
// Otherwise, just update the counts.
|
||||
for (final parentKey in event.parentChain) {
|
||||
final parentIndex = items.indexWhere((e) => e.ratingKey == parentKey);
|
||||
if (parentIndex != -1) {
|
||||
final item = items[parentIndex];
|
||||
final newLeafCount = (item.leafCount ?? 1) - event.leafCount;
|
||||
if (newLeafCount <= 0) {
|
||||
setState(() {
|
||||
items.removeAt(parentIndex);
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
items[parentIndex] = item.copyWith(leafCount: newLeafCount);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get focusNodeDebugLabel => 'browse_first_item';
|
||||
|
||||
|
||||
@@ -38,7 +38,9 @@ import '../widgets/horizontal_scroll_with_arrows.dart';
|
||||
import '../widgets/media_context_menu.dart';
|
||||
import '../widgets/placeholder_container.dart';
|
||||
import '../mixins/watch_state_aware.dart';
|
||||
import '../mixins/deletion_aware.dart';
|
||||
import '../utils/watch_state_notifier.dart';
|
||||
import '../utils/deletion_notifier.dart';
|
||||
import 'season_detail_screen.dart';
|
||||
|
||||
class MediaDetailScreen extends StatefulWidget {
|
||||
@@ -51,7 +53,7 @@ class MediaDetailScreen extends StatefulWidget {
|
||||
State<MediaDetailScreen> createState() => _MediaDetailScreenState();
|
||||
}
|
||||
|
||||
class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAware {
|
||||
class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAware, DeletionAware {
|
||||
List<PlexMetadata> _seasons = [];
|
||||
bool _isLoadingSeasons = false;
|
||||
PlexMetadata? _fullMetadata;
|
||||
@@ -74,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 {
|
||||
@@ -84,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
|
||||
@@ -92,6 +112,81 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Set<String>? get deletionRatingKeys {
|
||||
final keys = <String>{widget.metadata.ratingKey};
|
||||
for (final season in _seasons) {
|
||||
keys.add(season.ratingKey);
|
||||
}
|
||||
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;
|
||||
|
||||
// If we have a season that matches the rating key exactly, then remove it from our list
|
||||
final seasonIndex = _seasons.indexWhere((s) => s.ratingKey == event.ratingKey);
|
||||
if (seasonIndex != -1) {
|
||||
setState(() {
|
||||
_seasons.removeAt(seasonIndex);
|
||||
});
|
||||
|
||||
// If the show has no more seasons, navigate back up to the library
|
||||
if (_seasons.isEmpty && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
_refreshWatchState();
|
||||
return;
|
||||
}
|
||||
|
||||
// If a child item was delete, then update our list to reflect that.
|
||||
// If all children were deleted, remove our item.
|
||||
// Otherwise, just update the counts.
|
||||
for (final parentKey in event.parentChain) {
|
||||
final idx = _seasons.indexWhere((s) => s.ratingKey == parentKey);
|
||||
if (idx != -1) {
|
||||
final season = _seasons[idx];
|
||||
final newLeafCount = (season.leafCount ?? 1) - 1;
|
||||
if (newLeafCount <= 0) {
|
||||
// Season is now empty, remove it
|
||||
setState(() {
|
||||
_seasons.removeAt(idx);
|
||||
});
|
||||
|
||||
// Otherwise we have no more seasons, so navigate up
|
||||
if (_seasons.isEmpty && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
// Otherwise just update the counts
|
||||
_seasons[idx] = season.copyWith(leafCount: newLeafCount);
|
||||
});
|
||||
}
|
||||
_refreshWatchState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lightweight refresh for watch state changes - no loader, preserves scroll
|
||||
Future<void> _refreshWatchState() async {
|
||||
final client = _getClientForMetadata(context);
|
||||
|
||||
@@ -24,7 +24,9 @@ import '../widgets/media_context_menu.dart';
|
||||
import '../widgets/placeholder_container.dart';
|
||||
import '../mixins/item_updatable.dart';
|
||||
import '../mixins/watch_state_aware.dart';
|
||||
import '../mixins/deletion_aware.dart';
|
||||
import '../utils/watch_state_notifier.dart';
|
||||
import '../utils/deletion_notifier.dart';
|
||||
import '../theme/mono_tokens.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
|
||||
@@ -38,7 +40,8 @@ class SeasonDetailScreen extends StatefulWidget {
|
||||
State<SeasonDetailScreen> createState() => _SeasonDetailScreenState();
|
||||
}
|
||||
|
||||
class _SeasonDetailScreenState extends State<SeasonDetailScreen> with ItemUpdatable, WatchStateAware, RouteAware {
|
||||
class _SeasonDetailScreenState extends State<SeasonDetailScreen>
|
||||
with ItemUpdatable, WatchStateAware, DeletionAware, RouteAware {
|
||||
PlexClient? _client;
|
||||
|
||||
@override
|
||||
@@ -52,10 +55,23 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen> with ItemUpdata
|
||||
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
|
||||
@@ -64,6 +80,41 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen> with ItemUpdata
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Set<String>? get deletionRatingKeys {
|
||||
final keys = _episodes.map((e) => e.ratingKey).toSet();
|
||||
keys.add(widget.season.ratingKey);
|
||||
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
|
||||
final index = _episodes.indexWhere((e) => e.ratingKey == event.ratingKey);
|
||||
if (index != -1) {
|
||||
setState(() {
|
||||
_episodes.removeAt(index);
|
||||
});
|
||||
// If that was the last episode, navigate back to the show view
|
||||
if (_episodes.isEmpty && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the correct PlexClient for this season's server
|
||||
PlexClient? _getClientForSeason(BuildContext context) {
|
||||
if (widget.isOffline || widget.season.serverId == null) {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import 'dart:async';
|
||||
import '../models/plex_metadata.dart';
|
||||
import 'app_logger.dart';
|
||||
|
||||
/// Event representing a media item deletion with parent chain for hierarchical invalidation
|
||||
class DeletionEvent {
|
||||
/// The ratingKey of the deleted item
|
||||
final String ratingKey;
|
||||
|
||||
/// Composite key: serverId:ratingKey
|
||||
final String globalKey;
|
||||
|
||||
/// Server this item belongs to
|
||||
final String serverId;
|
||||
|
||||
/// Parent chain for hierarchical invalidation
|
||||
/// For an episode: [seasonRatingKey, showRatingKey]
|
||||
/// For a season: [showRatingKey]
|
||||
/// For a movie: []
|
||||
final List<String> parentChain;
|
||||
|
||||
/// Media type of the deleted item
|
||||
final String mediaType;
|
||||
|
||||
/// Number of leaf items (episodes) contained in the deleted item.
|
||||
/// For an episode: 1. For a season: its episode count. For a show: its total episode count.
|
||||
final int leafCount;
|
||||
|
||||
DeletionEvent({
|
||||
required this.ratingKey,
|
||||
required this.serverId,
|
||||
required this.parentChain,
|
||||
required this.mediaType,
|
||||
this.leafCount = 1,
|
||||
}) : globalKey = '$serverId:$ratingKey';
|
||||
|
||||
/// Check if this event affects a specific item by ratingKey
|
||||
bool affectsItem(String ratingKey) => this.ratingKey == ratingKey || parentChain.contains(ratingKey);
|
||||
|
||||
/// Check if this event affects a specific globalKey
|
||||
bool affectsGlobalKey(String globalKey) =>
|
||||
this.globalKey == globalKey || parentChain.any((pk) => '$serverId:$pk' == globalKey);
|
||||
|
||||
/// 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)';
|
||||
}
|
||||
|
||||
/// Notifier for media deletion events across the app.
|
||||
///
|
||||
/// Singleton pattern following [WatchStateNotifier]. Screens subscribe
|
||||
/// to receive events when items are deleted from the server.
|
||||
class DeletionNotifier {
|
||||
static final DeletionNotifier _instance = DeletionNotifier._internal();
|
||||
|
||||
factory DeletionNotifier() => _instance;
|
||||
|
||||
DeletionNotifier._internal();
|
||||
|
||||
StreamController<DeletionEvent>? _controller;
|
||||
|
||||
StreamController<DeletionEvent> get _ensureController {
|
||||
if (_controller == null || _controller!.isClosed) {
|
||||
_controller = StreamController<DeletionEvent>.broadcast();
|
||||
}
|
||||
return _controller!;
|
||||
}
|
||||
|
||||
/// Stream of all deletion events
|
||||
Stream<DeletionEvent> get stream => _ensureController.stream;
|
||||
|
||||
/// Filter for events affecting a specific server
|
||||
Stream<DeletionEvent> forServer(String serverId) => stream.where((e) => e.serverId == serverId);
|
||||
|
||||
/// Filter for events affecting a specific item or its children
|
||||
Stream<DeletionEvent> forItem(String ratingKey) => stream.where((e) => e.affectsItem(ratingKey));
|
||||
|
||||
/// Emit a deletion event
|
||||
void notify(DeletionEvent event) {
|
||||
appLogger.d('DeletionNotifier: $event');
|
||||
_ensureController.add(event);
|
||||
}
|
||||
|
||||
/// Helper to emit a deletion event from metadata
|
||||
void notifyDeleted({required PlexMetadata metadata}) {
|
||||
notify(
|
||||
DeletionEvent(
|
||||
ratingKey: metadata.ratingKey,
|
||||
serverId: metadata.serverId ?? '',
|
||||
parentChain: _buildParentChain(metadata),
|
||||
mediaType: metadata.type,
|
||||
leafCount: metadata.leafCount ?? 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build parent chain from metadata's parent keys
|
||||
List<String> _buildParentChain(PlexMetadata metadata) {
|
||||
final chain = <String>[];
|
||||
if (metadata.parentRatingKey != null) {
|
||||
chain.add(metadata.parentRatingKey!);
|
||||
}
|
||||
if (metadata.grandparentRatingKey != null) {
|
||||
chain.add(metadata.grandparentRatingKey!);
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_controller?.close();
|
||||
_controller = null;
|
||||
}
|
||||
}
|
||||
@@ -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)';
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import '../focus/dpad_navigator.dart';
|
||||
import '../screens/media_detail_screen.dart';
|
||||
import '../screens/season_detail_screen.dart';
|
||||
import '../utils/smart_deletion_handler.dart';
|
||||
import '../utils/deletion_notifier.dart';
|
||||
import '../theme/mono_tokens.dart';
|
||||
import '../widgets/file_info_bottom_sheet.dart';
|
||||
import '../widgets/focusable_bottom_sheet.dart';
|
||||
@@ -1070,7 +1071,9 @@ class MediaContextMenuState extends State<MediaContextMenu> {
|
||||
if (context.mounted) {
|
||||
if (success) {
|
||||
showSuccessSnackBar(context, t.mediaMenu.mediaDeletedSuccessfully);
|
||||
// Trigger list refresh to remove the item from the view
|
||||
// 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);
|
||||
|
||||
Reference in New Issue
Block a user