diff --git a/.vscode/launch.json b/.vscode/launch.json index 641207a..a09b032 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,18 @@ "type": "dart", "args": [ "--flavor", - "development" + "development", + ] + }, + { + "name": "Fladder Development HTPC (debug)", + "request": "launch", + "type": "dart", + "args": [ + "--flavor", + "development", + "-a", + "--htpc", ] }, { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 62361c7..42b4f6f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1189,6 +1189,7 @@ "segmentActionAskToSkip": "Ask to skip", "segmentActionSkip": "Skip", "loading": "Loading", + "exitFladderTitle": "Exit Fladder", "castAndCrew": "Cast & Crew", "guestActor": "{count, plural, other{Guest Actors} one{Guest Actor}}", "@guestActor": { @@ -1220,5 +1221,4 @@ "hasLikedActor": "Has liked actor", "latest": "Latest", "recommended": "Recommended" - } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 35b4992..af06e73 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,7 +19,9 @@ import 'package:universal_html/html.dart' as html; import 'package:window_manager/window_manager.dart'; import 'package:fladder/models/account_model.dart'; +import 'package:fladder/models/settings/arguments_model.dart'; import 'package:fladder/models/syncing/i_synced_item.dart'; +import 'package:fladder/providers/arguments_provider.dart'; import 'package:fladder/providers/crash_log_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/shared_provider.dart'; @@ -51,7 +53,7 @@ Future> loadConfig() async { return jsonDecode(configString); } -void main() async { +void main(List args) async { WidgetsFlutterBinding.ensureInitialized(); final crashProvider = CrashLogNotifier(); @@ -95,6 +97,7 @@ void main() async { sharedPreferencesProvider.overrideWith((ref) => sharedPreferences), applicationInfoProvider.overrideWith((ref) => applicationInfo), crashLogProvider.overrideWith((ref) => crashProvider), + argumentsStateProvider.overrideWith((ref) => ArgumentsModel.fromArguments(args)), syncProvider.overrideWith((ref) => SyncNotifier( ref, !kIsWeb @@ -234,6 +237,10 @@ class _MainState extends ConsumerState
with WindowListener, WidgetsBinding windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.show(); await windowManager.focus(); + final startupArguments = ref.read(argumentsStateProvider); + if (startupArguments.htpcMode && !(await windowManager.isFullScreen())) { + await windowManager.setFullScreen(true); + } }); } else { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: []); diff --git a/lib/models/settings/arguments_model.dart b/lib/models/settings/arguments_model.dart new file mode 100644 index 0000000..d0cbe64 --- /dev/null +++ b/lib/models/settings/arguments_model.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'arguments_model.freezed.dart'; + +@freezed +class ArgumentsModel with _$ArgumentsModel { + const ArgumentsModel._(); + + factory ArgumentsModel({ + @Default(false) bool htpcMode, + }) = _ArgumentsModel; + + factory ArgumentsModel.fromArguments(List arguments) { + arguments = arguments.map((e) => e.trim()).toList(); + return ArgumentsModel( + htpcMode: arguments.contains('--htpc'), + ); + } +} diff --git a/lib/models/settings/arguments_model.freezed.dart b/lib/models/settings/arguments_model.freezed.dart new file mode 100644 index 0000000..f3b7669 --- /dev/null +++ b/lib/models/settings/arguments_model.freezed.dart @@ -0,0 +1,147 @@ +// 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 'arguments_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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'); + +/// @nodoc +mixin _$ArgumentsModel { + bool get htpcMode => throw _privateConstructorUsedError; + + /// Create a copy of ArgumentsModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ArgumentsModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ArgumentsModelCopyWith<$Res> { + factory $ArgumentsModelCopyWith( + ArgumentsModel value, $Res Function(ArgumentsModel) then) = + _$ArgumentsModelCopyWithImpl<$Res, ArgumentsModel>; + @useResult + $Res call({bool htpcMode}); +} + +/// @nodoc +class _$ArgumentsModelCopyWithImpl<$Res, $Val extends ArgumentsModel> + implements $ArgumentsModelCopyWith<$Res> { + _$ArgumentsModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ArgumentsModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? htpcMode = null, + }) { + return _then(_value.copyWith( + htpcMode: null == htpcMode + ? _value.htpcMode + : htpcMode // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ArgumentsModelImplCopyWith<$Res> + implements $ArgumentsModelCopyWith<$Res> { + factory _$$ArgumentsModelImplCopyWith(_$ArgumentsModelImpl value, + $Res Function(_$ArgumentsModelImpl) then) = + __$$ArgumentsModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool htpcMode}); +} + +/// @nodoc +class __$$ArgumentsModelImplCopyWithImpl<$Res> + extends _$ArgumentsModelCopyWithImpl<$Res, _$ArgumentsModelImpl> + implements _$$ArgumentsModelImplCopyWith<$Res> { + __$$ArgumentsModelImplCopyWithImpl( + _$ArgumentsModelImpl _value, $Res Function(_$ArgumentsModelImpl) _then) + : super(_value, _then); + + /// Create a copy of ArgumentsModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? htpcMode = null, + }) { + return _then(_$ArgumentsModelImpl( + htpcMode: null == htpcMode + ? _value.htpcMode + : htpcMode // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$ArgumentsModelImpl extends _ArgumentsModel { + _$ArgumentsModelImpl({this.htpcMode = false}) : super._(); + + @override + @JsonKey() + final bool htpcMode; + + @override + String toString() { + return 'ArgumentsModel(htpcMode: $htpcMode)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ArgumentsModelImpl && + (identical(other.htpcMode, htpcMode) || + other.htpcMode == htpcMode)); + } + + @override + int get hashCode => Object.hash(runtimeType, htpcMode); + + /// Create a copy of ArgumentsModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ArgumentsModelImplCopyWith<_$ArgumentsModelImpl> get copyWith => + __$$ArgumentsModelImplCopyWithImpl<_$ArgumentsModelImpl>( + this, _$identity); +} + +abstract class _ArgumentsModel extends ArgumentsModel { + factory _ArgumentsModel({final bool htpcMode}) = _$ArgumentsModelImpl; + _ArgumentsModel._() : super._(); + + @override + bool get htpcMode; + + /// Create a copy of ArgumentsModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ArgumentsModelImplCopyWith<_$ArgumentsModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/providers/arguments_provider.dart b/lib/providers/arguments_provider.dart new file mode 100644 index 0000000..c46af5a --- /dev/null +++ b/lib/providers/arguments_provider.dart @@ -0,0 +1,5 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/models/settings/arguments_model.dart'; + +final argumentsStateProvider = StateProvider((ref) => ArgumentsModel()); diff --git a/lib/screens/photo_viewer/photo_viewer_controls.dart b/lib/screens/photo_viewer/photo_viewer_controls.dart index 91e344d..a65ea21 100644 --- a/lib/screens/photo_viewer/photo_viewer_controls.dart +++ b/lib/screens/photo_viewer/photo_viewer_controls.dart @@ -20,9 +20,8 @@ import 'package:fladder/util/input_handler.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/throttler.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; import 'package:fladder/widgets/shared/elevated_icon.dart'; -import 'package:fladder/widgets/shared/full_screen_button.dart' - if (dart.library.html) 'package:fladder/widgets/shared/full_screen_button_web.dart'; import 'package:fladder/widgets/shared/progress_floating_button.dart'; class PhotoViewerControls extends ConsumerStatefulWidget { @@ -131,7 +130,7 @@ class _PhotoViewerControllsState extends ConsumerState with @override void dispose() { timerController.dispose(); - closeFullScreen(); + fullScreenHelper.closeFullScreen(ref); windowManager.removeListener(this); super.dispose(); } diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index 0a64edb..51512ca 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -25,7 +25,6 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/bitrate_helper.dart'; import 'package:fladder/util/box_fit_extension.dart'; import 'package:fladder/util/localization_helper.dart'; -import 'package:fladder/util/option_dialogue.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; @RoutePage() @@ -74,22 +73,18 @@ class _PlayerSettingsPageState extends ConsumerState { ], ), SettingsListTile( - label: Text(context.localized.videoScalingFillScreenTitle), - subLabel: Text(videoSettings.videoFit.label(context)), - onTap: () => openMultiSelectOptions( - context, - label: context.localized.videoScalingFillScreenTitle, - items: BoxFit.values, - selected: [ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit))], - onChanged: (values) => ref.read(videoPlayerSettingsProvider.notifier).setFitType(values.first), - itemBuilder: (type, selected, tap) => RadioListTile( - groupValue: ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit)), - title: Text(type.label(context)), - value: type, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - contentPadding: EdgeInsets.zero, - onChanged: (value) => tap(), - ), + label: Text(context.localized.videoScaling), + trailing: EnumBox( + current: videoSettings.videoFit.label(context), + itemBuilder: (context) => BoxFit.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => ref.read(videoPlayerSettingsProvider.notifier).setFitType(entry), + ), + ) + .toList(), ), ), SettingsListTile( @@ -363,7 +358,7 @@ class _StatusIndicator extends StatelessWidget { ), const SizedBox(width: 6), ], - label, + Flexible(child: label), ], ); } diff --git a/lib/screens/settings/settings_list_tile.dart b/lib/screens/settings/settings_list_tile.dart index 3539456..8e2ecba 100644 --- a/lib/screens/settings/settings_list_tile.dart +++ b/lib/screens/settings/settings_list_tile.dart @@ -8,7 +8,7 @@ class SettingsListTile extends StatelessWidget { final Widget? trailing; final bool selected; final IconData? icon; - final Widget? suffix; + final Widget? leading; final Color? contentColor; final Function()? onTap; const SettingsListTile({ @@ -16,7 +16,7 @@ class SettingsListTile extends StatelessWidget { this.subLabel, this.trailing, this.selected = false, - this.suffix, + this.leading, this.icon, this.contentColor, this.onTap, @@ -27,7 +27,7 @@ class SettingsListTile extends StatelessWidget { Widget build(BuildContext context) { final iconWidget = icon != null ? Icon(icon) : null; - final leadingWidget = (suffix ?? iconWidget) != null + final leadingWidget = (leading ?? iconWidget) != null ? Padding( padding: const EdgeInsets.only(left: 8, right: 16.0), child: AnimatedContainer( @@ -38,11 +38,11 @@ class SettingsListTile extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12), - child: (suffix ?? iconWidget), + child: (leading ?? iconWidget), ), ), ) - : suffix ?? const SizedBox(); + : leading ?? const SizedBox(); return Card( elevation: selected ? 2 : 0, color: selected ? Theme.of(context).colorScheme.surfaceContainerLow : Colors.transparent, @@ -57,7 +57,7 @@ class SettingsListTile extends StatelessWidget { horizontal: 16, vertical: 12, ).copyWith( - left: (suffix ?? iconWidget) != null ? 0 : null, + left: (leading ?? iconWidget) != null ? 0 : null, ), child: ConstrainedBox( constraints: const BoxConstraints( diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 1f15c3e..937e5f5 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -3,7 +3,9 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:window_manager/window_manager.dart'; +import 'package:fladder/providers/arguments_provider.dart'; import 'package:fladder/providers/auth_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; @@ -11,6 +13,7 @@ import 'package:fladder/screens/settings/quick_connect_window.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/shared/fladder_icon.dart'; +import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/theme_extensions.dart'; @@ -137,7 +140,7 @@ class _SettingsScreenState extends ConsumerState { label: Text(context.localized.about), subLabel: const Text("Fladder"), selected: containsRoute(const AboutSettingsRoute()), - suffix: Opacity( + leading: Opacity( opacity: 1, child: FladderIconOutlined( size: 24, @@ -146,6 +149,20 @@ class _SettingsScreenState extends ConsumerState { ), onTap: () => navigateTo(const AboutSettingsRoute()), ), + if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) ...[ + SettingsListTile( + label: Text(context.localized.exitFladderTitle), + icon: IconsaxPlusLinear.close_square, + onTap: () async { + final manager = WindowManager.instance; + if (await manager.isClosable()) { + manager.close(); + } else { + fladderSnackbar(context, title: context.localized.somethingWentWrong); + } + }, + ), + ], ], floatingActionButton: Padding( padding: EdgeInsets.symmetric(horizontal: MediaQuery.paddingOf(context).horizontal), diff --git a/lib/screens/shared/default_title_bar.dart b/lib/screens/shared/default_title_bar.dart index 9ad3dec..d12ff14 100644 --- a/lib/screens/shared/default_title_bar.dart +++ b/lib/screens/shared/default_title_bar.dart @@ -4,7 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:fladder/providers/arguments_provider.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; class DefaultTitleBar extends ConsumerStatefulWidget { final String? label; @@ -33,6 +35,7 @@ class _DefaultTitleBarState extends ConsumerState with WindowLi @override Widget build(BuildContext context) { + if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) return const SizedBox.shrink(); final brightness = widget.brightness ?? Theme.of(context).brightness; final iconColor = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65); return MouseRegion( @@ -77,11 +80,9 @@ class _DefaultTitleBarState extends ConsumerState with WindowLi children: [ FutureBuilder>(future: Future.microtask(() async { final isMinimized = await windowManager.isMinimized(); - final isFullScreen = await windowManager.isFullScreen(); - return [isMinimized, isFullScreen]; + return [isMinimized]; }), builder: (context, snapshot) { final isMinimized = snapshot.data?.firstOrNull ?? false; - final fullScreen = snapshot.data?.lastOrNull ?? false; return IconButton( style: IconButton.styleFrom( hoverColor: brightness == Brightness.light @@ -89,9 +90,7 @@ class _DefaultTitleBarState extends ConsumerState with WindowLi : Colors.white.withValues(alpha: 0.2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2))), onPressed: () async { - if (fullScreen) { - await windowManager.setFullScreen(false); - } + fullScreenHelper.closeFullScreen(ref); if (isMinimized) { windowManager.restore(); } else { @@ -111,12 +110,10 @@ class _DefaultTitleBarState extends ConsumerState with WindowLi FutureBuilder>( future: Future.microtask(() async { final isMaximized = await windowManager.isMaximized(); - final isFullScreen = await windowManager.isFullScreen(); - return [isMaximized, isFullScreen]; + return [isMaximized]; }), builder: (BuildContext context, AsyncSnapshot> snapshot) { final maximized = snapshot.data?.firstOrNull ?? false; - final fullScreen = snapshot.data?.lastOrNull ?? false; return IconButton( style: IconButton.styleFrom( hoverColor: brightness == Brightness.light @@ -125,15 +122,12 @@ class _DefaultTitleBarState extends ConsumerState with WindowLi shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), ), onPressed: () async { - if (fullScreen && maximized) { - await windowManager.setFullScreen(false); + fullScreenHelper.closeFullScreen(ref); + if (maximized) { await windowManager.unmaximize(); return; } - - if (fullScreen) { - await windowManager.setFullScreen(false); - } else if (!maximized) { + if (!maximized) { await windowManager.maximize(); } else { await windowManager.unmaximize(); diff --git a/lib/screens/video_player/components/video_player_next_wrapper.dart b/lib/screens/video_player/components/video_player_next_wrapper.dart index 3f9ed40..ffbdad0 100644 --- a/lib/screens/video_player/components/video_player_next_wrapper.dart +++ b/lib/screens/video_player/components/video_player_next_wrapper.dart @@ -20,9 +20,8 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart'; -import 'package:fladder/widgets/shared/full_screen_button.dart' - if (dart.library.html) 'package:fladder/widgets/shared/full_screen_button_web.dart'; import 'package:fladder/widgets/shared/progress_floating_button.dart'; class VideoPlayerNextWrapper extends ConsumerStatefulWidget { @@ -132,7 +131,7 @@ class _VideoPlayerNextWrapperState extends ConsumerState if (AdaptiveLayout.of(context).inputDevice != InputDevice.pointer) { ScreenBrightness().resetApplicationScreenBrightness(); } else { - closeFullScreen(); + fullScreenHelper.closeFullScreen(ref); } SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 96fa276..52a6f41 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -31,8 +31,7 @@ import 'package:fladder/util/input_handler.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/string_extensions.dart'; -import 'package:fladder/widgets/shared/full_screen_button.dart' - if (dart.library.html) 'package:fladder/widgets/shared/full_screen_button_web.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; class DesktopControls extends ConsumerStatefulWidget { const DesktopControls({super.key}); @@ -86,7 +85,7 @@ class _DesktopControlsState extends ConsumerState { return true; } if (value.logicalKey == LogicalKeyboardKey.keyF) { - toggleFullScreen(ref); + fullScreenHelper.toggleFullScreen(ref); return true; } if (value.logicalKey == LogicalKeyboardKey.arrowUp) { @@ -137,7 +136,7 @@ class _DesktopControlsState extends ConsumerState { ? () => player.playOrPause() : () => toggleOverlay(), onDoubleTap: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer - ? () => toggleFullScreen(ref) + ? () => fullScreenHelper.toggleFullScreen(ref) : null, ), ), @@ -665,6 +664,6 @@ class _DesktopControlsState extends ConsumerState { Future disableFullScreen() async { resetTimer(); - closeFullScreen(); + fullScreenHelper.closeFullScreen(ref); } } diff --git a/lib/util/adaptive_layout/adaptive_layout.dart b/lib/util/adaptive_layout/adaptive_layout.dart index b780296..29fc6f0 100644 --- a/lib/util/adaptive_layout/adaptive_layout.dart +++ b/lib/util/adaptive_layout/adaptive_layout.dart @@ -9,6 +9,7 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout_model.dart'; import 'package:fladder/util/debug_banner.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/poster_defaults.dart'; +import 'package:fladder/util/resolution_checker.dart'; enum InputDevice { touch, @@ -209,7 +210,11 @@ class _AdaptiveLayoutBuilderState extends ConsumerState { controller: controller, posterDefaults: posterDefaults, ), - child: widget.adaptiveLayout == null ? DebugBanner(child: widget.child(context)) : widget.child(context), + child: Builder( + builder: (context) => ResolutionChecker( + child: widget.adaptiveLayout == null ? DebugBanner(child: widget.child(context)) : widget.child(context), + ), + ), ), ); } diff --git a/lib/util/item_base_model/play_item_helpers.dart b/lib/util/item_base_model/play_item_helpers.dart index 82e682d..583a697 100644 --- a/lib/util/item_base_model/play_item_helpers.dart +++ b/lib/util/item_base_model/play_item_helpers.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:window_manager/window_manager.dart'; import 'package:fladder/models/book_model.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -26,6 +25,7 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/list_extensions.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; Future _showLoadingIndicator(BuildContext context) async { return showDialog( @@ -103,10 +103,7 @@ Future _playVideo( ), ); if (AdaptiveLayout.of(context).isDesktop) { - final fullScreen = await windowManager.isFullScreen(); - if (fullScreen) { - await windowManager.setFullScreen(false); - } + fullScreenHelper.closeFullScreen(ref); } if (context.mounted) { context.refreshData(); diff --git a/lib/util/resolution_checker.dart b/lib/util/resolution_checker.dart new file mode 100644 index 0000000..a13c110 --- /dev/null +++ b/lib/util/resolution_checker.dart @@ -0,0 +1,63 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:screen_retriever/screen_retriever.dart'; +import 'package:window_manager/window_manager.dart'; + +import 'package:fladder/providers/arguments_provider.dart'; + +class ResolutionChecker extends ConsumerStatefulWidget { + final Widget child; + const ResolutionChecker({required this.child, super.key}); + + @override + ConsumerState createState() => _ResolutionCheckerState(); +} + +class _ResolutionCheckerState extends ConsumerState { + Size? lastResolution; + Timer? _timer; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((value) async { + if (ref.read(argumentsStateProvider).htpcMode) { + lastResolution = (await screenRetriever.getPrimaryDisplay()).size; + _timer = Timer.periodic(const Duration(seconds: 2), (timer) => checkResolution()); + } + }); + } + + Future checkResolution() async { + if (!mounted) return; + final newResolution = (await screenRetriever.getPrimaryDisplay()).size; + if (lastResolution != newResolution) { + lastResolution = newResolution; + shouldSetResolution(); + } + } + + Future shouldSetResolution() async { + if (lastResolution != null) { + final isFullScreen = await windowManager.isFullScreen(); + if (isFullScreen) { + await windowManager.setFullScreen(false); + } + await windowManager.setFullScreen(true); + } + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/widgets/full_screen_helpers/full_screen_helper_desktop.dart b/lib/widgets/full_screen_helpers/full_screen_helper_desktop.dart new file mode 100644 index 0000000..8095df4 --- /dev/null +++ b/lib/widgets/full_screen_helpers/full_screen_helper_desktop.dart @@ -0,0 +1,28 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:window_manager/window_manager.dart'; + +import 'package:fladder/providers/arguments_provider.dart'; +import 'package:fladder/providers/video_player_provider.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; + +class FullScreenHelper implements FullScreenWrapper { + const FullScreenHelper._(); + factory FullScreenHelper.instantiate() => const FullScreenHelper._(); + @override + Future closeFullScreen(WidgetRef ref) async { + if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) return; + final isFullScreen = await windowManager.isFullScreen(); + if (isFullScreen) { + await windowManager.setFullScreen(false); + } + ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: false)); + } + + @override + Future toggleFullScreen(WidgetRef ref) async { + if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) return; + final isFullScreen = await windowManager.isFullScreen(); + await windowManager.setFullScreen(!isFullScreen); + ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: !isFullScreen)); + } +} diff --git a/lib/widgets/full_screen_helpers/full_screen_helper_web.dart b/lib/widgets/full_screen_helpers/full_screen_helper_web.dart new file mode 100644 index 0000000..69d546d --- /dev/null +++ b/lib/widgets/full_screen_helpers/full_screen_helper_web.dart @@ -0,0 +1,34 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:universal_html/html.dart' as html; + +import 'package:fladder/providers/video_player_provider.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; + +class FullScreenHelper implements FullScreenWrapper { + const FullScreenHelper._(); + factory FullScreenHelper.instantiate() => const FullScreenHelper._(); + @override + Future closeFullScreen(WidgetRef ref) async { + if (html.document.fullscreenElement != null) { + html.document.exitFullscreen(); + await Future.delayed(const Duration(milliseconds: 500)); + ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: false)); + } + } + + @override + Future toggleFullScreen(WidgetRef ref) async { + final isFullScreen = html.document.fullscreenElement != null; + + if (isFullScreen) { + html.document.exitFullscreen(); + //Wait for 1 second + await Future.delayed(const Duration(seconds: 1)); + } else { + await html.document.documentElement?.requestFullscreen(); + } + ref + .read(mediaPlaybackProvider.notifier) + .update((state) => state.copyWith(fullScreen: html.document.fullscreenElement != null)); + } +} diff --git a/lib/widgets/full_screen_helpers/full_screen_wrapper.dart b/lib/widgets/full_screen_helpers/full_screen_wrapper.dart new file mode 100644 index 0000000..2328248 --- /dev/null +++ b/lib/widgets/full_screen_helpers/full_screen_wrapper.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/providers/arguments_provider.dart'; +import 'package:fladder/providers/video_player_provider.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_helper_desktop.dart' + if (dart.library.html) 'package:fladder/widgets/full_screen_helpers/full_screen_helper_web.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; + +final FullScreenHelper fullScreenHelper = FullScreenHelper.instantiate(); + +abstract class FullScreenWrapper { + Future closeFullScreen(WidgetRef ref); + Future toggleFullScreen(WidgetRef ref); +} + +class FullScreenButton extends ConsumerWidget { + const FullScreenButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) return const SizedBox.shrink(); + final fullScreen = ref.watch(mediaPlaybackProvider.select((value) => value.fullScreen)); + return IconButton( + onPressed: () => fullScreenHelper.toggleFullScreen(ref), + icon: Icon( + fullScreen ? IconsaxPlusLinear.screenmirroring : IconsaxPlusLinear.maximize_4, + ), + ); + } +} diff --git a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart index c5de4ff..965c3d2 100644 --- a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart +++ b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart @@ -58,7 +58,7 @@ class _SideNavigationBarState extends ConsumerState { void stopTimer() { timer?.cancel(); - timer = Timer(const Duration(milliseconds: 350), () { + timer = Timer(const Duration(milliseconds: 125), () { setState(() { showOnHover = false; }); @@ -93,6 +93,7 @@ class _SideNavigationBarState extends ConsumerState { child: MouseRegion( onEnter: (value) => startTimer(), onExit: (event) => stopTimer(), + onHover: (value) => startTimer(), child: Column( children: [ if (isDesktop && AdaptiveLayout.of(context).platform != TargetPlatform.macOS) ...{ diff --git a/lib/widgets/shared/full_screen_button.dart b/lib/widgets/shared/full_screen_button.dart deleted file mode 100644 index 4720dde..0000000 --- a/lib/widgets/shared/full_screen_button.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:window_manager/window_manager.dart'; - -import 'package:fladder/providers/video_player_provider.dart'; - -Future closeFullScreen() async { - final isFullScreen = await windowManager.isFullScreen(); - if (isFullScreen) { - await windowManager.setFullScreen(false); - } -} - -Future toggleFullScreen(WidgetRef ref) async { - final isFullScreen = await windowManager.isFullScreen(); - await windowManager.setFullScreen(!isFullScreen); - ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: !isFullScreen)); -} - -class FullScreenButton extends ConsumerWidget { - const FullScreenButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final fullScreen = ref.watch(mediaPlaybackProvider.select((value) => value.fullScreen)); - return IconButton( - onPressed: () => toggleFullScreen(ref), - icon: Icon( - fullScreen ? IconsaxPlusLinear.screenmirroring : IconsaxPlusLinear.maximize_4, - ), - ); - } -} diff --git a/lib/widgets/shared/full_screen_button_web.dart b/lib/widgets/shared/full_screen_button_web.dart deleted file mode 100644 index 2a48335..0000000 --- a/lib/widgets/shared/full_screen_button_web.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:universal_html/html.dart' as html; - -import 'package:fladder/providers/video_player_provider.dart'; - -Future closeFullScreen() async { - if (html.document.fullscreenElement != null) { - html.document.exitFullscreen(); - await Future.delayed(const Duration(milliseconds: 500)); - } -} - -Future toggleFullScreen(WidgetRef ref) async { - final isFullScreen = html.document.fullscreenElement != null; - - if (isFullScreen) { - html.document.exitFullscreen(); - //Wait for 1 second - await Future.delayed(const Duration(seconds: 1)); - } else { - await html.document.documentElement?.requestFullscreen(); - } - ref - .read(mediaPlaybackProvider.notifier) - .update((state) => state.copyWith(fullScreen: html.document.fullscreenElement != null)); -} - -class FullScreenButton extends ConsumerWidget { - const FullScreenButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final fullScreen = ref.watch(mediaPlaybackProvider.select((value) => value.fullScreen)); - return IconButton( - onPressed: () => toggleFullScreen(ref), - icon: Icon( - fullScreen ? IconsaxPlusLinear.screenmirroring : IconsaxPlusLinear.maximize_4, - ), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index 26da9be..6ca594b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1531,7 +1531,7 @@ packages: source: hosted version: "2.1.0" screen_retriever: - dependency: transitive + dependency: "direct main" description: name: screen_retriever sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" @@ -1807,14 +1807,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - super_sliver_list: - dependency: "direct main" - description: - name: super_sliver_list - sha256: b1e1e64d08ce40e459b9bb5d9f8e361617c26b8c9f3bb967760b0f436b6e3f56 - url: "https://pub.dev" - source: hosted - version: "0.4.1" swagger_dart_code_generator: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 09c2634..51b8648 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,6 +88,7 @@ dependencies: font_awesome_flutter: ^10.8.0 reorderable_grid: ^1.0.10 overflow_view: ^0.4.0 + flutter_sticky_header: ^0.7.0 # Navigation auto_route: ^9.3.0+1 @@ -109,6 +110,7 @@ dependencies: window_manager: ^0.4.3 smtc_windows: ^1.0.0 background_downloader: ^8.9.4 + screen_retriever: ^0.2.0 # Data isar: ^4.0.0-dev.14 @@ -123,8 +125,6 @@ dependencies: share_plus: ^10.1.4 archive: ^4.0.2 dart_mappable: ^4.3.0 - super_sliver_list: ^0.4.1 - flutter_sticky_header: ^0.7.0 dev_dependencies: flutter_test: