refactor: remove player navigation animation
This commit is contained in:
+2
-18
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user