Files
plezy/lib/utils/media_navigation_helper.dart
T
edde746 551be5bbef feat: dynamic library tabs and shared library navigation
- Replace hardcoded 4-tab system with data-driven visible tab list
- Shared libraries show only Browse + Playlists tabs
- Fix dpad navigation for shared libraries (no hidden tab targets)
- Switch to TickerProviderStateMixin to support tab controller recreation
- Navigate to library screen when tapping shared library sections
- Persist tab selection by name instead of index
2026-03-15 21:36:17 +01:00

158 lines
5.8 KiB
Dart

import 'package:flutter/material.dart';
import '../models/plex_metadata.dart';
import '../models/plex_playlist.dart';
import '../screens/collection_detail_screen.dart';
import '../screens/main_screen.dart';
import '../screens/media_detail_screen.dart';
import '../screens/playlist/playlist_detail_screen.dart';
import '../utils/global_key_utils.dart';
import 'video_player_navigation.dart';
/// Result of media navigation indicating what action was taken
enum MediaNavigationResult {
/// Navigation completed successfully
navigated,
/// Navigation completed, parent list should be refreshed (e.g., collection deleted)
listRefreshNeeded,
/// Item type not supported (e.g., music content)
unsupported,
/// Item is a library section — navigated to that library
librarySelected,
}
/// Navigates to the appropriate screen based on the item type.
///
/// For episodes, starts playback directly via video player.
/// For movies, starts playback directly if [playDirectly] is true, otherwise
/// navigates to media detail screen.
/// For seasons, navigates to season detail screen.
/// For playlists, navigates to playlist detail screen.
/// For collections, navigates to collection detail screen.
/// For other types (shows), navigates to media detail screen.
/// For music types (artist, album, track), returns [MediaNavigationResult.unsupported].
///
/// The [onRefresh] callback is invoked with the item's ratingKey after
/// returning from the detail screen, allowing the caller to refresh state.
///
/// Set [isOffline] to true for downloaded content without server access.
///
/// Set [playDirectly] to true to play movies immediately (e.g., from continue watching).
///
/// Returns a [MediaNavigationResult] indicating what action was taken:
/// - [MediaNavigationResult.navigated]: Navigation completed, item refresh handled
/// - [MediaNavigationResult.listRefreshNeeded]: Caller should refresh entire list
/// - [MediaNavigationResult.unsupported]: Item type not supported, caller should handle
Future<MediaNavigationResult> navigateToMediaItem(
BuildContext context,
dynamic item, {
void Function(String)? onRefresh,
bool isOffline = false,
bool playDirectly = false,
}) async {
// Handle playlists
if (item is PlexPlaylist) {
await Navigator.push(context, MaterialPageRoute(builder: (context) => PlaylistDetailScreen(playlist: item)));
return MediaNavigationResult.navigated;
}
final metadata = item as PlexMetadata;
// Handle library section items (shared whole-library entries)
if (metadata.isLibrarySection) {
final sectionKey = metadata.librarySectionKey;
if (sectionKey != null && metadata.serverId != null) {
final libraryGlobalKey = buildGlobalKey(metadata.serverId!, sectionKey);
MainScreenFocusScope.of(context)?.selectLibrary?.call(libraryGlobalKey);
return MediaNavigationResult.librarySelected;
}
return MediaNavigationResult.unsupported;
}
switch (metadata.mediaType) {
case PlexMediaType.collection:
final result = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (context) => CollectionDetailScreen(collection: metadata)),
);
// If collection was deleted, signal that list refresh is needed
if (result == true) {
return MediaNavigationResult.listRefreshNeeded;
}
return MediaNavigationResult.navigated;
case PlexMediaType.artist:
case PlexMediaType.album:
case PlexMediaType.track:
// Music types not supported
return MediaNavigationResult.unsupported;
case PlexMediaType.clip:
case PlexMediaType.episode:
// For episodes and clips (trailers/extras), start playback directly
final result = await navigateToVideoPlayer(context, metadata: metadata, isOffline: isOffline);
if (result == true) {
onRefresh?.call(metadata.ratingKey);
}
return MediaNavigationResult.navigated;
case PlexMediaType.movie:
if (playDirectly) {
// For movies in continue watching, start playback directly
final result = await navigateToVideoPlayer(context, metadata: metadata, isOffline: isOffline);
if (result == true) {
onRefresh?.call(metadata.ratingKey);
}
return MediaNavigationResult.navigated;
}
// Fall through to default case for detail screen
continue defaultCase;
case PlexMediaType.season:
// Navigate to the parent show with the season tab pre-selected
if (metadata.parentRatingKey != null) {
final showStub = PlexMetadata(
ratingKey: metadata.parentRatingKey!,
key: '/library/metadata/${metadata.parentRatingKey}',
type: 'show',
title: metadata.grandparentTitle ?? metadata.parentTitle ?? metadata.displayTitle,
thumb: metadata.grandparentThumb ?? metadata.parentThumb,
art: metadata.grandparentArt,
serverId: metadata.serverId,
serverName: metadata.serverName,
);
final result = await Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (context) => MediaDetailScreen(
metadata: showStub,
isOffline: isOffline,
initialSeasonIndex: metadata.index,
),
),
);
if (result == true) {
onRefresh?.call(metadata.ratingKey);
}
return MediaNavigationResult.navigated;
}
continue defaultCase;
defaultCase:
default:
// For all other types (shows, movies), show detail screen
final result = await Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (context) => MediaDetailScreen(metadata: metadata, isOffline: isOffline),
),
);
if (result == true) {
onRefresh?.call(metadata.ratingKey);
}
return MediaNavigationResult.navigated;
}
}