mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08: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
|
|
@ -1139,7 +1139,7 @@
|
||||||
},
|
},
|
||||||
"noVideoPlayerOptions": "The selected backend has no options",
|
"noVideoPlayerOptions": "The selected backend has no options",
|
||||||
"mdkExperimental": "MDK is still in a experimental stage",
|
"mdkExperimental": "MDK is still in a experimental stage",
|
||||||
"skipButtonLabel": "(S)kip {segment}",
|
"skipButtonLabel": "Skip {segment}",
|
||||||
"@skipButtonLabel": {
|
"@skipButtonLabel": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"segment": {
|
"segment": {
|
||||||
|
|
@ -1292,5 +1292,29 @@
|
||||||
"syncAllFiles": "Sync all files",
|
"syncAllFiles": "Sync all files",
|
||||||
"usePostersForLibraryIconsTitle": "Show posters for library icons",
|
"usePostersForLibraryIconsTitle": "Show posters for library icons",
|
||||||
"usePostersForLibraryIconsDesc": "Show posters instead of icons for libraries",
|
"usePostersForLibraryIconsDesc": "Show posters instead of icons for libraries",
|
||||||
"offline": "Offline"
|
"offline": "Offline",
|
||||||
|
"shortCuts": "Shortcuts",
|
||||||
|
"skipForwardLength": "Skip forward length",
|
||||||
|
"skipBackLength": "Skip back length",
|
||||||
|
"playPause": "Play/Pause",
|
||||||
|
"seekForward": "Seek Forward",
|
||||||
|
"seekBack": "Seek Back",
|
||||||
|
"mute": "Mute",
|
||||||
|
"volumeUp": "Volume Up",
|
||||||
|
"volumeDown": "Volume Down",
|
||||||
|
"nextVideo": "Next Video",
|
||||||
|
"prevVideo": "Previous Video",
|
||||||
|
"nextChapter": "Next Chapter",
|
||||||
|
"prevChapter": "Previous Chapter",
|
||||||
|
"fullScreen": "Full Screen",
|
||||||
|
"skipMediaSegment": "Skip Media Segment",
|
||||||
|
"exit": "Exit",
|
||||||
|
"shortCutAlreadyAssigned": "Shortcut '{hotKey}' already assigned",
|
||||||
|
"@shortCutAlreadyAssigned": {
|
||||||
|
"placeholders": {
|
||||||
|
"hotKey": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -35,6 +35,7 @@ class AccountModel with _$AccountModel {
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
|
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false) ServerConfiguration? serverConfiguration,
|
@JsonKey(includeFromJson: false, includeToJson: false) ServerConfiguration? serverConfiguration,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false) UserConfiguration? userConfiguration,
|
@JsonKey(includeFromJson: false, includeToJson: false) UserConfiguration? userConfiguration,
|
||||||
|
UserSettings? userSettings,
|
||||||
}) = _AccountModel;
|
}) = _AccountModel;
|
||||||
|
|
||||||
factory AccountModel.fromJson(Map<String, dynamic> json) => _$AccountModelFromJson(json);
|
factory AccountModel.fromJson(Map<String, dynamic> json) => _$AccountModelFromJson(json);
|
||||||
|
|
@ -50,6 +51,16 @@ class AccountModel with _$AccountModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Freezed(copyWith: true)
|
||||||
|
class UserSettings with _$UserSettings {
|
||||||
|
factory UserSettings({
|
||||||
|
@Default(Duration(seconds: 30)) Duration skipForwardDuration,
|
||||||
|
@Default(Duration(seconds: 10)) Duration skipBackDuration,
|
||||||
|
}) = _UserSettings;
|
||||||
|
|
||||||
|
factory UserSettings.fromJson(Map<String, dynamic> json) => _$UserSettingsFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
enum Authentication {
|
enum Authentication {
|
||||||
autoLogin(0),
|
autoLogin(0),
|
||||||
biometrics(1),
|
biometrics(1),
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ mixin _$AccountModel {
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
UserConfiguration? get userConfiguration =>
|
UserConfiguration? get userConfiguration =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
|
UserSettings? get userSettings => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this AccountModel to a JSON map.
|
/// Serializes this AccountModel to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
@ -73,7 +74,10 @@ abstract class $AccountModelCopyWith<$Res> {
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
ServerConfiguration? serverConfiguration,
|
ServerConfiguration? serverConfiguration,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
UserConfiguration? userConfiguration});
|
UserConfiguration? userConfiguration,
|
||||||
|
UserSettings? userSettings});
|
||||||
|
|
||||||
|
$UserSettingsCopyWith<$Res>? get userSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
@ -105,6 +109,7 @@ class _$AccountModelCopyWithImpl<$Res, $Val extends AccountModel>
|
||||||
Object? policy = freezed,
|
Object? policy = freezed,
|
||||||
Object? serverConfiguration = freezed,
|
Object? serverConfiguration = freezed,
|
||||||
Object? userConfiguration = freezed,
|
Object? userConfiguration = freezed,
|
||||||
|
Object? userSettings = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
name: null == name
|
name: null == name
|
||||||
|
|
@ -163,8 +168,26 @@ class _$AccountModelCopyWithImpl<$Res, $Val extends AccountModel>
|
||||||
? _value.userConfiguration
|
? _value.userConfiguration
|
||||||
: userConfiguration // ignore: cast_nullable_to_non_nullable
|
: userConfiguration // ignore: cast_nullable_to_non_nullable
|
||||||
as UserConfiguration?,
|
as UserConfiguration?,
|
||||||
|
userSettings: freezed == userSettings
|
||||||
|
? _value.userSettings
|
||||||
|
: userSettings // ignore: cast_nullable_to_non_nullable
|
||||||
|
as UserSettings?,
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of AccountModel
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$UserSettingsCopyWith<$Res>? get userSettings {
|
||||||
|
if (_value.userSettings == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $UserSettingsCopyWith<$Res>(_value.userSettings!, (value) {
|
||||||
|
return _then(_value.copyWith(userSettings: value) as $Val);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
@ -191,7 +214,11 @@ abstract class _$$AccountModelImplCopyWith<$Res>
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
ServerConfiguration? serverConfiguration,
|
ServerConfiguration? serverConfiguration,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
UserConfiguration? userConfiguration});
|
UserConfiguration? userConfiguration,
|
||||||
|
UserSettings? userSettings});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$UserSettingsCopyWith<$Res>? get userSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
@ -221,6 +248,7 @@ class __$$AccountModelImplCopyWithImpl<$Res>
|
||||||
Object? policy = freezed,
|
Object? policy = freezed,
|
||||||
Object? serverConfiguration = freezed,
|
Object? serverConfiguration = freezed,
|
||||||
Object? userConfiguration = freezed,
|
Object? userConfiguration = freezed,
|
||||||
|
Object? userSettings = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$AccountModelImpl(
|
return _then(_$AccountModelImpl(
|
||||||
name: null == name
|
name: null == name
|
||||||
|
|
@ -279,6 +307,10 @@ class __$$AccountModelImplCopyWithImpl<$Res>
|
||||||
? _value.userConfiguration
|
? _value.userConfiguration
|
||||||
: userConfiguration // ignore: cast_nullable_to_non_nullable
|
: userConfiguration // ignore: cast_nullable_to_non_nullable
|
||||||
as UserConfiguration?,
|
as UserConfiguration?,
|
||||||
|
userSettings: freezed == userSettings
|
||||||
|
? _value.userSettings
|
||||||
|
: userSettings // ignore: cast_nullable_to_non_nullable
|
||||||
|
as UserSettings?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -302,7 +334,8 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
this.serverConfiguration,
|
this.serverConfiguration,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
this.userConfiguration})
|
this.userConfiguration,
|
||||||
|
this.userSettings})
|
||||||
: _latestItemsExcludes = latestItemsExcludes,
|
: _latestItemsExcludes = latestItemsExcludes,
|
||||||
_searchQueryHistory = searchQueryHistory,
|
_searchQueryHistory = searchQueryHistory,
|
||||||
_savedFilters = savedFilters,
|
_savedFilters = savedFilters,
|
||||||
|
|
@ -368,10 +401,12 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
|
||||||
@override
|
@override
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
final UserConfiguration? userConfiguration;
|
final UserConfiguration? userConfiguration;
|
||||||
|
@override
|
||||||
|
final UserSettings? userSettings;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||||
return 'AccountModel(name: $name, id: $id, avatar: $avatar, lastUsed: $lastUsed, authMethod: $authMethod, localPin: $localPin, credentials: $credentials, latestItemsExcludes: $latestItemsExcludes, searchQueryHistory: $searchQueryHistory, quickConnectState: $quickConnectState, savedFilters: $savedFilters, policy: $policy, serverConfiguration: $serverConfiguration, userConfiguration: $userConfiguration)';
|
return 'AccountModel(name: $name, id: $id, avatar: $avatar, lastUsed: $lastUsed, authMethod: $authMethod, localPin: $localPin, credentials: $credentials, latestItemsExcludes: $latestItemsExcludes, searchQueryHistory: $searchQueryHistory, quickConnectState: $quickConnectState, savedFilters: $savedFilters, policy: $policy, serverConfiguration: $serverConfiguration, userConfiguration: $userConfiguration, userSettings: $userSettings)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -392,7 +427,8 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
|
||||||
..add(DiagnosticsProperty('savedFilters', savedFilters))
|
..add(DiagnosticsProperty('savedFilters', savedFilters))
|
||||||
..add(DiagnosticsProperty('policy', policy))
|
..add(DiagnosticsProperty('policy', policy))
|
||||||
..add(DiagnosticsProperty('serverConfiguration', serverConfiguration))
|
..add(DiagnosticsProperty('serverConfiguration', serverConfiguration))
|
||||||
..add(DiagnosticsProperty('userConfiguration', userConfiguration));
|
..add(DiagnosticsProperty('userConfiguration', userConfiguration))
|
||||||
|
..add(DiagnosticsProperty('userSettings', userSettings));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a copy of AccountModel
|
/// Create a copy of AccountModel
|
||||||
|
|
@ -429,7 +465,8 @@ abstract class _AccountModel extends AccountModel {
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
final ServerConfiguration? serverConfiguration,
|
final ServerConfiguration? serverConfiguration,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
final UserConfiguration? userConfiguration}) = _$AccountModelImpl;
|
final UserConfiguration? userConfiguration,
|
||||||
|
final UserSettings? userSettings}) = _$AccountModelImpl;
|
||||||
const _AccountModel._() : super._();
|
const _AccountModel._() : super._();
|
||||||
|
|
||||||
factory _AccountModel.fromJson(Map<String, dynamic> json) =
|
factory _AccountModel.fromJson(Map<String, dynamic> json) =
|
||||||
|
|
@ -466,6 +503,8 @@ abstract class _AccountModel extends AccountModel {
|
||||||
@override
|
@override
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
UserConfiguration? get userConfiguration;
|
UserConfiguration? get userConfiguration;
|
||||||
|
@override
|
||||||
|
UserSettings? get userSettings;
|
||||||
|
|
||||||
/// Create a copy of AccountModel
|
/// Create a copy of AccountModel
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
|
@ -474,3 +513,170 @@ abstract class _AccountModel extends AccountModel {
|
||||||
_$$AccountModelImplCopyWith<_$AccountModelImpl> get copyWith =>
|
_$$AccountModelImplCopyWith<_$AccountModelImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UserSettings _$UserSettingsFromJson(Map<String, dynamic> json) {
|
||||||
|
return _UserSettings.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$UserSettings {
|
||||||
|
Duration get skipForwardDuration => throw _privateConstructorUsedError;
|
||||||
|
Duration get skipBackDuration => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this UserSettings to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of UserSettings
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$UserSettingsCopyWith<UserSettings> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $UserSettingsCopyWith<$Res> {
|
||||||
|
factory $UserSettingsCopyWith(
|
||||||
|
UserSettings value, $Res Function(UserSettings) then) =
|
||||||
|
_$UserSettingsCopyWithImpl<$Res, UserSettings>;
|
||||||
|
@useResult
|
||||||
|
$Res call({Duration skipForwardDuration, Duration skipBackDuration});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$UserSettingsCopyWithImpl<$Res, $Val extends UserSettings>
|
||||||
|
implements $UserSettingsCopyWith<$Res> {
|
||||||
|
_$UserSettingsCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of UserSettings
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? skipForwardDuration = null,
|
||||||
|
Object? skipBackDuration = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
skipForwardDuration: null == skipForwardDuration
|
||||||
|
? _value.skipForwardDuration
|
||||||
|
: skipForwardDuration // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Duration,
|
||||||
|
skipBackDuration: null == skipBackDuration
|
||||||
|
? _value.skipBackDuration
|
||||||
|
: skipBackDuration // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Duration,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$UserSettingsImplCopyWith<$Res>
|
||||||
|
implements $UserSettingsCopyWith<$Res> {
|
||||||
|
factory _$$UserSettingsImplCopyWith(
|
||||||
|
_$UserSettingsImpl value, $Res Function(_$UserSettingsImpl) then) =
|
||||||
|
__$$UserSettingsImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({Duration skipForwardDuration, Duration skipBackDuration});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$UserSettingsImplCopyWithImpl<$Res>
|
||||||
|
extends _$UserSettingsCopyWithImpl<$Res, _$UserSettingsImpl>
|
||||||
|
implements _$$UserSettingsImplCopyWith<$Res> {
|
||||||
|
__$$UserSettingsImplCopyWithImpl(
|
||||||
|
_$UserSettingsImpl _value, $Res Function(_$UserSettingsImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of UserSettings
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? skipForwardDuration = null,
|
||||||
|
Object? skipBackDuration = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$UserSettingsImpl(
|
||||||
|
skipForwardDuration: null == skipForwardDuration
|
||||||
|
? _value.skipForwardDuration
|
||||||
|
: skipForwardDuration // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Duration,
|
||||||
|
skipBackDuration: null == skipBackDuration
|
||||||
|
? _value.skipBackDuration
|
||||||
|
: skipBackDuration // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Duration,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$UserSettingsImpl with DiagnosticableTreeMixin implements _UserSettings {
|
||||||
|
_$UserSettingsImpl(
|
||||||
|
{this.skipForwardDuration = const Duration(seconds: 30),
|
||||||
|
this.skipBackDuration = const Duration(seconds: 10)});
|
||||||
|
|
||||||
|
factory _$UserSettingsImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$UserSettingsImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final Duration skipForwardDuration;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final Duration skipBackDuration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||||
|
return 'UserSettings(skipForwardDuration: $skipForwardDuration, skipBackDuration: $skipBackDuration)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties
|
||||||
|
..add(DiagnosticsProperty('type', 'UserSettings'))
|
||||||
|
..add(DiagnosticsProperty('skipForwardDuration', skipForwardDuration))
|
||||||
|
..add(DiagnosticsProperty('skipBackDuration', skipBackDuration));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of UserSettings
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$UserSettingsImplCopyWith<_$UserSettingsImpl> get copyWith =>
|
||||||
|
__$$UserSettingsImplCopyWithImpl<_$UserSettingsImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$UserSettingsImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _UserSettings implements UserSettings {
|
||||||
|
factory _UserSettings(
|
||||||
|
{final Duration skipForwardDuration,
|
||||||
|
final Duration skipBackDuration}) = _$UserSettingsImpl;
|
||||||
|
|
||||||
|
factory _UserSettings.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$UserSettingsImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get skipForwardDuration;
|
||||||
|
@override
|
||||||
|
Duration get skipBackDuration;
|
||||||
|
|
||||||
|
/// Create a copy of UserSettings
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$UserSettingsImplCopyWith<_$UserSettingsImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ _$AccountModelImpl _$$AccountModelImplFromJson(Map<String, dynamic> json) =>
|
||||||
LibraryFiltersModel.fromJson(e as Map<String, dynamic>))
|
LibraryFiltersModel.fromJson(e as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
const [],
|
const [],
|
||||||
|
userSettings: json['userSettings'] == null
|
||||||
|
? null
|
||||||
|
: UserSettings.fromJson(json['userSettings'] as Map<String, dynamic>),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$AccountModelImplToJson(_$AccountModelImpl instance) =>
|
Map<String, dynamic> _$$AccountModelImplToJson(_$AccountModelImpl instance) =>
|
||||||
|
|
@ -46,6 +49,7 @@ Map<String, dynamic> _$$AccountModelImplToJson(_$AccountModelImpl instance) =>
|
||||||
'searchQueryHistory': instance.searchQueryHistory,
|
'searchQueryHistory': instance.searchQueryHistory,
|
||||||
'quickConnectState': instance.quickConnectState,
|
'quickConnectState': instance.quickConnectState,
|
||||||
'savedFilters': instance.savedFilters,
|
'savedFilters': instance.savedFilters,
|
||||||
|
'userSettings': instance.userSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$AuthenticationEnumMap = {
|
const _$AuthenticationEnumMap = {
|
||||||
|
|
@ -54,3 +58,20 @@ const _$AuthenticationEnumMap = {
|
||||||
Authentication.passcode: 'passcode',
|
Authentication.passcode: 'passcode',
|
||||||
Authentication.none: 'none',
|
Authentication.none: 'none',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_$UserSettingsImpl _$$UserSettingsImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UserSettingsImpl(
|
||||||
|
skipForwardDuration: json['skipForwardDuration'] == null
|
||||||
|
? const Duration(seconds: 30)
|
||||||
|
: Duration(
|
||||||
|
microseconds: (json['skipForwardDuration'] as num).toInt()),
|
||||||
|
skipBackDuration: json['skipBackDuration'] == null
|
||||||
|
? const Duration(seconds: 10)
|
||||||
|
: Duration(microseconds: (json['skipBackDuration'] as num).toInt()),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$UserSettingsImplToJson(_$UserSettingsImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'skipForwardDuration': instance.skipForwardDuration.inMicroseconds,
|
||||||
|
'skipBackDuration': instance.skipBackDuration.inMicroseconds,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,35 @@ import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/settings/key_combinations.dart';
|
||||||
import 'package:fladder/util/custom_color_themes.dart';
|
import 'package:fladder/util/custom_color_themes.dart';
|
||||||
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
|
|
||||||
part 'client_settings_model.freezed.dart';
|
part 'client_settings_model.freezed.dart';
|
||||||
part 'client_settings_model.g.dart';
|
part 'client_settings_model.g.dart';
|
||||||
|
|
||||||
|
enum GlobalHotKeys {
|
||||||
|
search,
|
||||||
|
exit;
|
||||||
|
|
||||||
|
const GlobalHotKeys();
|
||||||
|
|
||||||
|
String label(BuildContext context) {
|
||||||
|
return switch (this) {
|
||||||
|
GlobalHotKeys.search => context.localized.search,
|
||||||
|
GlobalHotKeys.exit => context.localized.exitFladderTitle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Freezed(copyWith: true)
|
@Freezed(copyWith: true)
|
||||||
class ClientSettingsModel with _$ClientSettingsModel {
|
class ClientSettingsModel with _$ClientSettingsModel {
|
||||||
const ClientSettingsModel._();
|
const ClientSettingsModel._();
|
||||||
|
|
||||||
factory ClientSettingsModel({
|
factory ClientSettingsModel({
|
||||||
String? syncPath,
|
String? syncPath,
|
||||||
@Default(Vector2(x: 0, y: 0)) Vector2 position,
|
@Default(Vector2(x: 0, y: 0)) Vector2 position,
|
||||||
|
|
@ -39,10 +57,16 @@ 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,
|
||||||
}) = _ClientSettingsModel;
|
}) = _ClientSettingsModel;
|
||||||
|
|
||||||
factory ClientSettingsModel.fromJson(Map<String, dynamic> json) => _$ClientSettingsModelFromJson(json);
|
factory ClientSettingsModel.fromJson(Map<String, dynamic> json) => _$ClientSettingsModelFromJson(json);
|
||||||
|
|
||||||
|
Map<GlobalHotKeys, KeyCombination> get currentShortcuts =>
|
||||||
|
_defaultGlobalHotKeys.map((key, value) => MapEntry(key, shortcuts[key] ?? value));
|
||||||
|
|
||||||
|
Map<GlobalHotKeys, KeyCombination> get defaultShortCuts => _defaultGlobalHotKeys;
|
||||||
|
|
||||||
Brightness statusBarBrightness(BuildContext context) {
|
Brightness statusBarBrightness(BuildContext context) {
|
||||||
return switch (themeMode) {
|
return switch (themeMode) {
|
||||||
ThemeMode.dark => Brightness.light,
|
ThemeMode.dark => Brightness.light,
|
||||||
|
|
@ -132,3 +156,12 @@ class Vector2 {
|
||||||
|
|
||||||
static Vector2 fromPosition(Offset windowPosition) => Vector2(x: windowPosition.dx, y: windowPosition.dy);
|
static Vector2 fromPosition(Offset windowPosition) => Vector2(x: windowPosition.dx, y: windowPosition.dy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<GlobalHotKeys, KeyCombination> get _defaultGlobalHotKeys => {
|
||||||
|
for (var hotKey in GlobalHotKeys.values)
|
||||||
|
hotKey: switch (hotKey) {
|
||||||
|
GlobalHotKeys.search =>
|
||||||
|
KeyCombination(key: LogicalKeyboardKey.keyK, modifier: LogicalKeyboardKey.controlLeft),
|
||||||
|
GlobalHotKeys.exit => KeyCombination(key: LogicalKeyboardKey.keyQ, modifier: LogicalKeyboardKey.controlLeft),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ 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 =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this ClientSettingsModel to a JSON map.
|
/// Serializes this ClientSettingsModel to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
@ -86,7 +88,8 @@ abstract class $ClientSettingsModelCopyWith<$Res> {
|
||||||
bool checkForUpdates,
|
bool checkForUpdates,
|
||||||
bool usePosterForLibrary,
|
bool usePosterForLibrary,
|
||||||
String? lastViewedUpdate,
|
String? lastViewedUpdate,
|
||||||
int? libraryPageSize});
|
int? libraryPageSize,
|
||||||
|
Map<GlobalHotKeys, KeyCombination?> shortcuts});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
@ -128,6 +131,7 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
||||||
Object? usePosterForLibrary = null,
|
Object? usePosterForLibrary = null,
|
||||||
Object? lastViewedUpdate = freezed,
|
Object? lastViewedUpdate = freezed,
|
||||||
Object? libraryPageSize = freezed,
|
Object? libraryPageSize = freezed,
|
||||||
|
Object? shortcuts = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
syncPath: freezed == syncPath
|
syncPath: freezed == syncPath
|
||||||
|
|
@ -226,6 +230,10 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
||||||
? _value.libraryPageSize
|
? _value.libraryPageSize
|
||||||
: libraryPageSize // ignore: cast_nullable_to_non_nullable
|
: libraryPageSize // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,
|
as int?,
|
||||||
|
shortcuts: null == shortcuts
|
||||||
|
? _value.shortcuts
|
||||||
|
: shortcuts // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<GlobalHotKeys, KeyCombination?>,
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +270,8 @@ abstract class _$$ClientSettingsModelImplCopyWith<$Res>
|
||||||
bool checkForUpdates,
|
bool checkForUpdates,
|
||||||
bool usePosterForLibrary,
|
bool usePosterForLibrary,
|
||||||
String? lastViewedUpdate,
|
String? lastViewedUpdate,
|
||||||
int? libraryPageSize});
|
int? libraryPageSize,
|
||||||
|
Map<GlobalHotKeys, KeyCombination?> shortcuts});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
@ -302,6 +311,7 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
||||||
Object? usePosterForLibrary = null,
|
Object? usePosterForLibrary = null,
|
||||||
Object? lastViewedUpdate = freezed,
|
Object? lastViewedUpdate = freezed,
|
||||||
Object? libraryPageSize = freezed,
|
Object? libraryPageSize = freezed,
|
||||||
|
Object? shortcuts = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$ClientSettingsModelImpl(
|
return _then(_$ClientSettingsModelImpl(
|
||||||
syncPath: freezed == syncPath
|
syncPath: freezed == syncPath
|
||||||
|
|
@ -400,6 +410,10 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
||||||
? _value.libraryPageSize
|
? _value.libraryPageSize
|
||||||
: libraryPageSize // ignore: cast_nullable_to_non_nullable
|
: libraryPageSize // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,
|
as int?,
|
||||||
|
shortcuts: null == shortcuts
|
||||||
|
? _value._shortcuts
|
||||||
|
: shortcuts // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<GlobalHotKeys, KeyCombination?>,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -432,8 +446,10 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
this.checkForUpdates = true,
|
this.checkForUpdates = true,
|
||||||
this.usePosterForLibrary = false,
|
this.usePosterForLibrary = false,
|
||||||
this.lastViewedUpdate,
|
this.lastViewedUpdate,
|
||||||
this.libraryPageSize})
|
this.libraryPageSize,
|
||||||
: super._();
|
final Map<GlobalHotKeys, KeyCombination?> shortcuts = const {}})
|
||||||
|
: _shortcuts = shortcuts,
|
||||||
|
super._();
|
||||||
|
|
||||||
factory _$ClientSettingsModelImpl.fromJson(Map<String, dynamic> json) =>
|
factory _$ClientSettingsModelImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$ClientSettingsModelImplFromJson(json);
|
_$$ClientSettingsModelImplFromJson(json);
|
||||||
|
|
@ -505,10 +521,18 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
final String? lastViewedUpdate;
|
final String? lastViewedUpdate;
|
||||||
@override
|
@override
|
||||||
final int? libraryPageSize;
|
final int? libraryPageSize;
|
||||||
|
final Map<GlobalHotKeys, KeyCombination?> _shortcuts;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<GlobalHotKeys, KeyCombination?> get shortcuts {
|
||||||
|
if (_shortcuts is EqualUnmodifiableMapView) return _shortcuts;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_shortcuts);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||||
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundPosters: $backgroundPosters, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize)';
|
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundPosters: $backgroundPosters, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize, shortcuts: $shortcuts)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -541,7 +565,8 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
..add(DiagnosticsProperty('checkForUpdates', checkForUpdates))
|
..add(DiagnosticsProperty('checkForUpdates', checkForUpdates))
|
||||||
..add(DiagnosticsProperty('usePosterForLibrary', usePosterForLibrary))
|
..add(DiagnosticsProperty('usePosterForLibrary', usePosterForLibrary))
|
||||||
..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate))
|
..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate))
|
||||||
..add(DiagnosticsProperty('libraryPageSize', libraryPageSize));
|
..add(DiagnosticsProperty('libraryPageSize', libraryPageSize))
|
||||||
|
..add(DiagnosticsProperty('shortcuts', shortcuts));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a copy of ClientSettingsModel
|
/// Create a copy of ClientSettingsModel
|
||||||
|
|
@ -586,7 +611,9 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
||||||
final bool checkForUpdates,
|
final bool checkForUpdates,
|
||||||
final bool usePosterForLibrary,
|
final bool usePosterForLibrary,
|
||||||
final String? lastViewedUpdate,
|
final String? lastViewedUpdate,
|
||||||
final int? libraryPageSize}) = _$ClientSettingsModelImpl;
|
final int? libraryPageSize,
|
||||||
|
final Map<GlobalHotKeys, KeyCombination?> shortcuts}) =
|
||||||
|
_$ClientSettingsModelImpl;
|
||||||
_ClientSettingsModel._() : super._();
|
_ClientSettingsModel._() : super._();
|
||||||
|
|
||||||
factory _ClientSettingsModel.fromJson(Map<String, dynamic> json) =
|
factory _ClientSettingsModel.fromJson(Map<String, dynamic> json) =
|
||||||
|
|
@ -641,6 +668,8 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
||||||
String? get lastViewedUpdate;
|
String? get lastViewedUpdate;
|
||||||
@override
|
@override
|
||||||
int? get libraryPageSize;
|
int? get libraryPageSize;
|
||||||
|
@override
|
||||||
|
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.
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,14 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson(
|
||||||
usePosterForLibrary: json['usePosterForLibrary'] as bool? ?? false,
|
usePosterForLibrary: json['usePosterForLibrary'] as bool? ?? false,
|
||||||
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(
|
||||||
|
(k, e) => MapEntry(
|
||||||
|
$enumDecode(_$GlobalHotKeysEnumMap, k),
|
||||||
|
e == null
|
||||||
|
? null
|
||||||
|
: KeyCombination.fromJson(e as Map<String, dynamic>)),
|
||||||
|
) ??
|
||||||
|
const {},
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$ClientSettingsModelImplToJson(
|
Map<String, dynamic> _$$ClientSettingsModelImplToJson(
|
||||||
|
|
@ -75,6 +83,8 @@ Map<String, dynamic> _$$ClientSettingsModelImplToJson(
|
||||||
'usePosterForLibrary': instance.usePosterForLibrary,
|
'usePosterForLibrary': instance.usePosterForLibrary,
|
||||||
'lastViewedUpdate': instance.lastViewedUpdate,
|
'lastViewedUpdate': instance.lastViewedUpdate,
|
||||||
'libraryPageSize': instance.libraryPageSize,
|
'libraryPageSize': instance.libraryPageSize,
|
||||||
|
'shortcuts': instance.shortcuts
|
||||||
|
.map((k, e) => MapEntry(_$GlobalHotKeysEnumMap[k]!, e)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$ThemeModeEnumMap = {
|
const _$ThemeModeEnumMap = {
|
||||||
|
|
@ -112,3 +122,8 @@ const _$DynamicSchemeVariantEnumMap = {
|
||||||
DynamicSchemeVariant.rainbow: 'rainbow',
|
DynamicSchemeVariant.rainbow: 'rainbow',
|
||||||
DynamicSchemeVariant.fruitSalad: 'fruitSalad',
|
DynamicSchemeVariant.fruitSalad: 'fruitSalad',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _$GlobalHotKeysEnumMap = {
|
||||||
|
GlobalHotKeys.search: 'search',
|
||||||
|
GlobalHotKeys.exit: 'exit',
|
||||||
|
};
|
||||||
|
|
|
||||||
70
lib/models/settings/key_combinations.dart
Normal file
70
lib/models/settings/key_combinations.dart
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
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)
|
||||||
|
class KeyCombination with _$KeyCombination {
|
||||||
|
const KeyCombination._();
|
||||||
|
|
||||||
|
factory KeyCombination({
|
||||||
|
@LogicalKeyboardSerializer() LogicalKeyboardKey? modifier,
|
||||||
|
@LogicalKeyboardSerializer() required LogicalKeyboardKey key,
|
||||||
|
}) = _KeyCombination;
|
||||||
|
|
||||||
|
factory KeyCombination.fromJson(Map<String, dynamic> json) => _$KeyCombinationFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant other) {
|
||||||
|
return other is KeyCombination && other.key.keyId == key.keyId && other.modifier?.keyId == modifier?.keyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => key.hashCode ^ modifier.hashCode;
|
||||||
|
|
||||||
|
String get label => [modifier?.label, key.label].nonNulls.join(" + ");
|
||||||
|
|
||||||
|
static final Set<LogicalKeyboardKey> shiftKeys = {
|
||||||
|
LogicalKeyboardKey.shift,
|
||||||
|
LogicalKeyboardKey.shiftLeft,
|
||||||
|
LogicalKeyboardKey.shiftRight,
|
||||||
|
};
|
||||||
|
|
||||||
|
static final altKeys = {
|
||||||
|
LogicalKeyboardKey.alt,
|
||||||
|
LogicalKeyboardKey.altRight,
|
||||||
|
LogicalKeyboardKey.altLeft,
|
||||||
|
};
|
||||||
|
|
||||||
|
static final ctrlKeys = {
|
||||||
|
LogicalKeyboardKey.control,
|
||||||
|
LogicalKeyboardKey.controlLeft,
|
||||||
|
LogicalKeyboardKey.controlRight,
|
||||||
|
};
|
||||||
|
|
||||||
|
static final modifierKeys = {
|
||||||
|
...shiftKeys,
|
||||||
|
...altKeys,
|
||||||
|
...ctrlKeys,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogicalKeyboardSerializer extends JsonConverter<LogicalKeyboardKey, String> {
|
||||||
|
const LogicalKeyboardSerializer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
LogicalKeyboardKey fromJson(String json) {
|
||||||
|
return LogicalKeyboardKey.findKeyByKeyId(int.parse(jsonDecode(json))) ?? LogicalKeyboardKey.abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toJson(LogicalKeyboardKey object) {
|
||||||
|
return jsonEncode(object.keyId.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
79
lib/models/settings/key_combinations.freezed.dart
Normal file
79
lib/models/settings/key_combinations.freezed.dart
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'key_combinations.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
KeyCombination _$KeyCombinationFromJson(Map<String, dynamic> json) {
|
||||||
|
return _KeyCombination.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$KeyCombination {
|
||||||
|
@LogicalKeyboardSerializer()
|
||||||
|
LogicalKeyboardKey? get modifier => throw _privateConstructorUsedError;
|
||||||
|
@LogicalKeyboardSerializer()
|
||||||
|
LogicalKeyboardKey get key => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this KeyCombination to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$KeyCombinationImpl extends _KeyCombination {
|
||||||
|
_$KeyCombinationImpl(
|
||||||
|
{@LogicalKeyboardSerializer() this.modifier,
|
||||||
|
@LogicalKeyboardSerializer() required this.key})
|
||||||
|
: super._();
|
||||||
|
|
||||||
|
factory _$KeyCombinationImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$KeyCombinationImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@LogicalKeyboardSerializer()
|
||||||
|
final LogicalKeyboardKey? modifier;
|
||||||
|
@override
|
||||||
|
@LogicalKeyboardSerializer()
|
||||||
|
final LogicalKeyboardKey key;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'KeyCombination(modifier: $modifier, key: $key)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$KeyCombinationImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _KeyCombination extends KeyCombination {
|
||||||
|
factory _KeyCombination(
|
||||||
|
{@LogicalKeyboardSerializer() final LogicalKeyboardKey? modifier,
|
||||||
|
@LogicalKeyboardSerializer() required final LogicalKeyboardKey key}) =
|
||||||
|
_$KeyCombinationImpl;
|
||||||
|
_KeyCombination._() : super._();
|
||||||
|
|
||||||
|
factory _KeyCombination.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$KeyCombinationImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
@LogicalKeyboardSerializer()
|
||||||
|
LogicalKeyboardKey? get modifier;
|
||||||
|
@override
|
||||||
|
@LogicalKeyboardSerializer()
|
||||||
|
LogicalKeyboardKey get key;
|
||||||
|
}
|
||||||
34
lib/models/settings/key_combinations.g.dart
Normal file
34
lib/models/settings/key_combinations.g.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'key_combinations.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$KeyCombinationImpl _$$KeyCombinationImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$KeyCombinationImpl(
|
||||||
|
modifier: _$JsonConverterFromJson<String, LogicalKeyboardKey>(
|
||||||
|
json['modifier'], const LogicalKeyboardSerializer().fromJson),
|
||||||
|
key: const LogicalKeyboardSerializer().fromJson(json['key'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$KeyCombinationImplToJson(
|
||||||
|
_$KeyCombinationImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'modifier': _$JsonConverterToJson<String, LogicalKeyboardKey>(
|
||||||
|
instance.modifier, const LogicalKeyboardSerializer().toJson),
|
||||||
|
'key': const LogicalKeyboardSerializer().toJson(instance.key),
|
||||||
|
};
|
||||||
|
|
||||||
|
Value? _$JsonConverterFromJson<Json, Value>(
|
||||||
|
Object? json,
|
||||||
|
Value? Function(Json json) fromJson,
|
||||||
|
) =>
|
||||||
|
json == null ? null : fromJson(json as Json);
|
||||||
|
|
||||||
|
Json? _$JsonConverterToJson<Json, Value>(
|
||||||
|
Value? value,
|
||||||
|
Json? Function(Value value) toJson,
|
||||||
|
) =>
|
||||||
|
value == null ? null : toJson(value);
|
||||||
|
|
@ -6,12 +6,49 @@ import 'package:flutter/widgets.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/items/media_segments_model.dart';
|
import 'package:fladder/models/items/media_segments_model.dart';
|
||||||
|
import 'package:fladder/models/settings/key_combinations.dart';
|
||||||
import 'package:fladder/util/bitrate_helper.dart';
|
import 'package:fladder/util/bitrate_helper.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
|
|
||||||
part 'video_player_settings.freezed.dart';
|
part 'video_player_settings.freezed.dart';
|
||||||
part 'video_player_settings.g.dart';
|
part 'video_player_settings.g.dart';
|
||||||
|
|
||||||
|
enum VideoHotKeys {
|
||||||
|
playPause,
|
||||||
|
seekForward,
|
||||||
|
seekBack,
|
||||||
|
mute,
|
||||||
|
volumeUp,
|
||||||
|
volumeDown,
|
||||||
|
nextVideo,
|
||||||
|
prevVideo,
|
||||||
|
nextChapter,
|
||||||
|
prevChapter,
|
||||||
|
fullScreen,
|
||||||
|
skipMediaSegment,
|
||||||
|
exit;
|
||||||
|
|
||||||
|
const VideoHotKeys();
|
||||||
|
|
||||||
|
String label(BuildContext context) {
|
||||||
|
return switch (this) {
|
||||||
|
VideoHotKeys.playPause => context.localized.playPause,
|
||||||
|
VideoHotKeys.seekForward => context.localized.seekForward,
|
||||||
|
VideoHotKeys.seekBack => context.localized.seekBack,
|
||||||
|
VideoHotKeys.mute => context.localized.mute,
|
||||||
|
VideoHotKeys.volumeUp => context.localized.volumeUp,
|
||||||
|
VideoHotKeys.volumeDown => context.localized.volumeDown,
|
||||||
|
VideoHotKeys.nextVideo => context.localized.nextVideo,
|
||||||
|
VideoHotKeys.prevVideo => context.localized.prevVideo,
|
||||||
|
VideoHotKeys.nextChapter => context.localized.nextChapter,
|
||||||
|
VideoHotKeys.prevChapter => context.localized.prevChapter,
|
||||||
|
VideoHotKeys.fullScreen => context.localized.fullScreen,
|
||||||
|
VideoHotKeys.skipMediaSegment => context.localized.skipMediaSegment,
|
||||||
|
VideoHotKeys.exit => context.localized.exit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Freezed(copyWith: true)
|
@Freezed(copyWith: true)
|
||||||
class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
|
class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
|
||||||
const VideoPlayerSettingsModel._();
|
const VideoPlayerSettingsModel._();
|
||||||
|
|
@ -31,6 +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,
|
||||||
}) = _VideoPlayerSettingsModel;
|
}) = _VideoPlayerSettingsModel;
|
||||||
|
|
||||||
double get volume => switch (defaultTargetPlatform) {
|
double get volume => switch (defaultTargetPlatform) {
|
||||||
|
|
@ -42,6 +80,11 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
|
||||||
|
|
||||||
PlayerOptions get wantedPlayer => playerOptions ?? PlayerOptions.platformDefaults;
|
PlayerOptions get wantedPlayer => playerOptions ?? PlayerOptions.platformDefaults;
|
||||||
|
|
||||||
|
Map<VideoHotKeys, KeyCombination> get currentShortcuts =>
|
||||||
|
_defaultVideoHotKeys.map((key, value) => MapEntry(key, hotKeys[key] ?? value));
|
||||||
|
|
||||||
|
Map<VideoHotKeys, KeyCombination> get defaultShortCuts => _defaultVideoHotKeys;
|
||||||
|
|
||||||
bool playerSame(VideoPlayerSettingsModel other) {
|
bool playerSame(VideoPlayerSettingsModel other) {
|
||||||
return other.hardwareAccel == hardwareAccel &&
|
return other.hardwareAccel == hardwareAccel &&
|
||||||
other.useLibass == useLibass &&
|
other.useLibass == useLibass &&
|
||||||
|
|
@ -118,3 +161,22 @@ enum AutoNextType {
|
||||||
AutoNextType.static => context.localized.autoNextOffStaticDesc,
|
AutoNextType.static => context.localized.autoNextOffStaticDesc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<VideoHotKeys, KeyCombination> 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.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.nextChapter => KeyCombination(key: LogicalKeyboardKey.pageUp),
|
||||||
|
VideoHotKeys.prevChapter => KeyCombination(key: LogicalKeyboardKey.pageDown),
|
||||||
|
VideoHotKeys.fullScreen => KeyCombination(key: LogicalKeyboardKey.keyF),
|
||||||
|
VideoHotKeys.skipMediaSegment => KeyCombination(key: LogicalKeyboardKey.keyS),
|
||||||
|
VideoHotKeys.exit => KeyCombination(key: LogicalKeyboardKey.escape),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ 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 =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this VideoPlayerSettingsModel to a JSON map.
|
/// Serializes this VideoPlayerSettingsModel to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
@ -68,7 +70,8 @@ abstract class $VideoPlayerSettingsModelCopyWith<$Res> {
|
||||||
Bitrate maxHomeBitrate,
|
Bitrate maxHomeBitrate,
|
||||||
Bitrate maxInternetBitrate,
|
Bitrate maxInternetBitrate,
|
||||||
String? audioDevice,
|
String? audioDevice,
|
||||||
Map<MediaSegmentType, SegmentSkip> segmentSkipSettings});
|
Map<MediaSegmentType, SegmentSkip> segmentSkipSettings,
|
||||||
|
Map<VideoHotKeys, KeyCombination?> hotKeys});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
@ -101,6 +104,7 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res,
|
||||||
Object? maxInternetBitrate = null,
|
Object? maxInternetBitrate = null,
|
||||||
Object? audioDevice = freezed,
|
Object? audioDevice = freezed,
|
||||||
Object? segmentSkipSettings = null,
|
Object? segmentSkipSettings = null,
|
||||||
|
Object? hotKeys = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
screenBrightness: freezed == screenBrightness
|
screenBrightness: freezed == screenBrightness
|
||||||
|
|
@ -159,6 +163,10 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res,
|
||||||
? _value.segmentSkipSettings
|
? _value.segmentSkipSettings
|
||||||
: segmentSkipSettings // ignore: cast_nullable_to_non_nullable
|
: segmentSkipSettings // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<MediaSegmentType, SegmentSkip>,
|
as Map<MediaSegmentType, SegmentSkip>,
|
||||||
|
hotKeys: null == hotKeys
|
||||||
|
? _value.hotKeys
|
||||||
|
: hotKeys // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<VideoHotKeys, KeyCombination?>,
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -186,7 +194,8 @@ abstract class _$$VideoPlayerSettingsModelImplCopyWith<$Res>
|
||||||
Bitrate maxHomeBitrate,
|
Bitrate maxHomeBitrate,
|
||||||
Bitrate maxInternetBitrate,
|
Bitrate maxInternetBitrate,
|
||||||
String? audioDevice,
|
String? audioDevice,
|
||||||
Map<MediaSegmentType, SegmentSkip> segmentSkipSettings});
|
Map<MediaSegmentType, SegmentSkip> segmentSkipSettings,
|
||||||
|
Map<VideoHotKeys, KeyCombination?> hotKeys});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
@ -218,6 +227,7 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res>
|
||||||
Object? maxInternetBitrate = null,
|
Object? maxInternetBitrate = null,
|
||||||
Object? audioDevice = freezed,
|
Object? audioDevice = freezed,
|
||||||
Object? segmentSkipSettings = null,
|
Object? segmentSkipSettings = null,
|
||||||
|
Object? hotKeys = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$VideoPlayerSettingsModelImpl(
|
return _then(_$VideoPlayerSettingsModelImpl(
|
||||||
screenBrightness: freezed == screenBrightness
|
screenBrightness: freezed == screenBrightness
|
||||||
|
|
@ -276,6 +286,10 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res>
|
||||||
? _value._segmentSkipSettings
|
? _value._segmentSkipSettings
|
||||||
: segmentSkipSettings // ignore: cast_nullable_to_non_nullable
|
: segmentSkipSettings // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<MediaSegmentType, SegmentSkip>,
|
as Map<MediaSegmentType, SegmentSkip>,
|
||||||
|
hotKeys: null == hotKeys
|
||||||
|
? _value._hotKeys
|
||||||
|
: hotKeys // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<VideoHotKeys, KeyCombination?>,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -299,9 +313,11 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
|
||||||
this.maxInternetBitrate = Bitrate.original,
|
this.maxInternetBitrate = Bitrate.original,
|
||||||
this.audioDevice,
|
this.audioDevice,
|
||||||
final Map<MediaSegmentType, SegmentSkip> segmentSkipSettings =
|
final Map<MediaSegmentType, SegmentSkip> segmentSkipSettings =
|
||||||
defaultSegmentSkipValues})
|
defaultSegmentSkipValues,
|
||||||
|
final Map<VideoHotKeys, KeyCombination?> hotKeys = const {}})
|
||||||
: _allowedOrientations = allowedOrientations,
|
: _allowedOrientations = allowedOrientations,
|
||||||
_segmentSkipSettings = segmentSkipSettings,
|
_segmentSkipSettings = segmentSkipSettings,
|
||||||
|
_hotKeys = hotKeys,
|
||||||
super._();
|
super._();
|
||||||
|
|
||||||
factory _$VideoPlayerSettingsModelImpl.fromJson(Map<String, dynamic> json) =>
|
factory _$VideoPlayerSettingsModelImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
|
@ -361,9 +377,18 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
|
||||||
return EqualUnmodifiableMapView(_segmentSkipSettings);
|
return EqualUnmodifiableMapView(_segmentSkipSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Map<VideoHotKeys, KeyCombination?> _hotKeys;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<VideoHotKeys, KeyCombination?> get hotKeys {
|
||||||
|
if (_hotKeys is EqualUnmodifiableMapView) return _hotKeys;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_hotKeys);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||||
return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings)';
|
return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -384,7 +409,8 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
|
||||||
..add(DiagnosticsProperty('maxHomeBitrate', maxHomeBitrate))
|
..add(DiagnosticsProperty('maxHomeBitrate', maxHomeBitrate))
|
||||||
..add(DiagnosticsProperty('maxInternetBitrate', maxInternetBitrate))
|
..add(DiagnosticsProperty('maxInternetBitrate', maxInternetBitrate))
|
||||||
..add(DiagnosticsProperty('audioDevice', audioDevice))
|
..add(DiagnosticsProperty('audioDevice', audioDevice))
|
||||||
..add(DiagnosticsProperty('segmentSkipSettings', segmentSkipSettings));
|
..add(DiagnosticsProperty('segmentSkipSettings', segmentSkipSettings))
|
||||||
|
..add(DiagnosticsProperty('hotKeys', hotKeys));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a copy of VideoPlayerSettingsModel
|
/// Create a copy of VideoPlayerSettingsModel
|
||||||
|
|
@ -419,7 +445,8 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel {
|
||||||
final Bitrate maxHomeBitrate,
|
final Bitrate maxHomeBitrate,
|
||||||
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}) =
|
||||||
_$VideoPlayerSettingsModelImpl;
|
_$VideoPlayerSettingsModelImpl;
|
||||||
_VideoPlayerSettingsModel._() : super._();
|
_VideoPlayerSettingsModel._() : super._();
|
||||||
|
|
||||||
|
|
@ -454,6 +481,8 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel {
|
||||||
String? get audioDevice;
|
String? get audioDevice;
|
||||||
@override
|
@override
|
||||||
Map<MediaSegmentType, SegmentSkip> get segmentSkipSettings;
|
Map<MediaSegmentType, SegmentSkip> get segmentSkipSettings;
|
||||||
|
@override
|
||||||
|
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.
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,14 @@ _$VideoPlayerSettingsModelImpl _$$VideoPlayerSettingsModelImplFromJson(
|
||||||
$enumDecode(_$SegmentSkipEnumMap, e)),
|
$enumDecode(_$SegmentSkipEnumMap, e)),
|
||||||
) ??
|
) ??
|
||||||
defaultSegmentSkipValues,
|
defaultSegmentSkipValues,
|
||||||
|
hotKeys: (json['hotKeys'] as Map<String, dynamic>?)?.map(
|
||||||
|
(k, e) => MapEntry(
|
||||||
|
$enumDecode(_$VideoHotKeysEnumMap, k),
|
||||||
|
e == null
|
||||||
|
? null
|
||||||
|
: KeyCombination.fromJson(e as Map<String, dynamic>)),
|
||||||
|
) ??
|
||||||
|
const {},
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$VideoPlayerSettingsModelImplToJson(
|
Map<String, dynamic> _$$VideoPlayerSettingsModelImplToJson(
|
||||||
|
|
@ -60,6 +68,8 @@ Map<String, dynamic> _$$VideoPlayerSettingsModelImplToJson(
|
||||||
'audioDevice': instance.audioDevice,
|
'audioDevice': instance.audioDevice,
|
||||||
'segmentSkipSettings': instance.segmentSkipSettings.map((k, e) =>
|
'segmentSkipSettings': instance.segmentSkipSettings.map((k, e) =>
|
||||||
MapEntry(_$MediaSegmentTypeEnumMap[k]!, _$SegmentSkipEnumMap[e]!)),
|
MapEntry(_$MediaSegmentTypeEnumMap[k]!, _$SegmentSkipEnumMap[e]!)),
|
||||||
|
'hotKeys': instance.hotKeys
|
||||||
|
.map((k, e) => MapEntry(_$VideoHotKeysEnumMap[k]!, e)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$BoxFitEnumMap = {
|
const _$BoxFitEnumMap = {
|
||||||
|
|
@ -123,3 +133,19 @@ const _$MediaSegmentTypeEnumMap = {
|
||||||
MediaSegmentType.outro: 'outro',
|
MediaSegmentType.outro: 'outro',
|
||||||
MediaSegmentType.intro: 'intro',
|
MediaSegmentType.intro: 'intro',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _$VideoHotKeysEnumMap = {
|
||||||
|
VideoHotKeys.playPause: 'playPause',
|
||||||
|
VideoHotKeys.seekForward: 'seekForward',
|
||||||
|
VideoHotKeys.seekBack: 'seekBack',
|
||||||
|
VideoHotKeys.mute: 'mute',
|
||||||
|
VideoHotKeys.volumeUp: 'volumeUp',
|
||||||
|
VideoHotKeys.volumeDown: 'volumeDown',
|
||||||
|
VideoHotKeys.nextVideo: 'nextVideo',
|
||||||
|
VideoHotKeys.prevVideo: 'prevVideo',
|
||||||
|
VideoHotKeys.nextChapter: 'nextChapter',
|
||||||
|
VideoHotKeys.prevChapter: 'prevChapter',
|
||||||
|
VideoHotKeys.fullScreen: 'fullScreen',
|
||||||
|
VideoHotKeys.skipMediaSegment: 'skipMediaSegment',
|
||||||
|
VideoHotKeys.exit: 'exit',
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ part of 'connectivity_provider.dart';
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$connectivityStatusHash() =>
|
String _$connectivityStatusHash() =>
|
||||||
r'7a4ac96d163a479bd34fc6a3efcd556755f8d5e9';
|
r'8c58479db511f2431942655adf3f5021e8f0290c';
|
||||||
|
|
||||||
/// See also [ConnectivityStatus].
|
/// See also [ConnectivityStatus].
|
||||||
@ProviderFor(ConnectivityStatus)
|
@ProviderFor(ConnectivityStatus)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ import 'package:fladder/providers/sync_provider.dart';
|
||||||
import 'package:fladder/providers/user_provider.dart';
|
import 'package:fladder/providers/user_provider.dart';
|
||||||
import 'package:fladder/util/jellyfin_extension.dart';
|
import 'package:fladder/util/jellyfin_extension.dart';
|
||||||
|
|
||||||
|
const _userSettings = "usersettings";
|
||||||
|
const _client = "fladder";
|
||||||
|
|
||||||
class ServerQueryResult {
|
class ServerQueryResult {
|
||||||
final List<BaseItemDto> original;
|
final List<BaseItemDto> original;
|
||||||
final List<ItemBaseModel> items;
|
final List<ItemBaseModel> items;
|
||||||
|
|
@ -745,6 +748,34 @@ class JellyService {
|
||||||
Future<Response<ServerConfiguration>> systemConfigurationGet() => api.systemConfigurationGet();
|
Future<Response<ServerConfiguration>> systemConfigurationGet() => api.systemConfigurationGet();
|
||||||
Future<Response<PublicSystemInfo>> systemInfoPublicGet() => api.systemInfoPublicGet();
|
Future<Response<PublicSystemInfo>> systemInfoPublicGet() => api.systemInfoPublicGet();
|
||||||
|
|
||||||
|
Future<Response<UserSettings>> getCustomConfig() async {
|
||||||
|
final response = await api.displayPreferencesDisplayPreferencesIdGet(
|
||||||
|
displayPreferencesId: _userSettings,
|
||||||
|
userId: account?.id ?? "",
|
||||||
|
$client: _client,
|
||||||
|
);
|
||||||
|
final customPrefs = response.body?.customPrefs?.parseValues();
|
||||||
|
final userPrefs = customPrefs != null ? UserSettings.fromJson(customPrefs) : UserSettings();
|
||||||
|
return response.copyWith(
|
||||||
|
body: userPrefs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response<dynamic>> setCustomConfig(UserSettings currentSettings) async {
|
||||||
|
final currentDisplayPreferences = await api.displayPreferencesDisplayPreferencesIdGet(
|
||||||
|
displayPreferencesId: _userSettings,
|
||||||
|
$client: _client,
|
||||||
|
);
|
||||||
|
return api.displayPreferencesDisplayPreferencesIdPost(
|
||||||
|
displayPreferencesId: 'usersettings',
|
||||||
|
userId: account?.id ?? "",
|
||||||
|
$client: _client,
|
||||||
|
body: currentDisplayPreferences.body?.copyWith(
|
||||||
|
customPrefs: currentSettings.toJson(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Response> sessionsLogoutPost() => api.sessionsLogoutPost();
|
Future<Response> sessionsLogoutPost() => api.sessionsLogoutPost();
|
||||||
|
|
||||||
Future<Response<String>> itemsItemIdDownloadGet({
|
Future<Response<String>> itemsItemIdDownloadGet({
|
||||||
|
|
@ -1116,3 +1147,31 @@ class JellyService {
|
||||||
return _updateUserConfiguration(updated);
|
return _updateUserConfiguration(updated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ParsedMap on Map<String, dynamic> {
|
||||||
|
Map<String, dynamic> parseValues() {
|
||||||
|
Map<String, dynamic> parsedMap = {};
|
||||||
|
|
||||||
|
for (var entry in entries) {
|
||||||
|
String key = entry.key;
|
||||||
|
dynamic value = entry.value;
|
||||||
|
|
||||||
|
if (value is String) {
|
||||||
|
// Try to parse the string to a number or boolean
|
||||||
|
if (int.tryParse(value) != null) {
|
||||||
|
parsedMap[key] = int.tryParse(value);
|
||||||
|
} else if (double.tryParse(value) != null) {
|
||||||
|
parsedMap[key] = double.tryParse(value);
|
||||||
|
} else if (value.toLowerCase() == 'true' || value.toLowerCase() == 'false') {
|
||||||
|
parsedMap[key] = value.toLowerCase() == 'true';
|
||||||
|
} else {
|
||||||
|
parsedMap[key] = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parsedMap[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/settings/client_settings_model.dart';
|
import 'package:fladder/models/settings/client_settings_model.dart';
|
||||||
|
import 'package:fladder/models/settings/key_combinations.dart';
|
||||||
import 'package:fladder/providers/shared_provider.dart';
|
import 'package:fladder/providers/shared_provider.dart';
|
||||||
import 'package:fladder/util/custom_color_themes.dart';
|
import 'package:fladder/util/custom_color_themes.dart';
|
||||||
import 'package:fladder/util/debouncer.dart';
|
import 'package:fladder/util/debouncer.dart';
|
||||||
|
|
@ -58,4 +59,15 @@ class ClientSettingsNotifier extends StateNotifier<ClientSettingsModel> {
|
||||||
state = state.copyWith(schemeVariant: type ?? state.schemeVariant);
|
state = state.copyWith(schemeVariant: type ?? state.schemeVariant);
|
||||||
|
|
||||||
void setRequireWifi(bool value) => state = state.copyWith(requireWifi: value);
|
void setRequireWifi(bool value) => state = state.copyWith(requireWifi: value);
|
||||||
|
|
||||||
|
void setShortcuts(MapEntry<GlobalHotKeys, KeyCombination?> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:screen_brightness/screen_brightness.dart';
|
import 'package:screen_brightness/screen_brightness.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/settings/key_combinations.dart';
|
||||||
import 'package:fladder/models/settings/video_player_settings.dart';
|
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||||
import 'package:fladder/providers/shared_provider.dart';
|
import 'package:fladder/providers/shared_provider.dart';
|
||||||
import 'package:fladder/providers/video_player_provider.dart';
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
|
|
@ -67,4 +69,51 @@ 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) {
|
||||||
|
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 nextChapter() {
|
||||||
|
final chapters = ref.read(playBackModel)?.chapters ?? [];
|
||||||
|
final currentPosition = ref.read(videoPlayerProvider.select((value) => value.lastState?.position));
|
||||||
|
|
||||||
|
if (chapters.isNotEmpty && currentPosition != null) {
|
||||||
|
final currentChapter = chapters.lastWhereOrNull((element) => element.startPosition <= currentPosition);
|
||||||
|
|
||||||
|
if (currentChapter != null) {
|
||||||
|
final nextChapterIndex = chapters.indexOf(currentChapter) + 1;
|
||||||
|
if (nextChapterIndex < chapters.length) {
|
||||||
|
ref.read(videoPlayerProvider).seek(chapters[nextChapterIndex].startPosition);
|
||||||
|
} else {
|
||||||
|
ref.read(videoPlayerProvider).seek(currentChapter.startPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void prevChapter() {
|
||||||
|
final chapters = ref.read(playBackModel)?.chapters ?? [];
|
||||||
|
final currentPosition = ref.read(videoPlayerProvider.select((value) => value.lastState?.position));
|
||||||
|
|
||||||
|
if (chapters.isNotEmpty && currentPosition != null) {
|
||||||
|
final currentChapter = chapters.lastWhereOrNull((element) => element.startPosition <= currentPosition);
|
||||||
|
|
||||||
|
if (currentChapter != null) {
|
||||||
|
final prevChapterIndex = chapters.indexOf(currentChapter) - 1;
|
||||||
|
if (prevChapterIndex >= 0) {
|
||||||
|
ref.read(videoPlayerProvider).seek(chapters[prevChapterIndex].startPosition);
|
||||||
|
} else {
|
||||||
|
ref.read(videoPlayerProvider).seek(currentChapter.startPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'sync_provider_helpers.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$syncedItemHash() => r'7b1178ba78529ebf65425aa4cb8b239a28c7914b';
|
String _$syncedItemHash() => r'8342c557accf52fd0a8561274ecf9b77b5cf7acd';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
@ -157,7 +157,7 @@ class _SyncedItemProviderElement
|
||||||
ItemBaseModel? get item => (origin as SyncedItemProvider).item;
|
ItemBaseModel? get item => (origin as SyncedItemProvider).item;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$syncedChildrenHash() => r'2b6ce1611750785060df6317ce0ea25e2dc0aeb4';
|
String _$syncedChildrenHash() => r'75e25432f33e0fe31708618b7ba744430523a4d3';
|
||||||
|
|
||||||
abstract class _$SyncedChildren
|
abstract class _$SyncedChildren
|
||||||
extends BuildlessAutoDisposeAsyncNotifier<List<SyncedItem>> {
|
extends BuildlessAutoDisposeAsyncNotifier<List<SyncedItem>> {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@ class User extends _$User {
|
||||||
var response = await api.usersMeGet();
|
var response = await api.usersMeGet();
|
||||||
var quickConnectStatus = await api.quickConnectEnabled();
|
var quickConnectStatus = await api.quickConnectEnabled();
|
||||||
var systemConfiguration = await api.systemConfigurationGet();
|
var systemConfiguration = await api.systemConfigurationGet();
|
||||||
|
|
||||||
|
final customConfig = await api.getCustomConfig();
|
||||||
|
|
||||||
if (response.isSuccessful && response.body != null) {
|
if (response.isSuccessful && response.body != null) {
|
||||||
userState = state?.copyWith(
|
userState = state?.copyWith(
|
||||||
name: response.body?.name ?? state?.name ?? "",
|
name: response.body?.name ?? state?.name ?? "",
|
||||||
|
|
@ -48,6 +51,7 @@ class User extends _$User {
|
||||||
userConfiguration: response.body?.configuration,
|
userConfiguration: response.body?.configuration,
|
||||||
quickConnectState: quickConnectStatus.body ?? false,
|
quickConnectState: quickConnectStatus.body ?? false,
|
||||||
latestItemsExcludes: response.body?.configuration?.latestItemsExcludes ?? [],
|
latestItemsExcludes: response.body?.configuration?.latestItemsExcludes ?? [],
|
||||||
|
userSettings: customConfig.body,
|
||||||
);
|
);
|
||||||
return response.copyWith(body: state);
|
return response.copyWith(body: state);
|
||||||
}
|
}
|
||||||
|
|
@ -68,6 +72,25 @@ class User extends _$User {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setBackwardSpeed(int value) {
|
||||||
|
final userSettings = state?.userSettings?.copyWith(skipBackDuration: Duration(seconds: value));
|
||||||
|
if (userSettings != null) {
|
||||||
|
updateCustomConfig(userSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setForwardSpeed(int value) {
|
||||||
|
final userSettings = state?.userSettings?.copyWith(skipForwardDuration: Duration(seconds: value));
|
||||||
|
if (userSettings != null) {
|
||||||
|
updateCustomConfig(userSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response<dynamic>> updateCustomConfig(UserSettings settings) async {
|
||||||
|
state = state?.copyWith(userSettings: settings);
|
||||||
|
return api.setCustomConfig(settings);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Response> refreshMetaData(
|
Future<Response> refreshMetaData(
|
||||||
String itemId, {
|
String itemId, {
|
||||||
MetadataRefresh? metadataRefreshMode,
|
MetadataRefresh? metadataRefreshMode,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ final showSyncButtonProviderProvider = AutoDisposeProvider<bool>.internal(
|
||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef<bool>;
|
typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef<bool>;
|
||||||
String _$userHash() => r'56fca6515c42347fa99dcdcf4f2d8a977335243a';
|
String _$userHash() => r'24b34a88eae11aec1e377a82d1e507f293b7816a';
|
||||||
|
|
||||||
/// See also [User].
|
/// See also [User].
|
||||||
@ProviderFor(User)
|
@ProviderFor(User)
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,14 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:iconsax_plus/iconsax_plus.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/providers/user_provider.dart';
|
||||||
import 'package:fladder/routes/auto_router.gr.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/localization_helper.dart';
|
||||||
import 'package:fladder/util/string_extensions.dart';
|
import 'package:fladder/util/string_extensions.dart';
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
||||||
|
|
@ -17,8 +22,7 @@ enum HomeTabs {
|
||||||
dashboard,
|
dashboard,
|
||||||
library,
|
library,
|
||||||
favorites,
|
favorites,
|
||||||
sync,
|
sync;
|
||||||
;
|
|
||||||
|
|
||||||
const HomeTabs();
|
const HomeTabs();
|
||||||
|
|
||||||
|
|
@ -120,7 +124,27 @@ class HomeScreen extends ConsumerWidget {
|
||||||
})
|
})
|
||||||
.nonNulls
|
.nonNulls
|
||||||
.toList();
|
.toList();
|
||||||
return HeroControllerScope(
|
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(),
|
controller: HeroController(),
|
||||||
child: AutoRouter(
|
child: AutoRouter(
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
|
|
@ -131,6 +155,7 @@ class HomeScreen extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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_advanced.dart';
|
||||||
import 'package:fladder/screens/settings/client_sections/client_settings_dashboard.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_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_theme.dart';
|
||||||
import 'package:fladder/screens/settings/client_sections/client_settings_visual.dart';
|
import 'package:fladder/screens/settings/client_sections/client_settings_visual.dart';
|
||||||
import 'package:fladder/screens/settings/settings_list_tile.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),
|
const SizedBox(height: 12),
|
||||||
...buildClientSettingsDashboard(context, ref),
|
...buildClientSettingsDashboard(context, ref),
|
||||||
const SizedBox(height: 12),
|
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/providers/user_provider.dart';
|
||||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||||
import 'package:fladder/screens/settings/settings_scaffold.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_label_divider.dart';
|
||||||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||||
import 'package:fladder/screens/settings/widgets/settings_message_box.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 connectionState = ref.watch(connectivityStatusProvider);
|
||||||
|
|
||||||
|
final userSettings = ref.watch(userProvider.select((value) => value?.userSettings));
|
||||||
|
|
||||||
return SettingsScaffold(
|
return SettingsScaffold(
|
||||||
label: context.localized.settingsPlayerTitle,
|
label: context.localized.settingsPlayerTitle,
|
||||||
items: [
|
items: [
|
||||||
|
|
@ -169,6 +172,70 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
const SizedBox(height: 12),
|
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), [
|
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.playbackTrackSelection), [
|
||||||
SettingsListTile(
|
SettingsListTile(
|
||||||
label: Text(context.localized.rememberAudioSelections),
|
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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:async/async.dart';
|
import 'package:async/async.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/providers/video_player_provider.dart';
|
||||||
import 'package:fladder/util/input_handler.dart';
|
import 'package:fladder/util/input_handler.dart';
|
||||||
import 'package:fladder/util/localization_helper.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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InputHandler(
|
return InputHandler(
|
||||||
autoFocus: true,
|
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: IgnorePointer(
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
duration: const Duration(milliseconds: 500),
|
duration: const Duration(milliseconds: 500),
|
||||||
|
|
@ -108,6 +88,29 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void seekBack({int seconds = -10}) => onSeekStart(seconds);
|
bool _onKey(VideoHotKeys value) {
|
||||||
void seekForward({int seconds = 30}) => onSeekStart(seconds);
|
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:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/providers/settings/video_player_settings_provider.dart';
|
||||||
import 'package:fladder/util/list_padding.dart';
|
import 'package:fladder/util/list_padding.dart';
|
||||||
|
|
@ -19,6 +19,8 @@ class VideoVolumeSlider extends ConsumerStatefulWidget {
|
||||||
class _VideoVolumeSliderState extends ConsumerState<VideoVolumeSlider> {
|
class _VideoVolumeSliderState extends ConsumerState<VideoVolumeSlider> {
|
||||||
bool sliderActive = false;
|
bool sliderActive = false;
|
||||||
|
|
||||||
|
double? previousVolume;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final volume = ref.watch(videoPlayerSettingsProvider.select((value) => value.volume));
|
final volume = ref.watch(videoPlayerSettingsProvider.select((value) => value.volume));
|
||||||
|
|
@ -27,7 +29,12 @@ class _VideoVolumeSliderState extends ConsumerState<VideoVolumeSlider> {
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(volumeIcon(volume)),
|
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(
|
AnimatedSize(
|
||||||
duration: const Duration(milliseconds: 250),
|
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:iconsax_plus/iconsax_plus.dart';
|
||||||
import 'package:screen_brightness/screen_brightness.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/items/media_segments_model.dart';
|
||||||
import 'package:fladder/models/media_playback_model.dart';
|
import 'package:fladder/models/media_playback_model.dart';
|
||||||
import 'package:fladder/models/playback/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/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/providers/user_provider.dart';
|
||||||
import 'package:fladder/providers/video_player_provider.dart';
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
import 'package:fladder/screens/shared/default_title_bar.dart';
|
import 'package:fladder/screens/shared/default_title_bar.dart';
|
||||||
import 'package:fladder/screens/shared/media/components/item_logo.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,
|
() => mounted ? toggleOverlay(value: false) : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
double? previousVolume;
|
||||||
|
|
||||||
final fadeDuration = const Duration(milliseconds: 350);
|
final fadeDuration = const Duration(milliseconds: 350);
|
||||||
bool showOverlay = true;
|
bool showOverlay = true;
|
||||||
bool wasPlaying = false;
|
bool wasPlaying = false;
|
||||||
|
|
@ -55,55 +60,6 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
late final double topPadding = MediaQuery.of(context).viewPadding.top;
|
late final double topPadding = MediaQuery.of(context).viewPadding.top;
|
||||||
late final double bottomPadding = MediaQuery.of(context).viewPadding.bottom;
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -116,8 +72,9 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
final player = ref.watch(videoPlayerProvider);
|
final player = ref.watch(videoPlayerProvider);
|
||||||
final subtitleWidget = player.subtitleWidget(showOverlay, controlsKey: _bottomControlsKey);
|
final subtitleWidget = player.subtitleWidget(showOverlay, controlsKey: _bottomControlsKey);
|
||||||
return InputHandler(
|
return InputHandler(
|
||||||
autoFocus: false,
|
autoFocus: true,
|
||||||
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
|
keyMap: ref.watch(videoPlayerSettingsProvider.select((value) => value.currentShortcuts)),
|
||||||
|
keyMapResult: (result) => _onKey(result),
|
||||||
child: PopScope(
|
child: PopScope(
|
||||||
canPop: false,
|
canPop: false,
|
||||||
onPopInvokedWithResult: (didPop, result) {
|
onPopInvokedWithResult: (didPop, result) {
|
||||||
|
|
@ -536,8 +493,6 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
return Consumer(
|
return Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final previousVideo = ref.watch(playBackModel.select((value) => value?.previousVideo));
|
final previousVideo = ref.watch(playBackModel.select((value) => value?.previousVideo));
|
||||||
final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering));
|
|
||||||
|
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: previousVideo?.detailedName(context) ?? "",
|
message: previousVideo?.detailedName(context) ?? "",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|
@ -547,9 +502,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
),
|
),
|
||||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: previousVideo != null && !buffering
|
onPressed: loadPreviousVideo(ref, video: previousVideo),
|
||||||
? () => ref.read(playbackModelHelper).loadNewVideo(previousVideo)
|
|
||||||
: null,
|
|
||||||
iconSize: 30,
|
iconSize: 30,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
IconsaxPlusLinear.backward,
|
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 {
|
Widget get nextVideoButton {
|
||||||
return Consumer(
|
return Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final nextVideo = ref.watch(playBackModel.select((value) => value?.nextVideo));
|
final nextVideo = ref.watch(playBackModel.select((value) => value?.nextVideo));
|
||||||
final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering));
|
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: nextVideo?.detailedName(context) ?? "",
|
message: nextVideo?.detailedName(context) ?? "",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|
@ -574,8 +532,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
),
|
),
|
||||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed:
|
onPressed: loadNextVideo(ref, video: nextVideo),
|
||||||
nextVideo != null && !buffering ? () => ref.read(playbackModelHelper).loadNewVideo(nextVideo) : null,
|
|
||||||
iconSize: 30,
|
iconSize: 30,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
IconsaxPlusLinear.forward,
|
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) {
|
Widget seekBackwardButton(WidgetRef ref) {
|
||||||
|
final backwardSpeed =
|
||||||
|
ref.read(userProvider.select((value) => value?.userSettings?.skipBackDuration.inSeconds ?? 30));
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () => seekBack(ref),
|
onPressed: () => seekBack(ref, seconds: backwardSpeed),
|
||||||
tooltip: "-10",
|
tooltip: "-$backwardSpeed",
|
||||||
iconSize: 40,
|
iconSize: 40,
|
||||||
icon: const Icon(
|
icon: Stack(
|
||||||
IconsaxPlusLinear.backward_10_seconds,
|
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) {
|
Widget seekForwardButton(WidgetRef ref) {
|
||||||
|
final forwardSpeed =
|
||||||
|
ref.read(userProvider.select((value) => value?.userSettings?.skipForwardDuration.inSeconds ?? 30));
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () => seekForward(ref),
|
onPressed: () => seekForward(ref, seconds: forwardSpeed),
|
||||||
tooltip: "15",
|
tooltip: forwardSpeed.toString(),
|
||||||
iconSize: 40,
|
iconSize: 40,
|
||||||
icon: const Stack(
|
icon: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
children: [
|
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();
|
resetTimer();
|
||||||
fullScreenHelper.closeFullScreen(ref);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,32 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class InputHandler extends StatefulWidget {
|
import 'package:fladder/models/settings/key_combinations.dart';
|
||||||
|
|
||||||
|
class InputHandler<T> extends StatefulWidget {
|
||||||
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 Map<T, KeyCombination>? keyMap;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
const InputHandler({
|
const InputHandler({
|
||||||
required this.child,
|
required this.child,
|
||||||
this.autoFocus = true,
|
this.autoFocus = true,
|
||||||
this.onKeyEvent,
|
this.onKeyEvent,
|
||||||
|
this.keyMapResult,
|
||||||
|
this.keyMap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<InputHandler> createState() => _InputHandlerState();
|
State<InputHandler> createState() => _InputHandlerState<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InputHandlerState extends State<InputHandler> {
|
class _InputHandlerState<T> extends State<InputHandler<T>> {
|
||||||
final focusNode = FocusNode();
|
final focusNode = FocusNode();
|
||||||
|
|
||||||
|
LogicalKeyboardKey? pressedModifier;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -35,8 +44,38 @@ class _InputHandlerState extends State<InputHandler> {
|
||||||
focusNode.requestFocus();
|
focusNode.requestFocus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onKeyEvent: widget.onKeyEvent,
|
onKeyEvent: (node, event) => _onKey(event),
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KeyEventResult _onKey(KeyEvent value) {
|
||||||
|
final keyMap = widget.keyMap?.entries.nonNulls.toList() ?? [];
|
||||||
|
if (value is KeyDownEvent || value is KeyRepeatEvent) {
|
||||||
|
if (KeyCombination.modifierKeys.contains(value.logicalKey)) {
|
||||||
|
pressedModifier = value.logicalKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var entry in keyMap) {
|
||||||
|
final hotKey = entry.key;
|
||||||
|
final keyCombination = entry.value;
|
||||||
|
|
||||||
|
bool isMainKeyPressed = value.logicalKey == keyCombination.key;
|
||||||
|
bool isModifierKeyPressed = pressedModifier == keyCombination.modifier;
|
||||||
|
|
||||||
|
if (isMainKeyPressed && isModifierKeyPressed) {
|
||||||
|
if (widget.keyMapResult?.call(hotKey) ?? false) {
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
} else {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (value is KeyUpEvent) {
|
||||||
|
if (KeyCombination.modifierKeys.contains(value.logicalKey)) {
|
||||||
|
pressedModifier = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue