feat: Customizable shortcuts/hotkeys (#439)

This implements the logic for allowing hotkeys with modifiers.
Implemented globalhotkeys and videocontrol hotkeys
Also implements saving the forward backwards seconds to the user.

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-08-08 16:36:50 +02:00 committed by GitHub
parent 23385d8e62
commit fa30e634b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1360 additions and 162 deletions

View file

@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:async/async.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/account_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/localization_helper.dart';
@ -48,35 +51,12 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
});
}
bool _onKey(KeyEvent value) {
if (value is KeyRepeatEvent) {
if (value.logicalKey == LogicalKeyboardKey.arrowLeft) {
seekBack();
return true;
}
if (value.logicalKey == LogicalKeyboardKey.arrowRight) {
seekForward();
return true;
}
}
if (value is KeyDownEvent) {
if (value.logicalKey == LogicalKeyboardKey.arrowLeft) {
seekBack();
return true;
}
if (value.logicalKey == LogicalKeyboardKey.arrowRight) {
seekForward();
return true;
}
}
return false;
}
@override
Widget build(BuildContext context) {
return InputHandler(
autoFocus: true,
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
keyMap: ref.watch(videoPlayerSettingsProvider.select((value) => value.currentShortcuts)),
keyMapResult: (result) => _onKey(result),
child: IgnorePointer(
child: AnimatedOpacity(
duration: const Duration(milliseconds: 500),
@ -108,6 +88,29 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
);
}
void seekBack({int seconds = -10}) => onSeekStart(seconds);
void seekForward({int seconds = 30}) => onSeekStart(seconds);
bool _onKey(VideoHotKeys value) {
switch (value) {
case VideoHotKeys.seekForward:
seekForward();
return true;
case VideoHotKeys.seekBack:
seekBack();
return true;
default:
break;
}
return false;
}
void seekBack() {
final seconds = -ref.read(userProvider
.select((value) => (value?.userSettings?.skipBackDuration ?? UserSettings().skipBackDuration).inSeconds));
onSeekStart(seconds);
}
void seekForward() {
final seconds = ref.read(userProvider
.select((value) => (value?.userSettings?.skipForwardDuration ?? UserSettings().skipForwardDuration).inSeconds));
onSeekStart(seconds);
}
}

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:iconsax_plus/iconsax_plus.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/list_padding.dart';
@ -19,6 +19,8 @@ class VideoVolumeSlider extends ConsumerStatefulWidget {
class _VideoVolumeSliderState extends ConsumerState<VideoVolumeSlider> {
bool sliderActive = false;
double? previousVolume;
@override
Widget build(BuildContext context) {
final volume = ref.watch(videoPlayerSettingsProvider.select((value) => value.volume));
@ -27,7 +29,12 @@ class _VideoVolumeSliderState extends ConsumerState<VideoVolumeSlider> {
children: [
IconButton(
icon: Icon(volumeIcon(volume)),
onPressed: () => ref.read(videoPlayerSettingsProvider.notifier).setVolume(0),
onPressed: () {
if (volume != 0) {
previousVolume = volume;
}
ref.read(videoPlayerSettingsProvider.notifier).setVolume(volume == 0 ? (previousVolume ?? 100) : 0);
},
),
AnimatedSize(
duration: const Duration(milliseconds: 250),