mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
feat: Playback rate keyboard shortcuts (#484)
This commit is contained in:
commit
d6863fe504
8 changed files with 132 additions and 3 deletions
|
|
@ -1303,6 +1303,8 @@
|
||||||
"mute": "Mute",
|
"mute": "Mute",
|
||||||
"volumeUp": "Volume Up",
|
"volumeUp": "Volume Up",
|
||||||
"volumeDown": "Volume Down",
|
"volumeDown": "Volume Down",
|
||||||
|
"speedUp": "Speed Up",
|
||||||
|
"speedDown": "Speed Down",
|
||||||
"nextVideo": "Next Video",
|
"nextVideo": "Next Video",
|
||||||
"prevVideo": "Previous Video",
|
"prevVideo": "Previous Video",
|
||||||
"nextChapter": "Next Chapter",
|
"nextChapter": "Next Chapter",
|
||||||
|
|
@ -1326,5 +1328,13 @@
|
||||||
"type": "int"
|
"type": "int"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"speedIndicator": "Playback rate: {speed}",
|
||||||
|
"@speedIndicator": {
|
||||||
|
"placeholders": {
|
||||||
|
"speed": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +20,8 @@ enum VideoHotKeys {
|
||||||
mute,
|
mute,
|
||||||
volumeUp,
|
volumeUp,
|
||||||
volumeDown,
|
volumeDown,
|
||||||
|
speedUp,
|
||||||
|
speedDown,
|
||||||
nextVideo,
|
nextVideo,
|
||||||
prevVideo,
|
prevVideo,
|
||||||
nextChapter,
|
nextChapter,
|
||||||
|
|
@ -38,6 +40,8 @@ enum VideoHotKeys {
|
||||||
VideoHotKeys.mute => context.localized.mute,
|
VideoHotKeys.mute => context.localized.mute,
|
||||||
VideoHotKeys.volumeUp => context.localized.volumeUp,
|
VideoHotKeys.volumeUp => context.localized.volumeUp,
|
||||||
VideoHotKeys.volumeDown => context.localized.volumeDown,
|
VideoHotKeys.volumeDown => context.localized.volumeDown,
|
||||||
|
VideoHotKeys.speedUp => context.localized.speedUp,
|
||||||
|
VideoHotKeys.speedDown => context.localized.speedDown,
|
||||||
VideoHotKeys.nextVideo => context.localized.nextVideo,
|
VideoHotKeys.nextVideo => context.localized.nextVideo,
|
||||||
VideoHotKeys.prevVideo => context.localized.prevVideo,
|
VideoHotKeys.prevVideo => context.localized.prevVideo,
|
||||||
VideoHotKeys.nextChapter => context.localized.nextChapter,
|
VideoHotKeys.nextChapter => context.localized.nextChapter,
|
||||||
|
|
@ -180,6 +184,8 @@ Map<VideoHotKeys, KeyCombination> get _defaultVideoHotKeys => {
|
||||||
VideoHotKeys.mute => KeyCombination(key: LogicalKeyboardKey.keyM),
|
VideoHotKeys.mute => KeyCombination(key: LogicalKeyboardKey.keyM),
|
||||||
VideoHotKeys.volumeUp => KeyCombination(key: LogicalKeyboardKey.arrowUp),
|
VideoHotKeys.volumeUp => KeyCombination(key: LogicalKeyboardKey.arrowUp),
|
||||||
VideoHotKeys.volumeDown => KeyCombination(key: LogicalKeyboardKey.arrowDown),
|
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 =>
|
VideoHotKeys.prevVideo =>
|
||||||
KeyCombination(key: LogicalKeyboardKey.keyP, modifier: LogicalKeyboardKey.shiftLeft),
|
KeyCombination(key: LogicalKeyboardKey.keyP, modifier: LogicalKeyboardKey.shiftLeft),
|
||||||
VideoHotKeys.nextVideo =>
|
VideoHotKeys.nextVideo =>
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,8 @@ const _$VideoHotKeysEnumMap = {
|
||||||
VideoHotKeys.mute: 'mute',
|
VideoHotKeys.mute: 'mute',
|
||||||
VideoHotKeys.volumeUp: 'volumeUp',
|
VideoHotKeys.volumeUp: 'volumeUp',
|
||||||
VideoHotKeys.volumeDown: 'volumeDown',
|
VideoHotKeys.volumeDown: 'volumeDown',
|
||||||
|
VideoHotKeys.speedUp: 'speedUp',
|
||||||
|
VideoHotKeys.speedDown: 'speedDown',
|
||||||
VideoHotKeys.nextVideo: 'nextVideo',
|
VideoHotKeys.nextVideo: 'nextVideo',
|
||||||
VideoHotKeys.prevVideo: 'prevVideo',
|
VideoHotKeys.prevVideo: 'prevVideo',
|
||||||
VideoHotKeys.nextChapter: 'nextChapter',
|
VideoHotKeys.nextChapter: 'nextChapter',
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ final videoPlayerSettingsProvider =
|
||||||
return VideoPlayerSettingsProviderNotifier(ref);
|
return VideoPlayerSettingsProviderNotifier(ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final playbackRateProvider = StateProvider<double>((ref) => 1.0);
|
||||||
|
|
||||||
class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSettingsModel> {
|
class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSettingsModel> {
|
||||||
VideoPlayerSettingsProviderNotifier(this.ref) : super(VideoPlayerSettingsModel());
|
VideoPlayerSettingsProviderNotifier(this.ref) : super(VideoPlayerSettingsModel());
|
||||||
|
|
||||||
|
|
@ -67,11 +69,24 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
|
||||||
ref.read(videoPlayerProvider).setVolume(value);
|
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) =>
|
void toggleOrientation(Set<DeviceOrientation>? orientation) =>
|
||||||
state = state.copyWith(allowedOrientations: orientation);
|
state = state.copyWith(allowedOrientations: orientation);
|
||||||
|
|
||||||
void setShortcuts(MapEntry<VideoHotKeys, KeyCombination> newEntry) {
|
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() {
|
void nextChapter() {
|
||||||
|
|
|
||||||
|
|
@ -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/modal_bottom_sheet.dart';
|
||||||
import 'package:fladder/widgets/shared/spaced_list_tile.dart';
|
import 'package:fladder/widgets/shared/spaced_list_tile.dart';
|
||||||
|
|
||||||
final playbackRateProvider = StateProvider<double>((ref) => 1.0);
|
|
||||||
|
|
||||||
Future<void> showVideoPlayerOptions(BuildContext context, Function() minimizePlayer) {
|
Future<void> showVideoPlayerOptions(BuildContext context, Function() minimizePlayer) {
|
||||||
return showBottomSheetPill(
|
return showBottomSheetPill(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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_options_sheet.dart';
|
||||||
import 'package:fladder/screens/video_player/components/video_player_quality_controls.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_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_player_volume_indicator.dart';
|
||||||
import 'package:fladder/screens/video_player/components/video_progress_bar.dart';
|
import 'package:fladder/screens/video_player/components/video_progress_bar.dart';
|
||||||
import 'package:fladder/screens/video_player/components/video_volume_slider.dart';
|
import 'package:fladder/screens/video_player/components/video_volume_slider.dart';
|
||||||
|
|
@ -125,6 +126,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
),
|
),
|
||||||
const VideoPlayerSeekIndicator(),
|
const VideoPlayerSeekIndicator(),
|
||||||
const VideoPlayerVolumeIndicator(),
|
const VideoPlayerVolumeIndicator(),
|
||||||
|
const VideoPlayerSpeedIndicator(),
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
|
final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
|
||||||
|
|
@ -697,6 +699,14 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
resetTimer();
|
resetTimer();
|
||||||
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5);
|
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5);
|
||||||
return true;
|
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:
|
case VideoHotKeys.fullScreen:
|
||||||
fullScreenHelper.toggleFullScreen(ref);
|
fullScreenHelper.toggleFullScreen(ref);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,7 @@ class MediaControlsWrapper extends BaseAudioHandler {
|
||||||
Future<int> setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async =>
|
Future<int> setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async =>
|
||||||
await _player?.setSubtitleTrack(model, playbackModel) ?? -1;
|
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
|
@override
|
||||||
Future<void> seek(Duration position) {
|
Future<void> seek(Duration position) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue