mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-08 23:18:16 -07:00
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:
parent
23385d8e62
commit
fa30e634b4
29 changed files with 1360 additions and 162 deletions
|
|
@ -4,9 +4,14 @@ import 'package:flutter/material.dart';
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'package:fladder/models/settings/client_settings_model.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/util/input_handler.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
||||
|
|
@ -17,8 +22,7 @@ enum HomeTabs {
|
|||
dashboard,
|
||||
library,
|
||||
favorites,
|
||||
sync,
|
||||
;
|
||||
sync;
|
||||
|
||||
const HomeTabs();
|
||||
|
||||
|
|
@ -120,16 +124,37 @@ class HomeScreen extends ConsumerWidget {
|
|||
})
|
||||
.nonNulls
|
||||
.toList();
|
||||
return HeroControllerScope(
|
||||
controller: HeroController(),
|
||||
child: AutoRouter(
|
||||
builder: (context, child) {
|
||||
return NavigationScaffold(
|
||||
destinations: destinations.nonNulls.toList(),
|
||||
currentRouteName: context.router.current.name,
|
||||
nestedChild: child,
|
||||
);
|
||||
},
|
||||
return InputHandler<GlobalHotKeys>(
|
||||
autoFocus: false,
|
||||
keyMapResult: (result) {
|
||||
switch (result) {
|
||||
case GlobalHotKeys.search:
|
||||
context.navigateTo(LibrarySearchRoute());
|
||||
return true;
|
||||
case GlobalHotKeys.exit:
|
||||
Future.microtask(() async {
|
||||
final manager = WindowManager.instance;
|
||||
if (await manager.isClosable()) {
|
||||
manager.close();
|
||||
} else {
|
||||
fladderSnackbar(context, title: context.localized.somethingWentWrong);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
},
|
||||
keyMap: ref.watch(clientSettingsProvider.select((value) => value.currentShortcuts)),
|
||||
child: HeroControllerScope(
|
||||
controller: HeroController(),
|
||||
child: AutoRouter(
|
||||
builder: (context, child) {
|
||||
return NavigationScaffold(
|
||||
destinations: destinations.nonNulls.toList(),
|
||||
currentRouteName: context.router.current.name,
|
||||
nestedChild: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/settings/client_settings_model.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/screens/settings/widgets/key_listener.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
||||
List<Widget> buildClientSettingsShortCuts(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
final clientSettings = ref.watch(clientSettingsProvider);
|
||||
return settingsListGroup(
|
||||
context,
|
||||
SettingsLabelDivider(label: context.localized.shortCuts),
|
||||
[
|
||||
...GlobalHotKeys.values.map(
|
||||
(entry) {
|
||||
final currentEntry = clientSettings.shortcuts[entry];
|
||||
final defaultEntry = clientSettings.defaultShortCuts[entry]!;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
entry.label(context),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: KeyCombinationWidget(
|
||||
currentKey: currentEntry,
|
||||
defaultKey: defaultEntry,
|
||||
onChanged: (value) =>
|
||||
ref.read(clientSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import 'package:fladder/routes/auto_router.gr.dart';
|
|||
import 'package:fladder/screens/settings/client_sections/client_settings_advanced.dart';
|
||||
import 'package:fladder/screens/settings/client_sections/client_settings_dashboard.dart';
|
||||
import 'package:fladder/screens/settings/client_sections/client_settings_download.dart';
|
||||
import 'package:fladder/screens/settings/client_sections/client_settings_shortcuts.dart';
|
||||
import 'package:fladder/screens/settings/client_sections/client_settings_theme.dart';
|
||||
import 'package:fladder/screens/settings/client_sections/client_settings_visual.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
|
|
@ -61,6 +62,10 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
|
|||
},
|
||||
),
|
||||
]),
|
||||
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[
|
||||
const SizedBox(height: 12),
|
||||
...buildClientSettingsShortCuts(context, ref),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
...buildClientSettingsDashboard(context, ref),
|
||||
const SizedBox(height: 12),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
|||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/settings/settings_scaffold.dart';
|
||||
import 'package:fladder/screens/settings/widgets/key_listener.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_message_box.dart';
|
||||
|
|
@ -43,6 +44,8 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
|
||||
final connectionState = ref.watch(connectivityStatusProvider);
|
||||
|
||||
final userSettings = ref.watch(userProvider.select((value) => value?.userSettings));
|
||||
|
||||
return SettingsScaffold(
|
||||
label: context.localized.settingsPlayerTitle,
|
||||
items: [
|
||||
|
|
@ -169,6 +172,70 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
...settingsListGroup(
|
||||
context,
|
||||
SettingsLabelDivider(label: context.localized.shortCuts),
|
||||
[
|
||||
if (userSettings != null)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.skipBackLength),
|
||||
trailing: SizedBox(
|
||||
width: 125,
|
||||
child: IntInputField(
|
||||
suffix: context.localized.seconds(10),
|
||||
controller: TextEditingController(text: userSettings.skipBackDuration.inSeconds.toString()),
|
||||
onSubmitted: (value) {
|
||||
if (value != null) {
|
||||
ref.read(userProvider.notifier).setBackwardSpeed(value);
|
||||
}
|
||||
},
|
||||
)),
|
||||
),
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.skipForwardLength),
|
||||
trailing: SizedBox(
|
||||
width: 125,
|
||||
child: IntInputField(
|
||||
suffix: context.localized.seconds(10),
|
||||
controller: TextEditingController(text: userSettings!.skipForwardDuration.inSeconds.toString()),
|
||||
onSubmitted: (value) {
|
||||
if (value != null) {
|
||||
ref.read(userProvider.notifier).setForwardSpeed(value);
|
||||
}
|
||||
},
|
||||
)),
|
||||
),
|
||||
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer)
|
||||
...VideoHotKeys.values.map(
|
||||
(entry) {
|
||||
final currentEntry = videoSettings.hotKeys[entry];
|
||||
final defaultEntry = videoSettings.defaultShortCuts[entry]!;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
entry.label(context),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: KeyCombinationWidget(
|
||||
currentKey: currentEntry,
|
||||
defaultKey: defaultEntry,
|
||||
onChanged: (value) =>
|
||||
ref.read(videoPlayerSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.playbackTrackSelection), [
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.rememberAudioSelections),
|
||||
|
|
|
|||
172
lib/screens/settings/widgets/key_listener.dart
Normal file
172
lib/screens/settings/widgets/key_listener.dart
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/settings/key_combinations.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
||||
class KeyCombinationWidget extends ConsumerStatefulWidget {
|
||||
final KeyCombination? currentKey;
|
||||
final KeyCombination defaultKey;
|
||||
final Function(KeyCombination? value) onChanged;
|
||||
|
||||
KeyCombinationWidget({required this.currentKey, required this.defaultKey, required this.onChanged, super.key});
|
||||
|
||||
@override
|
||||
KeyCombinationWidgetState createState() => KeyCombinationWidgetState();
|
||||
}
|
||||
|
||||
class KeyCombinationWidgetState extends ConsumerState<KeyCombinationWidget> {
|
||||
final focusNode = FocusNode();
|
||||
bool _isListening = false;
|
||||
LogicalKeyboardKey? _pressedKey;
|
||||
LogicalKeyboardKey? _pressedModifier;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopListening();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
setState(() {
|
||||
_isListening = true;
|
||||
_pressedKey = null;
|
||||
_pressedModifier = null;
|
||||
});
|
||||
}
|
||||
|
||||
void _stopListening() {
|
||||
setState(() {
|
||||
_isListening = false;
|
||||
if (_pressedKey != null) {
|
||||
final newKeyComb = KeyCombination(
|
||||
key: _pressedKey!,
|
||||
modifier: _pressedModifier,
|
||||
);
|
||||
if (newKeyComb == widget.defaultKey) {
|
||||
widget.onChanged(null);
|
||||
} else {
|
||||
widget.onChanged(newKeyComb);
|
||||
}
|
||||
}
|
||||
_pressedKey = null;
|
||||
_pressedModifier = null;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleKeyEvent(KeyEvent event) {
|
||||
final videoHotKeys = ref.read(videoPlayerSettingsProvider.select((value) => value.currentShortcuts)).values;
|
||||
final clientHotKeys = ref.read(clientSettingsProvider.select((value) => value.currentShortcuts)).values;
|
||||
final activeHotKeys = [...videoHotKeys, ...clientHotKeys].toList();
|
||||
|
||||
if (_isListening) {
|
||||
focusNode.requestFocus();
|
||||
setState(() {
|
||||
if (event is KeyDownEvent) {
|
||||
if (KeyCombination.modifierKeys.contains(event.logicalKey)) {
|
||||
_pressedModifier = event.logicalKey;
|
||||
} else {
|
||||
final currentHotKey = KeyCombination(key: event.logicalKey, modifier: _pressedModifier);
|
||||
bool isExistingHotkey = activeHotKeys.any((element) {
|
||||
return element == currentHotKey && currentHotKey != (widget.currentKey ?? widget.defaultKey);
|
||||
});
|
||||
|
||||
if (!isExistingHotkey) {
|
||||
_pressedKey = event.logicalKey;
|
||||
_stopListening();
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
fladderSnackbar(context, title: context.localized.shortCutAlreadyAssigned(currentHotKey.label));
|
||||
}
|
||||
_stopListening();
|
||||
}
|
||||
}
|
||||
} else if (event is KeyUpEvent) {
|
||||
if (KeyCombination.modifierKeys.contains(event.logicalKey) && _pressedModifier == event.logicalKey) {
|
||||
_pressedModifier = null;
|
||||
} else if (_pressedKey == event.logicalKey) {
|
||||
_pressedKey = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_pressedKey = null;
|
||||
_pressedModifier = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentModifier =
|
||||
_pressedModifier ?? (widget.currentKey != null ? widget.currentKey?.modifier : widget.defaultKey.modifier);
|
||||
final currentKey = _pressedKey ?? (widget.currentKey?.key ?? widget.defaultKey.key);
|
||||
final currentHotKey = KeyCombination(key: currentKey, modifier: currentModifier);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 50),
|
||||
child: InkWell(
|
||||
onTap: _isListening ? null : _startListening,
|
||||
child: Card(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 6,
|
||||
children: [
|
||||
Text(currentHotKey.label),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: _isListening
|
||||
? KeyboardListener(
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: widget.currentKey == null
|
||||
? null
|
||||
: () {
|
||||
_pressedKey = null;
|
||||
_pressedModifier = null;
|
||||
widget.onChanged(null);
|
||||
},
|
||||
iconSize: 24,
|
||||
icon: const Icon(IconsaxPlusBold.broom),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension LogicalKeyExtension on LogicalKeyboardKey {
|
||||
String get label {
|
||||
return switch (this) { LogicalKeyboardKey.space => "Space", _ => keyLabel };
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -10,11 +10,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/media_segments_model.dart';
|
||||
import 'package:fladder/models/media_playback_model.dart';
|
||||
import 'package:fladder/models/playback/playback_model.dart';
|
||||
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.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/screens/shared/default_title_bar.dart';
|
||||
import 'package:fladder/screens/shared/media/components/item_logo.dart';
|
||||
|
|
@ -48,6 +51,8 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
() => mounted ? toggleOverlay(value: false) : null,
|
||||
);
|
||||
|
||||
double? previousVolume;
|
||||
|
||||
final fadeDuration = const Duration(milliseconds: 350);
|
||||
bool showOverlay = true;
|
||||
bool wasPlaying = false;
|
||||
|
|
@ -55,55 +60,6 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
late final double topPadding = MediaQuery.of(context).viewPadding.top;
|
||||
late final double bottomPadding = MediaQuery.of(context).viewPadding.bottom;
|
||||
|
||||
bool _onKey(KeyEvent value) {
|
||||
final mediaSegments = ref.read(playBackModel.select((value) => value?.mediaSegments));
|
||||
final position = ref.read(mediaPlaybackProvider).position;
|
||||
MediaSegment? segment = mediaSegments?.atPosition(position);
|
||||
if (value is KeyRepeatEvent) {
|
||||
if (value.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
resetTimer();
|
||||
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5);
|
||||
return true;
|
||||
}
|
||||
if (value.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
resetTimer();
|
||||
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (value is KeyDownEvent) {
|
||||
if (value.logicalKey == LogicalKeyboardKey.keyS) {
|
||||
if (segment != null) {
|
||||
skipToSegmentEnd(segment);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (value.logicalKey == LogicalKeyboardKey.escape) {
|
||||
disableFullScreen();
|
||||
return true;
|
||||
}
|
||||
if (value.logicalKey == LogicalKeyboardKey.space) {
|
||||
ref.read(videoPlayerProvider).playOrPause();
|
||||
return true;
|
||||
}
|
||||
if (value.logicalKey == LogicalKeyboardKey.keyF) {
|
||||
fullScreenHelper.toggleFullScreen(ref);
|
||||
return true;
|
||||
}
|
||||
if (value.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
resetTimer();
|
||||
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5);
|
||||
return true;
|
||||
}
|
||||
if (value.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
resetTimer();
|
||||
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -116,8 +72,9 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
final player = ref.watch(videoPlayerProvider);
|
||||
final subtitleWidget = player.subtitleWidget(showOverlay, controlsKey: _bottomControlsKey);
|
||||
return InputHandler(
|
||||
autoFocus: false,
|
||||
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
|
||||
autoFocus: true,
|
||||
keyMap: ref.watch(videoPlayerSettingsProvider.select((value) => value.currentShortcuts)),
|
||||
keyMapResult: (result) => _onKey(result),
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
|
|
@ -536,8 +493,6 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
return Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final previousVideo = ref.watch(playBackModel.select((value) => value?.previousVideo));
|
||||
final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering));
|
||||
|
||||
return Tooltip(
|
||||
message: previousVideo?.detailedName(context) ?? "",
|
||||
textAlign: TextAlign.center,
|
||||
|
|
@ -547,9 +502,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
),
|
||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||
child: IconButton(
|
||||
onPressed: previousVideo != null && !buffering
|
||||
? () => ref.read(playbackModelHelper).loadNewVideo(previousVideo)
|
||||
: null,
|
||||
onPressed: loadPreviousVideo(ref, video: previousVideo),
|
||||
iconSize: 30,
|
||||
icon: const Icon(
|
||||
IconsaxPlusLinear.backward,
|
||||
|
|
@ -560,11 +513,16 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
);
|
||||
}
|
||||
|
||||
Function()? loadPreviousVideo(WidgetRef ref, {ItemBaseModel? video}) {
|
||||
final previousVideo = video ?? ref.read(playBackModel.select((value) => value?.previousVideo));
|
||||
final buffering = ref.read(mediaPlaybackProvider.select((value) => value.buffering));
|
||||
return previousVideo != null && !buffering ? () => ref.read(playbackModelHelper).loadNewVideo(previousVideo) : null;
|
||||
}
|
||||
|
||||
Widget get nextVideoButton {
|
||||
return Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final nextVideo = ref.watch(playBackModel.select((value) => value?.nextVideo));
|
||||
final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering));
|
||||
return Tooltip(
|
||||
message: nextVideo?.detailedName(context) ?? "",
|
||||
textAlign: TextAlign.center,
|
||||
|
|
@ -574,8 +532,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
),
|
||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||
child: IconButton(
|
||||
onPressed:
|
||||
nextVideo != null && !buffering ? () => ref.read(playbackModelHelper).loadNewVideo(nextVideo) : null,
|
||||
onPressed: loadNextVideo(ref, video: nextVideo),
|
||||
iconSize: 30,
|
||||
icon: const Icon(
|
||||
IconsaxPlusLinear.forward,
|
||||
|
|
@ -586,25 +543,62 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
);
|
||||
}
|
||||
|
||||
Function()? loadNextVideo(WidgetRef ref, {ItemBaseModel? video}) {
|
||||
final nextVideo = video ?? ref.read(playBackModel.select((value) => value?.nextVideo));
|
||||
final buffering = ref.read(mediaPlaybackProvider.select((value) => value.buffering));
|
||||
return nextVideo != null && !buffering ? () => ref.read(playbackModelHelper).loadNewVideo(nextVideo) : null;
|
||||
}
|
||||
|
||||
Widget seekBackwardButton(WidgetRef ref) {
|
||||
final backwardSpeed =
|
||||
ref.read(userProvider.select((value) => value?.userSettings?.skipBackDuration.inSeconds ?? 30));
|
||||
return IconButton(
|
||||
onPressed: () => seekBack(ref),
|
||||
tooltip: "-10",
|
||||
onPressed: () => seekBack(ref, seconds: backwardSpeed),
|
||||
tooltip: "-$backwardSpeed",
|
||||
iconSize: 40,
|
||||
icon: const Icon(
|
||||
IconsaxPlusLinear.backward_10_seconds,
|
||||
icon: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
IconsaxPlusBroken.refresh,
|
||||
size: 45,
|
||||
),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, 1),
|
||||
child: Text(
|
||||
"-$backwardSpeed",
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget seekForwardButton(WidgetRef ref) {
|
||||
final forwardSpeed =
|
||||
ref.read(userProvider.select((value) => value?.userSettings?.skipForwardDuration.inSeconds ?? 30));
|
||||
return IconButton(
|
||||
onPressed: () => seekForward(ref),
|
||||
tooltip: "15",
|
||||
onPressed: () => seekForward(ref, seconds: forwardSpeed),
|
||||
tooltip: forwardSpeed.toString(),
|
||||
iconSize: 40,
|
||||
icon: const Stack(
|
||||
icon: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Icon(IconsaxPlusLinear.forward_15_seconds),
|
||||
Transform.flip(
|
||||
flipX: true,
|
||||
child: const Icon(
|
||||
IconsaxPlusBroken.refresh,
|
||||
size: 45,
|
||||
),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, 1),
|
||||
child: Text(
|
||||
forwardSpeed.toString(),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -678,4 +672,58 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
resetTimer();
|
||||
fullScreenHelper.closeFullScreen(ref);
|
||||
}
|
||||
|
||||
bool _onKey(VideoHotKeys value) {
|
||||
final mediaSegments = ref.read(playBackModel.select((value) => value?.mediaSegments));
|
||||
final position = ref.read(mediaPlaybackProvider).position;
|
||||
|
||||
MediaSegment? segment = mediaSegments?.atPosition(position);
|
||||
|
||||
final volume = ref.read(videoPlayerSettingsProvider.select((value) => value.volume));
|
||||
|
||||
switch (value) {
|
||||
case VideoHotKeys.playPause:
|
||||
ref.read(videoPlayerProvider).playOrPause();
|
||||
return true;
|
||||
case VideoHotKeys.volumeUp:
|
||||
resetTimer();
|
||||
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5);
|
||||
return true;
|
||||
case VideoHotKeys.volumeDown:
|
||||
resetTimer();
|
||||
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5);
|
||||
return true;
|
||||
case VideoHotKeys.fullScreen:
|
||||
fullScreenHelper.toggleFullScreen(ref);
|
||||
return true;
|
||||
case VideoHotKeys.skipMediaSegment:
|
||||
if (segment != null) {
|
||||
skipToSegmentEnd(segment);
|
||||
}
|
||||
return true;
|
||||
case VideoHotKeys.exit:
|
||||
disableFullScreen();
|
||||
return true;
|
||||
case VideoHotKeys.mute:
|
||||
if (volume != 0) {
|
||||
previousVolume = volume;
|
||||
}
|
||||
ref.read(videoPlayerSettingsProvider.notifier).setVolume(volume == 0 ? (previousVolume ?? 100) : 0);
|
||||
return true;
|
||||
case VideoHotKeys.nextVideo:
|
||||
loadNextVideo(ref)?.call();
|
||||
return true;
|
||||
case VideoHotKeys.prevVideo:
|
||||
loadPreviousVideo(ref)?.call();
|
||||
return true;
|
||||
case VideoHotKeys.nextChapter:
|
||||
ref.read(videoPlayerSettingsProvider.notifier).nextChapter();
|
||||
return true;
|
||||
case VideoHotKeys.prevChapter:
|
||||
ref.read(videoPlayerSettingsProvider.notifier).prevChapter();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue