feat: Playback rate keyboard shortcuts (#484)

This commit is contained in:
PartyDonut 2025-09-19 19:04:43 +02:00 committed by GitHub
commit d6863fe504
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 132 additions and 3 deletions

View file

@ -1303,6 +1303,8 @@
"mute": "Mute",
"volumeUp": "Volume Up",
"volumeDown": "Volume Down",
"speedUp": "Speed Up",
"speedDown": "Speed Down",
"nextVideo": "Next Video",
"prevVideo": "Previous Video",
"nextChapter": "Next Chapter",
@ -1326,5 +1328,13 @@
"type": "int"
}
}
},
"speedIndicator": "Playback rate: {speed}",
"@speedIndicator": {
"placeholders": {
"speed": {
"type": "double"
}
}
}
}

View file

@ -20,6 +20,8 @@ enum VideoHotKeys {
mute,
volumeUp,
volumeDown,
speedUp,
speedDown,
nextVideo,
prevVideo,
nextChapter,
@ -38,6 +40,8 @@ enum VideoHotKeys {
VideoHotKeys.mute => context.localized.mute,
VideoHotKeys.volumeUp => context.localized.volumeUp,
VideoHotKeys.volumeDown => context.localized.volumeDown,
VideoHotKeys.speedUp => context.localized.speedUp,
VideoHotKeys.speedDown => context.localized.speedDown,
VideoHotKeys.nextVideo => context.localized.nextVideo,
VideoHotKeys.prevVideo => context.localized.prevVideo,
VideoHotKeys.nextChapter => context.localized.nextChapter,
@ -180,6 +184,8 @@ Map<VideoHotKeys, KeyCombination> get _defaultVideoHotKeys => {
VideoHotKeys.mute => KeyCombination(key: LogicalKeyboardKey.keyM),
VideoHotKeys.volumeUp => KeyCombination(key: LogicalKeyboardKey.arrowUp),
VideoHotKeys.volumeDown => KeyCombination(key: LogicalKeyboardKey.arrowDown),
VideoHotKeys.speedUp => KeyCombination(key: LogicalKeyboardKey.arrowUp, modifier: LogicalKeyboardKey.controlLeft),
VideoHotKeys.speedDown => KeyCombination(key: LogicalKeyboardKey.arrowDown, modifier: LogicalKeyboardKey.controlLeft),
VideoHotKeys.prevVideo =>
KeyCombination(key: LogicalKeyboardKey.keyP, modifier: LogicalKeyboardKey.shiftLeft),
VideoHotKeys.nextVideo =>

View file

@ -138,6 +138,8 @@ const _$VideoHotKeysEnumMap = {
VideoHotKeys.mute: 'mute',
VideoHotKeys.volumeUp: 'volumeUp',
VideoHotKeys.volumeDown: 'volumeDown',
VideoHotKeys.speedUp: 'speedUp',
VideoHotKeys.speedDown: 'speedDown',
VideoHotKeys.nextVideo: 'nextVideo',
VideoHotKeys.prevVideo: 'prevVideo',
VideoHotKeys.nextChapter: 'nextChapter',

View file

@ -15,6 +15,8 @@ final videoPlayerSettingsProvider =
return VideoPlayerSettingsProviderNotifier(ref);
});
final playbackRateProvider = StateProvider<double>((ref) => 1.0);
class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSettingsModel> {
VideoPlayerSettingsProviderNotifier(this.ref) : super(VideoPlayerSettingsModel());
@ -67,11 +69,24 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
ref.read(videoPlayerProvider).setVolume(value);
}
void steppedSpeed(double i) {
var value = double.parse(
((ref.read(playbackRateProvider) + i).clamp(0.25, 3)).toStringAsFixed(2),
);
if ((value - 1.0).abs() <= 0.06) {
value = 1.0;
}
ref.read(playbackRateProvider.notifier).state = value;
ref.read(videoPlayerProvider).setSpeed(value);
}
void toggleOrientation(Set<DeviceOrientation>? orientation) =>
state = state.copyWith(allowedOrientations: orientation);
void setShortcuts(MapEntry<VideoHotKeys, KeyCombination> newEntry) {
state = state.copyWith(hotKeys: state.hotKeys.setOrRemove(newEntry, state.defaultShortCuts));
state = state.copyWith(hotKeys: state.hotKeys.setOrRemove(newEntry, state.defaultShortCuts));
}
void nextChapter() {

View file

@ -34,7 +34,6 @@ import 'package:fladder/widgets/shared/fladder_slider.dart';
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
import 'package:fladder/widgets/shared/spaced_list_tile.dart';
final playbackRateProvider = StateProvider<double>((ref) => 1.0);
Future<void> showVideoPlayerOptions(BuildContext context, Function() minimizePlayer) {
return showBottomSheetPill(

View file

@ -0,0 +1,87 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:async/async.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/util/localization_helper.dart';
class VideoPlayerSpeedIndicator extends ConsumerStatefulWidget {
const VideoPlayerSpeedIndicator({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _VideoPlayerSpeedIndicatorState();
}
class _VideoPlayerSpeedIndicatorState extends ConsumerState<VideoPlayerSpeedIndicator> {
late double currentSpeed = ref.read(playbackRateProvider);
bool showIndicator = false;
late final timer = RestartableTimer(const Duration(seconds: 1), () {
setState(() {
showIndicator = false;
});
});
@override
void dispose() {
showIndicator = false;
timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
ref.listen(
playbackRateProvider,
(previous, next) {
setState(() {
showIndicator = true;
currentSpeed = next;
});
timer.reset();
},
);
return IgnorePointer(
child: AnimatedOpacity(
duration: const Duration(milliseconds: 250),
opacity: showIndicator ? 1 : 0,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: [
Transform.rotate(
angle: currentSpeed < 1 ? pi : 0,
child: Icon(speedIcon(currentSpeed)),
),
Text(context.localized.speedIndicator(currentSpeed)),
],
),
),
),
),
),
);
}
}
IconData speedIcon(double value) {
if (value < 1) {
return IconsaxPlusBroken.flash;
}
if (value == 1) {
return IconsaxPlusLinear.flash_slash;
}
return IconsaxPlusLinear.flash;
}

View file

@ -26,6 +26,7 @@ import 'package:fladder/screens/video_player/components/video_player_controls_ex
import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart';
import 'package:fladder/screens/video_player/components/video_player_quality_controls.dart';
import 'package:fladder/screens/video_player/components/video_player_seek_indicator.dart';
import 'package:fladder/screens/video_player/components/video_player_speed_indicator.dart';
import 'package:fladder/screens/video_player/components/video_player_volume_indicator.dart';
import 'package:fladder/screens/video_player/components/video_progress_bar.dart';
import 'package:fladder/screens/video_player/components/video_volume_slider.dart';
@ -125,6 +126,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
),
const VideoPlayerSeekIndicator(),
const VideoPlayerVolumeIndicator(),
const VideoPlayerSpeedIndicator(),
Consumer(
builder: (context, ref, child) {
final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
@ -697,6 +699,14 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
resetTimer();
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5);
return true;
case VideoHotKeys.speedUp:
resetTimer();
ref.read(videoPlayerSettingsProvider.notifier).steppedSpeed(0.1);
return true;
case VideoHotKeys.speedDown:
resetTimer();
ref.read(videoPlayerSettingsProvider.notifier).steppedSpeed(-0.1);
return true;
case VideoHotKeys.fullScreen:
fullScreenHelper.toggleFullScreen(ref);
return true;

View file

@ -297,7 +297,7 @@ class MediaControlsWrapper extends BaseAudioHandler {
Future<int> setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async =>
await _player?.setSubtitleTrack(model, playbackModel) ?? -1;
Future<void> setVolume(double speed) async => _player?.setVolume(speed);
Future<void> setVolume(double volume) async => _player?.setVolume(volume);
@override
Future<void> seek(Duration position) {