Files
plezy/lib/utils/video_player_navigation.dart
T

174 lines
6.6 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
2025-11-27 07:32:07 +01:00
import 'package:provider/provider.dart';
2025-11-27 07:32:07 +01:00
import '../mpv/mpv.dart';
import '../models/plex_metadata.dart';
import '../models/plex_video_playback_data.dart';
import '../providers/download_provider.dart';
import '../screens/video_player_screen.dart';
2026-02-11 14:59:22 +01:00
import '../services/external_player_service.dart';
2025-11-04 10:28:37 +01:00
import '../services/settings_service.dart';
2026-02-11 14:59:22 +01:00
import '../utils/provider_extensions.dart';
2025-12-14 21:03:11 +01:00
import 'app_logger.dart';
2025-12-18 08:42:18 +01:00
const String kVideoPlayerRouteName = '/video_player';
/// Navigates to the VideoPlayerScreen with instant transitions to prevent white flash.
///
/// This utility function provides a consistent way to navigate to the video player
/// across the app, using PageRouteBuilder with zero-duration transitions to eliminate
/// the white flash that occurs with MaterialPageRoute.
///
/// Parameters:
/// - [context]: The build context for navigation
/// - [metadata]: The Plex metadata for the content to play
/// - [preferredAudioTrack]: Optional audio track to select on playback start
/// - [preferredSubtitleTrack]: Optional subtitle track to select on playback start
2025-11-04 10:28:37 +01:00
/// - [selectedMediaIndex]: Optional media version index to use; if not provided,
/// loads the saved preference for the series/movie. Defaults to 0 if no preference exists.
/// - [usePushReplacement]: If true, replaces current route instead of pushing;
/// useful for episode-to-episode navigation. Defaults to false.
2025-12-11 19:15:06 +01:00
/// - [isOffline]: If true, plays from downloaded content without requiring server connection.
///
/// Returns a Future that completes with a boolean indicating whether the content
/// was watched, or null if navigation was cancelled.
Future<bool?> navigateToVideoPlayer(
BuildContext context, {
required PlexMetadata metadata,
2025-12-02 16:53:54 +01:00
AudioTrack? preferredAudioTrack,
SubtitleTrack? preferredSubtitleTrack,
2025-11-04 10:28:37 +01:00
int? selectedMediaIndex,
bool usePushReplacement = false,
2025-12-11 19:15:06 +01:00
bool isOffline = false,
PlexVideoPlaybackData? playbackData,
}) async {
2026-02-23 13:48:23 +01:00
// Extract context-dependent values before any async operations
2025-11-05 11:03:35 +01:00
final navigator = Navigator.of(context);
2026-02-23 13:48:23 +01:00
final downloadProvider = context.read<DownloadProvider>();
final client = isOffline ? null : context.getClientForMetadata(metadata);
2025-11-05 11:03:35 +01:00
2025-11-04 10:28:37 +01:00
// Load saved media version preference if not explicitly provided
int mediaIndex = selectedMediaIndex ?? 0;
var effectivePlaybackData = playbackData;
2025-11-04 10:28:37 +01:00
if (selectedMediaIndex == null) {
try {
final settingsService = await SettingsService.getInstance();
final seriesKey = metadata.grandparentRatingKey ?? metadata.ratingKey;
2025-12-15 02:29:03 +01:00
final savedPreference = settingsService.getMediaVersionPreference(seriesKey);
2025-11-04 10:28:37 +01:00
if (savedPreference != null) {
mediaIndex = savedPreference;
// Pre-parsed playbackData was built with mediaIndex=0; invalidate if
// the resolved index differs so the player re-fetches with the correct one
if (savedPreference != 0) {
effectivePlaybackData = null;
}
2025-11-04 10:28:37 +01:00
}
} catch (e) {
// Ignore errors loading preference, use default
}
}
// Check if external player is enabled
try {
final settingsService = await SettingsService.getInstance();
if (settingsService.getUseExternalPlayer()) {
bool launched = false;
if (isOffline) {
// Offline mode: resolve local file path for the external player
final globalKey = metadata.globalKey;
final videoPath = await downloadProvider.getVideoFilePath(globalKey);
2026-02-23 13:48:23 +01:00
if (videoPath != null && context.mounted) {
final videoUrl = videoPath.contains('://') ? videoPath : 'file://$videoPath';
launched = await ExternalPlayerService.launch(context: context, videoUrl: videoUrl);
}
2026-02-23 13:48:23 +01:00
} else if (context.mounted) {
launched = await ExternalPlayerService.launch(
2026-02-11 14:59:22 +01:00
context: context,
metadata: metadata,
2026-02-23 13:48:23 +01:00
client: client!,
2026-02-11 14:59:22 +01:00
mediaIndex: mediaIndex,
);
}
if (launched) return null;
// Fall through to built-in player on failure
2026-02-11 14:59:22 +01:00
}
} catch (e) {
appLogger.w('External player launch failed, falling back to built-in player', error: e);
2026-02-11 14:59:22 +01:00
}
2025-12-18 08:42:18 +01:00
// Prevent stacking an identical video player when already active
if (!usePushReplacement &&
VideoPlayerScreenState.activeRatingKey == metadata.ratingKey &&
VideoPlayerScreenState.activeMediaIndex == mediaIndex) {
appLogger.d(
'Video player already active for ${metadata.ratingKey} (mediaIndex=$mediaIndex), skipping duplicate navigation',
);
return null;
}
final route = PageRouteBuilder<bool>(
2025-12-18 08:42:18 +01:00
settings: const RouteSettings(name: kVideoPlayerRouteName),
pageBuilder: (context, animation, secondaryAnimation) => VideoPlayerScreen(
metadata: metadata,
preferredAudioTrack: preferredAudioTrack,
preferredSubtitleTrack: preferredSubtitleTrack,
2025-11-04 10:28:37 +01:00
selectedMediaIndex: mediaIndex,
2025-12-11 19:15:06 +01:00
isOffline: isOffline,
playbackData: effectivePlaybackData,
),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
);
2026-02-15 03:14:04 +01:00
return usePushReplacement ? navigator.pushReplacement<bool, bool>(route) : navigator.push<bool>(route);
}
2025-12-14 21:03:11 +01:00
/// Navigates to the video player and optionally refreshes content when returning.
///
/// This helper consolidates the common pattern of:
/// 1. Navigating to the video player
/// 2. Logging the return
/// 3. Calling a refresh callback if not offline
///
/// Parameters:
/// - [context]: The build context for navigation
/// - [metadata]: The Plex metadata for the content to play
/// - [isOffline]: If true, plays from downloaded content
/// - [onRefresh]: Optional callback to refresh data when returning from playback
/// (only called when not offline)
/// - All other parameters are passed through to [navigateToVideoPlayer]
Future<bool?> navigateToVideoPlayerWithRefresh(
BuildContext context, {
required PlexMetadata metadata,
bool isOffline = false,
VoidCallback? onRefresh,
AudioTrack? preferredAudioTrack,
SubtitleTrack? preferredSubtitleTrack,
int? selectedMediaIndex,
bool usePushReplacement = false,
PlexVideoPlaybackData? playbackData,
2025-12-14 21:03:11 +01:00
}) async {
final result = await navigateToVideoPlayer(
context,
metadata: metadata,
isOffline: isOffline,
preferredAudioTrack: preferredAudioTrack,
preferredSubtitleTrack: preferredSubtitleTrack,
selectedMediaIndex: selectedMediaIndex,
usePushReplacement: usePushReplacement,
playbackData: playbackData,
2025-12-14 21:03:11 +01:00
);
appLogger.d('Returned from playback, refreshing metadata');
// Refresh data when returning from video player (skip if offline)
if (!isOffline && onRefresh != null) {
onRefresh();
}
return result;
}