diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 965a85b..fe09e62 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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" + } + } } } \ No newline at end of file diff --git a/lib/models/settings/video_player_settings.dart b/lib/models/settings/video_player_settings.dart index fbe2d52..a735ba1 100644 --- a/lib/models/settings/video_player_settings.dart +++ b/lib/models/settings/video_player_settings.dart @@ -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 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 => diff --git a/lib/models/settings/video_player_settings.g.dart b/lib/models/settings/video_player_settings.g.dart index c44b4ee..096d0bc 100644 --- a/lib/models/settings/video_player_settings.g.dart +++ b/lib/models/settings/video_player_settings.g.dart @@ -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', diff --git a/lib/providers/settings/video_player_settings_provider.dart b/lib/providers/settings/video_player_settings_provider.dart index f6e4920..bb66947 100644 --- a/lib/providers/settings/video_player_settings_provider.dart +++ b/lib/providers/settings/video_player_settings_provider.dart @@ -15,6 +15,8 @@ final videoPlayerSettingsProvider = return VideoPlayerSettingsProviderNotifier(ref); }); +final playbackRateProvider = StateProvider((ref) => 1.0); + class VideoPlayerSettingsProviderNotifier extends StateNotifier { VideoPlayerSettingsProviderNotifier(this.ref) : super(VideoPlayerSettingsModel()); @@ -67,11 +69,24 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier? orientation) => state = state.copyWith(allowedOrientations: orientation); void setShortcuts(MapEntry newEntry) { - state = state.copyWith(hotKeys: state.hotKeys.setOrRemove(newEntry, state.defaultShortCuts)); + state = state.copyWith(hotKeys: state.hotKeys.setOrRemove(newEntry, state.defaultShortCuts)); } void nextChapter() { diff --git a/lib/screens/video_player/components/video_player_options_sheet.dart b/lib/screens/video_player/components/video_player_options_sheet.dart index affbe25..266286f 100644 --- a/lib/screens/video_player/components/video_player_options_sheet.dart +++ b/lib/screens/video_player/components/video_player_options_sheet.dart @@ -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((ref) => 1.0); Future showVideoPlayerOptions(BuildContext context, Function() minimizePlayer) { return showBottomSheetPill( diff --git a/lib/screens/video_player/components/video_player_speed_indicator.dart b/lib/screens/video_player/components/video_player_speed_indicator.dart new file mode 100644 index 0000000..7a4c779 --- /dev/null +++ b/lib/screens/video_player/components/video_player_speed_indicator.dart @@ -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 createState() => _VideoPlayerSpeedIndicatorState(); +} + +class _VideoPlayerSpeedIndicatorState extends ConsumerState { + 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; +} diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 6639b57..62523a5 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -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 { ), 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 { 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; diff --git a/lib/wrappers/media_control_wrapper.dart b/lib/wrappers/media_control_wrapper.dart index bb03a5c..53faf19 100644 --- a/lib/wrappers/media_control_wrapper.dart +++ b/lib/wrappers/media_control_wrapper.dart @@ -297,7 +297,7 @@ class MediaControlsWrapper extends BaseAudioHandler { Future setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async => await _player?.setSubtitleTrack(model, playbackModel) ?? -1; - Future setVolume(double speed) async => _player?.setVolume(speed); + Future setVolume(double volume) async => _player?.setVolume(volume); @override Future seek(Duration position) {