chore: Shortcuts improvements (#446)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-08-09 16:17:14 +02:00 committed by GitHub
parent d0d6a2ffa6
commit 70e346b8a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 481 additions and 198 deletions

View file

@ -79,7 +79,7 @@ class ClientSettingsModel with _$ClientSettingsModel {
@Default(false) bool usePosterForLibrary, @Default(false) bool usePosterForLibrary,
String? lastViewedUpdate, String? lastViewedUpdate,
int? libraryPageSize, int? libraryPageSize,
@Default({}) Map<GlobalHotKeys, KeyCombination?> shortcuts, @Default({}) Map<GlobalHotKeys, KeyCombination> shortcuts,
}) = _ClientSettingsModel; }) = _ClientSettingsModel;
factory ClientSettingsModel.fromJson(Map<String, dynamic> json) => _$ClientSettingsModelFromJson(json); factory ClientSettingsModel.fromJson(Map<String, dynamic> json) => _$ClientSettingsModelFromJson(json);

View file

@ -45,7 +45,7 @@ mixin _$ClientSettingsModel {
bool get usePosterForLibrary => throw _privateConstructorUsedError; bool get usePosterForLibrary => throw _privateConstructorUsedError;
String? get lastViewedUpdate => throw _privateConstructorUsedError; String? get lastViewedUpdate => throw _privateConstructorUsedError;
int? get libraryPageSize => throw _privateConstructorUsedError; int? get libraryPageSize => throw _privateConstructorUsedError;
Map<GlobalHotKeys, KeyCombination?> get shortcuts => Map<GlobalHotKeys, KeyCombination> get shortcuts =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
/// Serializes this ClientSettingsModel to a JSON map. /// Serializes this ClientSettingsModel to a JSON map.
@ -89,7 +89,7 @@ abstract class $ClientSettingsModelCopyWith<$Res> {
bool usePosterForLibrary, bool usePosterForLibrary,
String? lastViewedUpdate, String? lastViewedUpdate,
int? libraryPageSize, int? libraryPageSize,
Map<GlobalHotKeys, KeyCombination?> shortcuts}); Map<GlobalHotKeys, KeyCombination> shortcuts});
} }
/// @nodoc /// @nodoc
@ -233,7 +233,7 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
shortcuts: null == shortcuts shortcuts: null == shortcuts
? _value.shortcuts ? _value.shortcuts
: shortcuts // ignore: cast_nullable_to_non_nullable : shortcuts // ignore: cast_nullable_to_non_nullable
as Map<GlobalHotKeys, KeyCombination?>, as Map<GlobalHotKeys, KeyCombination>,
) as $Val); ) as $Val);
} }
} }
@ -271,7 +271,7 @@ abstract class _$$ClientSettingsModelImplCopyWith<$Res>
bool usePosterForLibrary, bool usePosterForLibrary,
String? lastViewedUpdate, String? lastViewedUpdate,
int? libraryPageSize, int? libraryPageSize,
Map<GlobalHotKeys, KeyCombination?> shortcuts}); Map<GlobalHotKeys, KeyCombination> shortcuts});
} }
/// @nodoc /// @nodoc
@ -413,7 +413,7 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
shortcuts: null == shortcuts shortcuts: null == shortcuts
? _value._shortcuts ? _value._shortcuts
: shortcuts // ignore: cast_nullable_to_non_nullable : shortcuts // ignore: cast_nullable_to_non_nullable
as Map<GlobalHotKeys, KeyCombination?>, as Map<GlobalHotKeys, KeyCombination>,
)); ));
} }
} }
@ -447,7 +447,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
this.usePosterForLibrary = false, this.usePosterForLibrary = false,
this.lastViewedUpdate, this.lastViewedUpdate,
this.libraryPageSize, this.libraryPageSize,
final Map<GlobalHotKeys, KeyCombination?> shortcuts = const {}}) final Map<GlobalHotKeys, KeyCombination> shortcuts = const {}})
: _shortcuts = shortcuts, : _shortcuts = shortcuts,
super._(); super._();
@ -521,10 +521,10 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
final String? lastViewedUpdate; final String? lastViewedUpdate;
@override @override
final int? libraryPageSize; final int? libraryPageSize;
final Map<GlobalHotKeys, KeyCombination?> _shortcuts; final Map<GlobalHotKeys, KeyCombination> _shortcuts;
@override @override
@JsonKey() @JsonKey()
Map<GlobalHotKeys, KeyCombination?> get shortcuts { Map<GlobalHotKeys, KeyCombination> get shortcuts {
if (_shortcuts is EqualUnmodifiableMapView) return _shortcuts; if (_shortcuts is EqualUnmodifiableMapView) return _shortcuts;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_shortcuts); return EqualUnmodifiableMapView(_shortcuts);
@ -612,7 +612,7 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
final bool usePosterForLibrary, final bool usePosterForLibrary,
final String? lastViewedUpdate, final String? lastViewedUpdate,
final int? libraryPageSize, final int? libraryPageSize,
final Map<GlobalHotKeys, KeyCombination?> shortcuts}) = final Map<GlobalHotKeys, KeyCombination> shortcuts}) =
_$ClientSettingsModelImpl; _$ClientSettingsModelImpl;
_ClientSettingsModel._() : super._(); _ClientSettingsModel._() : super._();
@ -669,7 +669,7 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
@override @override
int? get libraryPageSize; int? get libraryPageSize;
@override @override
Map<GlobalHotKeys, KeyCombination?> get shortcuts; Map<GlobalHotKeys, KeyCombination> get shortcuts;
/// Create a copy of ClientSettingsModel /// Create a copy of ClientSettingsModel
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View file

@ -49,11 +49,8 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson(
lastViewedUpdate: json['lastViewedUpdate'] as String?, lastViewedUpdate: json['lastViewedUpdate'] as String?,
libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(), libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(),
shortcuts: (json['shortcuts'] as Map<String, dynamic>?)?.map( shortcuts: (json['shortcuts'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry( (k, e) => MapEntry($enumDecode(_$GlobalHotKeysEnumMap, k),
$enumDecode(_$GlobalHotKeysEnumMap, k), KeyCombination.fromJson(e as Map<String, dynamic>)),
e == null
? null
: KeyCombination.fromJson(e as Map<String, dynamic>)),
) ?? ) ??
const {}, const {},
); );

View file

@ -4,31 +4,65 @@ import 'package:flutter/services.dart';
import 'package:freezed_annotation/freezed_annotation.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.freezed.dart';
part 'key_combinations.g.dart'; part 'key_combinations.g.dart';
@Freezed(toJson: true, fromJson: true) @Freezed(toJson: true, fromJson: true, copyWith: true)
class KeyCombination with _$KeyCombination { class KeyCombination with _$KeyCombination {
const KeyCombination._(); const KeyCombination._();
factory KeyCombination({ factory KeyCombination({
@LogicalKeyboardSerializer() LogicalKeyboardKey? key,
@LogicalKeyboardSerializer() LogicalKeyboardKey? modifier, @LogicalKeyboardSerializer() LogicalKeyboardKey? modifier,
@LogicalKeyboardSerializer() required LogicalKeyboardKey key, @LogicalKeyboardSerializer() LogicalKeyboardKey? altKey,
@LogicalKeyboardSerializer() LogicalKeyboardKey? altModifier,
}) = _KeyCombination; }) = _KeyCombination;
factory KeyCombination.fromJson(Map<String, dynamic> json) => _$KeyCombinationFromJson(json); factory KeyCombination.fromJson(Map<String, dynamic> json) => _$KeyCombinationFromJson(json);
@override @override
bool operator ==(covariant other) { 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 @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<LogicalKeyboardKey> shiftKeys = { static final Set<LogicalKeyboardKey> shiftKeys = {
LogicalKeyboardKey.shift, LogicalKeyboardKey.shift,
@ -68,3 +102,26 @@ class LogicalKeyboardSerializer extends JsonConverter<LogicalKeyboardKey, String
return jsonEncode(object.keyId.toString()); return jsonEncode(object.keyId.toString());
} }
} }
extension LogicalKeyExtension on LogicalKeyboardKey {
String get label {
return switch (this) { LogicalKeyboardKey.space => "Space", _ => keyLabel };
}
}
extension KeyMapExtension<T> on Map<T, KeyCombination> {
Map<T, KeyCombination> setOrRemove(MapEntry<T, KeyCombination> newEntry, Map<T, KeyCombination> 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;
}
}
}

View file

@ -20,38 +20,173 @@ KeyCombination _$KeyCombinationFromJson(Map<String, dynamic> json) {
/// @nodoc /// @nodoc
mixin _$KeyCombination { mixin _$KeyCombination {
@LogicalKeyboardSerializer()
LogicalKeyboardKey? get key => throw _privateConstructorUsedError;
@LogicalKeyboardSerializer() @LogicalKeyboardSerializer()
LogicalKeyboardKey? get modifier => throw _privateConstructorUsedError; LogicalKeyboardKey? get modifier => throw _privateConstructorUsedError;
@LogicalKeyboardSerializer() @LogicalKeyboardSerializer()
LogicalKeyboardKey get key => throw _privateConstructorUsedError; LogicalKeyboardKey? get altKey => throw _privateConstructorUsedError;
@LogicalKeyboardSerializer()
LogicalKeyboardKey? get altModifier => throw _privateConstructorUsedError;
/// Serializes this KeyCombination to a JSON map. /// Serializes this KeyCombination to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> 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<KeyCombination> 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 /// @nodoc
@JsonSerializable() @JsonSerializable()
class _$KeyCombinationImpl extends _KeyCombination { class _$KeyCombinationImpl extends _KeyCombination {
_$KeyCombinationImpl( _$KeyCombinationImpl(
{@LogicalKeyboardSerializer() this.modifier, {@LogicalKeyboardSerializer() this.key,
@LogicalKeyboardSerializer() required this.key}) @LogicalKeyboardSerializer() this.modifier,
@LogicalKeyboardSerializer() this.altKey,
@LogicalKeyboardSerializer() this.altModifier})
: super._(); : super._();
factory _$KeyCombinationImpl.fromJson(Map<String, dynamic> json) => factory _$KeyCombinationImpl.fromJson(Map<String, dynamic> json) =>
_$$KeyCombinationImplFromJson(json); _$$KeyCombinationImplFromJson(json);
@override
@LogicalKeyboardSerializer()
final LogicalKeyboardKey? key;
@override @override
@LogicalKeyboardSerializer() @LogicalKeyboardSerializer()
final LogicalKeyboardKey? modifier; final LogicalKeyboardKey? modifier;
@override @override
@LogicalKeyboardSerializer() @LogicalKeyboardSerializer()
final LogicalKeyboardKey key; final LogicalKeyboardKey? altKey;
@override
@LogicalKeyboardSerializer()
final LogicalKeyboardKey? altModifier;
@override @override
String toString() { 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 @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return _$$KeyCombinationImplToJson( return _$$KeyCombinationImplToJson(
@ -62,18 +197,33 @@ class _$KeyCombinationImpl extends _KeyCombination {
abstract class _KeyCombination extends KeyCombination { abstract class _KeyCombination extends KeyCombination {
factory _KeyCombination( factory _KeyCombination(
{@LogicalKeyboardSerializer() final LogicalKeyboardKey? modifier, {@LogicalKeyboardSerializer() final LogicalKeyboardKey? key,
@LogicalKeyboardSerializer() required final LogicalKeyboardKey key}) = @LogicalKeyboardSerializer() final LogicalKeyboardKey? modifier,
@LogicalKeyboardSerializer() final LogicalKeyboardKey? altKey,
@LogicalKeyboardSerializer() final LogicalKeyboardKey? altModifier}) =
_$KeyCombinationImpl; _$KeyCombinationImpl;
_KeyCombination._() : super._(); _KeyCombination._() : super._();
factory _KeyCombination.fromJson(Map<String, dynamic> json) = factory _KeyCombination.fromJson(Map<String, dynamic> json) =
_$KeyCombinationImpl.fromJson; _$KeyCombinationImpl.fromJson;
@override
@LogicalKeyboardSerializer()
LogicalKeyboardKey? get key;
@override @override
@LogicalKeyboardSerializer() @LogicalKeyboardSerializer()
LogicalKeyboardKey? get modifier; LogicalKeyboardKey? get modifier;
@override @override
@LogicalKeyboardSerializer() @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;
} }

View file

@ -8,17 +8,27 @@ part of 'key_combinations.dart';
_$KeyCombinationImpl _$$KeyCombinationImplFromJson(Map<String, dynamic> json) => _$KeyCombinationImpl _$$KeyCombinationImplFromJson(Map<String, dynamic> json) =>
_$KeyCombinationImpl( _$KeyCombinationImpl(
key: _$JsonConverterFromJson<String, LogicalKeyboardKey>(
json['key'], const LogicalKeyboardSerializer().fromJson),
modifier: _$JsonConverterFromJson<String, LogicalKeyboardKey>( modifier: _$JsonConverterFromJson<String, LogicalKeyboardKey>(
json['modifier'], const LogicalKeyboardSerializer().fromJson), json['modifier'], const LogicalKeyboardSerializer().fromJson),
key: const LogicalKeyboardSerializer().fromJson(json['key'] as String), altKey: _$JsonConverterFromJson<String, LogicalKeyboardKey>(
json['altKey'], const LogicalKeyboardSerializer().fromJson),
altModifier: _$JsonConverterFromJson<String, LogicalKeyboardKey>(
json['altModifier'], const LogicalKeyboardSerializer().fromJson),
); );
Map<String, dynamic> _$$KeyCombinationImplToJson( Map<String, dynamic> _$$KeyCombinationImplToJson(
_$KeyCombinationImpl instance) => _$KeyCombinationImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'key': _$JsonConverterToJson<String, LogicalKeyboardKey>(
instance.key, const LogicalKeyboardSerializer().toJson),
'modifier': _$JsonConverterToJson<String, LogicalKeyboardKey>( 'modifier': _$JsonConverterToJson<String, LogicalKeyboardKey>(
instance.modifier, const LogicalKeyboardSerializer().toJson), instance.modifier, const LogicalKeyboardSerializer().toJson),
'key': const LogicalKeyboardSerializer().toJson(instance.key), 'altKey': _$JsonConverterToJson<String, LogicalKeyboardKey>(
instance.altKey, const LogicalKeyboardSerializer().toJson),
'altModifier': _$JsonConverterToJson<String, LogicalKeyboardKey>(
instance.altModifier, const LogicalKeyboardSerializer().toJson),
}; };
Value? _$JsonConverterFromJson<Json, Value>( Value? _$JsonConverterFromJson<Json, Value>(

View file

@ -68,7 +68,7 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
@Default(Bitrate.original) Bitrate maxInternetBitrate, @Default(Bitrate.original) Bitrate maxInternetBitrate,
String? audioDevice, String? audioDevice,
@Default(defaultSegmentSkipValues) Map<MediaSegmentType, SegmentSkip> segmentSkipSettings, @Default(defaultSegmentSkipValues) Map<MediaSegmentType, SegmentSkip> segmentSkipSettings,
@Default({}) Map<VideoHotKeys, KeyCombination?> hotKeys, @Default({}) Map<VideoHotKeys, KeyCombination> hotKeys,
}) = _VideoPlayerSettingsModel; }) = _VideoPlayerSettingsModel;
double get volume => switch (defaultTargetPlatform) { double get volume => switch (defaultTargetPlatform) {
@ -165,14 +165,25 @@ enum AutoNextType {
Map<VideoHotKeys, KeyCombination> get _defaultVideoHotKeys => { Map<VideoHotKeys, KeyCombination> get _defaultVideoHotKeys => {
for (var hotKey in VideoHotKeys.values) for (var hotKey in VideoHotKeys.values)
hotKey: switch (hotKey) { hotKey: switch (hotKey) {
VideoHotKeys.playPause => KeyCombination(key: LogicalKeyboardKey.space), VideoHotKeys.playPause => KeyCombination(
VideoHotKeys.seekForward => KeyCombination(key: LogicalKeyboardKey.arrowRight), key: LogicalKeyboardKey.space,
VideoHotKeys.seekBack => KeyCombination(key: LogicalKeyboardKey.arrowLeft), 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.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.prevVideo => KeyCombination(key: LogicalKeyboardKey.keyP, modifier: LogicalKeyboardKey.shift), VideoHotKeys.prevVideo =>
VideoHotKeys.nextVideo => KeyCombination(key: LogicalKeyboardKey.keyN, modifier: LogicalKeyboardKey.shift), KeyCombination(key: LogicalKeyboardKey.keyP, modifier: LogicalKeyboardKey.shiftLeft),
VideoHotKeys.nextVideo =>
KeyCombination(key: LogicalKeyboardKey.keyN, modifier: LogicalKeyboardKey.shiftLeft),
VideoHotKeys.nextChapter => KeyCombination(key: LogicalKeyboardKey.pageUp), VideoHotKeys.nextChapter => KeyCombination(key: LogicalKeyboardKey.pageUp),
VideoHotKeys.prevChapter => KeyCombination(key: LogicalKeyboardKey.pageDown), VideoHotKeys.prevChapter => KeyCombination(key: LogicalKeyboardKey.pageDown),
VideoHotKeys.fullScreen => KeyCombination(key: LogicalKeyboardKey.keyF), VideoHotKeys.fullScreen => KeyCombination(key: LogicalKeyboardKey.keyF),

View file

@ -37,7 +37,7 @@ mixin _$VideoPlayerSettingsModel {
String? get audioDevice => throw _privateConstructorUsedError; String? get audioDevice => throw _privateConstructorUsedError;
Map<MediaSegmentType, SegmentSkip> get segmentSkipSettings => Map<MediaSegmentType, SegmentSkip> get segmentSkipSettings =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
Map<VideoHotKeys, KeyCombination?> get hotKeys => Map<VideoHotKeys, KeyCombination> get hotKeys =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
/// Serializes this VideoPlayerSettingsModel to a JSON map. /// Serializes this VideoPlayerSettingsModel to a JSON map.
@ -71,7 +71,7 @@ abstract class $VideoPlayerSettingsModelCopyWith<$Res> {
Bitrate maxInternetBitrate, Bitrate maxInternetBitrate,
String? audioDevice, String? audioDevice,
Map<MediaSegmentType, SegmentSkip> segmentSkipSettings, Map<MediaSegmentType, SegmentSkip> segmentSkipSettings,
Map<VideoHotKeys, KeyCombination?> hotKeys}); Map<VideoHotKeys, KeyCombination> hotKeys});
} }
/// @nodoc /// @nodoc
@ -166,7 +166,7 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res,
hotKeys: null == hotKeys hotKeys: null == hotKeys
? _value.hotKeys ? _value.hotKeys
: hotKeys // ignore: cast_nullable_to_non_nullable : hotKeys // ignore: cast_nullable_to_non_nullable
as Map<VideoHotKeys, KeyCombination?>, as Map<VideoHotKeys, KeyCombination>,
) as $Val); ) as $Val);
} }
} }
@ -195,7 +195,7 @@ abstract class _$$VideoPlayerSettingsModelImplCopyWith<$Res>
Bitrate maxInternetBitrate, Bitrate maxInternetBitrate,
String? audioDevice, String? audioDevice,
Map<MediaSegmentType, SegmentSkip> segmentSkipSettings, Map<MediaSegmentType, SegmentSkip> segmentSkipSettings,
Map<VideoHotKeys, KeyCombination?> hotKeys}); Map<VideoHotKeys, KeyCombination> hotKeys});
} }
/// @nodoc /// @nodoc
@ -289,7 +289,7 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res>
hotKeys: null == hotKeys hotKeys: null == hotKeys
? _value._hotKeys ? _value._hotKeys
: hotKeys // ignore: cast_nullable_to_non_nullable : hotKeys // ignore: cast_nullable_to_non_nullable
as Map<VideoHotKeys, KeyCombination?>, as Map<VideoHotKeys, KeyCombination>,
)); ));
} }
} }
@ -314,7 +314,7 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
this.audioDevice, this.audioDevice,
final Map<MediaSegmentType, SegmentSkip> segmentSkipSettings = final Map<MediaSegmentType, SegmentSkip> segmentSkipSettings =
defaultSegmentSkipValues, defaultSegmentSkipValues,
final Map<VideoHotKeys, KeyCombination?> hotKeys = const {}}) final Map<VideoHotKeys, KeyCombination> hotKeys = const {}})
: _allowedOrientations = allowedOrientations, : _allowedOrientations = allowedOrientations,
_segmentSkipSettings = segmentSkipSettings, _segmentSkipSettings = segmentSkipSettings,
_hotKeys = hotKeys, _hotKeys = hotKeys,
@ -377,10 +377,10 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
return EqualUnmodifiableMapView(_segmentSkipSettings); return EqualUnmodifiableMapView(_segmentSkipSettings);
} }
final Map<VideoHotKeys, KeyCombination?> _hotKeys; final Map<VideoHotKeys, KeyCombination> _hotKeys;
@override @override
@JsonKey() @JsonKey()
Map<VideoHotKeys, KeyCombination?> get hotKeys { Map<VideoHotKeys, KeyCombination> get hotKeys {
if (_hotKeys is EqualUnmodifiableMapView) return _hotKeys; if (_hotKeys is EqualUnmodifiableMapView) return _hotKeys;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_hotKeys); return EqualUnmodifiableMapView(_hotKeys);
@ -446,7 +446,7 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel {
final Bitrate maxInternetBitrate, final Bitrate maxInternetBitrate,
final String? audioDevice, final String? audioDevice,
final Map<MediaSegmentType, SegmentSkip> segmentSkipSettings, final Map<MediaSegmentType, SegmentSkip> segmentSkipSettings,
final Map<VideoHotKeys, KeyCombination?> hotKeys}) = final Map<VideoHotKeys, KeyCombination> hotKeys}) =
_$VideoPlayerSettingsModelImpl; _$VideoPlayerSettingsModelImpl;
_VideoPlayerSettingsModel._() : super._(); _VideoPlayerSettingsModel._() : super._();
@ -482,7 +482,7 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel {
@override @override
Map<MediaSegmentType, SegmentSkip> get segmentSkipSettings; Map<MediaSegmentType, SegmentSkip> get segmentSkipSettings;
@override @override
Map<VideoHotKeys, KeyCombination?> get hotKeys; Map<VideoHotKeys, KeyCombination> get hotKeys;
/// Create a copy of VideoPlayerSettingsModel /// Create a copy of VideoPlayerSettingsModel
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View file

@ -39,11 +39,8 @@ _$VideoPlayerSettingsModelImpl _$$VideoPlayerSettingsModelImplFromJson(
) ?? ) ??
defaultSegmentSkipValues, defaultSegmentSkipValues,
hotKeys: (json['hotKeys'] as Map<String, dynamic>?)?.map( hotKeys: (json['hotKeys'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry( (k, e) => MapEntry($enumDecode(_$VideoHotKeysEnumMap, k),
$enumDecode(_$VideoHotKeysEnumMap, k), KeyCombination.fromJson(e as Map<String, dynamic>)),
e == null
? null
: KeyCombination.fromJson(e as Map<String, dynamic>)),
) ?? ) ??
const {}, const {},
); );

View file

@ -60,14 +60,6 @@ class ClientSettingsNotifier extends StateNotifier<ClientSettingsModel> {
void setRequireWifi(bool value) => state = state.copyWith(requireWifi: value); void setRequireWifi(bool value) => state = state.copyWith(requireWifi: value);
void setShortcuts(MapEntry<GlobalHotKeys, KeyCombination?> mapEntry) { void setShortcuts(MapEntry<GlobalHotKeys, KeyCombination> newEntry) =>
final newShortCuts = Map.fromEntries(state.shortcuts.entries); state = state.copyWith(shortcuts: state.shortcuts.setOrRemove(newEntry, state.defaultShortCuts));
newShortCuts.update(
mapEntry.key,
(value) => mapEntry.value,
ifAbsent: () => mapEntry.value,
);
newShortCuts.removeWhere((key, value) => value == null);
state = state.copyWith(shortcuts: newShortCuts);
}
} }

View file

@ -70,15 +70,8 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
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) {
final currentShortcuts = Map.fromEntries(state.hotKeys.entries); state = state.copyWith(hotKeys: state.hotKeys.setOrRemove(newEntry, state.defaultShortCuts));
currentShortcuts.update(
newEntry.key,
(value) => newEntry.value,
ifAbsent: () => newEntry.value,
);
currentShortcuts.removeWhere((key, value) => value == null);
state = state.copyWith(hotKeys: currentShortcuts);
} }
void nextChapter() { void nextChapter() {

View file

@ -19,31 +19,26 @@ List<Widget> buildClientSettingsShortCuts(
SettingsLabelDivider(label: context.localized.shortCuts), SettingsLabelDivider(label: context.localized.shortCuts),
[ [
...GlobalHotKeys.values.map( ...GlobalHotKeys.values.map(
(entry) { (entry) => Padding(
final currentEntry = clientSettings.shortcuts[entry]; padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
final defaultEntry = clientSettings.defaultShortCuts[entry]!; child: Row(
return Padding( children: [
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), Expanded(
child: Row( child: Text(
children: [ entry.label(context),
Expanded( style: Theme.of(context).textTheme.titleLarge,
child: Text(
entry.label(context),
style: Theme.of(context).textTheme.titleLarge,
),
), ),
Flexible( ),
child: KeyCombinationWidget( Flexible(
currentKey: currentEntry, child: KeyCombinationWidget(
defaultKey: defaultEntry, currentKey: clientSettings.shortcuts[entry],
onChanged: (value) => defaultKey: clientSettings.defaultShortCuts[entry]!,
ref.read(clientSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)), onChanged: (value) => ref.read(clientSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)),
), ),
) )
], ],
), ),
); ),
},
) )
], ],
); );

View file

@ -207,31 +207,27 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
), ),
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer)
...VideoHotKeys.values.map( ...VideoHotKeys.values.map(
(entry) { (entry) => Padding(
final currentEntry = videoSettings.hotKeys[entry]; padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
final defaultEntry = videoSettings.defaultShortCuts[entry]!; child: Row(
return Padding( children: [
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), Expanded(
child: Row( child: Text(
children: [ entry.label(context),
Expanded( style: Theme.of(context).textTheme.titleLarge,
child: Text(
entry.label(context),
style: Theme.of(context).textTheme.titleLarge,
),
), ),
Flexible( ),
child: KeyCombinationWidget( Flexible(
currentKey: currentEntry, child: KeyCombinationWidget(
defaultKey: defaultEntry, currentKey: videoSettings.hotKeys[entry],
onChanged: (value) => defaultKey: videoSettings.defaultShortCuts[entry]!,
ref.read(videoPlayerSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)), onChanged: (value) =>
), ref.read(videoPlayerSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)),
) ),
], )
), ],
); ),
}, ),
) )
], ],
), ),

View file

@ -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/client_settings_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/theme.dart';
import 'package:fladder/util/localization_helper.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? currentKey;
final KeyCombination defaultKey; 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 @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<KeyCombinationWidget> { 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<KeyListenerWidget> {
final focusNode = FocusNode(); final focusNode = FocusNode();
bool _isListening = false; bool _isListening = false;
bool _showClearButton = false;
LogicalKeyboardKey? _pressedKey; LogicalKeyboardKey? _pressedKey;
LogicalKeyboardKey? _pressedModifier; LogicalKeyboardKey? _pressedModifier;
void setIsListening(bool value) {
changingShortCut = value;
_isListening = value;
}
@override @override
void dispose() { void dispose() {
changingShortCut = false;
_isListening = false; _isListening = false;
_pressedKey = null; _pressedKey = null;
_pressedModifier = null; _pressedModifier = null;
@ -36,8 +109,9 @@ class KeyCombinationWidgetState extends ConsumerState<KeyCombinationWidget> {
} }
void _startListening() { void _startListening() {
if (changingShortCut) return;
setState(() { setState(() {
_isListening = true; setIsListening(true);
_pressedKey = null; _pressedKey = null;
_pressedModifier = null; _pressedModifier = null;
}); });
@ -45,17 +119,13 @@ class KeyCombinationWidgetState extends ConsumerState<KeyCombinationWidget> {
void _stopListening() { void _stopListening() {
setState(() { setState(() {
_isListening = false; setIsListening(false);
if (_pressedKey != null) { if (_pressedKey != null) {
final newKeyComb = KeyCombination( final newKeyComb = KeyCombination(
key: _pressedKey!, key: _pressedKey!,
modifier: _pressedModifier, modifier: _pressedModifier,
); );
if (newKeyComb == widget.defaultKey) { widget.onChanged(newKeyComb);
widget.onChanged(null);
} else {
widget.onChanged(newKeyComb);
}
} }
_pressedKey = null; _pressedKey = null;
_pressedModifier = null; _pressedModifier = null;
@ -76,7 +146,7 @@ class KeyCombinationWidgetState extends ConsumerState<KeyCombinationWidget> {
} else { } else {
final currentHotKey = KeyCombination(key: event.logicalKey, modifier: _pressedModifier); final currentHotKey = KeyCombination(key: event.logicalKey, modifier: _pressedModifier);
bool isExistingHotkey = activeHotKeys.any((element) { bool isExistingHotkey = activeHotKeys.any((element) {
return element == currentHotKey && currentHotKey != (widget.currentKey ?? widget.defaultKey); return element.containsSameSet(currentHotKey) && currentHotKey != widget.currentKey;
}); });
if (!isExistingHotkey) { if (!isExistingHotkey) {
@ -103,72 +173,78 @@ class KeyCombinationWidgetState extends ConsumerState<KeyCombinationWidget> {
} }
} }
void showClearButton(bool value) {
setState(() {
_showClearButton = value;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final currentModifier = final currentModifier = _pressedModifier ?? (widget.currentKey?.modifier);
_pressedModifier ?? (widget.currentKey != null ? widget.currentKey?.modifier : widget.defaultKey.modifier); final currentKey = _pressedKey ?? widget.currentKey?.key;
final currentKey = _pressedKey ?? (widget.currentKey?.key ?? widget.defaultKey.key); final currentHotKey = currentKey == null ? null : KeyCombination(key: currentKey, modifier: currentModifier);
final currentHotKey = KeyCombination(key: currentKey, modifier: currentModifier); return MouseRegion(
return Row( onEnter: (event) => showClearButton(true),
mainAxisAlignment: MainAxisAlignment.end, onExit: (event) => showClearButton(false),
children: [ child: ClipRRect(
ConstrainedBox( borderRadius: FladderTheme.smallShape.borderRadius,
constraints: const BoxConstraints(minWidth: 50), child: InkWell(
child: InkWell( onTap: _isListening ? _stopListening : _startListening,
onTap: _isListening ? null : _startListening, onSecondaryTap: () {
child: Card( setState(() {
color: Theme.of(context).colorScheme.primaryContainer, setIsListening(false);
child: Padding( widget.onChanged(null);
padding: const EdgeInsets.only(left: 12), });
child: Row( },
mainAxisAlignment: MainAxisAlignment.end, child: Container(
mainAxisSize: MainAxisSize.min, color: Theme.of(context).colorScheme.primaryContainer,
spacing: 6, child: AnimatedSize(
children: [ duration: const Duration(milliseconds: 125),
Text(currentHotKey.label), child: Stack(
AnimatedSwitcher( alignment: Alignment.center,
duration: const Duration(milliseconds: 250), children: [
child: _isListening Padding(
? KeyboardListener( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
focusNode: focusNode, child: Row(
autofocus: true, spacing: 8,
onKeyEvent: _handleKeyEvent, children: [
child: const Padding( if (_showClearButton && currentHotKey != null)
padding: EdgeInsets.all(8.0), GestureDetector(
child: SizedBox( onTap: () {
height: 24, setIsListening(false);
width: 24, widget.onChanged(null);
child: Center( },
child: CircularProgressIndicator(), child: const Icon(
), IconsaxPlusLinear.trash,
), size: 17,
),
)
: IconButton(
onPressed: widget.currentKey == null
? null
: () {
_pressedKey = null;
_pressedModifier = null;
widget.onChanged(null);
},
iconSize: 24,
icon: const Icon(IconsaxPlusBold.broom),
), ),
) ),
], 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 };
}
}

View file

@ -1,9 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:fladder/models/settings/key_combinations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class InputHandler<T> extends StatefulWidget { import 'package:fladder/models/settings/key_combinations.dart';
import 'package:fladder/screens/settings/widgets/key_listener.dart';
class InputHandler<T> extends ConsumerStatefulWidget {
final bool autoFocus; final bool autoFocus;
final KeyEventResult Function(FocusNode node, KeyEvent event)? onKeyEvent; final KeyEventResult Function(FocusNode node, KeyEvent event)? onKeyEvent;
final bool Function(T result)? keyMapResult; final bool Function(T result)? keyMapResult;
@ -19,10 +22,10 @@ class InputHandler<T> extends StatefulWidget {
}); });
@override @override
State<InputHandler> createState() => _InputHandlerState<T>(); ConsumerState<InputHandler> createState() => _InputHandlerState<T>();
} }
class _InputHandlerState<T> extends State<InputHandler<T>> { class _InputHandlerState<T> extends ConsumerState<InputHandler<T>> {
final focusNode = FocusNode(); final focusNode = FocusNode();
LogicalKeyboardKey? pressedModifier; LogicalKeyboardKey? pressedModifier;
@ -50,6 +53,8 @@ class _InputHandlerState<T> extends State<InputHandler<T>> {
} }
KeyEventResult _onKey(KeyEvent value) { KeyEventResult _onKey(KeyEvent value) {
if (changingShortCut) return KeyEventResult.ignored;
final keyMap = widget.keyMap?.entries.nonNulls.toList() ?? []; final keyMap = widget.keyMap?.entries.nonNulls.toList() ?? [];
if (value is KeyDownEvent || value is KeyRepeatEvent) { if (value is KeyDownEvent || value is KeyRepeatEvent) {
if (KeyCombination.modifierKeys.contains(value.logicalKey)) { if (KeyCombination.modifierKeys.contains(value.logicalKey)) {
@ -63,7 +68,11 @@ class _InputHandlerState<T> extends State<InputHandler<T>> {
bool isMainKeyPressed = value.logicalKey == keyCombination.key; bool isMainKeyPressed = value.logicalKey == keyCombination.key;
bool isModifierKeyPressed = pressedModifier == keyCombination.modifier; 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) { if (widget.keyMapResult?.call(hotKey) ?? false) {
return KeyEventResult.handled; return KeyEventResult.handled;
} else { } else {