diff --git a/lib/models/settings/client_settings_model.dart b/lib/models/settings/client_settings_model.dart index 509c04d..02d6698 100644 --- a/lib/models/settings/client_settings_model.dart +++ b/lib/models/settings/client_settings_model.dart @@ -79,7 +79,7 @@ class ClientSettingsModel with _$ClientSettingsModel { @Default(false) bool usePosterForLibrary, String? lastViewedUpdate, int? libraryPageSize, - @Default({}) Map shortcuts, + @Default({}) Map shortcuts, }) = _ClientSettingsModel; factory ClientSettingsModel.fromJson(Map json) => _$ClientSettingsModelFromJson(json); diff --git a/lib/models/settings/client_settings_model.freezed.dart b/lib/models/settings/client_settings_model.freezed.dart index bc0f182..f4d49bb 100644 --- a/lib/models/settings/client_settings_model.freezed.dart +++ b/lib/models/settings/client_settings_model.freezed.dart @@ -45,7 +45,7 @@ mixin _$ClientSettingsModel { bool get usePosterForLibrary => throw _privateConstructorUsedError; String? get lastViewedUpdate => throw _privateConstructorUsedError; int? get libraryPageSize => throw _privateConstructorUsedError; - Map get shortcuts => + Map get shortcuts => throw _privateConstructorUsedError; /// Serializes this ClientSettingsModel to a JSON map. @@ -89,7 +89,7 @@ abstract class $ClientSettingsModelCopyWith<$Res> { bool usePosterForLibrary, String? lastViewedUpdate, int? libraryPageSize, - Map shortcuts}); + Map shortcuts}); } /// @nodoc @@ -233,7 +233,7 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel> shortcuts: null == shortcuts ? _value.shortcuts : shortcuts // ignore: cast_nullable_to_non_nullable - as Map, + as Map, ) as $Val); } } @@ -271,7 +271,7 @@ abstract class _$$ClientSettingsModelImplCopyWith<$Res> bool usePosterForLibrary, String? lastViewedUpdate, int? libraryPageSize, - Map shortcuts}); + Map shortcuts}); } /// @nodoc @@ -413,7 +413,7 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res> shortcuts: null == shortcuts ? _value._shortcuts : shortcuts // ignore: cast_nullable_to_non_nullable - as Map, + as Map, )); } } @@ -447,7 +447,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel this.usePosterForLibrary = false, this.lastViewedUpdate, this.libraryPageSize, - final Map shortcuts = const {}}) + final Map shortcuts = const {}}) : _shortcuts = shortcuts, super._(); @@ -521,10 +521,10 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel final String? lastViewedUpdate; @override final int? libraryPageSize; - final Map _shortcuts; + final Map _shortcuts; @override @JsonKey() - Map get shortcuts { + Map get shortcuts { if (_shortcuts is EqualUnmodifiableMapView) return _shortcuts; // ignore: implicit_dynamic_type return EqualUnmodifiableMapView(_shortcuts); @@ -612,7 +612,7 @@ abstract class _ClientSettingsModel extends ClientSettingsModel { final bool usePosterForLibrary, final String? lastViewedUpdate, final int? libraryPageSize, - final Map shortcuts}) = + final Map shortcuts}) = _$ClientSettingsModelImpl; _ClientSettingsModel._() : super._(); @@ -669,7 +669,7 @@ abstract class _ClientSettingsModel extends ClientSettingsModel { @override int? get libraryPageSize; @override - Map get shortcuts; + Map get shortcuts; /// Create a copy of ClientSettingsModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/settings/client_settings_model.g.dart b/lib/models/settings/client_settings_model.g.dart index 8166712..e71b796 100644 --- a/lib/models/settings/client_settings_model.g.dart +++ b/lib/models/settings/client_settings_model.g.dart @@ -49,11 +49,8 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson( lastViewedUpdate: json['lastViewedUpdate'] as String?, libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(), shortcuts: (json['shortcuts'] as Map?)?.map( - (k, e) => MapEntry( - $enumDecode(_$GlobalHotKeysEnumMap, k), - e == null - ? null - : KeyCombination.fromJson(e as Map)), + (k, e) => MapEntry($enumDecode(_$GlobalHotKeysEnumMap, k), + KeyCombination.fromJson(e as Map)), ) ?? const {}, ); diff --git a/lib/models/settings/key_combinations.dart b/lib/models/settings/key_combinations.dart index 92bb871..8bcd5b7 100644 --- a/lib/models/settings/key_combinations.dart +++ b/lib/models/settings/key_combinations.dart @@ -4,31 +4,65 @@ import 'package:flutter/services.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:fladder/screens/settings/widgets/key_listener.dart'; - part 'key_combinations.freezed.dart'; part 'key_combinations.g.dart'; -@Freezed(toJson: true, fromJson: true) +@Freezed(toJson: true, fromJson: true, copyWith: true) class KeyCombination with _$KeyCombination { const KeyCombination._(); factory KeyCombination({ + @LogicalKeyboardSerializer() LogicalKeyboardKey? key, @LogicalKeyboardSerializer() LogicalKeyboardKey? modifier, - @LogicalKeyboardSerializer() required LogicalKeyboardKey key, + @LogicalKeyboardSerializer() LogicalKeyboardKey? altKey, + @LogicalKeyboardSerializer() LogicalKeyboardKey? altModifier, }) = _KeyCombination; factory KeyCombination.fromJson(Map json) => _$KeyCombinationFromJson(json); @override bool operator ==(covariant other) { - return other is KeyCombination && other.key.keyId == key.keyId && other.modifier?.keyId == modifier?.keyId; + return other is KeyCombination && + other.key?.keyId == key?.keyId && + other.modifier?.keyId == modifier?.keyId && + other.altKey?.keyId == altKey?.keyId && + other.altModifier?.keyId == altModifier?.keyId; + } + + bool containsSameSet(KeyCombination other) { + return (key == other.key && modifier == other.modifier) || (altKey == other.key && altModifier == other.modifier); } @override - int get hashCode => key.hashCode ^ modifier.hashCode; + int get hashCode => key.hashCode ^ modifier.hashCode ^ altKey.hashCode ^ altModifier.hashCode; - String get label => [modifier?.label, key.label].nonNulls.join(" + "); + String get label => [modifier?.label, key?.label].nonNulls.join(" + "); + String get altLabel => [altModifier?.label, altKey?.label].nonNulls.join(" + "); + + KeyCombination? get altSet => altKey != null + ? copyWith( + key: altKey!, + modifier: altModifier, + ) + : null; + + KeyCombination setKeys( + LogicalKeyboardKey? key, { + LogicalKeyboardKey? modifier, + bool alt = false, + }) { + return alt + ? copyWith( + altKey: key, + altModifier: modifier, + ) + : copyWith( + key: key ?? altKey, + modifier: key == null ? altModifier : modifier, + altKey: key == null ? null : altKey, + altModifier: key == null ? null : altModifier, + ); + } static final Set shiftKeys = { LogicalKeyboardKey.shift, @@ -68,3 +102,26 @@ class LogicalKeyboardSerializer extends JsonConverter "Space", _ => keyLabel }; + } +} + +extension KeyMapExtension on Map { + Map setOrRemove(MapEntry newEntry, Map defaultShortCuts) { + if (newEntry.value == defaultShortCuts[newEntry.key]) { + final currentShortcuts = Map.fromEntries(entries); + return (currentShortcuts..remove(newEntry.key)); + } else { + final currentShortcuts = Map.fromEntries(entries); + currentShortcuts.update( + newEntry.key, + (value) => newEntry.value, + ifAbsent: () => newEntry.value, + ); + return currentShortcuts; + } + } +} diff --git a/lib/models/settings/key_combinations.freezed.dart b/lib/models/settings/key_combinations.freezed.dart index 9eea8a7..718b348 100644 --- a/lib/models/settings/key_combinations.freezed.dart +++ b/lib/models/settings/key_combinations.freezed.dart @@ -20,38 +20,173 @@ KeyCombination _$KeyCombinationFromJson(Map json) { /// @nodoc mixin _$KeyCombination { + @LogicalKeyboardSerializer() + LogicalKeyboardKey? get key => throw _privateConstructorUsedError; @LogicalKeyboardSerializer() LogicalKeyboardKey? get modifier => throw _privateConstructorUsedError; @LogicalKeyboardSerializer() - LogicalKeyboardKey get key => throw _privateConstructorUsedError; + LogicalKeyboardKey? get altKey => throw _privateConstructorUsedError; + @LogicalKeyboardSerializer() + LogicalKeyboardKey? get altModifier => throw _privateConstructorUsedError; /// Serializes this KeyCombination to a JSON map. Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of KeyCombination + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $KeyCombinationCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $KeyCombinationCopyWith<$Res> { + factory $KeyCombinationCopyWith( + KeyCombination value, $Res Function(KeyCombination) then) = + _$KeyCombinationCopyWithImpl<$Res, KeyCombination>; + @useResult + $Res call( + {@LogicalKeyboardSerializer() LogicalKeyboardKey? key, + @LogicalKeyboardSerializer() LogicalKeyboardKey? modifier, + @LogicalKeyboardSerializer() LogicalKeyboardKey? altKey, + @LogicalKeyboardSerializer() LogicalKeyboardKey? altModifier}); +} + +/// @nodoc +class _$KeyCombinationCopyWithImpl<$Res, $Val extends KeyCombination> + implements $KeyCombinationCopyWith<$Res> { + _$KeyCombinationCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of KeyCombination + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? key = freezed, + Object? modifier = freezed, + Object? altKey = freezed, + Object? altModifier = freezed, + }) { + return _then(_value.copyWith( + key: freezed == key + ? _value.key + : key // ignore: cast_nullable_to_non_nullable + as LogicalKeyboardKey?, + modifier: freezed == modifier + ? _value.modifier + : modifier // ignore: cast_nullable_to_non_nullable + as LogicalKeyboardKey?, + altKey: freezed == altKey + ? _value.altKey + : altKey // ignore: cast_nullable_to_non_nullable + as LogicalKeyboardKey?, + altModifier: freezed == altModifier + ? _value.altModifier + : altModifier // ignore: cast_nullable_to_non_nullable + as LogicalKeyboardKey?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$KeyCombinationImplCopyWith<$Res> + implements $KeyCombinationCopyWith<$Res> { + factory _$$KeyCombinationImplCopyWith(_$KeyCombinationImpl value, + $Res Function(_$KeyCombinationImpl) then) = + __$$KeyCombinationImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@LogicalKeyboardSerializer() LogicalKeyboardKey? key, + @LogicalKeyboardSerializer() LogicalKeyboardKey? modifier, + @LogicalKeyboardSerializer() LogicalKeyboardKey? altKey, + @LogicalKeyboardSerializer() LogicalKeyboardKey? altModifier}); +} + +/// @nodoc +class __$$KeyCombinationImplCopyWithImpl<$Res> + extends _$KeyCombinationCopyWithImpl<$Res, _$KeyCombinationImpl> + implements _$$KeyCombinationImplCopyWith<$Res> { + __$$KeyCombinationImplCopyWithImpl( + _$KeyCombinationImpl _value, $Res Function(_$KeyCombinationImpl) _then) + : super(_value, _then); + + /// Create a copy of KeyCombination + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? key = freezed, + Object? modifier = freezed, + Object? altKey = freezed, + Object? altModifier = freezed, + }) { + return _then(_$KeyCombinationImpl( + key: freezed == key + ? _value.key + : key // ignore: cast_nullable_to_non_nullable + as LogicalKeyboardKey?, + modifier: freezed == modifier + ? _value.modifier + : modifier // ignore: cast_nullable_to_non_nullable + as LogicalKeyboardKey?, + altKey: freezed == altKey + ? _value.altKey + : altKey // ignore: cast_nullable_to_non_nullable + as LogicalKeyboardKey?, + altModifier: freezed == altModifier + ? _value.altModifier + : altModifier // ignore: cast_nullable_to_non_nullable + as LogicalKeyboardKey?, + )); + } } /// @nodoc @JsonSerializable() class _$KeyCombinationImpl extends _KeyCombination { _$KeyCombinationImpl( - {@LogicalKeyboardSerializer() this.modifier, - @LogicalKeyboardSerializer() required this.key}) + {@LogicalKeyboardSerializer() this.key, + @LogicalKeyboardSerializer() this.modifier, + @LogicalKeyboardSerializer() this.altKey, + @LogicalKeyboardSerializer() this.altModifier}) : super._(); factory _$KeyCombinationImpl.fromJson(Map json) => _$$KeyCombinationImplFromJson(json); + @override + @LogicalKeyboardSerializer() + final LogicalKeyboardKey? key; @override @LogicalKeyboardSerializer() final LogicalKeyboardKey? modifier; @override @LogicalKeyboardSerializer() - final LogicalKeyboardKey key; + final LogicalKeyboardKey? altKey; + @override + @LogicalKeyboardSerializer() + final LogicalKeyboardKey? altModifier; @override String toString() { - return 'KeyCombination(modifier: $modifier, key: $key)'; + return 'KeyCombination(key: $key, modifier: $modifier, altKey: $altKey, altModifier: $altModifier)'; } + /// Create a copy of KeyCombination + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$KeyCombinationImplCopyWith<_$KeyCombinationImpl> get copyWith => + __$$KeyCombinationImplCopyWithImpl<_$KeyCombinationImpl>( + this, _$identity); + @override Map toJson() { return _$$KeyCombinationImplToJson( @@ -62,18 +197,33 @@ class _$KeyCombinationImpl extends _KeyCombination { abstract class _KeyCombination extends KeyCombination { factory _KeyCombination( - {@LogicalKeyboardSerializer() final LogicalKeyboardKey? modifier, - @LogicalKeyboardSerializer() required final LogicalKeyboardKey key}) = + {@LogicalKeyboardSerializer() final LogicalKeyboardKey? key, + @LogicalKeyboardSerializer() final LogicalKeyboardKey? modifier, + @LogicalKeyboardSerializer() final LogicalKeyboardKey? altKey, + @LogicalKeyboardSerializer() final LogicalKeyboardKey? altModifier}) = _$KeyCombinationImpl; _KeyCombination._() : super._(); factory _KeyCombination.fromJson(Map json) = _$KeyCombinationImpl.fromJson; + @override + @LogicalKeyboardSerializer() + LogicalKeyboardKey? get key; @override @LogicalKeyboardSerializer() LogicalKeyboardKey? get modifier; @override @LogicalKeyboardSerializer() - LogicalKeyboardKey get key; + LogicalKeyboardKey? get altKey; + @override + @LogicalKeyboardSerializer() + LogicalKeyboardKey? get altModifier; + + /// Create a copy of KeyCombination + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$KeyCombinationImplCopyWith<_$KeyCombinationImpl> get copyWith => + throw _privateConstructorUsedError; } diff --git a/lib/models/settings/key_combinations.g.dart b/lib/models/settings/key_combinations.g.dart index 7cb3cd8..2f420dd 100644 --- a/lib/models/settings/key_combinations.g.dart +++ b/lib/models/settings/key_combinations.g.dart @@ -8,17 +8,27 @@ part of 'key_combinations.dart'; _$KeyCombinationImpl _$$KeyCombinationImplFromJson(Map json) => _$KeyCombinationImpl( + key: _$JsonConverterFromJson( + json['key'], const LogicalKeyboardSerializer().fromJson), modifier: _$JsonConverterFromJson( json['modifier'], const LogicalKeyboardSerializer().fromJson), - key: const LogicalKeyboardSerializer().fromJson(json['key'] as String), + altKey: _$JsonConverterFromJson( + json['altKey'], const LogicalKeyboardSerializer().fromJson), + altModifier: _$JsonConverterFromJson( + json['altModifier'], const LogicalKeyboardSerializer().fromJson), ); Map _$$KeyCombinationImplToJson( _$KeyCombinationImpl instance) => { + 'key': _$JsonConverterToJson( + instance.key, const LogicalKeyboardSerializer().toJson), 'modifier': _$JsonConverterToJson( instance.modifier, const LogicalKeyboardSerializer().toJson), - 'key': const LogicalKeyboardSerializer().toJson(instance.key), + 'altKey': _$JsonConverterToJson( + instance.altKey, const LogicalKeyboardSerializer().toJson), + 'altModifier': _$JsonConverterToJson( + instance.altModifier, const LogicalKeyboardSerializer().toJson), }; Value? _$JsonConverterFromJson( diff --git a/lib/models/settings/video_player_settings.dart b/lib/models/settings/video_player_settings.dart index b22da94..30dfc6e 100644 --- a/lib/models/settings/video_player_settings.dart +++ b/lib/models/settings/video_player_settings.dart @@ -68,7 +68,7 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { @Default(Bitrate.original) Bitrate maxInternetBitrate, String? audioDevice, @Default(defaultSegmentSkipValues) Map segmentSkipSettings, - @Default({}) Map hotKeys, + @Default({}) Map hotKeys, }) = _VideoPlayerSettingsModel; double get volume => switch (defaultTargetPlatform) { @@ -165,14 +165,25 @@ enum AutoNextType { Map get _defaultVideoHotKeys => { for (var hotKey in VideoHotKeys.values) hotKey: switch (hotKey) { - VideoHotKeys.playPause => KeyCombination(key: LogicalKeyboardKey.space), - VideoHotKeys.seekForward => KeyCombination(key: LogicalKeyboardKey.arrowRight), - VideoHotKeys.seekBack => KeyCombination(key: LogicalKeyboardKey.arrowLeft), + VideoHotKeys.playPause => KeyCombination( + key: LogicalKeyboardKey.space, + altKey: LogicalKeyboardKey.keyK, + ), + VideoHotKeys.seekForward => KeyCombination( + key: LogicalKeyboardKey.arrowRight, + altKey: LogicalKeyboardKey.keyL, + ), + VideoHotKeys.seekBack => KeyCombination( + key: LogicalKeyboardKey.arrowLeft, + altKey: LogicalKeyboardKey.keyJ, + ), VideoHotKeys.mute => KeyCombination(key: LogicalKeyboardKey.keyM), VideoHotKeys.volumeUp => KeyCombination(key: LogicalKeyboardKey.arrowUp), VideoHotKeys.volumeDown => KeyCombination(key: LogicalKeyboardKey.arrowDown), - VideoHotKeys.prevVideo => KeyCombination(key: LogicalKeyboardKey.keyP, modifier: LogicalKeyboardKey.shift), - VideoHotKeys.nextVideo => KeyCombination(key: LogicalKeyboardKey.keyN, modifier: LogicalKeyboardKey.shift), + VideoHotKeys.prevVideo => + KeyCombination(key: LogicalKeyboardKey.keyP, modifier: LogicalKeyboardKey.shiftLeft), + VideoHotKeys.nextVideo => + KeyCombination(key: LogicalKeyboardKey.keyN, modifier: LogicalKeyboardKey.shiftLeft), VideoHotKeys.nextChapter => KeyCombination(key: LogicalKeyboardKey.pageUp), VideoHotKeys.prevChapter => KeyCombination(key: LogicalKeyboardKey.pageDown), VideoHotKeys.fullScreen => KeyCombination(key: LogicalKeyboardKey.keyF), diff --git a/lib/models/settings/video_player_settings.freezed.dart b/lib/models/settings/video_player_settings.freezed.dart index a08e5b4..9709551 100644 --- a/lib/models/settings/video_player_settings.freezed.dart +++ b/lib/models/settings/video_player_settings.freezed.dart @@ -37,7 +37,7 @@ mixin _$VideoPlayerSettingsModel { String? get audioDevice => throw _privateConstructorUsedError; Map get segmentSkipSettings => throw _privateConstructorUsedError; - Map get hotKeys => + Map get hotKeys => throw _privateConstructorUsedError; /// Serializes this VideoPlayerSettingsModel to a JSON map. @@ -71,7 +71,7 @@ abstract class $VideoPlayerSettingsModelCopyWith<$Res> { Bitrate maxInternetBitrate, String? audioDevice, Map segmentSkipSettings, - Map hotKeys}); + Map hotKeys}); } /// @nodoc @@ -166,7 +166,7 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res, hotKeys: null == hotKeys ? _value.hotKeys : hotKeys // ignore: cast_nullable_to_non_nullable - as Map, + as Map, ) as $Val); } } @@ -195,7 +195,7 @@ abstract class _$$VideoPlayerSettingsModelImplCopyWith<$Res> Bitrate maxInternetBitrate, String? audioDevice, Map segmentSkipSettings, - Map hotKeys}); + Map hotKeys}); } /// @nodoc @@ -289,7 +289,7 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res> hotKeys: null == hotKeys ? _value._hotKeys : hotKeys // ignore: cast_nullable_to_non_nullable - as Map, + as Map, )); } } @@ -314,7 +314,7 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel this.audioDevice, final Map segmentSkipSettings = defaultSegmentSkipValues, - final Map hotKeys = const {}}) + final Map hotKeys = const {}}) : _allowedOrientations = allowedOrientations, _segmentSkipSettings = segmentSkipSettings, _hotKeys = hotKeys, @@ -377,10 +377,10 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel return EqualUnmodifiableMapView(_segmentSkipSettings); } - final Map _hotKeys; + final Map _hotKeys; @override @JsonKey() - Map get hotKeys { + Map get hotKeys { if (_hotKeys is EqualUnmodifiableMapView) return _hotKeys; // ignore: implicit_dynamic_type return EqualUnmodifiableMapView(_hotKeys); @@ -446,7 +446,7 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel { final Bitrate maxInternetBitrate, final String? audioDevice, final Map segmentSkipSettings, - final Map hotKeys}) = + final Map hotKeys}) = _$VideoPlayerSettingsModelImpl; _VideoPlayerSettingsModel._() : super._(); @@ -482,7 +482,7 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel { @override Map get segmentSkipSettings; @override - Map get hotKeys; + Map get hotKeys; /// Create a copy of VideoPlayerSettingsModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/settings/video_player_settings.g.dart b/lib/models/settings/video_player_settings.g.dart index 6068a4f..365aef5 100644 --- a/lib/models/settings/video_player_settings.g.dart +++ b/lib/models/settings/video_player_settings.g.dart @@ -39,11 +39,8 @@ _$VideoPlayerSettingsModelImpl _$$VideoPlayerSettingsModelImplFromJson( ) ?? defaultSegmentSkipValues, hotKeys: (json['hotKeys'] as Map?)?.map( - (k, e) => MapEntry( - $enumDecode(_$VideoHotKeysEnumMap, k), - e == null - ? null - : KeyCombination.fromJson(e as Map)), + (k, e) => MapEntry($enumDecode(_$VideoHotKeysEnumMap, k), + KeyCombination.fromJson(e as Map)), ) ?? const {}, ); diff --git a/lib/providers/settings/client_settings_provider.dart b/lib/providers/settings/client_settings_provider.dart index d841dad..0c04bd2 100644 --- a/lib/providers/settings/client_settings_provider.dart +++ b/lib/providers/settings/client_settings_provider.dart @@ -60,14 +60,6 @@ class ClientSettingsNotifier extends StateNotifier { void setRequireWifi(bool value) => state = state.copyWith(requireWifi: value); - void setShortcuts(MapEntry mapEntry) { - final newShortCuts = Map.fromEntries(state.shortcuts.entries); - newShortCuts.update( - mapEntry.key, - (value) => mapEntry.value, - ifAbsent: () => mapEntry.value, - ); - newShortCuts.removeWhere((key, value) => value == null); - state = state.copyWith(shortcuts: newShortCuts); - } + void setShortcuts(MapEntry newEntry) => + state = state.copyWith(shortcuts: state.shortcuts.setOrRemove(newEntry, state.defaultShortCuts)); } diff --git a/lib/providers/settings/video_player_settings_provider.dart b/lib/providers/settings/video_player_settings_provider.dart index a98445b..f6e4920 100644 --- a/lib/providers/settings/video_player_settings_provider.dart +++ b/lib/providers/settings/video_player_settings_provider.dart @@ -70,15 +70,8 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier? orientation) => state = state.copyWith(allowedOrientations: orientation); - void setShortcuts(MapEntry newEntry) { - final currentShortcuts = Map.fromEntries(state.hotKeys.entries); - currentShortcuts.update( - newEntry.key, - (value) => newEntry.value, - ifAbsent: () => newEntry.value, - ); - currentShortcuts.removeWhere((key, value) => value == null); - state = state.copyWith(hotKeys: currentShortcuts); + void setShortcuts(MapEntry newEntry) { + state = state.copyWith(hotKeys: state.hotKeys.setOrRemove(newEntry, state.defaultShortCuts)); } void nextChapter() { diff --git a/lib/screens/settings/client_sections/client_settings_shortcuts.dart b/lib/screens/settings/client_sections/client_settings_shortcuts.dart index 5a4a01b..c266b32 100644 --- a/lib/screens/settings/client_sections/client_settings_shortcuts.dart +++ b/lib/screens/settings/client_sections/client_settings_shortcuts.dart @@ -19,31 +19,26 @@ List buildClientSettingsShortCuts( 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, - ), + (entry) => 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)), - ), - ) - ], - ), - ); - }, + ), + Flexible( + child: KeyCombinationWidget( + currentKey: clientSettings.shortcuts[entry], + defaultKey: clientSettings.defaultShortCuts[entry]!, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)), + ), + ) + ], + ), + ), ) ], ); diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index c33ddf5..c8d3fae 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -207,31 +207,27 @@ class _PlayerSettingsPageState extends ConsumerState { ), 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, - ), + (entry) => 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)), - ), - ) - ], - ), - ); - }, + ), + Flexible( + child: KeyCombinationWidget( + currentKey: videoSettings.hotKeys[entry], + defaultKey: videoSettings.defaultShortCuts[entry]!, + onChanged: (value) => + ref.read(videoPlayerSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)), + ), + ) + ], + ), + ), ) ], ), diff --git a/lib/screens/settings/widgets/key_listener.dart b/lib/screens/settings/widgets/key_listener.dart index 7fc0c03..895f188 100644 --- a/lib/screens/settings/widgets/key_listener.dart +++ b/lib/screens/settings/widgets/key_listener.dart @@ -8,27 +8,100 @@ 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/theme.dart'; import 'package:fladder/util/localization_helper.dart'; -class KeyCombinationWidget extends ConsumerStatefulWidget { +// Only use for actively checking if a shortcut is being changed +bool changingShortCut = false; + +class KeyCombinationWidget extends StatelessWidget { final KeyCombination? currentKey; final KeyCombination defaultKey; - final Function(KeyCombination? value) onChanged; + final Function(KeyCombination value) onChanged; - KeyCombinationWidget({required this.currentKey, required this.defaultKey, required this.onChanged, super.key}); + const KeyCombinationWidget({ + required this.currentKey, + required this.defaultKey, + required this.onChanged, + super.key, + }); @override - KeyCombinationWidgetState createState() => KeyCombinationWidgetState(); + Widget build(BuildContext context) { + final comboKey = currentKey ?? defaultKey; + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 50), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + spacing: 6, + children: [ + KeyListenerWidget( + currentKey: comboKey, + onChanged: (value) => onChanged(comboKey.setKeys( + value?.key, + modifier: value?.modifier, + )), + ), + if (comboKey.key != null) ...[ + const Opacity(opacity: 0.25, child: Text("alt")), + KeyListenerWidget( + currentKey: comboKey.altSet, + onChanged: (value) => onChanged(comboKey.setKeys( + value?.key, + modifier: value?.modifier, + alt: true, + )), + ), + ], + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: IconButton( + onPressed: currentKey == defaultKey || currentKey == null ? null : () => onChanged(defaultKey), + iconSize: 24, + icon: const Icon(IconsaxPlusBold.broom), + ), + ) + ], + ), + ), + ], + ); + } } -class KeyCombinationWidgetState extends ConsumerState { +class KeyListenerWidget extends ConsumerStatefulWidget { + final KeyCombination? currentKey; + final Function(KeyCombination? value) onChanged; + + KeyListenerWidget({ + required this.currentKey, + required this.onChanged, + super.key, + }); + + @override + KeyListenerWidgetState createState() => KeyListenerWidgetState(); +} + +class KeyListenerWidgetState extends ConsumerState { final focusNode = FocusNode(); bool _isListening = false; + bool _showClearButton = false; LogicalKeyboardKey? _pressedKey; LogicalKeyboardKey? _pressedModifier; + void setIsListening(bool value) { + changingShortCut = value; + _isListening = value; + } + @override void dispose() { + changingShortCut = false; _isListening = false; _pressedKey = null; _pressedModifier = null; @@ -36,8 +109,9 @@ class KeyCombinationWidgetState extends ConsumerState { } void _startListening() { + if (changingShortCut) return; setState(() { - _isListening = true; + setIsListening(true); _pressedKey = null; _pressedModifier = null; }); @@ -45,17 +119,13 @@ class KeyCombinationWidgetState extends ConsumerState { void _stopListening() { setState(() { - _isListening = false; + setIsListening(false); if (_pressedKey != null) { final newKeyComb = KeyCombination( key: _pressedKey!, modifier: _pressedModifier, ); - if (newKeyComb == widget.defaultKey) { - widget.onChanged(null); - } else { - widget.onChanged(newKeyComb); - } + widget.onChanged(newKeyComb); } _pressedKey = null; _pressedModifier = null; @@ -76,7 +146,7 @@ class KeyCombinationWidgetState extends ConsumerState { } else { final currentHotKey = KeyCombination(key: event.logicalKey, modifier: _pressedModifier); bool isExistingHotkey = activeHotKeys.any((element) { - return element == currentHotKey && currentHotKey != (widget.currentKey ?? widget.defaultKey); + return element.containsSameSet(currentHotKey) && currentHotKey != widget.currentKey; }); if (!isExistingHotkey) { @@ -103,72 +173,78 @@ class KeyCombinationWidgetState extends ConsumerState { } } + void showClearButton(bool value) { + setState(() { + _showClearButton = value; + }); + } + @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), + final currentModifier = _pressedModifier ?? (widget.currentKey?.modifier); + final currentKey = _pressedKey ?? widget.currentKey?.key; + final currentHotKey = currentKey == null ? null : KeyCombination(key: currentKey, modifier: currentModifier); + return MouseRegion( + onEnter: (event) => showClearButton(true), + onExit: (event) => showClearButton(false), + child: ClipRRect( + borderRadius: FladderTheme.smallShape.borderRadius, + child: InkWell( + onTap: _isListening ? _stopListening : _startListening, + onSecondaryTap: () { + setState(() { + setIsListening(false); + widget.onChanged(null); + }); + }, + child: Container( + color: Theme.of(context).colorScheme.primaryContainer, + child: AnimatedSize( + duration: const Duration(milliseconds: 125), + child: Stack( + alignment: Alignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + spacing: 8, + children: [ + if (_showClearButton && currentHotKey != null) + GestureDetector( + onTap: () { + setIsListening(false); + widget.onChanged(null); + }, + child: const Icon( + IconsaxPlusLinear.trash, + size: 17, ), - ) - ], - ), + ), + Text( + currentHotKey?.label ?? "+", + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ], + ), + ), + if (_isListening) + Positioned.fill( + child: KeyboardListener( + focusNode: focusNode, + autofocus: true, + onKeyEvent: _handleKeyEvent, + child: const Opacity( + opacity: 0.25, + child: LinearProgressIndicator(), + ), + ), + ), + ], ), ), ), ), - ], + ), ); } } - -extension LogicalKeyExtension on LogicalKeyboardKey { - String get label { - return switch (this) { LogicalKeyboardKey.space => "Space", _ => keyLabel }; - } -} diff --git a/lib/util/input_handler.dart b/lib/util/input_handler.dart index 1996020..379e2ed 100644 --- a/lib/util/input_handler.dart +++ b/lib/util/input_handler.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:fladder/models/settings/key_combinations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -class InputHandler extends StatefulWidget { +import 'package:fladder/models/settings/key_combinations.dart'; +import 'package:fladder/screens/settings/widgets/key_listener.dart'; + +class InputHandler extends ConsumerStatefulWidget { final bool autoFocus; final KeyEventResult Function(FocusNode node, KeyEvent event)? onKeyEvent; final bool Function(T result)? keyMapResult; @@ -19,10 +22,10 @@ class InputHandler extends StatefulWidget { }); @override - State createState() => _InputHandlerState(); + ConsumerState createState() => _InputHandlerState(); } -class _InputHandlerState extends State> { +class _InputHandlerState extends ConsumerState> { final focusNode = FocusNode(); LogicalKeyboardKey? pressedModifier; @@ -50,6 +53,8 @@ class _InputHandlerState extends State> { } KeyEventResult _onKey(KeyEvent value) { + if (changingShortCut) return KeyEventResult.ignored; + final keyMap = widget.keyMap?.entries.nonNulls.toList() ?? []; if (value is KeyDownEvent || value is KeyRepeatEvent) { if (KeyCombination.modifierKeys.contains(value.logicalKey)) { @@ -63,7 +68,11 @@ class _InputHandlerState extends State> { bool isMainKeyPressed = value.logicalKey == keyCombination.key; bool isModifierKeyPressed = pressedModifier == keyCombination.modifier; - if (isMainKeyPressed && isModifierKeyPressed) { + bool isAltKeyPressed = value.logicalKey == keyCombination.altKey; + + bool isAltModifierKeyPressed = pressedModifier == keyCombination.altModifier; + + if ((isMainKeyPressed && isModifierKeyPressed) || isAltKeyPressed && isAltModifierKeyPressed) { if (widget.keyMapResult?.call(hotKey) ?? false) { return KeyEventResult.handled; } else {