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.
This commit is contained in:
edde746
2026-03-11 23:08:39 +01:00
parent 7febfa9771
commit 5538e0627b
5 changed files with 145 additions and 361 deletions
+131 -28
View File
@@ -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<MediaDetailScreen>
@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<String>? get watchedRatingKeys {
final keys = <String>{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<MediaDetailScreen>
if (serverId == null) return null;
final keys = <String>{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<String>? get deletionRatingKeys {
final keys = <String>{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<MediaDetailScreen>
if (serverId == null) return null;
final keys = <String>{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<MediaDetailScreen>
.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<void> _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<MediaDetailScreen>
// 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<MediaDetailScreen>
_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<MediaDetailScreen>
// 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<MediaDetailScreen>
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<MediaDetailScreen>
if (widget.metadata.isShow) {
_loadSeasons();
} else if (widget.metadata.isSeason) {
_seasons = [widget.metadata];
_showEpisodesDirectly = true;
_fetchAllEpisodes();
}
}
}
@@ -1136,6 +1226,19 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
}
}
/// Load episodes from downloaded content for a season
void _loadEpisodesFromDownloads() {
final downloadProvider = context.read<DownloadProvider>();
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<void> _loadExtras() async {
// Only load extras for movies and shows
@@ -1174,7 +1277,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
final watchStateChanged = await Navigator.push<bool>(
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<MediaDetailScreen>
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<MediaDetailScreen>
// 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<MediaDetailScreen>
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<MediaDetailScreen>
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<MediaDetailScreen>
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<MediaDetailScreen>
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<MediaDetailScreen>
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,
+1 -9
View File
@@ -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<MediaNavigationResult> 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:
@@ -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<SeasonDetailScreen> createState() => _SeasonDetailScreenState();
}
class _SeasonDetailScreenState extends State<SeasonDetailScreen>
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<PlexMetadata> _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<String>? get watchedRatingKeys => _episodes.map((e) => e.ratingKey).toSet();
@override
String? get watchStateServerId => serverBoundServerId;
@override
Set<String>? 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<String>? get deletionRatingKeys {
final keys = _episodes.map((e) => e.ratingKey).toSet();
keys.add(widget.season.ratingKey);
return keys;
}
@override
String? get deletionServerId => serverBoundServerId;
@override
Set<String>? 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<void> _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<DownloadProvider>();
// 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<void> 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<DownloadProvider>();
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 {
+1 -2
View File
@@ -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.
+11 -10
View File
@@ -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<MediaContextMenu> {
);
}
// 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<MediaDetailScreen>();
final ancestorSeasonDetail = context.findAncestorWidgetOfExactType<SeasonDetailScreen>();
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<MediaContextMenu> {
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<SeasonDetailScreen>()?.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<MediaContextMenu> {
await _navigateToRelated(
context,
metadata!.parentRatingKey,
(metadata) => SeasonDetailScreen(season: metadata),
(metadata) => MediaDetailScreen(metadata: metadata),
t.messages.errorLoadingSeason,
);
break;