feat: playback speed controls

This commit is contained in:
edde746
2025-10-28 07:59:17 +01:00
parent b8a5afc08f
commit b3696035b5
2 changed files with 103 additions and 0 deletions
+14
View File
@@ -15,6 +15,7 @@ class VideoPlayerScreen extends StatefulWidget {
final PlexMetadata metadata;
final AudioTrack? preferredAudioTrack;
final SubtitleTrack? preferredSubtitleTrack;
final double? preferredPlaybackRate;
final PlexUserProfile? userProfile;
const VideoPlayerScreen({
@@ -23,6 +24,7 @@ class VideoPlayerScreen extends StatefulWidget {
required this.metadata,
this.preferredAudioTrack,
this.preferredSubtitleTrack,
this.preferredPlaybackRate,
this.userProfile,
});
@@ -545,6 +547,14 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
appLogger.i('Final subtitle selection: $finalSubtitle');
player.setSubtitleTrack(subtitleToSelect);
// Set playback rate if preferred rate was provided
if (widget.preferredPlaybackRate != null) {
appLogger.d(
'Setting preferred playback rate: ${widget.preferredPlaybackRate}x',
);
player.setRate(widget.preferredPlaybackRate!);
}
appLogger.d('Track selection complete');
}
@@ -621,6 +631,7 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
// Navigate to the next episode using pushReplacement to destroy current player
if (mounted) {
final currentRate = player.state.rate;
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => VideoPlayerScreen(
@@ -628,6 +639,7 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
metadata: _nextEpisode!,
preferredAudioTrack: currentAudioTrack,
preferredSubtitleTrack: currentSubtitleTrack,
preferredPlaybackRate: currentRate,
userProfile: widget.userProfile,
),
),
@@ -649,6 +661,7 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
// Navigate to the previous episode using pushReplacement to destroy current player
if (mounted) {
final currentRate = player.state.rate;
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => VideoPlayerScreen(
@@ -656,6 +669,7 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
metadata: _previousEpisode!,
preferredAudioTrack: currentAudioTrack,
preferredSubtitleTrack: currentSubtitleTrack,
preferredPlaybackRate: currentRate,
userProfile: widget.userProfile,
),
),
+89
View File
@@ -205,6 +205,10 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
return Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.speed, color: Colors.white),
onPressed: _showPlaybackSpeedBottomSheet,
),
if (_hasMultipleAudioTracks(tracks))
IconButton(
icon: const Icon(Icons.audiotrack, color: Colors.white),
@@ -1578,6 +1582,91 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
);
}
void _showPlaybackSpeedBottomSheet() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.grey[900],
isScrollControlled: true,
constraints: _getBottomSheetConstraints(),
builder: (context) => StreamBuilder<double>(
stream: widget.player.stream.rate,
initialData: widget.player.state.rate,
builder: (context, snapshot) {
final currentRate = snapshot.data ?? 1.0;
// Define available playback speeds
final speeds = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0];
return SafeArea(
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.75,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Icon(Icons.speed, color: Colors.white),
const SizedBox(width: 12),
const Text(
'Playback Speed',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
],
),
),
const Divider(color: Colors.white24, height: 1),
Expanded(
child: ListView.builder(
itemCount: speeds.length,
itemBuilder: (context, index) {
final speed = speeds[index];
final isSelected = (currentRate - speed).abs() < 0.01;
// Format speed label
final label = speed == 1.0
? 'Normal'
: '${speed.toStringAsFixed(2)}x';
return ListTile(
title: Text(
label,
style: TextStyle(
color: isSelected ? Colors.blue : Colors.white,
),
),
trailing: isSelected
? const Icon(
Icons.check,
color: Colors.blue,
)
: null,
onTap: () {
widget.player.setRate(speed);
Navigator.pop(context);
},
);
},
),
),
],
),
),
);
},
),
);
}
String _formatDuration(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);