feat: persist playback speed

close #237
This commit is contained in:
edde746
2026-01-15 00:16:17 +01:00
parent 09d79417fb
commit 045502405d
7 changed files with 31 additions and 21 deletions
+2 -5
View File
@@ -47,7 +47,6 @@ class VideoPlayerScreen extends StatefulWidget {
final PlexMetadata metadata; final PlexMetadata metadata;
final AudioTrack? preferredAudioTrack; final AudioTrack? preferredAudioTrack;
final SubtitleTrack? preferredSubtitleTrack; final SubtitleTrack? preferredSubtitleTrack;
final double? preferredPlaybackRate;
final int selectedMediaIndex; final int selectedMediaIndex;
final bool isOffline; final bool isOffline;
@@ -56,7 +55,6 @@ class VideoPlayerScreen extends StatefulWidget {
required this.metadata, required this.metadata,
this.preferredAudioTrack, this.preferredAudioTrack,
this.preferredSubtitleTrack, this.preferredSubtitleTrack,
this.preferredPlaybackRate,
this.selectedMediaIndex = 0, this.selectedMediaIndex = 0,
this.isOffline = false, this.isOffline = false,
}); });
@@ -1216,6 +1214,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
if (!mounted || player == null) return; if (!mounted || player == null) return;
final profileSettings = context.read<UserProfileProvider>().profileSettings; final profileSettings = context.read<UserProfileProvider>().profileSettings;
final settingsService = await SettingsService.getInstance();
final trackService = TrackSelectionService( final trackService = TrackSelectionService(
player: player!, player: player!,
profileSettings: profileSettings, profileSettings: profileSettings,
@@ -1226,7 +1225,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
await trackService.selectAndApplyTracks( await trackService.selectAndApplyTracks(
preferredAudioTrack: widget.preferredAudioTrack, preferredAudioTrack: widget.preferredAudioTrack,
preferredSubtitleTrack: widget.preferredSubtitleTrack, preferredSubtitleTrack: widget.preferredSubtitleTrack,
preferredPlaybackRate: widget.preferredPlaybackRate, defaultPlaybackSpeed: settingsService.getDefaultPlaybackSpeed(),
onAudioTrackChanged: _onAudioTrackChanged, onAudioTrackChanged: _onAudioTrackChanged,
onSubtitleTrackChanged: _onSubtitleTrackChanged, onSubtitleTrackChanged: _onSubtitleTrackChanged,
); );
@@ -1462,7 +1461,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
final currentAudioTrack = currentPlayer.state.track.audio; final currentAudioTrack = currentPlayer.state.track.audio;
final currentSubtitleTrack = currentPlayer.state.track.subtitle; final currentSubtitleTrack = currentPlayer.state.track.subtitle;
final currentRate = currentPlayer.state.rate;
// Pause and stop current playback // Pause and stop current playback
currentPlayer.pause(); currentPlayer.pause();
@@ -1479,7 +1477,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
metadata: episodeMetadata, metadata: episodeMetadata,
preferredAudioTrack: currentAudioTrack, preferredAudioTrack: currentAudioTrack,
preferredSubtitleTrack: currentSubtitleTrack, preferredSubtitleTrack: currentSubtitleTrack,
preferredPlaybackRate: currentRate,
usePushReplacement: true, usePushReplacement: true,
); );
} }
@@ -97,7 +97,6 @@ class EpisodeNavigationService {
metadata: episode, metadata: episode,
preferredAudioTrack: currentAudioTrack, preferredAudioTrack: currentAudioTrack,
preferredSubtitleTrack: currentSubtitleTrack, preferredSubtitleTrack: currentSubtitleTrack,
preferredPlaybackRate: currentPlaybackRate,
usePushReplacement: usePushReplacement, usePushReplacement: usePushReplacement,
); );
} }
+7 -4
View File
@@ -305,15 +305,18 @@ class KeyboardShortcutsService {
onPreviousChapter?.call(); onPreviousChapter?.call();
break; break;
case 'speed_increase': case 'speed_increase':
final newRate = (player.state.rate + 0.1).clamp(0.1, 3.0); final newRateUp = (player.state.rate + 0.1).clamp(0.1, 3.0);
player.setRate(newRate); player.setRate(newRateUp);
_settingsService.setDefaultPlaybackSpeed(newRateUp);
break; break;
case 'speed_decrease': case 'speed_decrease':
final newRate = (player.state.rate - 0.1).clamp(0.1, 3.0); final newRateDown = (player.state.rate - 0.1).clamp(0.1, 3.0);
player.setRate(newRate); player.setRate(newRateDown);
_settingsService.setDefaultPlaybackSpeed(newRateDown);
break; break;
case 'speed_reset': case 'speed_reset':
player.setRate(1.0); player.setRate(1.0);
_settingsService.setDefaultPlaybackSpeed(1.0);
break; break;
} }
} }
+11
View File
@@ -61,6 +61,7 @@ class SettingsService extends BaseSharedPreferencesService {
static const String _keyMaxVolume = 'max_volume'; static const String _keyMaxVolume = 'max_volume';
static const String _keyEnableDiscordRPC = 'enable_discord_rpc'; static const String _keyEnableDiscordRPC = 'enable_discord_rpc';
static const String _keyMatchContentFrameRate = 'match_content_frame_rate'; static const String _keyMatchContentFrameRate = 'match_content_frame_rate';
static const String _keyDefaultPlaybackSpeed = 'default_playback_speed';
SettingsService._(); SettingsService._();
@@ -947,6 +948,15 @@ class SettingsService extends BaseSharedPreferencesService {
return prefs.getBool(_keyMatchContentFrameRate) ?? false; // Default disabled return prefs.getBool(_keyMatchContentFrameRate) ?? false; // Default disabled
} }
// Default Playback Speed (0.5 to 3.0)
Future<void> setDefaultPlaybackSpeed(double speed) async {
await prefs.setDouble(_keyDefaultPlaybackSpeed, speed.clamp(0.5, 3.0));
}
double getDefaultPlaybackSpeed() {
return prefs.getDouble(_keyDefaultPlaybackSpeed) ?? 1.0; // Default: normal speed
}
// Reset all settings to defaults // Reset all settings to defaults
Future<void> resetAllSettings() async { Future<void> resetAllSettings() async {
await Future.wait([ await Future.wait([
@@ -989,6 +999,7 @@ class SettingsService extends BaseSharedPreferencesService {
prefs.remove(_keyMpvConfigPresets), prefs.remove(_keyMpvConfigPresets),
prefs.remove(_keyEnableDiscordRPC), prefs.remove(_keyEnableDiscordRPC),
prefs.remove(_keyMatchContentFrameRate), prefs.remove(_keyMatchContentFrameRate),
prefs.remove(_keyDefaultPlaybackSpeed),
]); ]);
} }
+4 -4
View File
@@ -446,7 +446,7 @@ class TrackSelectionService {
Future<void> selectAndApplyTracks({ Future<void> selectAndApplyTracks({
AudioTrack? preferredAudioTrack, AudioTrack? preferredAudioTrack,
SubtitleTrack? preferredSubtitleTrack, SubtitleTrack? preferredSubtitleTrack,
double? preferredPlaybackRate, double? defaultPlaybackSpeed,
Function(AudioTrack)? onAudioTrackChanged, Function(AudioTrack)? onAudioTrackChanged,
Function(SubtitleTrack)? onSubtitleTrackChanged, Function(SubtitleTrack)? onSubtitleTrackChanged,
}) async { }) async {
@@ -491,9 +491,9 @@ class TrackSelectionService {
onSubtitleTrackChanged(selectedSubtitleTrack); onSubtitleTrackChanged(selectedSubtitleTrack);
} }
// Set playback rate if preferred rate was provided // Apply default playback speed from settings
if (preferredPlaybackRate != null) { if (defaultPlaybackSpeed != null && defaultPlaybackSpeed != 1.0) {
player.setRate(preferredPlaybackRate); player.setRate(defaultPlaybackSpeed);
} }
} }
} }
-5
View File
@@ -19,7 +19,6 @@ const String kVideoPlayerRouteName = '/video_player';
/// - [metadata]: The Plex metadata for the content to play /// - [metadata]: The Plex metadata for the content to play
/// - [preferredAudioTrack]: Optional audio track to select on playback start /// - [preferredAudioTrack]: Optional audio track to select on playback start
/// - [preferredSubtitleTrack]: Optional subtitle track to select on playback start /// - [preferredSubtitleTrack]: Optional subtitle track to select on playback start
/// - [preferredPlaybackRate]: Optional playback speed to set on playback start
/// - [selectedMediaIndex]: Optional media version index to use; if not provided, /// - [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. /// loads the saved preference for the series/movie. Defaults to 0 if no preference exists.
/// - [usePushReplacement]: If true, replaces current route instead of pushing; /// - [usePushReplacement]: If true, replaces current route instead of pushing;
@@ -33,7 +32,6 @@ Future<bool?> navigateToVideoPlayer(
required PlexMetadata metadata, required PlexMetadata metadata,
AudioTrack? preferredAudioTrack, AudioTrack? preferredAudioTrack,
SubtitleTrack? preferredSubtitleTrack, SubtitleTrack? preferredSubtitleTrack,
double? preferredPlaybackRate,
int? selectedMediaIndex, int? selectedMediaIndex,
bool usePushReplacement = false, bool usePushReplacement = false,
bool isOffline = false, bool isOffline = false,
@@ -72,7 +70,6 @@ Future<bool?> navigateToVideoPlayer(
metadata: metadata, metadata: metadata,
preferredAudioTrack: preferredAudioTrack, preferredAudioTrack: preferredAudioTrack,
preferredSubtitleTrack: preferredSubtitleTrack, preferredSubtitleTrack: preferredSubtitleTrack,
preferredPlaybackRate: preferredPlaybackRate,
selectedMediaIndex: mediaIndex, selectedMediaIndex: mediaIndex,
isOffline: isOffline, isOffline: isOffline,
), ),
@@ -108,7 +105,6 @@ Future<bool?> navigateToVideoPlayerWithRefresh(
VoidCallback? onRefresh, VoidCallback? onRefresh,
AudioTrack? preferredAudioTrack, AudioTrack? preferredAudioTrack,
SubtitleTrack? preferredSubtitleTrack, SubtitleTrack? preferredSubtitleTrack,
double? preferredPlaybackRate,
int? selectedMediaIndex, int? selectedMediaIndex,
bool usePushReplacement = false, bool usePushReplacement = false,
}) async { }) async {
@@ -118,7 +114,6 @@ Future<bool?> navigateToVideoPlayerWithRefresh(
isOffline: isOffline, isOffline: isOffline,
preferredAudioTrack: preferredAudioTrack, preferredAudioTrack: preferredAudioTrack,
preferredSubtitleTrack: preferredSubtitleTrack, preferredSubtitleTrack: preferredSubtitleTrack,
preferredPlaybackRate: preferredPlaybackRate,
selectedMediaIndex: selectedMediaIndex, selectedMediaIndex: selectedMediaIndex,
usePushReplacement: usePushReplacement, usePushReplacement: usePushReplacement,
); );
@@ -338,9 +338,14 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
return ListTile( return ListTile(
title: Text(label, style: TextStyle(color: isSelected ? Colors.blue : Colors.white)), title: Text(label, style: TextStyle(color: isSelected ? Colors.blue : Colors.white)),
trailing: isSelected ? const AppIcon(Symbols.check_rounded, fill: 1, color: Colors.blue) : null, trailing: isSelected ? const AppIcon(Symbols.check_rounded, fill: 1, color: Colors.blue) : null,
onTap: () { onTap: () async {
widget.player.setRate(speed); widget.player.setRate(speed);
Navigator.pop(context); // Close sheet after selection // Save as default playback speed
final settings = await SettingsService.getInstance();
await settings.setDefaultPlaybackSpeed(speed);
if (context.mounted) {
Navigator.pop(context); // Close sheet after selection
}
}, },
); );
}, },