From 5538e0627bcec7dbc7483523328cffb79b47a411 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:08:39 +0100 Subject: [PATCH] refactor: remove SeasonDetailScreen, unify into MediaDetailScreen Extract EpisodeCard to its own widget file. MediaDetailScreen now handles isSeason by showing episodes directly, with proper watch state tracking, deletion handling, and D-pad focus navigation. Hide empty overview sections. --- lib/screens/media_detail_screen.dart | 159 +++++++-- lib/utils/media_navigation_helper.dart | 10 +- .../episode_card.dart} | 313 +----------------- lib/widgets/media_card.dart | 3 +- lib/widgets/media_context_menu.dart | 21 +- 5 files changed, 145 insertions(+), 361 deletions(-) rename lib/{screens/season_detail_screen.dart => widgets/episode_card.dart} (64%) diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index ccadc5e2..fa3da1d4 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -51,7 +51,7 @@ import '../mixins/mounted_set_state_mixin.dart'; import '../mixins/server_bound_media_mixin.dart'; import '../utils/watch_state_notifier.dart'; import '../utils/deletion_notifier.dart'; -import 'season_detail_screen.dart'; +import '../widgets/episode_card.dart'; class MediaDetailScreen extends StatefulWidget { final PlexMetadata metadata; @@ -121,12 +121,18 @@ class _MediaDetailScreenState extends State @override bool get isServerBoundOffline => widget.isOffline; - // WatchStateAware: watch the show/movie and all season ratingKeys + // WatchStateAware: watch the show/movie and all season/episode ratingKeys @override Set? get watchedRatingKeys { final keys = {widget.metadata.ratingKey}; - for (final season in _seasons) { - keys.add(season.ratingKey); + if (_showEpisodesDirectly) { + for (final ep in _episodes) { + keys.add(ep.ratingKey); + } + } else { + for (final season in _seasons) { + keys.add(season.ratingKey); + } } return keys; } @@ -140,25 +146,40 @@ class _MediaDetailScreenState extends State if (serverId == null) return null; final keys = {toServerBoundGlobalKey(widget.metadata.ratingKey, serverId: serverId)}; - for (final season in _seasons) { - keys.add(toServerBoundGlobalKey(season.ratingKey, serverId: season.serverId ?? serverId)); + if (_showEpisodesDirectly) { + for (final ep in _episodes) { + keys.add(toServerBoundGlobalKey(ep.ratingKey, serverId: ep.serverId ?? serverId)); + } + } else { + for (final season in _seasons) { + keys.add(toServerBoundGlobalKey(season.ratingKey, serverId: season.serverId ?? serverId)); + } } return keys; } @override void onWatchStateChanged(WatchStateEvent event) { - // Lightweight refresh - no loader, preserves scroll position if (!widget.isOffline) { - _refreshWatchState(); + if (_showEpisodesDirectly) { + _updateEpisodeWatchState(event.ratingKey); + } else { + _refreshWatchState(); + } } } @override Set? get deletionRatingKeys { final keys = {widget.metadata.ratingKey}; - for (final season in _seasons) { - keys.add(season.ratingKey); + if (_showEpisodesDirectly) { + for (final ep in _episodes) { + keys.add(ep.ratingKey); + } + } else { + for (final season in _seasons) { + keys.add(season.ratingKey); + } } return keys; } @@ -172,15 +193,37 @@ class _MediaDetailScreenState extends State if (serverId == null) return null; final keys = {toServerBoundGlobalKey(widget.metadata.ratingKey, serverId: serverId)}; - for (final season in _seasons) { - keys.add(toServerBoundGlobalKey(season.ratingKey, serverId: season.serverId ?? serverId)); + if (_showEpisodesDirectly) { + for (final ep in _episodes) { + keys.add(toServerBoundGlobalKey(ep.ratingKey, serverId: ep.serverId ?? serverId)); + } + } else { + for (final season in _seasons) { + keys.add(toServerBoundGlobalKey(season.ratingKey, serverId: season.serverId ?? serverId)); + } } return keys; } @override void onDeletionEvent(DeletionEvent event) { - if (widget.isOffline) return; + // Download-only deletions should only remove items when viewing offline content + if (event.isDownloadOnly && !widget.isOffline) return; + if (!event.isDownloadOnly && widget.isOffline) return; + + // When showing episodes directly (season view or flattened), handle episode deletion + if (_showEpisodesDirectly) { + final epIndex = _episodes.indexWhere((e) => e.ratingKey == event.ratingKey); + if (epIndex != -1) { + setState(() { + _episodes.removeAt(epIndex); + }); + if (_episodes.isEmpty && widget.metadata.isSeason && mounted) { + Navigator.of(context).pop(); + } + 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); @@ -258,12 +301,31 @@ class _MediaDetailScreenState extends State .map((s) => s.copyWith(serverId: widget.metadata.serverId, serverName: widget.metadata.serverName)) .toList(); }); + } else if (widget.metadata.isSeason) { + await _fetchAllEpisodes(); } } catch (e) { // Silently fail - data will refresh on next navigation } } + /// Update a single episode's watch state without refetching everything + Future _updateEpisodeWatchState(String ratingKey) async { + final client = _getClientForMetadata(context); + if (client == null) return; + try { + final refreshed = await client.getMetadataWithImages(ratingKey); + if (refreshed != null) { + setStateIfMounted(() { + final i = _episodes.indexWhere((e) => e.ratingKey == ratingKey); + if (i != -1) _episodes[i] = refreshed; + }); + } + } catch (_) { + // Silently fail + } + } + @override void initState() { super.initState(); @@ -365,6 +427,18 @@ class _MediaDetailScreenState extends State // No on deck episode, fetch first episode of first season await _playFirstEpisode(); } + } else if (metadata.isSeason) { + // For seasons, play the first episode + if (_episodes.isNotEmpty) { + await navigateToVideoPlayerWithRefresh( + context, + metadata: _episodes.first, + isOffline: widget.isOffline, + onRefresh: _loadFullMetadata, + ); + } else { + await _playFirstEpisode(); + } } else { appLogger.d('Playing: ${metadata.title}'); // For movies or episodes, play directly @@ -963,6 +1037,10 @@ class _MediaDetailScreenState extends State _loadSeasonsFromDownloads(); // Get offline OnDeck episode _loadOfflineOnDeckEpisode(); + } else if (widget.metadata.isSeason) { + _seasons = [widget.metadata]; + _showEpisodesDirectly = true; + _loadEpisodesFromDownloads(); } return; } @@ -1008,6 +1086,10 @@ class _MediaDetailScreenState extends State // Load seasons if it's a show if (metadata.isShow) { _loadSeasons(); + } else if (metadata.isSeason) { + _seasons = [widget.metadata]; + _showEpisodesDirectly = true; + _fetchAllEpisodes(); } // Load extras (trailers, behind-the-scenes, etc.) @@ -1024,6 +1106,10 @@ class _MediaDetailScreenState extends State if (widget.metadata.isShow) { _loadSeasons(); + } else if (widget.metadata.isSeason) { + _seasons = [widget.metadata]; + _showEpisodesDirectly = true; + _fetchAllEpisodes(); } } catch (e) { // Fallback to passed metadata on error @@ -1035,6 +1121,10 @@ class _MediaDetailScreenState extends State if (widget.metadata.isShow) { _loadSeasons(); + } else if (widget.metadata.isSeason) { + _seasons = [widget.metadata]; + _showEpisodesDirectly = true; + _fetchAllEpisodes(); } } } @@ -1136,6 +1226,19 @@ class _MediaDetailScreenState extends State } } + /// Load episodes from downloaded content for a season + void _loadEpisodesFromDownloads() { + final downloadProvider = context.read(); + final allEpisodes = downloadProvider.getDownloadedEpisodesForShow(widget.metadata.parentRatingKey ?? ''); + final seasonEpisodes = allEpisodes.where((ep) => ep.parentIndex == widget.metadata.index).toList() + ..sort((a, b) => (a.index ?? 0).compareTo(b.index ?? 0)); + + setState(() { + _episodes = seasonEpisodes; + _isLoadingEpisodes = false; + }); + } + /// Load extras (trailers, behind-the-scenes, etc.) Future _loadExtras() async { // Only load extras for movies and shows @@ -1174,7 +1277,7 @@ class _MediaDetailScreenState extends State final watchStateChanged = await Navigator.push( context, MaterialPageRoute( - builder: (context) => SeasonDetailScreen(season: season, isOffline: widget.isOffline), + builder: (context) => MediaDetailScreen(metadata: season, isOffline: widget.isOffline), ), ); if (watchStateChanged == true) { @@ -1212,13 +1315,13 @@ class _MediaDetailScreenState extends State final metadata = _fullMetadata ?? widget.metadata; // DOWN order: overview → seasons → cast → extras - if (metadata.summary != null) { + if (metadata.summary != null && metadata.summary!.isNotEmpty) { _overviewFocusNode.requestFocus(); _scrollSectionIntoView(_overviewSectionKey); return KeyEventResult.handled; } - if (metadata.isShow && _seasons.isNotEmpty) { + if ((metadata.isShow || metadata.isSeason) && _seasons.isNotEmpty) { _seasonsFocusNode.requestFocus(); _scrollSectionIntoView(_seasonsSectionKey); return KeyEventResult.handled; @@ -1320,7 +1423,7 @@ class _MediaDetailScreenState extends State // UP: overview → play button if (key.isUpKey) { final metadata = _fullMetadata ?? widget.metadata; - if (metadata.summary != null) { + if (metadata.summary != null && metadata.summary!.isNotEmpty) { _overviewFocusNode.requestFocus(); _scrollSectionIntoView(_overviewSectionKey); } else { @@ -1361,9 +1464,9 @@ class _MediaDetailScreenState extends State return KeyEventResult.handled; } - // DOWN: seasons (if show) → cast → extras + // DOWN: seasons/episodes (if show/season) → cast → extras if (key.isDownKey) { - if (metadata.isShow && _seasons.isNotEmpty) { + if ((metadata.isShow || metadata.isSeason) && _seasons.isNotEmpty) { _seasonsFocusNode.requestFocus(); _scrollSectionIntoView(_seasonsSectionKey); } else if (metadata.role != null && metadata.role!.isNotEmpty) { @@ -1503,16 +1606,16 @@ class _MediaDetailScreenState extends State return KeyEventResult.handled; } - // UP: cast → seasons (if show) → overview → play button + // UP: cast → seasons/episodes (if show/season) → overview → play button if (key.isUpKey) { final metadata = _fullMetadata ?? widget.metadata; if (metadata.role != null && metadata.role!.isNotEmpty) { _castFocusNode.requestFocus(); _scrollSectionIntoView(_castSectionKey); - } else if (metadata.isShow && _seasons.isNotEmpty) { + } else if ((metadata.isShow || metadata.isSeason) && _seasons.isNotEmpty) { _seasonsFocusNode.requestFocus(); _scrollSectionIntoView(_seasonsSectionKey); - } else if (metadata.summary != null) { + } else if (metadata.summary != null && metadata.summary!.isNotEmpty) { _overviewFocusNode.requestFocus(); _scrollSectionIntoView(_overviewSectionKey); } else { @@ -1557,12 +1660,12 @@ class _MediaDetailScreenState extends State return KeyEventResult.handled; } - // UP: seasons (if show) → overview → play button + // UP: seasons/episodes (if show/season) → overview → play button if (key.isUpKey) { - if (metadata.isShow && _seasons.isNotEmpty) { + if ((metadata.isShow || metadata.isSeason) && _seasons.isNotEmpty) { _seasonsFocusNode.requestFocus(); _scrollSectionIntoView(_seasonsSectionKey); - } else if (metadata.summary != null) { + } else if (metadata.summary != null && metadata.summary!.isNotEmpty) { _overviewFocusNode.requestFocus(); _scrollSectionIntoView(_overviewSectionKey); } else { @@ -2169,7 +2272,7 @@ class _MediaDetailScreenState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ // Summary - if (metadata.summary != null) ...[ + if (metadata.summary != null && metadata.summary!.isNotEmpty) ...[ Text( key: _overviewSectionKey, t.discover.overview, @@ -2214,8 +2317,8 @@ class _MediaDetailScreenState extends State const SizedBox(height: 24), ], - // Seasons / Episodes (for TV shows) - if (isShow) ...[ + // Seasons / Episodes (for TV shows and seasons) + if (isShow || metadata.isSeason) ...[ Text( key: _seasonsSectionKey, _showEpisodesDirectly ? t.libraries.groupings.episodes : t.discover.seasons, diff --git a/lib/utils/media_navigation_helper.dart b/lib/utils/media_navigation_helper.dart index 95d13103..0df7791a 100644 --- a/lib/utils/media_navigation_helper.dart +++ b/lib/utils/media_navigation_helper.dart @@ -3,7 +3,6 @@ import '../models/plex_metadata.dart'; import '../models/plex_playlist.dart'; import '../screens/collection_detail_screen.dart'; import '../screens/media_detail_screen.dart'; -import '../screens/season_detail_screen.dart'; import '../screens/playlist/playlist_detail_screen.dart'; import 'video_player_navigation.dart'; @@ -96,14 +95,7 @@ Future navigateToMediaItem( continue defaultCase; case PlexMediaType.season: - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SeasonDetailScreen(season: metadata, isOffline: isOffline), - ), - ); - onRefresh?.call(metadata.ratingKey); - return MediaNavigationResult.navigated; + continue defaultCase; defaultCase: default: diff --git a/lib/screens/season_detail_screen.dart b/lib/widgets/episode_card.dart similarity index 64% rename from lib/screens/season_detail_screen.dart rename to lib/widgets/episode_card.dart index 5de87649..de5d01f1 100644 --- a/lib/screens/season_detail_screen.dart +++ b/lib/widgets/episode_card.dart @@ -2,334 +2,23 @@ import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:plezy/widgets/app_icon.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; -import '../../services/plex_client.dart'; -import '../main.dart'; import '../focus/focusable_wrapper.dart'; -import '../focus/key_event_utils.dart'; -import '../focus/dpad_navigator.dart'; -import '../focus/input_mode_tracker.dart'; import '../models/download_models.dart'; import '../providers/download_provider.dart'; import '../providers/settings_provider.dart'; import '../utils/content_utils.dart'; -import '../services/download_storage_service.dart'; import '../widgets/collapsible_text.dart'; import '../widgets/plex_optimized_image.dart'; import '../models/plex_metadata.dart'; import '../utils/platform_detector.dart'; -import '../utils/video_player_navigation.dart'; import '../utils/formatters.dart'; -import '../widgets/desktop_app_bar.dart'; 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 '../mixins/mounted_set_state_mixin.dart'; -import '../mixins/server_bound_media_mixin.dart'; -import '../utils/watch_state_notifier.dart'; -import '../utils/deletion_notifier.dart'; import '../theme/mono_tokens.dart'; -import '../i18n/strings.g.dart'; - -class SeasonDetailScreen extends StatefulWidget { - final PlexMetadata season; - final bool isOffline; - - const SeasonDetailScreen({super.key, required this.season, this.isOffline = false}); - - @override - State createState() => _SeasonDetailScreenState(); -} - -class _SeasonDetailScreenState extends State - with ItemUpdatable, WatchStateAware, DeletionAware, RouteAware, MountedSetStateMixin, ServerBoundMediaMixin { - PlexClient? _client; - - @override - PlexClient get client { - final client = _client; - if (client == null) { - throw StateError('PlexClient unavailable for season ${widget.season.ratingKey}'); - } - return client; - } - - List _episodes = []; - bool _isLoadingEpisodes = false; - bool _watchStateChanged = false; - // Capture keyboard mode once at init to avoid rebuild dependency - bool _initialKeyboardMode = false; - bool _suppressNextBackKeyUp = false; - bool _routeSubscribed = false; - - @override - PlexMetadata get serverBoundMetadata => widget.season; - - @override - bool get isServerBoundOffline => widget.isOffline; - - // WatchStateAware: watch all episode ratingKeys - @override - Set? get watchedRatingKeys => _episodes.map((e) => e.ratingKey).toSet(); - - @override - String? get watchStateServerId => serverBoundServerId; - - @override - Set? get watchedGlobalKeys { - final serverId = serverBoundServerId; - if (serverId == null) return null; - - return _episodes.map((e) => toServerBoundGlobalKey(e.ratingKey, serverId: e.serverId ?? serverId)).toSet(); - } - - @override - void onWatchStateChanged(WatchStateEvent event) { - // Update the affected episode - if (!widget.isOffline && _client != null) { - updateItem(event.ratingKey); - } - } - - @override - Set? get deletionRatingKeys { - final keys = _episodes.map((e) => e.ratingKey).toSet(); - keys.add(widget.season.ratingKey); - return keys; - } - - @override - String? get deletionServerId => serverBoundServerId; - - @override - Set? get deletionGlobalKeys { - final serverId = serverBoundServerId; - if (serverId == null) return null; - - final keys = _episodes.map((e) => toServerBoundGlobalKey(e.ratingKey, serverId: e.serverId ?? serverId)).toSet(); - keys.add(toServerBoundGlobalKey(widget.season.ratingKey, serverId: serverId)); - return keys; - } - - @override - void onDeletionEvent(DeletionEvent event) { - // Download-only deletions should only remove items when viewing offline content - if (event.isDownloadOnly && !widget.isOffline) return; - - // 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(); - } - } - } - - @override - void initState() { - super.initState(); - // Initialize the client once in initState - WidgetsBinding.instance.addPostFrameCallback((_) { - // Capture keyboard mode once to avoid rebuild dependency when mode changes - _initialKeyboardMode = InputModeTracker.isKeyboardMode(context); - _client = getServerBoundClient(context); - _loadEpisodes(); - }); - } - - Future _loadEpisodes() async { - setState(() { - _isLoadingEpisodes = true; - }); - - if (widget.isOffline) { - // Load episodes from downloads - _loadEpisodesFromDownloads(); - return; - } - - try { - final client = _client; - if (client == null) { - setStateIfMounted(() { - _isLoadingEpisodes = false; - }); - return; - } - - // Episodes are automatically tagged with server info by PlexClient - final episodes = await client.getChildren(widget.season.ratingKey); - - setStateIfMounted(() { - _episodes = episodes; - _isLoadingEpisodes = false; - }); - } catch (e) { - setStateIfMounted(() { - _isLoadingEpisodes = false; - }); - } - } - - /// Load episodes from downloaded content - void _loadEpisodesFromDownloads() { - final downloadProvider = context.read(); - - // Get all downloaded episodes for the show (grandparentRatingKey) - final allEpisodes = downloadProvider.getDownloadedEpisodesForShow(widget.season.parentRatingKey ?? ''); - - // Filter to only this season's episodes - final seasonEpisodes = allEpisodes.where((ep) => ep.parentIndex == widget.season.index).toList() - ..sort((a, b) => (a.index ?? 0).compareTo(b.index ?? 0)); - - setState(() { - _episodes = seasonEpisodes; - _isLoadingEpisodes = false; - }); - } - - @override - Future updateItem(String ratingKey) async { - if (_client == null) { - return; - } - _watchStateChanged = true; - await super.updateItem(ratingKey); - } - - @override - void updateItemInLists(String ratingKey, PlexMetadata updatedMetadata) { - final index = _episodes.indexWhere((item) => item.ratingKey == ratingKey); - if (index != -1) { - _episodes[index] = updatedMetadata; - } - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (_routeSubscribed) return; - final route = ModalRoute.of(context); - if (route is PageRoute) { - routeObserver.subscribe(this, route); - _routeSubscribed = true; - } - } - - @override - void dispose() { - if (_routeSubscribed) { - routeObserver.unsubscribe(this); - _routeSubscribed = false; - } - super.dispose(); - } - - @override - void didPopNext() { - // Returning from a child route (e.g., video player). - // Suppress the first BACK KeyUp which can otherwise pop this route. - _suppressNextBackKeyUp = true; - } - - KeyEventResult _handleBackKeyEvent(KeyEvent event) { - if (_suppressNextBackKeyUp && event is KeyUpEvent && event.logicalKey.isBackKey) { - _suppressNextBackKeyUp = false; - return KeyEventResult.handled; - } - return handleBackKeyNavigation(context, event, result: _watchStateChanged); - } - - @override - Widget build(BuildContext context) { - final content = Focus( - onKeyEvent: (_, event) => _handleBackKeyEvent(event), - child: Scaffold( - body: CustomScrollView( - slivers: [ - CustomAppBar( - title: Text(widget.season.title), - pinned: true, - onBackPressed: () => Navigator.pop(context, _watchStateChanged), - ), - if (_isLoadingEpisodes) - const SliverFillRemaining(child: Center(child: CircularProgressIndicator())) - else if (_episodes.isEmpty) - SliverFillRemaining( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AppIcon(Symbols.movie_rounded, fill: 1, size: 64, color: tokens(context).textMuted), - const SizedBox(height: 16), - Text( - t.messages.noEpisodesFoundGeneral, - style: Theme.of(context).textTheme.titleLarge?.copyWith(color: tokens(context).textMuted), - ), - ], - ), - ), - ) - else - SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - final episode = _episodes[index]; - // Get local poster path for offline mode - String? localPosterPath; - if (widget.isOffline && episode.serverId != null) { - final downloadProvider = context.read(); - final globalKey = episode.globalKey; - // Get the artwork reference and convert to local file path - final artworkRef = downloadProvider.getArtworkPaths(globalKey); - localPosterPath = artworkRef?.getLocalPath(DownloadStorageService.instance, episode.serverId!); - } - return EpisodeCard( - episode: episode, - client: _client, - isOffline: widget.isOffline, - localPosterPath: localPosterPath, - autofocus: index == 0 && _initialKeyboardMode, - onTap: () async { - await navigateToVideoPlayerWithRefresh( - context, - metadata: episode, - isOffline: widget.isOffline, - onRefresh: _loadEpisodes, - ); - }, - onRefresh: widget.isOffline ? null : updateItem, - onListRefresh: widget.isOffline ? null : _loadEpisodes, - ); - }, childCount: _episodes.length), - ), - SliverPadding(padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom)), - ], - ), - ), - ); - - final blockSystemBack = Platform.isAndroid && InputModeTracker.isKeyboardMode(context); - if (!blockSystemBack) { - return content; - } - - return PopScope( - canPop: false, // Prevent system back from double-popping on Android keyboard/TV - // ignore: no-empty-block - required callback, blocks system back on Android TV - onPopInvokedWithResult: (didPop, result) {}, - child: content, - ); - } -} +import '../../services/plex_client.dart'; /// Episode card widget with D-pad long-press support class EpisodeCard extends StatefulWidget { diff --git a/lib/widgets/media_card.dart b/lib/widgets/media_card.dart index 1c7746f3..2ddf7418 100644 --- a/lib/widgets/media_card.dart +++ b/lib/widgets/media_card.dart @@ -12,7 +12,6 @@ import '../providers/download_provider.dart'; import '../services/download_storage_service.dart'; import '../providers/settings_provider.dart'; import '../screens/media_detail_screen.dart'; -import '../screens/season_detail_screen.dart'; import '../services/settings_service.dart'; import '../utils/provider_extensions.dart'; import '../utils/formatters.dart'; @@ -949,7 +948,7 @@ void _navigateToSeason(BuildContext context, PlexMetadata episode, {bool isOffli serverId: episode.serverId, serverName: episode.serverName, ); - Navigator.push(context, MaterialPageRoute(builder: (_) => SeasonDetailScreen(season: seasonStub, isOffline: isOffline))); + Navigator.push(context, MaterialPageRoute(builder: (_) => MediaDetailScreen(metadata: seasonStub, isOffline: isOffline))); } /// Navigate to the detail screen for a metadata item. diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index 501687fd..c639fe7c 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -7,6 +7,7 @@ import '../services/plex_client.dart'; import '../services/play_queue_launcher.dart'; import '../models/plex_metadata.dart'; import '../models/plex_playlist.dart'; +import '../utils/content_utils.dart'; import '../providers/download_provider.dart'; import '../providers/multi_server_provider.dart'; import '../providers/offline_mode_provider.dart'; @@ -23,7 +24,6 @@ import '../focus/focusable_button.dart'; import '../focus/dpad_navigator.dart'; import '../screens/media_detail_screen.dart'; import '../screens/metadata_edit_screen.dart'; -import '../screens/season_detail_screen.dart'; import '../utils/smart_deletion_handler.dart'; import '../utils/deletion_notifier.dart'; import '../theme/mono_tokens.dart'; @@ -220,12 +220,12 @@ class MediaContextMenuState extends State { ); } - // Go to Series (for episodes and seasons) — hide if already on that series' detail screen, - // or on a season screen belonging to the same series + // Go to Series (for episodes and seasons) — hide if already on that series' detail screen final ancestorMediaDetail = context.findAncestorWidgetOfExactType(); - final ancestorSeasonDetail = context.findAncestorWidgetOfExactType(); - final ancestorSeriesKey = - ancestorMediaDetail?.metadata.ratingKey ?? ancestorSeasonDetail?.season.parentRatingKey; + final ancestorMeta = ancestorMediaDetail?.metadata; + final ancestorSeriesKey = ancestorMeta != null && ancestorMeta.isSeason + ? ancestorMeta.parentRatingKey + : ancestorMeta?.ratingKey; // For episodes, the show key is grandparentRatingKey; for seasons, it's parentRatingKey final itemSeriesKey = mediaType == PlexMediaType.episode ? metadata.grandparentRatingKey : metadata.parentRatingKey; @@ -235,11 +235,12 @@ class MediaContextMenuState extends State { menuActions.add(_MenuAction(value: 'series', icon: Symbols.tv_rounded, label: t.mediaMenu.goToSeries)); } - // Go to Season (for episodes) — hide if already on that season's detail screen + // Go to Season (for episodes) — hide if already viewing that season's MediaDetailScreen if (mediaType == PlexMediaType.episode && metadata.parentTitle != null && - context.findAncestorWidgetOfExactType()?.season.ratingKey != - metadata.parentRatingKey) { + !(ancestorMeta != null && + ancestorMeta.isSeason && + ancestorMeta.ratingKey == metadata.parentRatingKey)) { menuActions.add( _MenuAction(value: 'season', icon: Symbols.playlist_play_rounded, label: t.mediaMenu.goToSeason), ); @@ -464,7 +465,7 @@ class MediaContextMenuState extends State { await _navigateToRelated( context, metadata!.parentRatingKey, - (metadata) => SeasonDetailScreen(season: metadata), + (metadata) => MediaDetailScreen(metadata: metadata), t.messages.errorLoadingSeason, ); break;