diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d3547cdcc..e22c77d30 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -61,7 +61,7 @@ PODS: - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS - - mdk (0.35.0) + - mdk (0.35.1) - media_kit_libs_ios_video (1.0.4): - Flutter - media_kit_video (0.0.1): @@ -77,9 +77,9 @@ PODS: - Flutter - screen_brightness_ios (0.1.0): - Flutter - - SDWebImage (5.21.3): - - SDWebImage/Core (= 5.21.3) - - SDWebImage/Core (5.21.3) + - SDWebImage (5.21.7): + - SDWebImage/Core (= 5.21.7) + - SDWebImage/Core (5.21.7) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -119,6 +119,8 @@ PODS: - video_player_avfoundation (0.0.1): - Flutter - FlutterMacOS + - volume_controller (0.0.1): + - Flutter - wakelock_plus (0.0.1): - Flutter - webview_flutter_wkwebview (0.0.1): @@ -154,6 +156,7 @@ DEPENDENCIES: - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) + - volume_controller (from `.symlinks/plugins/volume_controller/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) - workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`) @@ -220,6 +223,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" video_player_avfoundation: :path: ".symlinks/plugins/video_player_avfoundation/darwin" + volume_controller: + :path: ".symlinks/plugins/volume_controller/ios" wakelock_plus: :path: ".symlinks/plugins/wakelock_plus/ios" webview_flutter_wkwebview: @@ -243,7 +248,7 @@ SPEC CHECKSUMS: fvp: f4fdb89279e863eb09869bde7ba7fce9e81a16ab just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79 local_auth_darwin: 63c73d6d28cc3e239be2b6aa460ea6e317cd5100 - mdk: baa616b93f696c7066df0e5ebe057badfa9c462b + mdk: 59bbe9e2ac2a052455ab1b076c245680d66cf6c0 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 @@ -251,7 +256,7 @@ SPEC CHECKSUMS: permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 pointer_interceptor_ios: a78ffe95ad3c6cd52c42ad362c116d92355c3d42 screen_brightness_ios: 6a6f7794b67f07c4f1e24f6374b2d8ad367ffb39 - SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a + SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d @@ -260,6 +265,7 @@ SPEC CHECKSUMS: SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa video_player_avfoundation: 7993f492ae0bd77edaea24d9dc051d8bb2cd7c86 + volume_controller: 2e3de73d6e7e81a0067310d17fb70f2f86d71ac7 wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4 workmanager_apple: 7bac258335c310689a641e2d66e88d4845d372e9 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 053074aca..b3b7c68b5 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -141,6 +141,8 @@ }, "controls": "Controls", "@controls": {}, + "gestures": "Gestures", + "@gestures": {}, "dashboard": "Dashboard", "@dashboard": {}, "dashboardContinue": "Continue", @@ -1264,6 +1266,10 @@ "subtitleConfiguration": "Subtitle configuration", "off": "Off", "screenBrightness": "Screen brightness", + "enableEdgeGesturesTitle": "Enable edge gestures", + "enableEdgeGesturesDesc": "Control brightness and volume by sliding up and down on the edges of the screen", + "reverseEdgeGesturesTitle": "Swap edge gestures", + "reverseEdgeGesturesDesc": "Swap the side for brightness and volume control", "scale": "Scale", "playBackSettings": "Playback Settings", "settingsAutoNextTitle": "Next-up preview", @@ -1523,6 +1529,14 @@ } } }, + "brightnessIndicator": "Brightness: {brightness}", + "@brightnessIndicator": { + "placeholders": { + "brightness": { + "type": "int" + } + } + }, "speedIndicator": "Playback rate: {speed}", "@speedIndicator": { "placeholders": { diff --git a/lib/models/settings/video_player_settings.dart b/lib/models/settings/video_player_settings.dart index d0f397ea2..ab9b7816d 100644 --- a/lib/models/settings/video_player_settings.dart +++ b/lib/models/settings/video_player_settings.dart @@ -84,12 +84,11 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { @Default(2.0) double speedBoostRate, @Default(true) bool enableDoubleTapSeek, @Default(false) bool enableAdvancedVideoOptions, + @Default(true) bool enableEdgeGestures, + @Default(false) bool reverseEdgeGestures, }) = _VideoPlayerSettingsModel; - double get volume => switch (defaultTargetPlatform) { - TargetPlatform.android || TargetPlatform.iOS => 100, - _ => internalVolume, - }; + double get volume => internalVolume; factory VideoPlayerSettingsModel.fromJson(Map json) => _$VideoPlayerSettingsModelFromJson(json); diff --git a/lib/models/settings/video_player_settings.freezed.dart b/lib/models/settings/video_player_settings.freezed.dart index b9f974fb6..4506f254b 100644 --- a/lib/models/settings/video_player_settings.freezed.dart +++ b/lib/models/settings/video_player_settings.freezed.dart @@ -35,6 +35,8 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin { double get speedBoostRate; bool get enableDoubleTapSeek; bool get enableAdvancedVideoOptions; + bool get enableEdgeGestures; + bool get reverseEdgeGestures; /// Create a copy of VideoPlayerSettingsModel /// with the given fields replaced by the non-null parameter values. @@ -72,12 +74,14 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin { ..add(DiagnosticsProperty('speedBoostRate', speedBoostRate)) ..add(DiagnosticsProperty('enableDoubleTapSeek', enableDoubleTapSeek)) ..add(DiagnosticsProperty( - 'enableAdvancedVideoOptions', enableAdvancedVideoOptions)); + 'enableAdvancedVideoOptions', enableAdvancedVideoOptions)) + ..add(DiagnosticsProperty('enableEdgeGestures', enableEdgeGestures)) + ..add(DiagnosticsProperty('reverseEdgeGestures', reverseEdgeGestures)); } @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions)'; + return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures)'; } } @@ -108,7 +112,9 @@ abstract mixin class $VideoPlayerSettingsModelCopyWith<$Res> { bool enableSpeedBoost, double speedBoostRate, bool enableDoubleTapSeek, - bool enableAdvancedVideoOptions}); + bool enableAdvancedVideoOptions, + bool enableEdgeGestures, + bool reverseEdgeGestures}); } /// @nodoc @@ -145,6 +151,8 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res> Object? speedBoostRate = null, Object? enableDoubleTapSeek = null, Object? enableAdvancedVideoOptions = null, + Object? enableEdgeGestures = null, + Object? reverseEdgeGestures = null, }) { return _then(_self.copyWith( screenBrightness: freezed == screenBrightness @@ -231,6 +239,14 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res> ? _self.enableAdvancedVideoOptions : enableAdvancedVideoOptions // ignore: cast_nullable_to_non_nullable as bool, + enableEdgeGestures: null == enableEdgeGestures + ? _self.enableEdgeGestures + : enableEdgeGestures // ignore: cast_nullable_to_non_nullable + as bool, + reverseEdgeGestures: null == reverseEdgeGestures + ? _self.reverseEdgeGestures + : reverseEdgeGestures // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -349,7 +365,9 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { bool enableSpeedBoost, double speedBoostRate, bool enableDoubleTapSeek, - bool enableAdvancedVideoOptions)? + bool enableAdvancedVideoOptions, + bool enableEdgeGestures, + bool reverseEdgeGestures)? $default, { required TResult orElse(), }) { @@ -377,7 +395,9 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.enableSpeedBoost, _that.speedBoostRate, _that.enableDoubleTapSeek, - _that.enableAdvancedVideoOptions); + _that.enableAdvancedVideoOptions, + _that.enableEdgeGestures, + _that.reverseEdgeGestures); case _: return orElse(); } @@ -419,7 +439,9 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { bool enableSpeedBoost, double speedBoostRate, bool enableDoubleTapSeek, - bool enableAdvancedVideoOptions) + bool enableAdvancedVideoOptions, + bool enableEdgeGestures, + bool reverseEdgeGestures) $default, ) { final _that = this; @@ -446,7 +468,9 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.enableSpeedBoost, _that.speedBoostRate, _that.enableDoubleTapSeek, - _that.enableAdvancedVideoOptions); + _that.enableAdvancedVideoOptions, + _that.enableEdgeGestures, + _that.reverseEdgeGestures); case _: throw StateError('Unexpected subclass'); } @@ -487,7 +511,9 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { bool enableSpeedBoost, double speedBoostRate, bool enableDoubleTapSeek, - bool enableAdvancedVideoOptions)? + bool enableAdvancedVideoOptions, + bool enableEdgeGestures, + bool reverseEdgeGestures)? $default, ) { final _that = this; @@ -514,7 +540,9 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.enableSpeedBoost, _that.speedBoostRate, _that.enableDoubleTapSeek, - _that.enableAdvancedVideoOptions); + _that.enableAdvancedVideoOptions, + _that.enableEdgeGestures, + _that.reverseEdgeGestures); case _: return null; } @@ -547,7 +575,9 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel this.enableSpeedBoost = false, this.speedBoostRate = 2.0, this.enableDoubleTapSeek = true, - this.enableAdvancedVideoOptions = false}) + this.enableAdvancedVideoOptions = false, + this.enableEdgeGestures = true, + this.reverseEdgeGestures = false}) : _allowedOrientations = allowedOrientations, _segmentSkipSettings = segmentSkipSettings, _hotKeys = hotKeys, @@ -636,6 +666,12 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel @override @JsonKey() final bool enableAdvancedVideoOptions; + @override + @JsonKey() + final bool enableEdgeGestures; + @override + @JsonKey() + final bool reverseEdgeGestures; /// Create a copy of VideoPlayerSettingsModel /// with the given fields replaced by the non-null parameter values. @@ -678,12 +714,14 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel ..add(DiagnosticsProperty('speedBoostRate', speedBoostRate)) ..add(DiagnosticsProperty('enableDoubleTapSeek', enableDoubleTapSeek)) ..add(DiagnosticsProperty( - 'enableAdvancedVideoOptions', enableAdvancedVideoOptions)); + 'enableAdvancedVideoOptions', enableAdvancedVideoOptions)) + ..add(DiagnosticsProperty('enableEdgeGestures', enableEdgeGestures)) + ..add(DiagnosticsProperty('reverseEdgeGestures', reverseEdgeGestures)); } @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions)'; + return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures)'; } } @@ -716,7 +754,9 @@ abstract mixin class _$VideoPlayerSettingsModelCopyWith<$Res> bool enableSpeedBoost, double speedBoostRate, bool enableDoubleTapSeek, - bool enableAdvancedVideoOptions}); + bool enableAdvancedVideoOptions, + bool enableEdgeGestures, + bool reverseEdgeGestures}); } /// @nodoc @@ -753,6 +793,8 @@ class __$VideoPlayerSettingsModelCopyWithImpl<$Res> Object? speedBoostRate = null, Object? enableDoubleTapSeek = null, Object? enableAdvancedVideoOptions = null, + Object? enableEdgeGestures = null, + Object? reverseEdgeGestures = null, }) { return _then(_VideoPlayerSettingsModel( screenBrightness: freezed == screenBrightness @@ -839,6 +881,14 @@ class __$VideoPlayerSettingsModelCopyWithImpl<$Res> ? _self.enableAdvancedVideoOptions : enableAdvancedVideoOptions // ignore: cast_nullable_to_non_nullable as bool, + enableEdgeGestures: null == enableEdgeGestures + ? _self.enableEdgeGestures + : enableEdgeGestures // ignore: cast_nullable_to_non_nullable + as bool, + reverseEdgeGestures: null == reverseEdgeGestures + ? _self.reverseEdgeGestures + : reverseEdgeGestures // ignore: cast_nullable_to_non_nullable + as bool, )); } } diff --git a/lib/models/settings/video_player_settings.g.dart b/lib/models/settings/video_player_settings.g.dart index 4441a23cb..efdb4a6ab 100644 --- a/lib/models/settings/video_player_settings.g.dart +++ b/lib/models/settings/video_player_settings.g.dart @@ -52,6 +52,8 @@ _VideoPlayerSettingsModel _$VideoPlayerSettingsModelFromJson( enableDoubleTapSeek: json['enableDoubleTapSeek'] as bool? ?? true, enableAdvancedVideoOptions: json['enableAdvancedVideoOptions'] as bool? ?? false, + enableEdgeGestures: json['enableEdgeGestures'] as bool? ?? true, + reverseEdgeGestures: json['reverseEdgeGestures'] as bool? ?? false, ); Map _$VideoPlayerSettingsModelToJson( @@ -82,6 +84,8 @@ Map _$VideoPlayerSettingsModelToJson( 'speedBoostRate': instance.speedBoostRate, 'enableDoubleTapSeek': instance.enableDoubleTapSeek, 'enableAdvancedVideoOptions': instance.enableAdvancedVideoOptions, + 'enableEdgeGestures': instance.enableEdgeGestures, + 'reverseEdgeGestures': instance.reverseEdgeGestures, }; const _$BoxFitEnumMap = { diff --git a/lib/providers/settings/video_player_settings_provider.dart b/lib/providers/settings/video_player_settings_provider.dart index da71228cb..531d9fc8c 100644 --- a/lib/providers/settings/video_player_settings_provider.dart +++ b/lib/providers/settings/video_player_settings_provider.dart @@ -1,9 +1,12 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:screen_brightness/screen_brightness.dart'; +import 'package:volume_controller/volume_controller.dart'; import 'package:fladder/models/settings/key_combinations.dart'; import 'package:fladder/models/settings/video_player_settings.dart'; @@ -18,10 +21,38 @@ final videoPlayerSettingsProvider = final playbackRateProvider = StateProvider((ref) => 1.0); class VideoPlayerSettingsProviderNotifier extends StateNotifier { - VideoPlayerSettingsProviderNotifier(this.ref) : super(VideoPlayerSettingsModel()); + VideoPlayerSettingsProviderNotifier(this.ref) : super(VideoPlayerSettingsModel()) { + _initVolumeSync(); + } final Ref ref; + void _initVolumeSync() async { + // Initialize volume from system volume on mobile/supported platforms + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + VolumeController.instance.showSystemUI = false; + final initialVolume = await VolumeController.instance.getVolume(); + state = state.copyWith(internalVolume: initialVolume * 100); + + VolumeController.instance.addListener((volume) { + // Update both the model and the player when system volume changes (hardware buttons) + final newVolume = volume * 100; + if ((state.internalVolume - newVolume).abs() > 0.1) { + state = state.copyWith(internalVolume: newVolume); + ref.read(videoPlayerProvider).setVolume(newVolume); + } + }); + } + } + + @override + void dispose() { + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + VolumeController.instance.removeListener(); + } + super.dispose(); + } + @override set state(VideoPlayerSettingsModel value) { final oldState = super.state; @@ -63,12 +94,18 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier state = state.copyWith(enableDoubleTapSeek: value); void setEnableAdvancedVideoOptions(bool value) => state = state.copyWith(enableAdvancedVideoOptions: value); + + void setEnableEdgeGestures(bool value) => state = state.copyWith(enableEdgeGestures: value); + + void setReverseEdgeGestures(bool value) => state = state.copyWith(reverseEdgeGestures: value); } diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index a324ad774..1e04dcbd2 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -201,6 +201,74 @@ class _PlayerSettingsPageState extends ConsumerState { }, ), ), + if (AdaptiveLayout.inputDeviceOf(context) != InputDevice.touch) + ExpansionTile( + title: Text( + context.localized.keyboardShortCuts, + style: Theme.of(context).textTheme.titleLarge, + ), + children: VideoHotKeys.values + .map( + (entry) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Expanded( + child: Text( + entry.label(context), + style: Theme.of(context).textTheme.titleMedium, + ), + ), + Flexible( + child: KeyCombinationWidget( + currentKey: videoSettings.hotKeys[entry], + defaultKey: videoSettings.defaultShortCuts[entry]!, + onChanged: (value) => + ref.read(videoPlayerSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)), + ), + ), + ], + ), + ), + ) + .toList(), + ), + ], + ), + const SizedBox(height: 12), + ...settingsListGroup( + context, + SettingsLabelDivider(label: context.localized.gestures), + [ + if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.touch) ...[ + SettingsListTile( + label: Text(context.localized.enableDoubleTapSeekTitle), + subLabel: Text(context.localized.enableDoubleTapSeekDesc), + onTap: () => provider.setEnableDoubleTapSeek(!videoSettings.enableDoubleTapSeek), + trailing: Switch( + value: videoSettings.enableDoubleTapSeek, + onChanged: (value) => provider.setEnableDoubleTapSeek(value), + ), + ), + SettingsListTile( + label: Text(context.localized.enableEdgeGesturesTitle), + subLabel: Text(context.localized.enableEdgeGesturesDesc), + onTap: () => provider.setEnableEdgeGestures(!videoSettings.enableEdgeGestures), + trailing: Switch( + value: videoSettings.enableEdgeGestures, + onChanged: (value) => provider.setEnableEdgeGestures(value), + ), + ), + SettingsListTile( + label: Text(context.localized.reverseEdgeGesturesTitle), + subLabel: Text(context.localized.reverseEdgeGesturesDesc), + onTap: () => provider.setReverseEdgeGestures(!videoSettings.reverseEdgeGestures), + trailing: Switch( + value: videoSettings.reverseEdgeGestures, + onChanged: (value) => provider.setReverseEdgeGestures(value), + ), + ), + ], SettingsListTile( label: Text(context.localized.enableSpeedBoostTitle), subLabel: Text(context.localized.enableSpeedBoostDesc), @@ -249,48 +317,6 @@ class _PlayerSettingsPageState extends ConsumerState { ], ), ), - if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.touch) - SettingsListTile( - label: Text(context.localized.enableDoubleTapSeekTitle), - subLabel: Text(context.localized.enableDoubleTapSeekDesc), - onTap: () => provider.setEnableDoubleTapSeek(!videoSettings.enableDoubleTapSeek), - trailing: Switch( - value: videoSettings.enableDoubleTapSeek, - onChanged: (value) => provider.setEnableDoubleTapSeek(value), - ), - ), - if (AdaptiveLayout.inputDeviceOf(context) != InputDevice.touch) - ExpansionTile( - title: Text( - context.localized.keyboardShortCuts, - style: Theme.of(context).textTheme.titleLarge, - ), - children: VideoHotKeys.values - .map( - (entry) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ - Expanded( - child: Text( - entry.label(context), - style: Theme.of(context).textTheme.titleMedium, - ), - ), - Flexible( - child: KeyCombinationWidget( - currentKey: videoSettings.hotKeys[entry], - defaultKey: videoSettings.defaultShortCuts[entry]!, - onChanged: (value) => - ref.read(videoPlayerSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)), - ), - ), - ], - ), - ), - ) - .toList(), - ), ], ), const SizedBox(height: 12), diff --git a/lib/screens/video_player/components/video_player_brightness_indicator.dart b/lib/screens/video_player/components/video_player_brightness_indicator.dart new file mode 100644 index 000000000..07a2aff52 --- /dev/null +++ b/lib/screens/video_player/components/video_player_brightness_indicator.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; + +import 'package:async/async.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; + +import 'package:fladder/providers/settings/video_player_settings_provider.dart'; +import 'package:fladder/util/localization_helper.dart'; + +class VideoPlayerBrightnessIndicator extends ConsumerStatefulWidget { + const VideoPlayerBrightnessIndicator({super.key}); + + @override + ConsumerState createState() => _VideoPlayerBrightnessIndicatorState(); +} + +class _VideoPlayerBrightnessIndicatorState extends ConsumerState { + late double currentBrightness = + ref.read(videoPlayerSettingsProvider.select((value) => value.screenBrightness ?? 1.0)); + + bool showIndicator = false; + late final timer = RestartableTimer(const Duration(seconds: 1), () { + setState(() { + showIndicator = false; + }); + }); + + @override + void dispose() { + showIndicator = false; + timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + ref.listen( + videoPlayerSettingsProvider.select((value) => value.screenBrightness), + (previous, next) { + if (next == null) return; + setState(() { + showIndicator = true; + currentBrightness = next; + }); + timer.reset(); + }, + ); + return IgnorePointer( + child: AnimatedOpacity( + duration: const Duration(milliseconds: 250), + opacity: showIndicator ? 1 : 0, + child: Center( + child: Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.85), + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 12, + children: [ + const Icon( + IconsaxPlusLinear.sun_1, + ), + Text( + context.localized.brightnessIndicator((currentBrightness * 100).round()), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.white, + ), + ) + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 03c332693..0a983a315 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -25,6 +25,7 @@ import 'package:fladder/screens/video_player/components/video_playback_informati import 'package:fladder/screens/video_player/components/video_player_controls_extras.dart'; import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart'; import 'package:fladder/screens/video_player/components/video_player_quality_controls.dart'; +import 'package:fladder/screens/video_player/components/video_player_brightness_indicator.dart'; import 'package:fladder/screens/video_player/components/video_player_screenshot_indicator.dart'; import 'package:fladder/screens/video_player/components/video_player_seek_indicator.dart'; import 'package:fladder/screens/video_player/components/video_player_speed_indicator.dart'; @@ -73,6 +74,10 @@ class _DesktopControlsState extends ConsumerState { late final double topPadding = MediaQuery.of(context).viewPadding.top; late final double bottomPadding = MediaQuery.of(context).viewPadding.bottom; + String? _vDragSide; + double? _vDragStartValue; + double? _vDragLastValue; + @override void initState() { super.initState(); @@ -130,6 +135,9 @@ class _DesktopControlsState extends ConsumerState { : _handleDoubleTapSeek, onLongPressStart: initInputDevice == InputDevice.touch ? _handleLongPressStart : null, onLongPressEnd: initInputDevice == InputDevice.touch ? _handleLongPressEnd : null, + onVerticalDragStart: initInputDevice == InputDevice.touch ? _handleVerticalDragStart : null, + onVerticalDragUpdate: initInputDevice == InputDevice.touch ? _handleVerticalDragUpdate : null, + onVerticalDragEnd: initInputDevice == InputDevice.touch ? _handleVerticalDragEnd : null, ), ), if (subtitleWidget != null) subtitleWidget, @@ -155,6 +163,7 @@ class _DesktopControlsState extends ConsumerState { ), VideoPlayerSeekIndicator(controller: _seekController), const VideoPlayerVolumeIndicator(), + const VideoPlayerBrightnessIndicator(), const VideoPlayerSpeedIndicator(), const VideoPlayerScreenshotIndicator(), Consumer( @@ -830,6 +839,56 @@ class _DesktopControlsState extends ConsumerState { _deactivateSpeedBoost(); } + void _handleVerticalDragStart(DragStartDetails details) { + final settings = ref.read(videoPlayerSettingsProvider); + if (!settings.enableEdgeGestures) return; + + final size = MediaQuery.sizeOf(context); + final y = details.localPosition.dy; + // Safety margin of 10% top/bottom to avoid accidental system gestures (notification tray, home bar) + if (y < size.height * 0.1 || y > size.height * 0.9) { + _vDragSide = null; + return; + } + + final isLeft = details.localPosition.dx < size.width / 2; + final isBrightness = settings.reverseEdgeGestures ? !isLeft : isLeft; + + _vDragSide = isBrightness ? 'brightness' : 'volume'; + + if (isBrightness) { + _vDragStartValue = settings.screenBrightness ?? 1.0; + } else { + _vDragStartValue = settings.volume / 100; + } + _vDragLastValue = _vDragStartValue; + } + + void _handleVerticalDragUpdate(DragUpdateDetails details) { + if (_vDragSide == null || _vDragStartValue == null) return; + + final screenHeight = MediaQuery.sizeOf(context).height; + // Slide up to increase, down to decrease. + // details.delta.dy is positive when sliding down. + final delta = -details.primaryDelta! / (screenHeight * 0.7); // 70% of screen height for full range + final newValue = (_vDragLastValue! + delta).clamp(0.0, 1.0); + + if (newValue == _vDragLastValue) return; + _vDragLastValue = newValue; + + if (_vDragSide == 'brightness') { + ref.read(videoPlayerSettingsProvider.notifier).setScreenBrightness(newValue); + } else { + ref.read(videoPlayerSettingsProvider.notifier).setVolume(newValue * 100); + } + } + + void _handleVerticalDragEnd(DragEndDetails details) { + _vDragSide = null; + _vDragStartValue = null; + _vDragLastValue = null; + } + bool _onKey(VideoHotKeys value) { final mediaSegments = ref.read(playBackModel.select((value) => value?.mediaSegments)); final position = ref.read(mediaPlaybackProvider).position; diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 57aad92e7..728fe3f5b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -15,6 +15,7 @@ #include #include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -45,6 +46,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) volume_controller_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin"); + volume_controller_plugin_register_with_registrar(volume_controller_registrar); g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b0e985ae2..3b5a7ec2a 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_retriever_linux sqlite3_flutter_libs url_launcher_linux + volume_controller window_manager ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d75d38b40..2e109c29b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -29,6 +29,7 @@ import sqflite_darwin import sqlite3_flutter_libs import url_launcher_macos import video_player_avfoundation +import volume_controller import wakelock_plus import webview_flutter_wkwebview import window_manager @@ -58,6 +59,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) + VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 7ac706fef..0606aac41 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -81,6 +81,8 @@ PODS: - video_player_avfoundation (0.0.1): - Flutter - FlutterMacOS + - volume_controller (0.0.1): + - FlutterMacOS - wakelock_plus (0.0.1): - FlutterMacOS - webview_flutter_wkwebview (0.0.1): @@ -115,6 +117,7 @@ DEPENDENCIES: - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) + - volume_controller (from `Flutter/ephemeral/.symlinks/plugins/volume_controller/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) @@ -175,6 +178,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos video_player_avfoundation: :path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin + volume_controller: + :path: Flutter/ephemeral/.symlinks/plugins/volume_controller/macos wakelock_plus: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos webview_flutter_wkwebview: @@ -210,6 +215,7 @@ SPEC CHECKSUMS: sqlite3_flutter_libs: 86f82662868ee26ff3451f73cac9c5fc2a1f57fa url_launcher_macos: 175a54c831f4375a6cf895875f716ee5af3888ce video_player_avfoundation: 7993f492ae0bd77edaea24d9dc051d8bb2cd7c86 + volume_controller: 90a5978956cf18ebb7739bf5382fc1b0cfef66d0 wakelock_plus: 9d63063ffb7af1c215209769067c57103bde719d webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4 window_manager: e25faf20d88283a0d46e7b1a759d07261ca27575 diff --git a/pubspec.lock b/pubspec.lock index 7385c79a7..13b25db37 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2386,6 +2386,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + volume_controller: + dependency: "direct main" + description: + name: volume_controller + sha256: "5c1a13d2ea99d2f6753e7c660d0d3fab541f36da3999cafeb17b66fe49759ad7" + url: "https://pub.dev" + source: hosted + version: "3.4.1" wakelock_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 263dead89..e14c18ebb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -113,6 +113,7 @@ dependencies: package_info_plus: ^9.0.0 wakelock_plus: ^1.3.2 screen_brightness: ^2.1.7 + volume_controller: 3.4.1 window_manager: ^0.5.1 smtc_windows: ^1.1.0 background_downloader: ^9.2.3 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index a69433ed2..86e6b3679 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -20,6 +20,7 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -51,6 +52,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + VolumeControllerPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("VolumeControllerPluginCApi")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 57c435da8..9dd347404 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -17,6 +17,7 @@ list(APPEND FLUTTER_PLUGIN_LIST share_plus sqlite3_flutter_libs url_launcher_windows + volume_controller window_manager )