refactor: remove player navigation animation

This commit is contained in:
edde746
2025-11-01 09:28:47 +01:00
parent a81cf3b18f
commit 127500329b
8 changed files with 142 additions and 97 deletions
+2 -18
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:io' show Platform;
import 'package:media_kit/media_kit.dart';
import 'package:window_manager/window_manager.dart';
@@ -17,7 +16,7 @@ import 'providers/theme_provider.dart';
import 'utils/language_codes.dart';
import 'utils/app_logger.dart';
import 'utils/provider_extensions.dart';
import 'utils/platform_detector.dart';
import 'utils/orientation_helper.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -96,22 +95,7 @@ class _OrientationAwareSetupState extends State<OrientationAwareSetup> {
}
void _setOrientationPreferences() {
// Only lock orientation to portrait for phones
// Allow all orientations for tablets and desktop
if (PlatformDetector.isPhone(context)) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
} else {
// For tablets and desktop, allow all orientations
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
OrientationHelper.restoreDefaultOrientations(context);
}
@override
+7 -15
View File
@@ -18,7 +18,7 @@ import '../mixins/refreshable.dart';
import '../mixins/item_updatable.dart';
import '../utils/app_logger.dart';
import '../utils/provider_extensions.dart';
import 'video_player_screen.dart';
import '../utils/video_player_navigation.dart';
import 'auth_screen.dart';
class DiscoverScreen extends StatefulWidget {
@@ -569,14 +569,10 @@ class _DiscoverScreenState extends State<DiscoverScreen>
if (client == null) return;
appLogger.d('Navigating to VideoPlayerScreen for: ${heroItem.title}');
Navigator.push<bool>(
navigateToVideoPlayer(
context,
MaterialPageRoute(
builder: (context) => VideoPlayerScreen(
metadata: heroItem,
userProfile: widget.userProfile,
),
),
metadata: heroItem,
userProfile: widget.userProfile,
);
},
child: Container(
@@ -902,14 +898,10 @@ class _DiscoverScreenState extends State<DiscoverScreen>
if (client == null) return;
appLogger.d('Playing: ${heroItem.title}');
Navigator.push<bool>(
navigateToVideoPlayer(
context,
MaterialPageRoute(
builder: (context) => VideoPlayerScreen(
metadata: heroItem,
userProfile: widget.userProfile,
),
),
metadata: heroItem,
userProfile: widget.userProfile,
);
},
borderRadius: BorderRadius.circular(24),
+10 -22
View File
@@ -9,9 +9,9 @@ import '../widgets/app_bar_back_button.dart';
import '../widgets/media_context_menu.dart';
import '../utils/app_logger.dart';
import '../utils/provider_extensions.dart';
import '../utils/video_player_navigation.dart';
import '../theme/theme_helper.dart';
import 'season_detail_screen.dart';
import 'video_player_screen.dart';
class MediaDetailScreen extends StatefulWidget {
final PlexMetadata metadata;
@@ -214,14 +214,10 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
if (client == null) return;
appLogger.d('Playing first episode: ${firstEpisode.title}');
await Navigator.push(
await navigateToVideoPlayer(
context,
MaterialPageRoute(
builder: (context) => VideoPlayerScreen(
metadata: firstEpisode,
userProfile: widget.userProfile,
),
),
metadata: firstEpisode,
userProfile: widget.userProfile,
);
appLogger.d('Returned from playback, refreshing metadata');
// Refresh metadata when returning from video player
@@ -542,14 +538,10 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
appLogger.d(
'Playing on deck episode: ${_onDeckEpisode!.title}',
);
await Navigator.push(
await navigateToVideoPlayer(
context,
MaterialPageRoute(
builder: (context) => VideoPlayerScreen(
metadata: _onDeckEpisode!,
userProfile: widget.userProfile,
),
),
metadata: _onDeckEpisode!,
userProfile: widget.userProfile,
);
appLogger.d(
'Returned from playback, refreshing metadata',
@@ -567,14 +559,10 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
appLogger.d('Playing: ${metadata.title}');
// For movies or episodes, play directly
await Navigator.push(
await navigateToVideoPlayer(
context,
MaterialPageRoute(
builder: (context) => VideoPlayerScreen(
metadata: metadata,
userProfile: widget.userProfile,
),
),
metadata: metadata,
userProfile: widget.userProfile,
);
appLogger.d(
'Returned from playback, refreshing metadata',
+4 -8
View File
@@ -6,11 +6,11 @@ import '../models/plex_metadata.dart';
import '../models/plex_user_profile.dart';
import '../providers/plex_client_provider.dart';
import '../utils/provider_extensions.dart';
import '../utils/video_player_navigation.dart';
import '../widgets/desktop_app_bar.dart';
import '../widgets/media_context_menu.dart';
import '../mixins/item_updatable.dart';
import '../theme/theme_helper.dart';
import 'video_player_screen.dart';
class SeasonDetailScreen extends StatefulWidget {
final PlexMetadata season;
@@ -136,14 +136,10 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen>
metadata: episode,
onRefresh: updateItem,
onTap: () async {
await Navigator.push(
await navigateToVideoPlayer(
context,
MaterialPageRoute(
builder: (context) => VideoPlayerScreen(
metadata: episode,
userProfile: widget.userProfile,
),
),
metadata: episode,
userProfile: widget.userProfile,
);
// Refresh episodes when returning from video player
_loadEpisodes();
+14 -26
View File
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import '../models/plex_metadata.dart';
@@ -11,6 +10,8 @@ import '../widgets/plex_video_controls.dart';
import '../utils/language_codes.dart';
import '../utils/app_logger.dart';
import '../services/settings_service.dart';
import '../utils/orientation_helper.dart';
import '../utils/video_player_navigation.dart';
class VideoPlayerScreen extends StatefulWidget {
final PlexMetadata metadata;
@@ -83,11 +84,7 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
}
void _setLandscapeOrientation() {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
OrientationHelper.setLandscapeOrientation();
}
Future<void> _initializePlayer() async {
@@ -208,13 +205,9 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
// Send final stopped state
_sendProgress('stopped');
// Restore system UI
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
// Restore portrait-only orientation
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
// Restore system UI and orientation preferences
OrientationHelper.restoreSystemUI();
OrientationHelper.restoreDefaultOrientations(context);
player.dispose();
super.dispose();
@@ -678,19 +671,14 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
// Navigate to the episode using pushReplacement to destroy current player
if (mounted) {
final currentRate = player.state.rate;
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
VideoPlayerScreen(
metadata: episodeMetadata,
preferredAudioTrack: currentAudioTrack,
preferredSubtitleTrack: currentSubtitleTrack,
preferredPlaybackRate: currentRate,
userProfile: widget.userProfile,
),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
),
navigateToVideoPlayer(
context,
metadata: episodeMetadata,
preferredAudioTrack: currentAudioTrack,
preferredSubtitleTrack: currentSubtitleTrack,
preferredPlaybackRate: currentRate,
userProfile: widget.userProfile,
usePushReplacement: true,
);
}
}
+50
View File
@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'platform_detector.dart';
/// Helper class for managing device orientation preferences across the app.
class OrientationHelper {
/// Restores default orientation preferences based on device type.
///
/// For phones: Locks to portrait-only (up and down)
/// For tablets/desktop: Allows all orientations
///
/// This should be called when leaving full-screen experiences like
/// the video player to restore the app's default orientation behavior.
static void restoreDefaultOrientations(BuildContext context) {
final isPhone = PlatformDetector.isPhone(context);
if (isPhone) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
} else {
// For tablets and desktop, allow all orientations
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
}
/// Sets orientation to landscape-only mode.
///
/// Used by the video player to force landscape orientation during playback.
static void setLandscapeOrientation() {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
/// Restores edge-to-edge system UI mode.
///
/// Should be called when exiting full-screen mode.
static void restoreSystemUI() {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
}
+51
View File
@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import '../models/plex_metadata.dart';
import '../models/plex_user_profile.dart';
import '../screens/video_player_screen.dart';
/// 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
/// - [userProfile]: Optional user profile for track selection preferences
/// - [preferredAudioTrack]: Optional audio track to select on playback start
/// - [preferredSubtitleTrack]: Optional subtitle track to select on playback start
/// - [preferredPlaybackRate]: Optional playback speed to set on playback start
/// - [usePushReplacement]: If true, replaces current route instead of pushing;
/// useful for episode-to-episode navigation. Defaults to false.
///
/// 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,
PlexUserProfile? userProfile,
AudioTrack? preferredAudioTrack,
SubtitleTrack? preferredSubtitleTrack,
double? preferredPlaybackRate,
bool usePushReplacement = false,
}) async {
final route = PageRouteBuilder<bool>(
pageBuilder: (context, animation, secondaryAnimation) => VideoPlayerScreen(
metadata: metadata,
userProfile: userProfile,
preferredAudioTrack: preferredAudioTrack,
preferredSubtitleTrack: preferredSubtitleTrack,
preferredPlaybackRate: preferredPlaybackRate,
),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
);
if (usePushReplacement) {
return Navigator.of(context).pushReplacement<bool, bool>(route);
} else {
return Navigator.push<bool>(context, route);
}
}
+4 -8
View File
@@ -5,9 +5,9 @@ import '../models/plex_metadata.dart';
import '../models/plex_user_profile.dart';
import '../providers/plex_client_provider.dart';
import '../utils/provider_extensions.dart';
import '../utils/video_player_navigation.dart';
import '../screens/media_detail_screen.dart';
import '../screens/season_detail_screen.dart';
import '../screens/video_player_screen.dart';
import '../theme/theme_helper.dart';
import 'media_context_menu.dart';
@@ -44,14 +44,10 @@ class _MediaCardState extends State<MediaCard>
// For episodes, start playback directly
if (itemType == 'episode') {
final result = await Navigator.push<bool>(
final result = await navigateToVideoPlayer(
context,
MaterialPageRoute(
builder: (context) => VideoPlayerScreen(
metadata: widget.item,
userProfile: widget.userProfile,
),
),
metadata: widget.item,
userProfile: widget.userProfile,
);
// Refresh parent screen if result indicates it's needed
if (result == true) {