Files
plezy/lib/utils/watch_state_notifier.dart
T
edde746 4f736c555b fix: make PlexMetadata type/key/title nullable for folder items
Plex API returns Directory entries without type field, causing
TypeError crash in fromJson. Made fields nullable and migrated
callers to use mediaType enum and displayTitle getter instead
of raw string access.
2026-03-14 05:40:14 +01:00

125 lines
3.8 KiB
Dart

import '../models/plex_metadata.dart';
import 'app_logger.dart';
import 'base_notifier.dart';
import 'global_key_utils.dart';
import 'hierarchical_event_mixin.dart';
/// Types of watch state changes
enum WatchStateChangeType { watched, unwatched, progressUpdate }
/// Event representing a watch state change with parent chain for hierarchical invalidation
class WatchStateEvent with HierarchicalEventMixin {
/// The item that changed
@override
final String ratingKey;
/// Composite key: serverId:ratingKey
@override
final String globalKey;
/// Server this item belongs to
@override
final String serverId;
/// Type of change
final WatchStateChangeType changeType;
/// Parent chain for hierarchical invalidation
/// For an episode: [seasonRatingKey, showRatingKey]
/// For a season: [showRatingKey]
/// For a movie: []
@override
final List<String> parentChain;
/// Media type that changed
final String mediaType;
/// New progress value (for progressUpdate)
final int? viewOffset;
/// Whether item is now considered watched (>90% progress or marked)
final bool? isNowWatched;
WatchStateEvent({
required this.ratingKey,
required this.serverId,
required this.changeType,
required this.parentChain,
required this.mediaType,
this.viewOffset,
this.isNowWatched,
}) : globalKey = buildGlobalKey(serverId, ratingKey);
@override
String toString() => 'WatchStateEvent($changeType, $globalKey, parents: $parentChain)';
}
/// Notifier for watch state changes across the app.
///
/// Singleton pattern following [LibraryRefreshNotifier]. Screens subscribe
/// to receive events when items are marked watched/unwatched or progress updates.
class WatchStateNotifier extends BaseNotifier<WatchStateEvent> {
static final WatchStateNotifier _instance = WatchStateNotifier._internal();
factory WatchStateNotifier() => _instance;
WatchStateNotifier._internal();
/// Filter for events affecting a specific server
Stream<WatchStateEvent> forServer(String serverId) => stream.where((e) => e.serverId == serverId);
/// Filter for events affecting a specific item or its children
Stream<WatchStateEvent> forItem(String ratingKey) => stream.where((e) => e.affectsItem(ratingKey));
/// Emit a watch state event with logging
@override
void notify(WatchStateEvent event) {
appLogger.d('WatchStateNotifier: $event');
super.notify(event);
}
/// Helper to emit a watched/unwatched event from metadata
void notifyWatched({required PlexMetadata metadata, bool isNowWatched = true}) {
notify(
WatchStateEvent(
ratingKey: metadata.ratingKey,
serverId: metadata.serverId ?? '',
changeType: isNowWatched ? WatchStateChangeType.watched : WatchStateChangeType.unwatched,
parentChain: _buildParentChain(metadata),
mediaType: metadata.type ?? '',
isNowWatched: isNowWatched,
),
);
}
/// Helper to emit a progress update event
void notifyProgress({required PlexMetadata metadata, required int viewOffset, required int duration}) {
const threshold = 0.9;
final isNowWatched = duration > 0 && (viewOffset / duration) >= threshold;
notify(
WatchStateEvent(
ratingKey: metadata.ratingKey,
serverId: metadata.serverId ?? '',
changeType: WatchStateChangeType.progressUpdate,
parentChain: _buildParentChain(metadata),
mediaType: metadata.type ?? '',
viewOffset: viewOffset,
isNowWatched: isNowWatched,
),
);
}
/// 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;
}
}