feat: playback speed controls
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user