diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b93f0e4..b91cb33 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1158,5 +1158,14 @@ } } }, - "copyStreamUrl": "Copy stream url" + "copyStreamUrl": "Copy stream url", + "settingsLayoutSizesTitle": "Layout Sizes", + "settingsLayoutSizesDesc": "Choose which layout sizes the app can use based on window size", + "settingsLayoutModesTitle": "Layout Modes", + "settingsLayoutModesDesc": "Control whether the app can use single or dual-panel layouts", + "phone": "Phone", + "tablet": "Tablet", + "desktop": "Desktop", + "layoutModeSingle": "Single", + "layoutModeDual": "Dual" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 57e6d57..6f8516a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,6 +18,7 @@ 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/home_settings_model.dart'; import 'package:fladder/models/syncing/i_synced_item.dart'; import 'package:fladder/providers/crash_log_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; @@ -25,6 +26,7 @@ import 'package:fladder/providers/shared_provider.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; +import 'package:fladder/routes/auto_router.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/login/lock_screen.dart'; import 'package:fladder/theme.dart'; @@ -100,11 +102,11 @@ void main() async { )) ], child: AdaptiveLayoutBuilder( - fallBack: LayoutState.tablet, + fallBack: ViewSize.tablet, layoutPoints: [ - LayoutPoints(start: 0, end: 599, type: LayoutState.phone), - LayoutPoints(start: 600, end: 1919, type: LayoutState.tablet), - LayoutPoints(start: 1920, end: 3180, type: LayoutState.desktop), + LayoutPoints(start: 0, end: 599, type: ViewSize.phone), + LayoutPoints(start: 600, end: 1919, type: ViewSize.tablet), + LayoutPoints(start: 1920, end: 3180, type: ViewSize.desktop), ], child: const Main(), ), @@ -122,6 +124,7 @@ class Main extends ConsumerStatefulWidget with WindowListener { class _MainState extends ConsumerState
with WindowListener, WidgetsBindingObserver { DateTime dateTime = DateTime.now(); bool hidden = false; + late final autoRouter = AutoRouter(ref: ref); @override void didChangeAppLifecycleState(AppLifecycleState state) async { @@ -159,7 +162,7 @@ class _MainState extends ConsumerState
with WindowListener, WidgetsBinding await ref.read(videoPlayerProvider).pause(); if (context.mounted) { - AdaptiveLayout.of(context).router.push(const LockRoute()); + autoRouter.push(const LockRoute()); } } } @@ -298,7 +301,8 @@ class _MainState extends ConsumerState
with WindowListener, WidgetsBinding ), ), themeMode: themeMode, - routerConfig: AdaptiveLayout.routerOf(context).config(), + routerConfig: autoRouter.config(), + // routerConfig: AdaptiveLayout.routerOf(context).config(), ), ); }), diff --git a/lib/models/settings/home_settings_model.dart b/lib/models/settings/home_settings_model.dart index 2bbf044..82c8915 100644 --- a/lib/models/settings/home_settings_model.dart +++ b/lib/models/settings/home_settings_model.dart @@ -10,6 +10,8 @@ part 'home_settings_model.g.dart'; @freezed class HomeSettingsModel with _$HomeSettingsModel { factory HomeSettingsModel({ + @Default({...LayoutMode.values}) Set screenLayouts, + @Default({...ViewSize.values}) Set layoutStates, @Default(HomeBanner.carousel) HomeBanner homeBanner, @Default(HomeCarouselSettings.combined) HomeCarouselSettings carouselSettings, @Default(HomeNextUp.separate) HomeNextUp nextUp, @@ -18,6 +20,48 @@ class HomeSettingsModel with _$HomeSettingsModel { factory HomeSettingsModel.fromJson(Map json) => _$HomeSettingsModelFromJson(json); } +T selectAvailableOrSmaller(T value, Set availableOptions, List allOptions) { + if (availableOptions.contains(value)) { + return value; + } + + int index = allOptions.indexOf(value); + + for (int i = index - 1; i >= 0; i--) { + if (availableOptions.contains(allOptions[i])) { + return allOptions[i]; + } + } + + return availableOptions.first; +} + +enum ViewSize { + phone, + tablet, + desktop; + + const ViewSize(); + + String label(BuildContext context) => switch (this) { + ViewSize.phone => context.localized.phone, + ViewSize.tablet => context.localized.tablet, + ViewSize.desktop => context.localized.desktop, + }; +} + +enum LayoutMode { + single, + dual; + + const LayoutMode(); + + String label(BuildContext context) => switch (this) { + LayoutMode.single => context.localized.layoutModeSingle, + LayoutMode.dual => context.localized.layoutModeDual, + }; +} + enum HomeBanner { hide, carousel, diff --git a/lib/models/settings/home_settings_model.freezed.dart b/lib/models/settings/home_settings_model.freezed.dart index 264f33d..c3fde07 100644 --- a/lib/models/settings/home_settings_model.freezed.dart +++ b/lib/models/settings/home_settings_model.freezed.dart @@ -20,6 +20,8 @@ HomeSettingsModel _$HomeSettingsModelFromJson(Map json) { /// @nodoc mixin _$HomeSettingsModel { + Set get screenLayouts => throw _privateConstructorUsedError; + Set get layoutStates => throw _privateConstructorUsedError; HomeBanner get homeBanner => throw _privateConstructorUsedError; HomeCarouselSettings get carouselSettings => throw _privateConstructorUsedError; @@ -42,7 +44,9 @@ abstract class $HomeSettingsModelCopyWith<$Res> { _$HomeSettingsModelCopyWithImpl<$Res, HomeSettingsModel>; @useResult $Res call( - {HomeBanner homeBanner, + {Set screenLayouts, + Set layoutStates, + HomeBanner homeBanner, HomeCarouselSettings carouselSettings, HomeNextUp nextUp}); } @@ -62,11 +66,21 @@ class _$HomeSettingsModelCopyWithImpl<$Res, $Val extends HomeSettingsModel> @pragma('vm:prefer-inline') @override $Res call({ + Object? screenLayouts = null, + Object? layoutStates = null, Object? homeBanner = null, Object? carouselSettings = null, Object? nextUp = null, }) { return _then(_value.copyWith( + screenLayouts: null == screenLayouts + ? _value.screenLayouts + : screenLayouts // ignore: cast_nullable_to_non_nullable + as Set, + layoutStates: null == layoutStates + ? _value.layoutStates + : layoutStates // ignore: cast_nullable_to_non_nullable + as Set, homeBanner: null == homeBanner ? _value.homeBanner : homeBanner // ignore: cast_nullable_to_non_nullable @@ -92,7 +106,9 @@ abstract class _$$HomeSettingsModelImplCopyWith<$Res> @override @useResult $Res call( - {HomeBanner homeBanner, + {Set screenLayouts, + Set layoutStates, + HomeBanner homeBanner, HomeCarouselSettings carouselSettings, HomeNextUp nextUp}); } @@ -110,11 +126,21 @@ class __$$HomeSettingsModelImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ + Object? screenLayouts = null, + Object? layoutStates = null, Object? homeBanner = null, Object? carouselSettings = null, Object? nextUp = null, }) { return _then(_$HomeSettingsModelImpl( + screenLayouts: null == screenLayouts + ? _value._screenLayouts + : screenLayouts // ignore: cast_nullable_to_non_nullable + as Set, + layoutStates: null == layoutStates + ? _value._layoutStates + : layoutStates // ignore: cast_nullable_to_non_nullable + as Set, homeBanner: null == homeBanner ? _value.homeBanner : homeBanner // ignore: cast_nullable_to_non_nullable @@ -135,13 +161,35 @@ class __$$HomeSettingsModelImplCopyWithImpl<$Res> @JsonSerializable() class _$HomeSettingsModelImpl implements _HomeSettingsModel { _$HomeSettingsModelImpl( - {this.homeBanner = HomeBanner.carousel, + {final Set screenLayouts = const {...LayoutMode.values}, + final Set layoutStates = const {...ViewSize.values}, + this.homeBanner = HomeBanner.carousel, this.carouselSettings = HomeCarouselSettings.combined, - this.nextUp = HomeNextUp.separate}); + this.nextUp = HomeNextUp.separate}) + : _screenLayouts = screenLayouts, + _layoutStates = layoutStates; factory _$HomeSettingsModelImpl.fromJson(Map json) => _$$HomeSettingsModelImplFromJson(json); + final Set _screenLayouts; + @override + @JsonKey() + Set get screenLayouts { + if (_screenLayouts is EqualUnmodifiableSetView) return _screenLayouts; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_screenLayouts); + } + + final Set _layoutStates; + @override + @JsonKey() + Set get layoutStates { + if (_layoutStates is EqualUnmodifiableSetView) return _layoutStates; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_layoutStates); + } + @override @JsonKey() final HomeBanner homeBanner; @@ -154,7 +202,7 @@ class _$HomeSettingsModelImpl implements _HomeSettingsModel { @override String toString() { - return 'HomeSettingsModel(homeBanner: $homeBanner, carouselSettings: $carouselSettings, nextUp: $nextUp)'; + return 'HomeSettingsModel(screenLayouts: $screenLayouts, layoutStates: $layoutStates, homeBanner: $homeBanner, carouselSettings: $carouselSettings, nextUp: $nextUp)'; } @override @@ -162,6 +210,10 @@ class _$HomeSettingsModelImpl implements _HomeSettingsModel { return identical(this, other) || (other.runtimeType == runtimeType && other is _$HomeSettingsModelImpl && + const DeepCollectionEquality() + .equals(other._screenLayouts, _screenLayouts) && + const DeepCollectionEquality() + .equals(other._layoutStates, _layoutStates) && (identical(other.homeBanner, homeBanner) || other.homeBanner == homeBanner) && (identical(other.carouselSettings, carouselSettings) || @@ -171,8 +223,13 @@ class _$HomeSettingsModelImpl implements _HomeSettingsModel { @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => - Object.hash(runtimeType, homeBanner, carouselSettings, nextUp); + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_screenLayouts), + const DeepCollectionEquality().hash(_layoutStates), + homeBanner, + carouselSettings, + nextUp); /// Create a copy of HomeSettingsModel /// with the given fields replaced by the non-null parameter values. @@ -193,13 +250,19 @@ class _$HomeSettingsModelImpl implements _HomeSettingsModel { abstract class _HomeSettingsModel implements HomeSettingsModel { factory _HomeSettingsModel( - {final HomeBanner homeBanner, + {final Set screenLayouts, + final Set layoutStates, + final HomeBanner homeBanner, final HomeCarouselSettings carouselSettings, final HomeNextUp nextUp}) = _$HomeSettingsModelImpl; factory _HomeSettingsModel.fromJson(Map json) = _$HomeSettingsModelImpl.fromJson; + @override + Set get screenLayouts; + @override + Set get layoutStates; @override HomeBanner get homeBanner; @override diff --git a/lib/models/settings/home_settings_model.g.dart b/lib/models/settings/home_settings_model.g.dart index 63e1f49..d6d3041 100644 --- a/lib/models/settings/home_settings_model.g.dart +++ b/lib/models/settings/home_settings_model.g.dart @@ -9,6 +9,14 @@ part of 'home_settings_model.dart'; _$HomeSettingsModelImpl _$$HomeSettingsModelImplFromJson( Map json) => _$HomeSettingsModelImpl( + screenLayouts: (json['screenLayouts'] as List?) + ?.map((e) => $enumDecode(_$LayoutModeEnumMap, e)) + .toSet() ?? + const {...LayoutMode.values}, + layoutStates: (json['layoutStates'] as List?) + ?.map((e) => $enumDecode(_$ViewSizeEnumMap, e)) + .toSet() ?? + const {...ViewSize.values}, homeBanner: $enumDecodeNullable(_$HomeBannerEnumMap, json['homeBanner']) ?? HomeBanner.carousel, @@ -22,12 +30,27 @@ _$HomeSettingsModelImpl _$$HomeSettingsModelImplFromJson( Map _$$HomeSettingsModelImplToJson( _$HomeSettingsModelImpl instance) => { + 'screenLayouts': + instance.screenLayouts.map((e) => _$LayoutModeEnumMap[e]!).toList(), + 'layoutStates': + instance.layoutStates.map((e) => _$ViewSizeEnumMap[e]!).toList(), 'homeBanner': _$HomeBannerEnumMap[instance.homeBanner]!, 'carouselSettings': _$HomeCarouselSettingsEnumMap[instance.carouselSettings]!, 'nextUp': _$HomeNextUpEnumMap[instance.nextUp]!, }; +const _$LayoutModeEnumMap = { + LayoutMode.single: 'single', + LayoutMode.dual: 'dual', +}; + +const _$ViewSizeEnumMap = { + ViewSize.phone: 'phone', + ViewSize.tablet: 'tablet', + ViewSize.desktop: 'desktop', +}; + const _$HomeBannerEnumMap = { HomeBanner.hide: 'hide', HomeBanner.carousel: 'carousel', diff --git a/lib/providers/api_provider.g.dart b/lib/providers/api_provider.g.dart index f085299..dd49889 100644 --- a/lib/providers/api_provider.g.dart +++ b/lib/providers/api_provider.g.dart @@ -6,7 +6,7 @@ part of 'api_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$jellyApiHash() => r'c0cdc4127e7191523b1356e71c54c93f99020c1e'; +String _$jellyApiHash() => r'9bc824d28d17f88f40c768cefb637144e0fbf346'; /// See also [JellyApi]. @ProviderFor(JellyApi) diff --git a/lib/providers/settings/home_settings_provider.dart b/lib/providers/settings/home_settings_provider.dart index 4b9bf17..0d20f50 100644 --- a/lib/providers/settings/home_settings_provider.dart +++ b/lib/providers/settings/home_settings_provider.dart @@ -19,4 +19,8 @@ class HomeSettingsNotifier extends StateNotifier { } HomeSettingsModel update(HomeSettingsModel Function(HomeSettingsModel currentState) value) => state = value(state); + + void setLayoutModes(Set set) => state = state.copyWith(screenLayouts: set); + + void setViewSize(Set set) => state = state.copyWith(layoutStates: set); } diff --git a/lib/providers/sync/background_download_provider.g.dart b/lib/providers/sync/background_download_provider.g.dart index 580e81c..47ffb02 100644 --- a/lib/providers/sync/background_download_provider.g.dart +++ b/lib/providers/sync/background_download_provider.g.dart @@ -7,7 +7,7 @@ part of 'background_download_provider.dart'; // ************************************************************************** String _$backgroundDownloaderHash() => - r'9a9f91504ae4532ab37290ee9372d2e7a18380a9'; + r'997d9f4ba79dd0d9d30d5f283b36d5280d10dfaa'; /// See also [backgroundDownloader]. @ProviderFor(backgroundDownloader) diff --git a/lib/providers/user_provider.g.dart b/lib/providers/user_provider.g.dart index 81782fc..a699761 100644 --- a/lib/providers/user_provider.g.dart +++ b/lib/providers/user_provider.g.dart @@ -7,7 +7,7 @@ part of 'user_provider.dart'; // ************************************************************************** String _$showSyncButtonProviderHash() => - r'3468d7309f3859f7b60b1bd317e306e1f5f00555'; + r'c09f42cd6536425bf9417da41c83e15c135d0edb'; /// See also [showSyncButtonProvider]. @ProviderFor(showSyncButtonProvider) @@ -24,7 +24,7 @@ final showSyncButtonProviderProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef; -String _$userHash() => r'e83369c0d569d5a862aa1b92f3f0a45a9d1fe446'; +String _$userHash() => r'1ab1579051806f114e3f42873a2e100c14115900'; /// See also [User]. @ProviderFor(User) diff --git a/lib/routes/auto_router.dart b/lib/routes/auto_router.dart index e241e39..2f44c78 100644 --- a/lib/routes/auto_router.dart +++ b/lib/routes/auto_router.dart @@ -6,17 +6,16 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/login/lock_screen.dart'; -import 'package:fladder/util/adaptive_layout.dart'; + +const settingsPageRoute = "settings"; @AutoRouterConfig(replaceInRouteName: 'Screen|Page,Route') class AutoRouter extends RootStackRouter { AutoRouter({ - required this.layout, required this.ref, }); final WidgetRef ref; - final ScreenLayout layout; @override List get guards => [...super.guards, AuthGuard(ref: ref)]; @@ -27,44 +26,33 @@ class AutoRouter extends RootStackRouter { @override List get routes => [ ..._defaultRoutes, - ...(layout == ScreenLayout.dual ? desktopRoutes : mobileRoutes), + ...otherRoutes, ]; - final List mobileRoutes = [ + final List otherRoutes = [ _homeRoute.copyWith( children: [ - _dashboardRoute, - _favouritesRoute, - _syncedRoute, - ], - ), - AutoRoute(page: DetailsRoute.page, path: '/details', usesPathAsKey: true), - AutoRoute(page: LibrarySearchRoute.page, path: '/library', usesPathAsKey: true), - AutoRoute(page: SettingsRoute.page, path: '/settings'), - ..._settingsChildren.map( - (e) => e.copyWith(path: "/$e", initial: false), - ), - AutoRoute(page: LockRoute.page, path: '/locked'), - ]; - final List desktopRoutes = [ - _homeRoute.copyWith( - children: [ - _dashboardRoute, - _favouritesRoute, - _syncedRoute, + ...homeRoutes, AutoRoute(page: DetailsRoute.page, path: 'details', usesPathAsKey: true), AutoRoute(page: LibrarySearchRoute.page, path: 'library', usesPathAsKey: true), - AutoRoute( + CustomRoute( page: SettingsRoute.page, - path: 'settings', + path: settingsPageRoute, children: _settingsChildren, - ) + transitionsBuilder: TransitionsBuilders.fadeIn, + ), ], ), AutoRoute(page: LockRoute.page, path: '/locked'), ]; } +final List homeRoutes = [ + _dashboardRoute, + _favouritesRoute, + _syncedRoute, +]; + final List _defaultRoutes = [ AutoRoute(page: SplashRoute.page, path: '/splash'), AutoRoute(page: LoginRoute.page, path: '/login'), @@ -75,25 +63,25 @@ final AutoRoute _dashboardRoute = CustomRoute( page: DashboardRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, initial: true, - path: 'dashboard', maintainState: false, + path: 'dashboard', ); final AutoRoute _favouritesRoute = CustomRoute( page: FavouritesRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, - path: 'favourites', maintainState: false, + path: 'favourites', ); final AutoRoute _syncedRoute = CustomRoute( page: SyncedRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, - path: 'synced', maintainState: false, + path: 'synced', ); final List _settingsChildren = [ - CustomRoute( - page: ClientSettingsRoute.page, initial: true, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'client'), + CustomRoute(page: SettingsSelectionRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'list'), + CustomRoute(page: ClientSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'client'), CustomRoute(page: SecuritySettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'security'), CustomRoute(page: PlayerSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'player'), CustomRoute(page: AboutSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'about'), diff --git a/lib/routes/auto_router.gr.dart b/lib/routes/auto_router.gr.dart index 5459e04..3b79be3 100644 --- a/lib/routes/auto_router.gr.dart +++ b/lib/routes/auto_router.gr.dart @@ -8,11 +8,11 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i15; -import 'package:fladder/models/item_base_model.dart' as _i16; -import 'package:fladder/models/items/photos_model.dart' as _i19; +import 'package:auto_route/auto_route.dart' as _i16; +import 'package:fladder/models/item_base_model.dart' as _i17; +import 'package:fladder/models/items/photos_model.dart' as _i20; import 'package:fladder/models/library_search/library_search_options.dart' - as _i18; + as _i19; import 'package:fladder/routes/nested_details_screen.dart' as _i4; import 'package:fladder/screens/dashboard/dashboard_screen.dart' as _i3; import 'package:fladder/screens/favourites/favourites_screen.dart' as _i5; @@ -26,15 +26,17 @@ import 'package:fladder/screens/settings/client_settings_page.dart' as _i2; import 'package:fladder/screens/settings/player_settings_page.dart' as _i10; import 'package:fladder/screens/settings/security_settings_page.dart' as _i11; import 'package:fladder/screens/settings/settings_screen.dart' as _i12; -import 'package:fladder/screens/splash_screen.dart' as _i13; -import 'package:fladder/screens/syncing/synced_screen.dart' as _i14; -import 'package:flutter/foundation.dart' as _i17; -import 'package:flutter/material.dart' as _i20; +import 'package:fladder/screens/settings/settings_selection_screen.dart' + as _i13; +import 'package:fladder/screens/splash_screen.dart' as _i14; +import 'package:fladder/screens/syncing/synced_screen.dart' as _i15; +import 'package:flutter/foundation.dart' as _i18; +import 'package:flutter/material.dart' as _i21; /// generated route for /// [_i1.AboutSettingsPage] -class AboutSettingsRoute extends _i15.PageRouteInfo { - const AboutSettingsRoute({List<_i15.PageRouteInfo>? children}) +class AboutSettingsRoute extends _i16.PageRouteInfo { + const AboutSettingsRoute({List<_i16.PageRouteInfo>? children}) : super( AboutSettingsRoute.name, initialChildren: children, @@ -42,7 +44,7 @@ class AboutSettingsRoute extends _i15.PageRouteInfo { static const String name = 'AboutSettingsRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { return const _i1.AboutSettingsPage(); @@ -52,8 +54,8 @@ class AboutSettingsRoute extends _i15.PageRouteInfo { /// generated route for /// [_i2.ClientSettingsPage] -class ClientSettingsRoute extends _i15.PageRouteInfo { - const ClientSettingsRoute({List<_i15.PageRouteInfo>? children}) +class ClientSettingsRoute extends _i16.PageRouteInfo { + const ClientSettingsRoute({List<_i16.PageRouteInfo>? children}) : super( ClientSettingsRoute.name, initialChildren: children, @@ -61,7 +63,7 @@ class ClientSettingsRoute extends _i15.PageRouteInfo { static const String name = 'ClientSettingsRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { return const _i2.ClientSettingsPage(); @@ -71,8 +73,8 @@ class ClientSettingsRoute extends _i15.PageRouteInfo { /// generated route for /// [_i3.DashboardScreen] -class DashboardRoute extends _i15.PageRouteInfo { - const DashboardRoute({List<_i15.PageRouteInfo>? children}) +class DashboardRoute extends _i16.PageRouteInfo { + const DashboardRoute({List<_i16.PageRouteInfo>? children}) : super( DashboardRoute.name, initialChildren: children, @@ -80,7 +82,7 @@ class DashboardRoute extends _i15.PageRouteInfo { static const String name = 'DashboardRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { return const _i3.DashboardScreen(); @@ -90,12 +92,12 @@ class DashboardRoute extends _i15.PageRouteInfo { /// generated route for /// [_i4.DetailsScreen] -class DetailsRoute extends _i15.PageRouteInfo { +class DetailsRoute extends _i16.PageRouteInfo { DetailsRoute({ String id = '', - _i16.ItemBaseModel? item, - _i17.Key? key, - List<_i15.PageRouteInfo>? children, + _i17.ItemBaseModel? item, + _i18.Key? key, + List<_i16.PageRouteInfo>? children, }) : super( DetailsRoute.name, args: DetailsRouteArgs( @@ -109,7 +111,7 @@ class DetailsRoute extends _i15.PageRouteInfo { static const String name = 'DetailsRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { final queryParams = data.queryParams; @@ -137,9 +139,9 @@ class DetailsRouteArgs { final String id; - final _i16.ItemBaseModel? item; + final _i17.ItemBaseModel? item; - final _i17.Key? key; + final _i18.Key? key; @override String toString() { @@ -149,8 +151,8 @@ class DetailsRouteArgs { /// generated route for /// [_i5.FavouritesScreen] -class FavouritesRoute extends _i15.PageRouteInfo { - const FavouritesRoute({List<_i15.PageRouteInfo>? children}) +class FavouritesRoute extends _i16.PageRouteInfo { + const FavouritesRoute({List<_i16.PageRouteInfo>? children}) : super( FavouritesRoute.name, initialChildren: children, @@ -158,7 +160,7 @@ class FavouritesRoute extends _i15.PageRouteInfo { static const String name = 'FavouritesRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { return const _i5.FavouritesScreen(); @@ -168,8 +170,8 @@ class FavouritesRoute extends _i15.PageRouteInfo { /// generated route for /// [_i6.HomeScreen] -class HomeRoute extends _i15.PageRouteInfo { - const HomeRoute({List<_i15.PageRouteInfo>? children}) +class HomeRoute extends _i16.PageRouteInfo { + const HomeRoute({List<_i16.PageRouteInfo>? children}) : super( HomeRoute.name, initialChildren: children, @@ -177,7 +179,7 @@ class HomeRoute extends _i15.PageRouteInfo { static const String name = 'HomeRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { return const _i6.HomeScreen(); @@ -187,16 +189,16 @@ class HomeRoute extends _i15.PageRouteInfo { /// generated route for /// [_i7.LibrarySearchScreen] -class LibrarySearchRoute extends _i15.PageRouteInfo { +class LibrarySearchRoute extends _i16.PageRouteInfo { LibrarySearchRoute({ String? viewModelId, List? folderId, bool? favourites, - _i18.SortingOrder? sortOrder, - _i18.SortingOptions? sortingOptions, - _i19.PhotoModel? photoToView, - _i17.Key? key, - List<_i15.PageRouteInfo>? children, + _i19.SortingOrder? sortOrder, + _i19.SortingOptions? sortingOptions, + _i20.PhotoModel? photoToView, + _i18.Key? key, + List<_i16.PageRouteInfo>? children, }) : super( LibrarySearchRoute.name, args: LibrarySearchRouteArgs( @@ -220,7 +222,7 @@ class LibrarySearchRoute extends _i15.PageRouteInfo { static const String name = 'LibrarySearchRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { final queryParams = data.queryParams; @@ -262,13 +264,13 @@ class LibrarySearchRouteArgs { final bool? favourites; - final _i18.SortingOrder? sortOrder; + final _i19.SortingOrder? sortOrder; - final _i18.SortingOptions? sortingOptions; + final _i19.SortingOptions? sortingOptions; - final _i19.PhotoModel? photoToView; + final _i20.PhotoModel? photoToView; - final _i17.Key? key; + final _i18.Key? key; @override String toString() { @@ -278,8 +280,8 @@ class LibrarySearchRouteArgs { /// generated route for /// [_i8.LockScreen] -class LockRoute extends _i15.PageRouteInfo { - const LockRoute({List<_i15.PageRouteInfo>? children}) +class LockRoute extends _i16.PageRouteInfo { + const LockRoute({List<_i16.PageRouteInfo>? children}) : super( LockRoute.name, initialChildren: children, @@ -287,7 +289,7 @@ class LockRoute extends _i15.PageRouteInfo { static const String name = 'LockRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { return const _i8.LockScreen(); @@ -297,8 +299,8 @@ class LockRoute extends _i15.PageRouteInfo { /// generated route for /// [_i9.LoginScreen] -class LoginRoute extends _i15.PageRouteInfo { - const LoginRoute({List<_i15.PageRouteInfo>? children}) +class LoginRoute extends _i16.PageRouteInfo { + const LoginRoute({List<_i16.PageRouteInfo>? children}) : super( LoginRoute.name, initialChildren: children, @@ -306,7 +308,7 @@ class LoginRoute extends _i15.PageRouteInfo { static const String name = 'LoginRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { return const _i9.LoginScreen(); @@ -316,8 +318,8 @@ class LoginRoute extends _i15.PageRouteInfo { /// generated route for /// [_i10.PlayerSettingsPage] -class PlayerSettingsRoute extends _i15.PageRouteInfo { - const PlayerSettingsRoute({List<_i15.PageRouteInfo>? children}) +class PlayerSettingsRoute extends _i16.PageRouteInfo { + const PlayerSettingsRoute({List<_i16.PageRouteInfo>? children}) : super( PlayerSettingsRoute.name, initialChildren: children, @@ -325,7 +327,7 @@ class PlayerSettingsRoute extends _i15.PageRouteInfo { static const String name = 'PlayerSettingsRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { return const _i10.PlayerSettingsPage(); @@ -335,8 +337,8 @@ class PlayerSettingsRoute extends _i15.PageRouteInfo { /// generated route for /// [_i11.SecuritySettingsPage] -class SecuritySettingsRoute extends _i15.PageRouteInfo { - const SecuritySettingsRoute({List<_i15.PageRouteInfo>? children}) +class SecuritySettingsRoute extends _i16.PageRouteInfo { + const SecuritySettingsRoute({List<_i16.PageRouteInfo>? children}) : super( SecuritySettingsRoute.name, initialChildren: children, @@ -344,7 +346,7 @@ class SecuritySettingsRoute extends _i15.PageRouteInfo { static const String name = 'SecuritySettingsRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { return const _i11.SecuritySettingsPage(); @@ -354,8 +356,8 @@ class SecuritySettingsRoute extends _i15.PageRouteInfo { /// generated route for /// [_i12.SettingsScreen] -class SettingsRoute extends _i15.PageRouteInfo { - const SettingsRoute({List<_i15.PageRouteInfo>? children}) +class SettingsRoute extends _i16.PageRouteInfo { + const SettingsRoute({List<_i16.PageRouteInfo>? children}) : super( SettingsRoute.name, initialChildren: children, @@ -363,7 +365,7 @@ class SettingsRoute extends _i15.PageRouteInfo { static const String name = 'SettingsRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { return const _i12.SettingsScreen(); @@ -372,12 +374,31 @@ class SettingsRoute extends _i15.PageRouteInfo { } /// generated route for -/// [_i13.SplashScreen] -class SplashRoute extends _i15.PageRouteInfo { +/// [_i13.SettingsSelectionScreen] +class SettingsSelectionRoute extends _i16.PageRouteInfo { + const SettingsSelectionRoute({List<_i16.PageRouteInfo>? children}) + : super( + SettingsSelectionRoute.name, + initialChildren: children, + ); + + static const String name = 'SettingsSelectionRoute'; + + static _i16.PageInfo page = _i16.PageInfo( + name, + builder: (data) { + return const _i13.SettingsSelectionScreen(); + }, + ); +} + +/// generated route for +/// [_i14.SplashScreen] +class SplashRoute extends _i16.PageRouteInfo { SplashRoute({ dynamic Function(bool)? loggedIn, - _i20.Key? key, - List<_i15.PageRouteInfo>? children, + _i21.Key? key, + List<_i16.PageRouteInfo>? children, }) : super( SplashRoute.name, args: SplashRouteArgs( @@ -389,12 +410,12 @@ class SplashRoute extends _i15.PageRouteInfo { static const String name = 'SplashRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { final args = data.argsAs(orElse: () => const SplashRouteArgs()); - return _i13.SplashScreen( + return _i14.SplashScreen( loggedIn: args.loggedIn, key: args.key, ); @@ -410,7 +431,7 @@ class SplashRouteArgs { final dynamic Function(bool)? loggedIn; - final _i20.Key? key; + final _i21.Key? key; @override String toString() { @@ -419,12 +440,12 @@ class SplashRouteArgs { } /// generated route for -/// [_i14.SyncedScreen] -class SyncedRoute extends _i15.PageRouteInfo { +/// [_i15.SyncedScreen] +class SyncedRoute extends _i16.PageRouteInfo { SyncedRoute({ - _i20.ScrollController? navigationScrollController, - _i20.Key? key, - List<_i15.PageRouteInfo>? children, + _i21.ScrollController? navigationScrollController, + _i21.Key? key, + List<_i16.PageRouteInfo>? children, }) : super( SyncedRoute.name, args: SyncedRouteArgs( @@ -436,12 +457,12 @@ class SyncedRoute extends _i15.PageRouteInfo { static const String name = 'SyncedRoute'; - static _i15.PageInfo page = _i15.PageInfo( + static _i16.PageInfo page = _i16.PageInfo( name, builder: (data) { final args = data.argsAs(orElse: () => const SyncedRouteArgs()); - return _i14.SyncedScreen( + return _i15.SyncedScreen( navigationScrollController: args.navigationScrollController, key: args.key, ); @@ -455,9 +476,9 @@ class SyncedRouteArgs { this.key, }); - final _i20.ScrollController? navigationScrollController; + final _i21.ScrollController? navigationScrollController; - final _i20.Key? key; + final _i21.Key? key; @override String toString() { diff --git a/lib/screens/book_viewer/book_viewer_controls.dart b/lib/screens/book_viewer/book_viewer_controls.dart index f2a9ead..ac840c9 100644 --- a/lib/screens/book_viewer/book_viewer_controls.dart +++ b/lib/screens/book_viewer/book_viewer_controls.dart @@ -131,235 +131,243 @@ class _BookViewerControlsState extends ConsumerState { final previousChapter = details.previousChapter(bookViewerDetails.book); final nextChapter = details.nextChapter(bookViewerDetails.book); - return MediaQuery.removePadding( - context: context, - child: InputHandler( - onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored, - child: Stack( - children: [ - IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 500), - opacity: showControls ? 1 : 0, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - overlayColor.withValues(alpha: 1), - overlayColor.withValues(alpha: 0.65), - overlayColor.withValues(alpha: 0), - ], - ), - ), - child: Padding( - padding: EdgeInsets.only(top: topPadding).copyWith(bottom: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (AdaptiveLayout.of(context).isDesktop) - const Flexible( - child: DefaultTitleBar( - height: 50, - brightness: Brightness.dark, - ), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const BackButton(), - const SizedBox( - width: 16, - ), - Flexible( - child: Text( - bookViewerDetails.book?.name ?? "None", - style: Theme.of(context).textTheme.titleLarge, - ), - ) - ], - ), - const SizedBox(height: 16), - ], - ), + final double leftPadding = MediaQuery.of(context).viewPadding.left; + final double rightPadding = MediaQuery.of(context).viewPadding.right; + + return InputHandler( + onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored, + child: Stack( + children: [ + IgnorePointer( + ignoring: !showControls, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: showControls ? 1 : 0, + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + overlayColor.withValues(alpha: 1), + overlayColor.withValues(alpha: 0.65), + overlayColor.withValues(alpha: 0), + ], ), ), - if (!bookViewerDetails.loading) ...{ - if (bookViewerDetails.book != null && bookViewerDetails.pages.isNotEmpty) ...{ - Align( - alignment: Alignment.bottomCenter, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - overlayColor.withValues(alpha: 0), - overlayColor.withValues(alpha: 0.65), - overlayColor.withValues(alpha: 1), - ], + child: Padding( + padding: + EdgeInsets.only(top: topPadding, left: leftPadding, right: rightPadding).copyWith(bottom: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (AdaptiveLayout.of(context).isDesktop) + const Flexible( + child: DefaultTitleBar( + height: 50, + brightness: Brightness.dark, ), ), - child: Padding( - padding: EdgeInsets.only(bottom: bottomPadding).copyWith(top: 16, bottom: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 30), - Row( - children: [ - const SizedBox(width: 8), - Tooltip( - message: bookViewerSettings.readDirection == ReadDirection.leftToRight - ? previousChapter?.name != null - ? "Load ${previousChapter?.name}" - : "" - : nextChapter?.name != null - ? "Load ${nextChapter?.name}" - : "", - child: IconButton.filled( - onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight - ? previousChapter != null - ? () async => await loadNextBook(previousChapter) - : null - : nextChapter != null - ? () async => await loadNextBook(nextChapter) - : null, - icon: const Icon(IconsaxOutline.backward), - ), - ), - const SizedBox(width: 8), - Flexible( - child: Container( - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.7), - borderRadius: BorderRadius.circular(60), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Row( - children: [ - if (bookViewerSettings.readDirection == ReadDirection.leftToRight) - ...controls(currentPage, bookViewerSettings, bookViewerDetails) - else - ...controls(currentPage, bookViewerSettings, bookViewerDetails) - .reversed, - ], - ), - ), - ), - ), - const SizedBox(width: 8), - Tooltip( - message: bookViewerSettings.readDirection == ReadDirection.leftToRight - ? nextChapter?.name != null - ? "Load ${nextChapter?.name}" - : "" - : previousChapter?.name != null - ? "Load ${previousChapter?.name}" - : "", - child: IconButton.filled( - onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight - ? nextChapter != null - ? () async => await loadNextBook(nextChapter) - : null - : previousChapter != null - ? () async => await loadNextBook(previousChapter) - : null, - icon: const Icon(IconsaxOutline.forward), - ), - ), - const SizedBox(width: 8), - ], - ), - const SizedBox(height: 16), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Transform.flip( - flipX: bookViewerSettings.readDirection == ReadDirection.rightToLeft, - child: IconButton( - onPressed: () => widget.controller - .animateToPage(1, duration: pageAnimDuration, curve: pageAnimCurve), - icon: const Icon(IconsaxOutline.backward)), - ), - IconButton( - onPressed: () { - showBookViewerSettings(context); - }, - icon: const Icon(IconsaxOutline.setting_2), - ), - IconButton( - onPressed: chapters.length > 1 - ? () { - showBookViewerChapters( - context, - widget.provider, - onPressed: (book) async { - Navigator.of(context).pop(); - loadNextBook(book); - }, - ); - } - : () => fladderSnackbar(context, title: "No other chapters"), - icon: const Icon(IconsaxOutline.bookmark_2), - ) - ], - ), - ], + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const BackButton(), + const SizedBox( + width: 16, ), - ), + Flexible( + child: Text( + bookViewerDetails.book?.name ?? "None", + style: Theme.of(context).textTheme.titleLarge, + ), + ) + ], ), - ), - } else - const Center( - child: Card( - child: Padding( - padding: EdgeInsets.all(8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.menu_book_rounded), - SizedBox(width: 8), - Text("Unable to load book"), - ], - ), - ), - ), - ) - }, - ], - ), - ), - ), - if (bookViewerDetails.loading) - Center( - child: Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (bookViewerDetails.book != null) ...{ - Flexible( - child: Text("Loading ${bookViewerDetails.book?.name}", - style: Theme.of(context).textTheme.titleMedium), - ), - const SizedBox(width: 16), - }, - const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round), - ], + const SizedBox(height: 16), + ], + ), ), ), + if (!bookViewerDetails.loading) ...{ + if (bookViewerDetails.book != null && bookViewerDetails.pages.isNotEmpty) ...{ + Align( + alignment: Alignment.bottomCenter, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + overlayColor.withValues(alpha: 0), + overlayColor.withValues(alpha: 0.65), + overlayColor.withValues(alpha: 1), + ], + ), + ), + child: Padding( + padding: EdgeInsets.only( + bottom: bottomPadding, + left: leftPadding, + right: rightPadding, + ).copyWith( + top: 16, + bottom: 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 30), + Row( + children: [ + const SizedBox(width: 8), + Tooltip( + message: bookViewerSettings.readDirection == ReadDirection.leftToRight + ? previousChapter?.name != null + ? "Load ${previousChapter?.name}" + : "" + : nextChapter?.name != null + ? "Load ${nextChapter?.name}" + : "", + child: IconButton.filled( + onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight + ? previousChapter != null + ? () async => await loadNextBook(previousChapter) + : null + : nextChapter != null + ? () async => await loadNextBook(nextChapter) + : null, + icon: const Icon(IconsaxOutline.backward), + ), + ), + const SizedBox(width: 8), + Flexible( + child: Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.7), + borderRadius: BorderRadius.circular(60), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + if (bookViewerSettings.readDirection == ReadDirection.leftToRight) + ...controls(currentPage, bookViewerSettings, bookViewerDetails) + else + ...controls(currentPage, bookViewerSettings, bookViewerDetails) + .reversed, + ], + ), + ), + ), + ), + const SizedBox(width: 8), + Tooltip( + message: bookViewerSettings.readDirection == ReadDirection.leftToRight + ? nextChapter?.name != null + ? "Load ${nextChapter?.name}" + : "" + : previousChapter?.name != null + ? "Load ${previousChapter?.name}" + : "", + child: IconButton.filled( + onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight + ? nextChapter != null + ? () async => await loadNextBook(nextChapter) + : null + : previousChapter != null + ? () async => await loadNextBook(previousChapter) + : null, + icon: const Icon(IconsaxOutline.forward), + ), + ), + const SizedBox(width: 8), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Transform.flip( + flipX: bookViewerSettings.readDirection == ReadDirection.rightToLeft, + child: IconButton( + onPressed: () => widget.controller + .animateToPage(1, duration: pageAnimDuration, curve: pageAnimCurve), + icon: const Icon(IconsaxOutline.backward)), + ), + IconButton( + onPressed: () { + showBookViewerSettings(context); + }, + icon: const Icon(IconsaxOutline.setting_2), + ), + IconButton( + onPressed: chapters.length > 1 + ? () { + showBookViewerChapters( + context, + widget.provider, + onPressed: (book) async { + Navigator.of(context).pop(); + loadNextBook(book); + }, + ); + } + : () => fladderSnackbar(context, title: "No other chapters"), + icon: const Icon(IconsaxOutline.bookmark_2), + ) + ], + ), + ], + ), + ), + ), + ), + } else + const Center( + child: Card( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.menu_book_rounded), + SizedBox(width: 8), + Text("Unable to load book"), + ], + ), + ), + ), + ) + }, + ], + ), + ), + ), + if (bookViewerDetails.loading) + Center( + child: Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (bookViewerDetails.book != null) ...{ + Flexible( + child: Text("Loading ${bookViewerDetails.book?.name}", + style: Theme.of(context).textTheme.titleMedium), + ), + const SizedBox(width: 16), + }, + const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round), + ], + ), ), - ) - ], - ), + ), + ) + ], ), ); } diff --git a/lib/screens/dashboard/dashboard_screen.dart b/lib/screens/dashboard/dashboard_screen.dart index 459b246..c9bd49b 100644 --- a/lib/screens/dashboard/dashboard_screen.dart +++ b/lib/screens/dashboard/dashboard_screen.dart @@ -94,7 +94,7 @@ class _DashboardScreenState extends ConsumerState { controller: AdaptiveLayout.scrollOf(context), physics: const AlwaysScrollableScrollPhysics(), slivers: [ - if (AdaptiveLayout.of(context).layout == LayoutState.phone) + if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) NestedSliverAppBar( route: LibrarySearchRoute(), parent: context, @@ -102,7 +102,7 @@ class _DashboardScreenState extends ConsumerState { if (homeBanner && homeCarouselItems.isNotEmpty) ...{ SliverToBoxAdapter( child: Transform.translate( - offset: Offset(0, AdaptiveLayout.layoutOf(context) == LayoutState.phone ? -14 : 0), + offset: Offset(0, AdaptiveLayout.layoutOf(context) == ViewSize.phone ? -14 : 0), child: HomeBannerWidget(posters: homeCarouselItems), ), ), diff --git a/lib/screens/details_screens/components/overview_header.dart b/lib/screens/details_screens/components/overview_header.dart index 25b6a0f..90959be 100644 --- a/lib/screens/details_screens/components/overview_header.dart +++ b/lib/screens/details_screens/components/overview_header.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/images_models.dart'; import 'package:fladder/models/items/item_shared_models.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/screens/shared/media/components/chip_button.dart'; import 'package:fladder/screens/shared/media/components/media_header.dart'; import 'package:fladder/screens/shared/media/components/small_detail_widgets.dart'; @@ -55,7 +56,7 @@ class OverviewHeader extends ConsumerWidget { (MediaQuery.sizeOf(context).height - (MediaQuery.paddingOf(context).top + 150)).clamp(50, 1250).toDouble(); final crossAlignment = - AdaptiveLayout.of(context).layout != LayoutState.phone ? CrossAxisAlignment.start : CrossAxisAlignment.center; + AdaptiveLayout.viewSizeOf(context) != ViewSize.phone ? CrossAxisAlignment.start : CrossAxisAlignment.center; return ConstrainedBox( constraints: BoxConstraints( diff --git a/lib/screens/details_screens/episode_detail_screen.dart b/lib/screens/details_screens/episode_detail_screen.dart index 9d64f4c..42778bf 100644 --- a/lib/screens/details_screens/episode_detail_screen.dart +++ b/lib/screens/details_screens/episode_detail_screen.dart @@ -5,6 +5,7 @@ import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/items/episode_details_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/details_screens/components/media_stream_information.dart'; @@ -43,7 +44,7 @@ class _ItemDetailScreenState extends ConsumerState { final seasonDetails = details.series; final episodeDetails = details.episode; final wrapAlignment = - AdaptiveLayout.of(context).layout != LayoutState.phone ? WrapAlignment.start : WrapAlignment.center; + AdaptiveLayout.viewSizeOf(context) != ViewSize.phone ? WrapAlignment.start : WrapAlignment.center; return DetailScaffold( label: widget.item.name, diff --git a/lib/screens/details_screens/movie_detail_screen.dart b/lib/screens/details_screens/movie_detail_screen.dart index db815b3..444cf4b 100644 --- a/lib/screens/details_screens/movie_detail_screen.dart +++ b/lib/screens/details_screens/movie_detail_screen.dart @@ -1,3 +1,4 @@ +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:flutter/material.dart'; @@ -39,7 +40,7 @@ class _ItemDetailScreenState extends ConsumerState { Widget build(BuildContext context) { final details = ref.watch(providerInstance); final wrapAlignment = - AdaptiveLayout.of(context).layout != LayoutState.phone ? WrapAlignment.start : WrapAlignment.center; + AdaptiveLayout.viewSizeOf(context) != ViewSize.phone ? WrapAlignment.start : WrapAlignment.center; return DetailScaffold( label: widget.item.name, diff --git a/lib/screens/details_screens/person_detail_screen.dart b/lib/screens/details_screens/person_detail_screen.dart index 9ca4433..13ff8a5 100644 --- a/lib/screens/details_screens/person_detail_screen.dart +++ b/lib/screens/details_screens/person_detail_screen.dart @@ -1,3 +1,4 @@ +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; @@ -56,7 +57,7 @@ class _PersonDetailScreenState extends ConsumerState { decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), ), - width: AdaptiveLayout.of(context).layout == LayoutState.phone + width: AdaptiveLayout.viewSizeOf(context) == ViewSize.phone ? MediaQuery.of(context).size.width : MediaQuery.of(context).size.width / 3.5, child: AspectRatio( diff --git a/lib/screens/details_screens/series_detail_screen.dart b/lib/screens/details_screens/series_detail_screen.dart index d08fce0..7bd66bd 100644 --- a/lib/screens/details_screens/series_detail_screen.dart +++ b/lib/screens/details_screens/series_detail_screen.dart @@ -1,3 +1,4 @@ +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; @@ -43,7 +44,7 @@ class _SeriesDetailScreenState extends ConsumerState { Widget build(BuildContext context) { final details = ref.watch(providerId); final wrapAlignment = - AdaptiveLayout.of(context).layout != LayoutState.phone ? WrapAlignment.start : WrapAlignment.center; + AdaptiveLayout.viewSizeOf(context) != ViewSize.phone ? WrapAlignment.start : WrapAlignment.center; return DetailScaffold( label: details?.name ?? "", diff --git a/lib/screens/favourites/favourites_screen.dart b/lib/screens/favourites/favourites_screen.dart index 0abb420..fe78f14 100644 --- a/lib/screens/favourites/favourites_screen.dart +++ b/lib/screens/favourites/favourites_screen.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/shared/nested_scaffold.dart'; @@ -32,7 +33,7 @@ class FavouritesScreen extends ConsumerWidget { physics: const AlwaysScrollableScrollPhysics(), controller: AdaptiveLayout.scrollOf(context), slivers: [ - if (AdaptiveLayout.of(context).layout == LayoutState.phone) + if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) NestedSliverAppBar( searchTitle: "${context.localized.search} ${context.localized.favorites.toLowerCase()}...", parent: context, diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 45e82e6..39739f0 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -25,51 +25,54 @@ class HomeScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final canDownload = ref.watch(showSyncButtonProviderProvider); - final destinations = HomeTabs.values.map((e) { - switch (e) { - case HomeTabs.dashboard: - return DestinationModel( - label: context.localized.navigationDashboard, - icon: const Icon(IconsaxOutline.home), - selectedIcon: const Icon(IconsaxBold.home), - route: const DashboardRoute(), - action: () => context.router.navigate(const DashboardRoute()), - floatingActionButton: AdaptiveFab( - context: context, - title: context.localized.search, - key: Key(e.name.capitalize()), - onPressed: () => context.router.navigate(LibrarySearchRoute()), - child: const Icon(IconsaxOutline.search_normal_1), - ), - ); - case HomeTabs.favorites: - return DestinationModel( - label: context.localized.navigationFavorites, - icon: const Icon(IconsaxOutline.heart), - selectedIcon: const Icon(IconsaxBold.heart), - route: const FavouritesRoute(), - floatingActionButton: AdaptiveFab( - context: context, - title: context.localized.filter(0), - key: Key(e.name.capitalize()), - onPressed: () => context.router.navigate(LibrarySearchRoute(favourites: true)), - child: const Icon(IconsaxOutline.heart_search), - ), - action: () => context.router.navigate(const FavouritesRoute()), - ); - case HomeTabs.sync: - if (canDownload) { - return DestinationModel( - label: context.localized.navigationSync, - icon: const Icon(IconsaxOutline.cloud), - selectedIcon: const Icon(IconsaxBold.cloud), - route: SyncedRoute(), - action: () => context.router.navigate(SyncedRoute()), - ); + final destinations = HomeTabs.values + .map((e) { + switch (e) { + case HomeTabs.dashboard: + return DestinationModel( + label: context.localized.navigationDashboard, + icon: const Icon(IconsaxOutline.home), + selectedIcon: const Icon(IconsaxBold.home), + route: const DashboardRoute(), + action: () => context.router.navigate(const DashboardRoute()), + floatingActionButton: AdaptiveFab( + context: context, + title: context.localized.search, + key: Key(e.name.capitalize()), + onPressed: () => context.router.navigate(LibrarySearchRoute()), + child: const Icon(IconsaxOutline.search_normal_1), + ), + ); + case HomeTabs.favorites: + return DestinationModel( + label: context.localized.navigationFavorites, + icon: const Icon(IconsaxOutline.heart), + selectedIcon: const Icon(IconsaxBold.heart), + route: const FavouritesRoute(), + floatingActionButton: AdaptiveFab( + context: context, + title: context.localized.filter(0), + key: Key(e.name.capitalize()), + onPressed: () => context.router.navigate(LibrarySearchRoute(favourites: true)), + child: const Icon(IconsaxOutline.heart_search), + ), + action: () => context.router.navigate(const FavouritesRoute()), + ); + case HomeTabs.sync: + if (canDownload) { + return DestinationModel( + label: context.localized.navigationSync, + icon: const Icon(IconsaxOutline.cloud), + selectedIcon: const Icon(IconsaxBold.cloud), + route: SyncedRoute(), + action: () => context.router.navigate(SyncedRoute()), + ); + } + return null; } - return null; - } - }); + }) + .nonNulls + .toList(); return HeroControllerScope( controller: HeroController(), child: AutoRouter( diff --git a/lib/screens/library/tabs/timeline_tab.dart b/lib/screens/library/tabs/timeline_tab.dart index 653c6c7..d4abedf 100644 --- a/lib/screens/library/tabs/timeline_tab.dart +++ b/lib/screens/library/tabs/timeline_tab.dart @@ -1,4 +1,5 @@ import 'package:fladder/models/items/photos_model.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/models/view_model.dart'; import 'package:fladder/providers/library_provider.dart'; import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart'; @@ -27,7 +28,7 @@ class TimelineTab extends ConsumerStatefulWidget { class _TimelineTabState extends ConsumerState with AutomaticKeepAliveClientMixin { final itemScrollController = ItemScrollController(); double get posterCount { - if (AdaptiveLayout.of(context).layout == LayoutState.desktop) { + if (AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop) { return 200; } return 125; diff --git a/lib/screens/library_search/library_search_screen.dart b/lib/screens/library_search/library_search_screen.dart index 0c66d16..92b9d03 100644 --- a/lib/screens/library_search/library_search_screen.dart +++ b/lib/screens/library_search/library_search_screen.dart @@ -13,6 +13,7 @@ import 'package:fladder/models/library_search/library_search_model.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/models/playlist_model.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/library_search_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; @@ -227,7 +228,7 @@ class _LibrarySearchScreenState extends ConsumerState { child: MediaQuery.removeViewInsets( context: context, child: ClipRRect( - borderRadius: AdaptiveLayout.of(context).layout == LayoutState.desktop + borderRadius: AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop ? BorderRadius.circular(15) : BorderRadius.circular(0), child: FladderScrollbar( @@ -419,7 +420,7 @@ class _LibrarySearchScreenState extends ConsumerState { ), ); }), - if (AdaptiveLayout.of(context).layout == LayoutState.phone) ...[ + if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) ...[ const SizedBox(width: 6), const SizedBox.square(dimension: 46, child: SettingsUserIcon()), ], diff --git a/lib/screens/settings/client_sections/client_settings_advanced.dart b/lib/screens/settings/client_sections/client_settings_advanced.dart new file mode 100644 index 0000000..c4465fa --- /dev/null +++ b/lib/screens/settings/client_sections/client_settings_advanced.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/models/settings/home_settings_model.dart'; +import 'package:fladder/providers/settings/home_settings_provider.dart'; +import 'package:fladder/screens/settings/settings_list_tile.dart'; +import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/option_dialogue.dart'; + +List buildClientSettingsAdvanced(BuildContext context, WidgetRef ref) { + return [ + SettingsLabelDivider(label: context.localized.advanced), + SettingsListTile( + label: Text(context.localized.settingsLayoutSizesTitle), + subLabel: Text(context.localized.settingsLayoutSizesDesc), + onTap: () async { + final newItems = await openMultiSelectOptions( + context, + label: context.localized.settingsLayoutSizesTitle, + items: ViewSize.values, + allowMultiSelection: true, + selected: ref.read(homeSettingsProvider.select((value) => value.layoutStates.toList())), + itemBuilder: (type, selected, tap) => CheckboxListTile( + contentPadding: EdgeInsets.zero, + value: selected, + onChanged: (value) => tap(), + title: Text(type.name), + ), + ); + ref.read(homeSettingsProvider.notifier).setViewSize(newItems.toSet()); + }, + trailing: Card( + color: Theme.of(context).colorScheme.primaryContainer, + shadowColor: Colors.transparent, + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(ref + .watch(homeSettingsProvider.select((value) => value.layoutStates.toList())) + .map((e) => e.label(context)) + .join(', ')), + ), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsLayoutModesTitle), + subLabel: Text(context.localized.settingsLayoutModesDesc), + onTap: () async { + final newItems = await openMultiSelectOptions( + context, + label: context.localized.settingsLayoutModesTitle, + items: LayoutMode.values, + allowMultiSelection: true, + selected: ref.read(homeSettingsProvider.select((value) => value.screenLayouts.toList())), + itemBuilder: (type, selected, tap) => CheckboxListTile( + contentPadding: EdgeInsets.zero, + value: selected, + onChanged: (value) => tap(), + title: Text(type.label(context)), + ), + ); + ref.read(homeSettingsProvider.notifier).setLayoutModes(newItems.toSet()); + }, + trailing: Card( + color: Theme.of(context).colorScheme.primaryContainer, + shadowColor: Colors.transparent, + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(ref + .watch(homeSettingsProvider.select((value) => value.screenLayouts.toList())) + .map((e) => e.label(context)) + .join(', ')), + ), + ), + ), + ]; +} diff --git a/lib/screens/settings/client_sections/client_settings_dashboard.dart b/lib/screens/settings/client_sections/client_settings_dashboard.dart new file mode 100644 index 0000000..8377695 --- /dev/null +++ b/lib/screens/settings/client_sections/client_settings_dashboard.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/models/settings/home_settings_model.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; +import 'package:fladder/providers/settings/home_settings_provider.dart'; +import 'package:fladder/screens/settings/settings_list_tile.dart'; +import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/widgets/shared/enum_selection.dart'; + +List buildClientSettingsDashboard(BuildContext context, WidgetRef ref) { + final clientSettings = ref.watch(clientSettingsProvider); + return [ + SettingsLabelDivider(label: context.localized.dashboard), + SettingsListTile( + label: Text(context.localized.settingsHomeBannerTitle), + subLabel: Text(context.localized.settingsHomeBannerDescription), + trailing: EnumBox( + current: ref.watch( + homeSettingsProvider.select( + (value) => value.homeBanner.label(context), + ), + ), + itemBuilder: (context) => HomeBanner.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => + ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)), + ), + ) + .toList(), + ), + ), + if (ref.watch(homeSettingsProvider.select((value) => value.homeBanner)) != HomeBanner.hide) + SettingsListTile( + label: Text(context.localized.settingsHomeBannerInformationTitle), + subLabel: Text(context.localized.settingsHomeBannerInformationDesc), + trailing: EnumBox( + current: ref.watch( + homeSettingsProvider.select((value) => value.carouselSettings.label(context)), + ), + itemBuilder: (context) => HomeCarouselSettings.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => ref + .read(homeSettingsProvider.notifier) + .update((context) => context.copyWith(carouselSettings: entry)), + ), + ) + .toList(), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsHomeNextUpTitle), + subLabel: Text(context.localized.settingsHomeNextUpDesc), + trailing: EnumBox( + current: ref.watch( + homeSettingsProvider.select( + (value) => value.nextUp.label(context), + ), + ), + itemBuilder: (context) => HomeNextUp.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => + ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)), + ), + ) + .toList(), + ), + ), + SettingsListTile( + label: Text(context.localized.clientSettingsShowAllCollectionsTitle), + subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc), + onTap: () => ref + .read(clientSettingsProvider.notifier) + .update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)), + trailing: Switch( + value: clientSettings.showAllCollectionTypes, + onChanged: (value) => ref + .read(clientSettingsProvider.notifier) + .update((current) => current.copyWith(showAllCollectionTypes: value)), + ), + ), + const Divider(), + ]; +} diff --git a/lib/screens/settings/client_sections/client_settings_download.dart b/lib/screens/settings/client_sections/client_settings_download.dart new file mode 100644 index 0000000..6c3bc91 --- /dev/null +++ b/lib/screens/settings/client_sections/client_settings_download.dart @@ -0,0 +1,122 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:ficonsax/ficonsax.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/providers/settings/client_settings_provider.dart'; +import 'package:fladder/providers/sync_provider.dart'; +import 'package:fladder/providers/user_provider.dart'; +import 'package:fladder/screens/settings/settings_list_tile.dart'; +import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/shared/default_alert_dialog.dart'; +import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/size_formatting.dart'; + +List buildClientSettingsDownload(BuildContext context, WidgetRef ref, Function setState) { + final clientSettings = ref.watch(clientSettingsProvider); + final currentFolder = ref.watch(syncProvider.notifier).savePath; + final canSync = ref.watch(userProvider.select((value) => value?.canDownload ?? false)); + + return [ + if (canSync && !kIsWeb) ...[ + SettingsLabelDivider(label: context.localized.downloadsTitle), + if (AdaptiveLayout.of(context).isDesktop) ...[ + SettingsListTile( + label: Text(context.localized.downloadsPath), + subLabel: Text(currentFolder ?? "-"), + onTap: currentFolder != null + ? () async => await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.localized.pathEditTitle), + content: Text(context.localized.pathEditDesc), + actions: [ + ElevatedButton( + onPressed: () async { + String? selectedDirectory = await FilePicker.platform.getDirectoryPath( + dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); + if (selectedDirectory != null) { + ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); + } + Navigator.of(context).pop(); + }, + child: Text(context.localized.change), + ) + ], + ), + ) + : () async { + String? selectedDirectory = await FilePicker.platform + .getDirectoryPath(dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); + if (selectedDirectory != null) { + ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); + } + }, + trailing: currentFolder?.isNotEmpty == true + ? IconButton( + color: Theme.of(context).colorScheme.error, + onPressed: () async => await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.localized.pathClearTitle), + content: Text(context.localized.pathEditDesc), + actions: [ + ElevatedButton( + onPressed: () { + ref.read(clientSettingsProvider.notifier).setSyncPath(null); + Navigator.of(context).pop(); + }, + child: Text(context.localized.clear), + ) + ], + ), + ), + icon: const Icon(IconsaxOutline.folder_minus), + ) + : null, + ), + ], + FutureBuilder( + future: ref.watch(syncProvider.notifier).directorySize, + builder: (context, snapshot) { + final data = snapshot.data ?? 0; + return SettingsListTile( + label: Text(context.localized.downloadsSyncedData), + subLabel: Text(data.byteFormat ?? ""), + trailing: FilledButton( + onPressed: () { + showDefaultAlertDialog( + context, + context.localized.downloadsClearTitle, + context.localized.downloadsClearDesc, + (context) async { + await ref.read(syncProvider.notifier).clear(); + setState(() {}); + Navigator.of(context).pop(); + }, + context.localized.clear, + (context) => Navigator.of(context).pop(), + context.localized.cancel, + ); + }, + child: Text(context.localized.clear), + ), + ); + }, + ), + SettingsListTile( + label: Text(context.localized.clientSettingsRequireWifiTitle), + subLabel: Text(context.localized.clientSettingsRequireWifiDesc), + onTap: () => ref.read(clientSettingsProvider.notifier).setRequireWifi(!clientSettings.requireWifi), + trailing: Switch( + value: clientSettings.requireWifi, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setRequireWifi(value), + ), + ), + const Divider(), + ], + ]; +} diff --git a/lib/screens/settings/client_sections/client_settings_theme.dart b/lib/screens/settings/client_sections/client_settings_theme.dart new file mode 100644 index 0000000..de3b055 --- /dev/null +++ b/lib/screens/settings/client_sections/client_settings_theme.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/providers/settings/client_settings_provider.dart'; +import 'package:fladder/screens/settings/settings_list_tile.dart'; +import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/util/color_extensions.dart'; +import 'package:fladder/util/custom_color_themes.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/option_dialogue.dart'; +import 'package:fladder/util/theme_mode_extension.dart'; + +List buildClientSettingsTheme(BuildContext context, WidgetRef ref) { + final clientSettings = ref.watch(clientSettingsProvider); + return [ + SettingsLabelDivider(label: context.localized.theme), + SettingsListTile( + label: Text(context.localized.mode), + subLabel: Text(clientSettings.themeMode.label(context)), + onTap: () => openMultiSelectOptions( + context, + label: "${context.localized.theme} ${context.localized.mode}", + items: ThemeMode.values, + selected: [ref.read(clientSettingsProvider.select((value) => value.themeMode))], + onChanged: (values) => ref.read(clientSettingsProvider.notifier).setThemeMode(values.first), + itemBuilder: (type, selected, tap) => RadioListTile( + value: type, + title: Text(type.label(context)), + contentPadding: EdgeInsets.zero, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + groupValue: ref.read(clientSettingsProvider.select((value) => value.themeMode)), + onChanged: (value) => tap(), + ), + ), + ), + SettingsListTile( + label: Text(context.localized.color), + subLabel: Text(clientSettings.themeColor?.name ?? context.localized.dynamicText), + onTap: () => openMultiSelectOptions( + context, + label: context.localized.settingsLayoutModesTitle, + items: [null, ...ColorThemes.values], + selected: [(ref.read(clientSettingsProvider.select((value) => value.themeColor)))], + onChanged: (values) => ref.read(clientSettingsProvider.notifier).setThemeColor(values.first), + itemBuilder: (type, selected, tap) => RadioListTile( + groupValue: ref.read(clientSettingsProvider.select((value) => value.themeColor)), + contentPadding: EdgeInsets.zero, + value: type, + onChanged: (value) => tap(), + title: Row( + children: [ + Container( + height: 24, + width: 24, + decoration: BoxDecoration( + gradient: type == null + ? const SweepGradient( + center: FractionalOffset.center, + colors: [ + Color(0xFF4285F4), // blue + Color(0xFF34A853), // green + Color(0xFFFBBC05), // yellow + Color(0xFFEA4335), // red + Color(0xFF4285F4), // blue again to seamlessly transition to the start + ], + stops: [0.0, 0.25, 0.5, 0.75, 1.0], + ) + : null, + color: type?.color, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(width: 8), + Text(type?.name ?? context.localized.dynamicText), + ], + ), + ), + ), + ), + SettingsListTile( + label: Text(context.localized.clientSettingsSchemeVariantTitle), + subLabel: Text(clientSettings.schemeVariant.label(context)), + onTap: () async { + await openMultiSelectOptions( + context, + label: context.localized.settingsLayoutModesTitle, + items: DynamicSchemeVariant.values, + selected: [(ref.read(clientSettingsProvider.select((value) => value.schemeVariant)))], + onChanged: (values) => ref.read(clientSettingsProvider.notifier).setSchemeVariant(values.first), + itemBuilder: (type, selected, tap) => RadioListTile( + groupValue: selected ? type : null, + contentPadding: EdgeInsets.zero, + value: type, + onChanged: (value) => tap(), + title: Text(type.label(context)), + ), + ); + }, + ), + SettingsListTile( + label: Text(context.localized.amoledBlack), + subLabel: Text(clientSettings.amoledBlack ? context.localized.enabled : context.localized.disabled), + onTap: () => ref.read(clientSettingsProvider.notifier).setAmoledBlack(!clientSettings.amoledBlack), + trailing: Switch( + value: clientSettings.amoledBlack, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setAmoledBlack(value), + ), + ), + const Divider(), + ]; +} diff --git a/lib/screens/settings/client_sections/client_settings_visual.dart b/lib/screens/settings/client_sections/client_settings_visual.dart new file mode 100644 index 0000000..868946a --- /dev/null +++ b/lib/screens/settings/client_sections/client_settings_visual.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/providers/settings/client_settings_provider.dart'; +import 'package:fladder/screens/settings/settings_list_tile.dart'; +import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/shared/input_fields.dart'; +import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/widgets/shared/enum_selection.dart'; +import 'package:fladder/widgets/shared/fladder_slider.dart'; + +List buildClientSettingsVisual( + BuildContext context, + WidgetRef ref, + TextEditingController nextUpDaysEditor, + TextEditingController libraryPageSizeController, +) { + final clientSettings = ref.watch(clientSettingsProvider); + Locale currentLocale = WidgetsBinding.instance.platformDispatcher.locale; + return [ + SettingsLabelDivider(label: context.localized.settingsVisual), + SettingsListTile( + label: Text(context.localized.displayLanguage), + trailing: Localizations.override( + context: context, + locale: ref.watch( + clientSettingsProvider.select( + (value) => (value.selectedLocale ?? currentLocale), + ), + ), + child: Builder(builder: (context) { + return EnumBox( + current: context.localized.nativeName, + itemBuilder: (context) { + return [ + ...AppLocalizations.supportedLocales.map( + (entry) => PopupMenuItem( + value: entry, + child: Localizations.override( + context: context, + locale: entry, + child: Builder(builder: (context) { + return Text( + context.localized.nativeName, + ); + }), + ), + onTap: () => ref + .read(clientSettingsProvider.notifier) + .update((state) => state.copyWith(selectedLocale: entry)), + ), + ) + ]; + }, + ); + }), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsBlurredPlaceholderTitle), + subLabel: Text(context.localized.settingsBlurredPlaceholderDesc), + onTap: () => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(!clientSettings.blurPlaceHolders), + trailing: Switch( + value: clientSettings.blurPlaceHolders, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(value), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsBlurEpisodesTitle), + subLabel: Text(context.localized.settingsBlurEpisodesDesc), + onTap: () => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(!clientSettings.blurUpcomingEpisodes), + trailing: Switch( + value: clientSettings.blurUpcomingEpisodes, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(value), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsEnableOsMediaControls), + onTap: () => ref.read(clientSettingsProvider.notifier).setMediaKeys(!clientSettings.enableMediaKeys), + trailing: Switch( + value: clientSettings.enableMediaKeys, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setMediaKeys(value), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsNextUpCutoffDays), + trailing: SizedBox( + width: 100, + child: IntInputField( + suffix: context.localized.days, + controller: nextUpDaysEditor, + onSubmitted: (value) { + if (value != null) { + ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith( + nextUpDateCutoff: Duration(days: value), + )); + } + }, + )), + ), + SettingsListTile( + label: Text(context.localized.libraryPageSizeTitle), + subLabel: Text(context.localized.libraryPageSizeDesc), + trailing: SizedBox( + width: 100, + child: IntInputField( + controller: libraryPageSizeController, + placeHolder: "500", + onSubmitted: (value) => ref.read(clientSettingsProvider.notifier).update( + (current) => current.copyWith(libraryPageSize: value), + ), + )), + ), + SettingsListTile( + label: Text(AdaptiveLayout.of(context).isDesktop + ? context.localized.settingsShowScaleSlider + : context.localized.settingsPosterPinch), + onTap: () => ref.read(clientSettingsProvider.notifier).update( + (current) => current.copyWith(pinchPosterZoom: !current.pinchPosterZoom), + ), + trailing: Switch( + value: clientSettings.pinchPosterZoom, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).update( + (current) => current.copyWith(pinchPosterZoom: value), + ), + ), + ), + Column( + children: [ + SettingsListTile( + label: Text(context.localized.settingsPosterSize), + trailing: Text( + clientSettings.posterSize.toString(), + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: FladderSlider( + min: 0.5, + max: 1.5, + value: clientSettings.posterSize, + divisions: 20, + onChanged: (value) => + ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(posterSize: value)), + ), + ), + ], + ), + const Divider(), + ]; +} diff --git a/lib/screens/settings/client_settings_page.dart b/lib/screens/settings/client_settings_page.dart index 98e7382..07e1563 100644 --- a/lib/screens/settings/client_settings_page.dart +++ b/lib/screens/settings/client_settings_page.dart @@ -2,33 +2,23 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; -import 'package:ficonsax/ficonsax.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; -import 'package:fladder/providers/settings/home_settings_provider.dart'; import 'package:fladder/providers/shared_provider.dart'; -import 'package:fladder/providers/sync_provider.dart'; -import 'package:fladder/providers/user_provider.dart'; 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_dashboard.dart'; +import 'package:fladder/screens/settings/client_sections/client_settings_download.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/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; -import 'package:fladder/screens/shared/default_alert_dialog.dart'; -import 'package:fladder/screens/shared/input_fields.dart'; import 'package:fladder/util/adaptive_layout.dart'; -import 'package:fladder/util/color_extensions.dart'; -import 'package:fladder/util/custom_color_themes.dart'; import 'package:fladder/util/localization_helper.dart'; -import 'package:fladder/util/option_dialogue.dart'; import 'package:fladder/util/simple_duration_picker.dart'; -import 'package:fladder/util/size_formatting.dart'; -import 'package:fladder/util/theme_mode_extension.dart'; -import 'package:fladder/widgets/shared/enum_selection.dart'; -import 'package:fladder/widgets/shared/fladder_slider.dart'; @RoutePage() class ClientSettingsPage extends ConsumerStatefulWidget { @@ -48,114 +38,15 @@ class _ClientSettingsPageState extends ConsumerState { @override Widget build(BuildContext context) { final clientSettings = ref.watch(clientSettingsProvider); - final showBackground = AdaptiveLayout.of(context).layout != LayoutState.phone && - AdaptiveLayout.of(context).size != ScreenLayout.single; - final currentFolder = ref.watch(syncProvider.notifier).savePath; - Locale currentLocale = WidgetsBinding.instance.platformDispatcher.locale; + final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone && + AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; - final canSync = ref.watch(userProvider.select((value) => value?.canDownload ?? false)); return Card( elevation: showBackground ? 2 : 0, child: SettingsScaffold( label: "Fladder", items: [ - if (canSync && !kIsWeb) ...[ - SettingsLabelDivider(label: context.localized.downloadsTitle), - if (AdaptiveLayout.of(context).isDesktop) ...[ - SettingsListTile( - label: Text(context.localized.downloadsPath), - subLabel: Text(currentFolder ?? "-"), - onTap: currentFolder != null - ? () async => await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.localized.pathEditTitle), - content: Text(context.localized.pathEditDesc), - actions: [ - ElevatedButton( - onPressed: () async { - String? selectedDirectory = await FilePicker.platform.getDirectoryPath( - dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); - if (selectedDirectory != null) { - ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); - } - Navigator.of(context).pop(); - }, - child: Text(context.localized.change), - ) - ], - ), - ) - : () async { - String? selectedDirectory = await FilePicker.platform.getDirectoryPath( - dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); - if (selectedDirectory != null) { - ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); - } - }, - trailing: currentFolder?.isNotEmpty == true - ? IconButton( - color: Theme.of(context).colorScheme.error, - onPressed: () async => await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.localized.pathClearTitle), - content: Text(context.localized.pathEditDesc), - actions: [ - ElevatedButton( - onPressed: () { - ref.read(clientSettingsProvider.notifier).setSyncPath(null); - Navigator.of(context).pop(); - }, - child: Text(context.localized.clear), - ) - ], - ), - ), - icon: const Icon(IconsaxOutline.folder_minus), - ) - : null, - ), - ], - FutureBuilder( - future: ref.watch(syncProvider.notifier).directorySize, - builder: (context, snapshot) { - final data = snapshot.data ?? 0; - return SettingsListTile( - label: Text(context.localized.downloadsSyncedData), - subLabel: Text(data.byteFormat ?? ""), - trailing: FilledButton( - onPressed: () { - showDefaultAlertDialog( - context, - context.localized.downloadsClearTitle, - context.localized.downloadsClearDesc, - (context) async { - await ref.read(syncProvider.notifier).clear(); - setState(() {}); - Navigator.of(context).pop(); - }, - context.localized.clear, - (context) => Navigator.of(context).pop(), - context.localized.cancel, - ); - }, - child: Text(context.localized.clear), - ), - ); - }, - ), - SettingsListTile( - label: Text(context.localized.clientSettingsRequireWifiTitle), - subLabel: Text(context.localized.clientSettingsRequireWifiDesc), - onTap: () => ref.read(clientSettingsProvider.notifier).setRequireWifi(!clientSettings.requireWifi), - trailing: Switch( - value: clientSettings.requireWifi, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setRequireWifi(value), - ), - ), - const Divider(), - ], + ...buildClientSettingsDownload(context, ref, setState), SettingsLabelDivider(label: context.localized.lockscreen), SettingsListTile( label: Text(context.localized.timeOut), @@ -166,329 +57,18 @@ class _ClientSettingsPageState extends ConsumerState { initialValue: clientSettings.timeOut ?? const Duration(), ); - ref.read(clientSettingsProvider.notifier).setTimeOut(timePicker != null + if (timePicker == null) return; + + ref.read(clientSettingsProvider.notifier).setTimeOut(timePicker != Duration.zero ? Duration(minutes: timePicker.inMinutes, seconds: timePicker.inSeconds % 60) : null); }, ), const Divider(), - SettingsLabelDivider(label: context.localized.dashboard), - SettingsListTile( - label: Text(context.localized.settingsHomeBannerTitle), - subLabel: Text(context.localized.settingsHomeBannerDescription), - trailing: EnumBox( - current: ref.watch( - homeSettingsProvider.select( - (value) => value.homeBanner.label(context), - ), - ), - itemBuilder: (context) => HomeBanner.values - .map( - (entry) => PopupMenuItem( - value: entry, - child: Text(entry.label(context)), - onTap: () => ref - .read(homeSettingsProvider.notifier) - .update((context) => context.copyWith(homeBanner: entry)), - ), - ) - .toList(), - ), - ), - if (ref.watch(homeSettingsProvider.select((value) => value.homeBanner)) != HomeBanner.hide) - SettingsListTile( - label: Text(context.localized.settingsHomeBannerInformationTitle), - subLabel: Text(context.localized.settingsHomeBannerInformationDesc), - trailing: EnumBox( - current: ref.watch( - homeSettingsProvider.select((value) => value.carouselSettings.label(context)), - ), - itemBuilder: (context) => HomeCarouselSettings.values - .map( - (entry) => PopupMenuItem( - value: entry, - child: Text(entry.label(context)), - onTap: () => ref - .read(homeSettingsProvider.notifier) - .update((context) => context.copyWith(carouselSettings: entry)), - ), - ) - .toList(), - ), - ), - SettingsListTile( - label: Text(context.localized.settingsHomeNextUpTitle), - subLabel: Text(context.localized.settingsHomeNextUpDesc), - trailing: EnumBox( - current: ref.watch( - homeSettingsProvider.select( - (value) => value.nextUp.label(context), - ), - ), - itemBuilder: (context) => HomeNextUp.values - .map( - (entry) => PopupMenuItem( - value: entry, - child: Text(entry.label(context)), - onTap: () => - ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)), - ), - ) - .toList(), - ), - ), - SettingsListTile( - label: Text(context.localized.clientSettingsShowAllCollectionsTitle), - subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc), - onTap: () => ref - .read(clientSettingsProvider.notifier) - .update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)), - trailing: Switch( - value: clientSettings.showAllCollectionTypes, - onChanged: (value) => ref - .read(clientSettingsProvider.notifier) - .update((current) => current.copyWith(showAllCollectionTypes: value)), - ), - ), - const Divider(), - SettingsLabelDivider(label: context.localized.settingsVisual), - SettingsListTile( - label: Text(context.localized.displayLanguage), - trailing: Localizations.override( - context: context, - locale: ref.watch( - clientSettingsProvider.select( - (value) => (value.selectedLocale ?? currentLocale), - ), - ), - child: Builder(builder: (context) { - return EnumBox( - current: context.localized.nativeName, - itemBuilder: (context) { - return [ - ...AppLocalizations.supportedLocales.map( - (entry) => PopupMenuItem( - value: entry, - child: Localizations.override( - context: context, - locale: entry, - child: Builder(builder: (context) { - return Text( - context.localized.nativeName, - ); - }), - ), - onTap: () => ref - .read(clientSettingsProvider.notifier) - .update((state) => state.copyWith(selectedLocale: entry)), - ), - ) - ]; - }, - ); - }), - ), - ), - SettingsListTile( - label: Text(context.localized.settingsBlurredPlaceholderTitle), - subLabel: Text(context.localized.settingsBlurredPlaceholderDesc), - onTap: () => - ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(!clientSettings.blurPlaceHolders), - trailing: Switch( - value: clientSettings.blurPlaceHolders, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(value), - ), - ), - SettingsListTile( - label: Text(context.localized.settingsBlurEpisodesTitle), - subLabel: Text(context.localized.settingsBlurEpisodesDesc), - onTap: () => - ref.read(clientSettingsProvider.notifier).setBlurEpisodes(!clientSettings.blurUpcomingEpisodes), - trailing: Switch( - value: clientSettings.blurUpcomingEpisodes, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(value), - ), - ), - SettingsListTile( - label: Text(context.localized.settingsEnableOsMediaControls), - onTap: () => ref.read(clientSettingsProvider.notifier).setMediaKeys(!clientSettings.enableMediaKeys), - trailing: Switch( - value: clientSettings.enableMediaKeys, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setMediaKeys(value), - ), - ), - SettingsListTile( - label: Text(context.localized.settingsNextUpCutoffDays), - trailing: SizedBox( - width: 100, - child: IntInputField( - suffix: context.localized.days, - controller: nextUpDaysEditor, - onSubmitted: (value) { - if (value != null) { - ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith( - nextUpDateCutoff: Duration(days: value), - )); - } - }, - )), - ), - SettingsListTile( - label: Text(context.localized.libraryPageSizeTitle), - subLabel: Text(context.localized.libraryPageSizeDesc), - trailing: SizedBox( - width: 100, - child: IntInputField( - controller: libraryPageSizeController, - placeHolder: "500", - onSubmitted: (value) => ref.read(clientSettingsProvider.notifier).update( - (current) => current.copyWith(libraryPageSize: value), - ), - )), - ), - SettingsListTile( - label: Text(AdaptiveLayout.of(context).isDesktop - ? context.localized.settingsShowScaleSlider - : context.localized.settingsPosterPinch), - onTap: () => ref.read(clientSettingsProvider.notifier).update( - (current) => current.copyWith(pinchPosterZoom: !current.pinchPosterZoom), - ), - trailing: Switch( - value: clientSettings.pinchPosterZoom, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).update( - (current) => current.copyWith(pinchPosterZoom: value), - ), - ), - ), - Column( - children: [ - SettingsListTile( - label: Text(context.localized.settingsPosterSize), - trailing: Text( - clientSettings.posterSize.toString(), - style: Theme.of(context).textTheme.bodyLarge, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: FladderSlider( - min: 0.5, - max: 1.5, - value: clientSettings.posterSize, - divisions: 20, - onChanged: (value) => ref - .read(clientSettingsProvider.notifier) - .update((current) => current.copyWith(posterSize: value)), - ), - ), - const Divider(), - ], - ), - SettingsLabelDivider(label: context.localized.theme), - SettingsListTile( - label: Text(context.localized.mode), - subLabel: Text(clientSettings.themeMode.label(context)), - onTap: () => openOptionDialogue( - context, - label: "${context.localized.theme} ${context.localized.mode}", - items: ThemeMode.values, - itemBuilder: (type) => RadioListTile( - value: type, - title: Text(type?.label(context) ?? context.localized.other), - contentPadding: EdgeInsets.zero, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - groupValue: ref.read(clientSettingsProvider.select((value) => value.themeMode)), - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setThemeMode(value), - ), - ), - ), - SettingsListTile( - label: Text(context.localized.color), - subLabel: Text(clientSettings.themeColor?.name ?? context.localized.dynamicText), - onTap: () => openOptionDialogue( - context, - isNullable: !kIsWeb, - label: context.localized.themeColor, - items: ColorThemes.values, - itemBuilder: (type) => Consumer( - builder: (context, ref, child) => ListTile( - title: Row( - children: [ - Checkbox( - value: type == ref.watch(clientSettingsProvider.select((value) => value.themeColor)), - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setThemeColor(type), - ), - const SizedBox(width: 4), - Container( - height: 24, - width: 24, - decoration: BoxDecoration( - gradient: type == null - ? const SweepGradient( - center: FractionalOffset.center, - colors: [ - Color(0xFF4285F4), // blue - Color(0xFF34A853), // green - Color(0xFFFBBC05), // yellow - Color(0xFFEA4335), // red - Color(0xFF4285F4), // blue again to seamlessly transition to the start - ], - stops: [0.0, 0.25, 0.5, 0.75, 1.0], - ) - : null, - color: type?.color, - borderRadius: BorderRadius.circular(4), - ), - ), - const SizedBox(width: 8), - Text(type?.name ?? context.localized.dynamicText), - ], - ), - contentPadding: EdgeInsets.zero, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - onTap: () => ref.read(clientSettingsProvider.notifier).setThemeColor(type), - ), - ), - ), - ), - SettingsListTile( - label: Text(context.localized.clientSettingsSchemeVariantTitle), - subLabel: Text(clientSettings.schemeVariant.label(context)), - onTap: () => openOptionDialogue( - context, - isNullable: false, - label: context.localized.themeColor, - items: DynamicSchemeVariant.values, - itemBuilder: (type) => Consumer( - builder: (context, ref, child) => ListTile( - title: Row( - children: [ - Checkbox( - value: type == ref.watch(clientSettingsProvider.select((value) => value.schemeVariant)), - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setSchemeVariant(type), - ), - const SizedBox(width: 8), - Text(type?.label(context) ?? ""), - ], - ), - contentPadding: EdgeInsets.zero, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - onTap: () => ref.read(clientSettingsProvider.notifier).setSchemeVariant(type), - ), - ), - ), - ), - SettingsListTile( - label: Text(context.localized.amoledBlack), - subLabel: Text(clientSettings.amoledBlack ? context.localized.enabled : context.localized.disabled), - onTap: () => ref.read(clientSettingsProvider.notifier).setAmoledBlack(!clientSettings.amoledBlack), - trailing: Switch( - value: clientSettings.amoledBlack, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setAmoledBlack(value), - ), - ), - if (AdaptiveLayout.of(context).isDesktop) ...[ - const Divider(), + ...buildClientSettingsDashboard(context, ref), + ...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController), + ...buildClientSettingsTheme(context, ref), + if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[ SettingsLabelDivider(label: context.localized.controls), SettingsListTile( label: Text(context.localized.mouseDragSupport), @@ -499,11 +79,13 @@ class _ClientSettingsPageState extends ConsumerState { trailing: Switch( value: clientSettings.mouseDragSupport, onChanged: (value) => ref - .read(clientSettingsProvider.notifier) - .update((current) => current.copyWith(mouseDragSupport: !clientSettings.mouseDragSupport)), + .read(clientSettingsProvider.notifier) + .update((current) => current.copyWith(mouseDragSupport: !clientSettings.mouseDragSupport)), ), ), ], + const Divider(), + ...buildClientSettingsAdvanced(context, ref), if (kDebugMode) ...[ const SizedBox(height: 64), SettingsListTile( @@ -554,7 +136,6 @@ class _ClientSettingsPageState extends ConsumerState { }, ), ], - const SizedBox(height: 16), ], ), ); diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index 8b79cc9..d42e2c2 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; @@ -34,8 +35,8 @@ class _PlayerSettingsPageState extends ConsumerState { Widget build(BuildContext context) { final videoSettings = ref.watch(videoPlayerSettingsProvider); final provider = ref.read(videoPlayerSettingsProvider.notifier); - final showBackground = AdaptiveLayout.of(context).layout != LayoutState.phone && - AdaptiveLayout.of(context).size != ScreenLayout.single; + final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone && + AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; return Card( elevation: showBackground ? 2 : 0, child: SettingsScaffold( @@ -63,20 +64,19 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.videoScalingFillScreenTitle), subLabel: Text(videoSettings.videoFit.label(context)), - onTap: () => openOptionDialogue( + onTap: () => openMultiSelectOptions( context, label: context.localized.videoScalingFillScreenTitle, items: BoxFit.values, - itemBuilder: (type) => RadioListTile( - title: Text(type?.label(context) ?? ""), + 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, - groupValue: ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit)), - onChanged: (value) { - provider.setFitType(value); - Navigator.pop(context); - }, + onChanged: (value) => tap(), ), ), ), diff --git a/lib/screens/settings/security_settings_page.dart b/lib/screens/settings/security_settings_page.dart index fc570b2..bafaafa 100644 --- a/lib/screens/settings/security_settings_page.dart +++ b/lib/screens/settings/security_settings_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; @@ -21,8 +22,8 @@ class _UserSettingsPageState extends ConsumerState { @override Widget build(BuildContext context) { final user = ref.watch(userProvider); - final showBackground = AdaptiveLayout.of(context).layout != LayoutState.phone && - AdaptiveLayout.of(context).size != ScreenLayout.single; + final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone && + AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; return Card( elevation: showBackground ? 2 : 0, child: SettingsScaffold( diff --git a/lib/screens/settings/settings_scaffold.dart b/lib/screens/settings/settings_scaffold.dart index bdfd8a1..0121d67 100644 --- a/lib/screens/settings/settings_scaffold.dart +++ b/lib/screens/settings/settings_scaffold.dart @@ -1,8 +1,10 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/shared/user_icon.dart'; import 'package:fladder/util/adaptive_layout.dart'; @@ -10,26 +12,29 @@ import 'package:fladder/util/router_extension.dart'; class SettingsScaffold extends ConsumerWidget { final String label; - final bool showUserIcon; final ScrollController? scrollController; final List items; final List bottomActions; + final bool showUserIcon; + final bool showBackButtonNested; final Widget? floatingActionButton; const SettingsScaffold({ required this.label, - this.showUserIcon = false, this.scrollController, required this.items, this.bottomActions = const [], this.floatingActionButton, + this.showUserIcon = false, + this.showBackButtonNested = false, super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { final padding = MediaQuery.of(context).padding; + final singleLayout = AdaptiveLayout.layoutModeOf(context) == LayoutMode.single; return Scaffold( - backgroundColor: AdaptiveLayout.of(context).size == ScreenLayout.dual ? Colors.transparent : null, + backgroundColor: AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual ? Colors.transparent : null, floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: floatingActionButton, body: Column( @@ -38,10 +43,11 @@ class SettingsScaffold extends ConsumerWidget { child: CustomScrollView( controller: scrollController, slivers: [ - if (AdaptiveLayout.of(context).size == ScreenLayout.single) + if (singleLayout) SliverAppBar.large( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - leading: context.router.backButton(), + leading: BackButton( + onPressed: () => backAction(context), + ), flexibleSpace: FlexibleSpaceBar( titlePadding: const EdgeInsets.symmetric(horizontal: 16) .add(EdgeInsets.only(left: padding.left, right: padding.right, bottom: 4)), @@ -51,11 +57,12 @@ class SettingsScaffold extends ConsumerWidget { const Spacer(), if (showUserIcon) SizedBox.fromSize( - size: const Size.fromRadius(14), - child: UserIcon( - user: ref.watch(userProvider), - cornerRadius: 200, - )) + size: const Size.fromRadius(14), + child: UserIcon( + user: ref.watch(userProvider), + cornerRadius: 200, + ), + ) ], ), expandedTitleScale: 1.2, @@ -68,9 +75,15 @@ class SettingsScaffold extends ConsumerWidget { else SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Text(AdaptiveLayout.of(context).size == ScreenLayout.single ? label : "", - style: Theme.of(context).textTheme.headlineLarge), + padding: MediaQuery.paddingOf(context), + child: Row( + children: [ + if (showBackButtonNested) + BackButton( + onPressed: () => backAction(context), + ) + ], + ), ), ), SliverPadding( @@ -99,4 +112,16 @@ class SettingsScaffold extends ConsumerWidget { ), ); } + + void backAction(BuildContext context) { + if (kIsWeb) { + if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && context.tabsRouter.activeIndex != 0) { + context.tabsRouter.setActiveIndex(0); + } else { + context.router.popForced(); + } + } else { + context.router.popBack(); + } + } } diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 55fbce6..e435ee0 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -4,6 +4,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/auth_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; @@ -13,7 +14,6 @@ import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/shared/fladder_icon.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; -import 'package:fladder/util/router_extension.dart'; import 'package:fladder/util/theme_extensions.dart'; @RoutePage() @@ -27,84 +27,84 @@ class SettingsScreen extends ConsumerStatefulWidget { class _SettingsScreenState extends ConsumerState { final scrollController = ScrollController(); final minVerticalPadding = 20.0; + late LayoutMode lastAdaptiveLayout = AdaptiveLayout.layoutModeOf(context); @override Widget build(BuildContext context) { - if (AdaptiveLayout.of(context).size == ScreenLayout.single) { - return Card( - elevation: 0, - child: _leftPane(context), - ); - } else { - return AutoRouter( - builder: (context, content) { - return Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded(flex: 1, child: _leftPane(context)), - Expanded( - flex: 2, - child: content, - ), - ], - ); - }, - ); - } + return AutoTabsRouter( + builder: (context, content) { + checkForNullIndex(context); + return PopScope( + canPop: context.tabsRouter.activeIndex == 0 || AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual, + onPopInvokedWithResult: (didPop, result) { + if (!didPop) { + context.tabsRouter.setActiveIndex(0); + } + }, + child: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single + ? Card( + elevation: 0, + child: Stack( + children: [_leftPane(context), content], + ), + ) + : Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded(flex: 1, child: _leftPane(context)), + Expanded( + flex: 2, + child: content, + ), + ], + ), + ); + }, + ); + } + + //We have to navigate to the first screen after switching layouts && index == 0 otherwise the dual-layout is empty + void checkForNullIndex(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final currentIndex = context.tabsRouter.activeIndex; + if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual && currentIndex == 0) { + context.tabsRouter.setActiveIndex(1); + } + }); } IconData get deviceIcon { if (AdaptiveLayout.of(context).isDesktop) { return IconsaxOutline.monitor; } - switch (AdaptiveLayout.of(context).layout) { - case LayoutState.phone: + switch (AdaptiveLayout.viewSizeOf(context)) { + case ViewSize.phone: return IconsaxOutline.mobile; - case LayoutState.tablet: + case ViewSize.tablet: return IconsaxOutline.monitor; - case LayoutState.desktop: + case ViewSize.desktop: return IconsaxOutline.monitor; } } Widget _leftPane(BuildContext context) { - void navigateTo(PageRouteInfo route) { - AdaptiveLayout.of(context).size == ScreenLayout.single - ? context.router.navigate(route) - : context.router.replace(route); - } + void navigateTo(PageRouteInfo route) => context.tabsRouter.navigate(route); - bool containsRoute(PageRouteInfo route) { - return context.router.current.name == route.routeName; - } + bool containsRoute(PageRouteInfo route) => + AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual && context.tabsRouter.current.name == route.routeName; final quickConnectAvailable = ref.watch(userProvider.select((value) => value?.serverConfiguration?.quickConnectAvailable ?? false)); + return Container( color: context.colors.surface, child: SettingsScaffold( label: context.localized.settings, scrollController: scrollController, + showBackButtonNested: true, showUserIcon: true, items: [ - if (context.router.canNavigateBack && AdaptiveLayout.of(context).size == ScreenLayout.dual) - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: IconButton.filledTonal( - style: IconButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.surface.withValues(alpha: 0.8), - ), - onPressed: () => context.router.popBack(), - icon: Padding( - padding: EdgeInsets.all(AdaptiveLayout.of(context).inputDevice == InputDevice.pointer ? 0 : 4), - child: const Icon(IconsaxOutline.arrow_left_2), - ), - ), - ), - ), SettingsListTile( label: Text(context.localized.settingsClientTitle), subLabel: Text(context.localized.settingsClientDesc), diff --git a/lib/screens/settings/settings_selection_screen.dart b/lib/screens/settings/settings_selection_screen.dart new file mode 100644 index 0000000..f8939c3 --- /dev/null +++ b/lib/screens/settings/settings_selection_screen.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +import 'package:auto_route/auto_route.dart'; + +//Empty screen that "overlays" the settings selection on single layout +@RoutePage() +class SettingsSelectionScreen extends StatelessWidget { + const SettingsSelectionScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox.expand(); + } +} diff --git a/lib/screens/shared/chips/category_chip.dart b/lib/screens/shared/chips/category_chip.dart index 6d7e247..dba69ce 100644 --- a/lib/screens/shared/chips/category_chip.dart +++ b/lib/screens/shared/chips/category_chip.dart @@ -1,12 +1,15 @@ +import 'package:flutter/material.dart'; + import 'package:collection/collection.dart'; import 'package:ficonsax/ficonsax.dart'; + +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/map_bool_helper.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; import 'package:fladder/widgets/shared/modal_side_sheet.dart'; -import 'package:flutter/material.dart'; class CategoryChip extends StatelessWidget { final Map items; @@ -126,7 +129,7 @@ class CategoryChip extends StatelessWidget { ].addInBetween(const SizedBox(width: 6)), ); - if (AdaptiveLayout.of(context).layout != LayoutState.phone) { + if (AdaptiveLayout.viewSizeOf(context) != ViewSize.phone) { await showModalSideSheet( context, addDivider: true, diff --git a/lib/screens/shared/detail_scaffold.dart b/lib/screens/shared/detail_scaffold.dart index 271d936..619d342 100644 --- a/lib/screens/shared/detail_scaffold.dart +++ b/lib/screens/shared/detail_scaffold.dart @@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/images_models.dart'; import 'package:fladder/models/media_playback_model.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/theme.dart'; @@ -242,7 +243,8 @@ class _DetailScaffoldState extends ConsumerState { ), ), ), - if (AdaptiveLayout.of(context).size == ScreenLayout.single) + if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single || + AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) Container( margin: const EdgeInsets.symmetric(horizontal: 6), child: const SizedBox( diff --git a/lib/screens/shared/media/poster_widget.dart b/lib/screens/shared/media/poster_widget.dart index be2b898..f128ad5 100644 --- a/lib/screens/shared/media/poster_widget.dart +++ b/lib/screens/shared/media/poster_widget.dart @@ -1,13 +1,16 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/screens/shared/media/components/poster_image.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/item_base_model/play_item_helpers.dart'; import 'package:fladder/widgets/shared/clickable_text.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; class PosterWidget extends ConsumerWidget { final ItemBaseModel poster; @@ -69,7 +72,7 @@ class PosterWidget extends ConsumerWidget { children: [ Flexible( child: ClickableText( - onTap: AdaptiveLayout.of(context).layout != LayoutState.phone + onTap: AdaptiveLayout.viewSizeOf(context) != ViewSize.phone ? () => poster.parentBaseModel.navigateTo(context) : null, text: poster.title, diff --git a/lib/screens/shared/nested_scaffold.dart b/lib/screens/shared/nested_scaffold.dart index 129b856..2312673 100644 --- a/lib/screens/shared/nested_scaffold.dart +++ b/lib/screens/shared/nested_scaffold.dart @@ -1,9 +1,12 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/models/media_playback_model.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; class NestedScaffold extends ConsumerWidget { final Widget body; @@ -18,7 +21,7 @@ class NestedScaffold extends ConsumerWidget { backgroundColor: Colors.transparent, floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: switch (AdaptiveLayout.layoutOf(context)) { - LayoutState.phone => null, + ViewSize.phone => null, _ => switch (playerState) { VideoPlayerState.minimized => const Padding( padding: EdgeInsets.symmetric(horizontal: 8), diff --git a/lib/screens/syncing/synced_screen.dart b/lib/screens/syncing/synced_screen.dart index 7ae8d12..5fd1b02 100644 --- a/lib/screens/syncing/synced_screen.dart +++ b/lib/screens/syncing/synced_screen.dart @@ -1,19 +1,21 @@ +import 'package:flutter/material.dart'; + import 'package:auto_route/auto_route.dart'; import 'package:ficonsax/ficonsax.dart'; -import 'package:fladder/providers/sync_provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; +import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/shared/nested_scaffold.dart'; import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; import 'package:fladder/screens/syncing/sync_list_item.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/sliver_list_padding.dart'; import 'package:fladder/widgets/shared/pinch_poster_zoom.dart'; import 'package:fladder/widgets/shared/pull_to_refresh.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import 'package:fladder/util/sliver_list_padding.dart'; @RoutePage() class SyncedScreen extends ConsumerStatefulWidget { @@ -40,7 +42,7 @@ class _SyncedScreenState extends ConsumerState { physics: const AlwaysScrollableScrollPhysics(), controller: widget.navigationScrollController, slivers: [ - if (AdaptiveLayout.of(context).layout == LayoutState.phone) + if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) NestedSliverAppBar( searchTitle: "${context.localized.search} ...", parent: context, diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 7a75714..5ac9fbe 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -12,6 +12,7 @@ import 'package:screen_brightness/screen_brightness.dart'; import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/models/playback/playback_model.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; @@ -300,7 +301,7 @@ class _DesktopControlsState extends ConsumerState { IconButton( onPressed: () => showVideoPlayerOptions(context, () => minimizePlayer(context)), icon: const Icon(IconsaxOutline.more)), - if (AdaptiveLayout.layoutOf(context) == LayoutState.tablet) ...[ + if (AdaptiveLayout.layoutOf(context) == ViewSize.tablet) ...[ IconButton( onPressed: () => showSubSelection(context), icon: const Icon(IconsaxOutline.subtitle), @@ -310,7 +311,7 @@ class _DesktopControlsState extends ConsumerState { icon: const Icon(IconsaxOutline.audio_square), ), ], - if (AdaptiveLayout.layoutOf(context) == LayoutState.desktop) ...[ + if (AdaptiveLayout.layoutOf(context) == ViewSize.desktop) ...[ Flexible( child: ElevatedButton.icon( onPressed: () => showSubSelection(context), diff --git a/lib/util/adaptive_layout.dart b/lib/util/adaptive_layout.dart index a756a02..3638fa6 100644 --- a/lib/util/adaptive_layout.dart +++ b/lib/util/adaptive_layout.dart @@ -3,21 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/routes/auto_router.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; +import 'package:fladder/providers/settings/home_settings_provider.dart'; import 'package:fladder/util/debug_banner.dart'; import 'package:fladder/util/poster_defaults.dart'; -enum LayoutState { - phone, - tablet, - desktop, -} - -enum ScreenLayout { - single, - dual, -} - enum InputDevice { touch, pointer, @@ -26,7 +16,7 @@ enum InputDevice { class LayoutPoints { final double start; final double end; - final LayoutState type; + final ViewSize type; LayoutPoints({ required this.start, required this.end, @@ -36,7 +26,7 @@ class LayoutPoints { LayoutPoints copyWith({ double? start, double? end, - LayoutState? type, + ViewSize? type, }) { return LayoutPoints( start: start ?? this.start, @@ -60,23 +50,21 @@ class LayoutPoints { } class AdaptiveLayout extends InheritedWidget { - final LayoutState layout; - final ScreenLayout size; + final ViewSize viewSize; + final LayoutMode layoutMode; final InputDevice inputDevice; final TargetPlatform platform; final bool isDesktop; - final AutoRouter router; final PosterDefaults posterDefaults; final ScrollController controller; const AdaptiveLayout({ super.key, - required this.layout, - required this.size, + required this.viewSize, + required this.layoutMode, required this.inputDevice, required this.platform, required this.isDesktop, - required this.router, required this.posterDefaults, required this.controller, required super.child, @@ -86,9 +74,9 @@ class AdaptiveLayout extends InheritedWidget { return context.dependOnInheritedWidgetOfExactType(); } - static LayoutState layoutOf(BuildContext context) { + static ViewSize layoutOf(BuildContext context) { final AdaptiveLayout? result = maybeOf(context); - return result!.layout; + return result!.viewSize; } static PosterDefaults poster(BuildContext context) { @@ -96,11 +84,6 @@ class AdaptiveLayout extends InheritedWidget { return result!.posterDefaults; } - static AutoRouter routerOf(BuildContext context) { - final AdaptiveLayout? result = maybeOf(context); - return result!.router; - } - static AdaptiveLayout of(BuildContext context) { final AdaptiveLayout? result = maybeOf(context); return result!; @@ -111,14 +94,18 @@ class AdaptiveLayout extends InheritedWidget { return result!.controller; } + static LayoutMode layoutModeOf(BuildContext context) => maybeOf(context)!.layoutMode; + static ViewSize viewSizeOf(BuildContext context) => maybeOf(context)!.viewSize; + + static InputDevice inputDeviceOf(BuildContext context) => maybeOf(context)!.inputDevice; + @override bool updateShouldNotify(AdaptiveLayout oldWidget) { - return layout != oldWidget.layout || - size != oldWidget.size || + return viewSize != oldWidget.viewSize || + layoutMode != oldWidget.layoutMode || platform != oldWidget.platform || inputDevice != oldWidget.inputDevice || - isDesktop != oldWidget.isDesktop || - router != oldWidget.router; + isDesktop != oldWidget.isDesktop; } } @@ -126,7 +113,7 @@ const defaultTitleBarHeight = 35.0; class AdaptiveLayoutBuilder extends ConsumerStatefulWidget { final List layoutPoints; - final LayoutState fallBack; + final ViewSize fallBack; final Widget child; const AdaptiveLayoutBuilder({required this.layoutPoints, required this.child, required this.fallBack, super.key}); @@ -135,9 +122,8 @@ class AdaptiveLayoutBuilder extends ConsumerStatefulWidget { } class _AdaptiveLayoutBuilderState extends ConsumerState { - late LayoutState layout = widget.fallBack; - late ScreenLayout size = ScreenLayout.single; - AutoRouter? router; + late ViewSize viewSize = widget.fallBack; + late LayoutMode layoutMode = LayoutMode.single; late TargetPlatform currentPlatform = defaultTargetPlatform; late ScrollController controller = ScrollController(); @@ -158,47 +144,45 @@ class _AdaptiveLayoutBuilderState extends ConsumerState { } void calculateLayout() { - LayoutState? newType; + ViewSize? newType; for (var element in widget.layoutPoints) { if (MediaQuery.of(context).size.width > element.start && MediaQuery.of(context).size.width < element.end) { newType = element.type; } } - if (newType == LayoutState.phone && isDesktop) { - newType = LayoutState.tablet; - } - layout = newType ?? widget.fallBack; + viewSize = newType ?? widget.fallBack; } void calculateSize() { - ScreenLayout newSize; - if (MediaQuery.of(context).size.width > 0 && MediaQuery.of(context).size.width < 960 && !isDesktop) { - newSize = ScreenLayout.single; + LayoutMode newSize; + if (MediaQuery.of(context).size.width > 0 && MediaQuery.of(context).size.width < 960) { + newSize = LayoutMode.single; } else { - newSize = ScreenLayout.dual; + newSize = LayoutMode.dual; } - size = newSize; + layoutMode = newSize; } @override Widget build(BuildContext context) { + final acceptedLayouts = ref.watch(homeSettingsProvider.select((value) => value.screenLayouts)); + final acceptedViewSizes = ref.watch(homeSettingsProvider.select((value) => value.layoutStates)); return MediaQuery( data: MediaQuery.of(context).copyWith( padding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, ), child: AdaptiveLayout( - layout: layout, + viewSize: selectAvailableOrSmaller(viewSize, acceptedViewSizes, ViewSize.values), controller: controller, - size: size, + layoutMode: selectAvailableOrSmaller(layoutMode, acceptedLayouts, LayoutMode.values), inputDevice: (isDesktop || kIsWeb) ? InputDevice.pointer : InputDevice.touch, platform: currentPlatform, isDesktop: isDesktop, - router: router ??= AutoRouter(layout: size, ref: ref), - posterDefaults: switch (layout) { - LayoutState.phone => const PosterDefaults(size: 300, ratio: 0.55), - LayoutState.tablet => const PosterDefaults(size: 350, ratio: 0.55), - LayoutState.desktop => const PosterDefaults(size: 400, ratio: 0.55), + posterDefaults: switch (viewSize) { + ViewSize.phone => const PosterDefaults(size: 300, ratio: 0.55), + ViewSize.tablet => const PosterDefaults(size: 350, ratio: 0.55), + ViewSize.desktop => const PosterDefaults(size: 400, ratio: 0.55), }, child: DebugBanner(child: widget.child), ), diff --git a/lib/util/option_dialogue.dart b/lib/util/option_dialogue.dart index dae482f..88acdfa 100644 --- a/lib/util/option_dialogue.dart +++ b/lib/util/option_dialogue.dart @@ -1,31 +1,53 @@ import 'package:flutter/material.dart'; -Future openOptionDialogue( +Future> openMultiSelectOptions( BuildContext context, { required String label, + bool allowMultiSelection = false, + bool forceAtleastOne = true, + required List selected, required List items, - bool isNullable = false, - required Widget Function(T? type) itemBuilder, -}) { - return showDialog( + Function(List values)? onChanged, + required Widget Function(T type, bool selected, Function onTap) itemBuilder, +}) async { + Set currentSelection = selected.toSet(); + await showDialog( context: context, - builder: (context) { - return AlertDialog( + builder: (context) => StatefulBuilder( + builder: (context, setState) => AlertDialog( title: Text(label), content: SizedBox( width: MediaQuery.of(context).size.width * 0.65, child: ListView( physics: const AlwaysScrollableScrollPhysics(), shrinkWrap: true, - children: [ - if (isNullable) itemBuilder(null), - ...items.map( - (e) => itemBuilder(e), - ) - ], + children: items.map((item) { + bool isSelected = currentSelection.contains(item); + return itemBuilder( + item, + isSelected, + () { + setState(() { + if (allowMultiSelection) { + if (isSelected) { + if (!forceAtleastOne || currentSelection.length > 1) { + currentSelection.remove(item); + } + } else { + currentSelection.add(item); + } + } else { + currentSelection = {item}; + } + }); + onChanged?.call(currentSelection.toList()); + }, + ); + }).toList(), ), ), - ); - }, + ), + ), ); + return currentSelection.toList(); } diff --git a/lib/util/simple_duration_picker.dart b/lib/util/simple_duration_picker.dart index 8995e86..db86c0e 100644 --- a/lib/util/simple_duration_picker.dart +++ b/lib/util/simple_duration_picker.dart @@ -149,7 +149,7 @@ class SimpleDurationPicker extends ConsumerWidget { children: [ if (showNever) ...{ TextButton( - onPressed: () => onChanged(null), + onPressed: () => onChanged(Duration.zero), child: Text(context.localized.never), ), const Spacer(), diff --git a/lib/widgets/navigation_scaffold/components/navigation_body.dart b/lib/widgets/navigation_scaffold/components/navigation_body.dart index 4fd3d61..f581775 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_body.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_body.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/views_provider.dart'; +import 'package:fladder/routes/auto_router.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/util/adaptive_layout.dart'; @@ -50,6 +53,8 @@ class _NavigationBodyState extends ConsumerState { @override Widget build(BuildContext context) { final views = ref.watch(viewsProvider.select((value) => value.views)); + final hasOverlay = AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual || + homeRoutes.any((element) => element.name.contains(context.router.current.name)); ref.listen( clientSettingsProvider, (previous, next) { @@ -62,48 +67,66 @@ class _NavigationBodyState extends ConsumerState { ); return switch (AdaptiveLayout.layoutOf(context)) { - LayoutState.phone => MediaQuery.removePadding( + ViewSize.phone => MediaQuery.removePadding( context: widget.parentContext, child: widget.child, ), - LayoutState.tablet => Row( + ViewSize.tablet => Row( children: [ - navigationRail(context), + AnimatedFadeSize( + duration: const Duration(milliseconds: 250), + child: hasOverlay ? navigationRail(context) : const SizedBox(), + ), Flexible( - child: widget.child, + child: MediaQuery( + data: semiNestedPadding(context, hasOverlay), + child: widget.child, + ), ) ], ), - LayoutState.desktop => Row( + ViewSize.desktop => Row( children: [ AnimatedFadeSize( duration: const Duration(milliseconds: 125), - child: expandedSideBar - ? MediaQuery.removePadding( - context: widget.parentContext, - child: NestedNavigationDrawer( - isExpanded: expandedSideBar, - actionButton: actionButton(), - toggleExpanded: (value) { - setState(() { - expandedSideBar = value; - }); - }, - views: views, - destinations: widget.destinations, - currentLocation: widget.currentLocation, - ), - ) - : navigationRail(context), + child: hasOverlay + ? expandedSideBar + ? MediaQuery.removePadding( + context: widget.parentContext, + child: NestedNavigationDrawer( + isExpanded: expandedSideBar, + actionButton: actionButton(), + toggleExpanded: (value) { + setState(() { + expandedSideBar = value; + }); + }, + views: views, + destinations: widget.destinations, + currentLocation: widget.currentLocation, + ), + ) + : navigationRail(context) + : const SizedBox(), ), Flexible( - child: widget.child, + child: MediaQuery( + data: semiNestedPadding(context, hasOverlay), + child: widget.child, + ), ), ], ) }; } + MediaQueryData semiNestedPadding(BuildContext context, bool hasOverlay) { + final paddingOf = MediaQuery.paddingOf(context); + return MediaQuery.of(context).copyWith( + padding: paddingOf.copyWith(left: hasOverlay ? 0 : paddingOf.left), + ); + } + AdaptiveFab? actionButton() { return (widget.currentIndex >= 0 && widget.currentIndex < widget.destinations.length) ? widget.destinations[widget.currentIndex].floatingActionButton @@ -120,7 +143,8 @@ class _NavigationBodyState extends ConsumerState { style: Theme.of(context).textTheme.titleSmall, ), }, - if (AdaptiveLayout.of(context).platform == TargetPlatform.macOS) SizedBox(height: MediaQuery.of(context).padding.top), + if (AdaptiveLayout.of(context).platform == TargetPlatform.macOS) + SizedBox(height: MediaQuery.of(context).padding.top), Flexible( child: Padding( key: const Key('navigation_rail'), @@ -130,7 +154,7 @@ class _NavigationBodyState extends ConsumerState { children: [ IconButton( onPressed: () { - if (AdaptiveLayout.layoutOf(context) != LayoutState.desktop) { + if (AdaptiveLayout.layoutOf(context) != ViewSize.desktop) { widget.drawerKey.currentState?.openDrawer(); } else { setState(() { @@ -140,7 +164,7 @@ class _NavigationBodyState extends ConsumerState { }, icon: const Icon(IconsaxBold.menu), ), - if (AdaptiveLayout.of(context).size == ScreenLayout.dual) ...[ + if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual) ...[ const SizedBox(height: 8), AnimatedFadeSize( child: AnimatedSwitcher( diff --git a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart index 66b26fa..73cf9cb 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart @@ -6,6 +6,7 @@ import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/collection_types.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/models/view_model.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/metadata/refresh_metadata.dart'; @@ -116,11 +117,11 @@ class NestedNavigationDrawer extends ConsumerWidget { selected: currentLocation.contains(const SettingsRoute().routeName), icon: const SizedBox(width: 35, height: 35, child: SettingsUserIcon()), onPressed: () { - switch (AdaptiveLayout.of(context).size) { - case ScreenLayout.single: + switch (AdaptiveLayout.layoutModeOf(context)) { + case LayoutMode.single: const SettingsRoute().push(context); break; - case ScreenLayout.dual: + case LayoutMode.dual: context.router.push(const ClientSettingsRoute()); break; } @@ -135,11 +136,11 @@ class NestedNavigationDrawer extends ConsumerWidget { icon: const Icon(IconsaxOutline.setting_2), selected: currentLocation.contains(const SettingsRoute().routeName), onPressed: () { - switch (AdaptiveLayout.of(context).size) { - case ScreenLayout.single: + switch (AdaptiveLayout.layoutModeOf(context)) { + case LayoutMode.single: const SettingsRoute().push(context); break; - case ScreenLayout.dual: + case LayoutMode.dual: context.router.push(const ClientSettingsRoute()); break; } diff --git a/lib/widgets/navigation_scaffold/components/settings_user_icon.dart b/lib/widgets/navigation_scaffold/components/settings_user_icon.dart index f6b5931..0befea7 100644 --- a/lib/widgets/navigation_scaffold/components/settings_user_icon.dart +++ b/lib/widgets/navigation_scaffold/components/settings_user_icon.dart @@ -1,10 +1,14 @@ +import 'package:flutter/material.dart'; + import 'package:auto_route/auto_route.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/shared/user_icon.dart'; +import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; class SettingsUserIcon extends ConsumerWidget { const SettingsUserIcon({super.key}); @@ -19,7 +23,13 @@ class SettingsUserIcon extends ConsumerWidget { user: users, cornerRadius: 200, onLongPress: () => context.router.push(const LockRoute()), - onTap: () => context.router.navigate(const SettingsRoute()), + onTap: () { + if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) { + context.router.push(const SettingsRoute()); + } else { + context.router.push(const ClientSettingsRoute()); + } + }, ), ); } diff --git a/lib/widgets/navigation_scaffold/navigation_scaffold.dart b/lib/widgets/navigation_scaffold/navigation_scaffold.dart index 57c74cf..56b3c1b 100644 --- a/lib/widgets/navigation_scaffold/navigation_scaffold.dart +++ b/lib/widgets/navigation_scaffold/navigation_scaffold.dart @@ -3,8 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/media_playback_model.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/providers/views_provider.dart'; +import 'package:fladder/routes/auto_router.dart'; import 'package:fladder/screens/shared/nested_bottom_appbar.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; @@ -65,7 +67,7 @@ class _NavigationScaffoldState extends ConsumerState { extendBody: true, floatingActionButtonLocation: playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null, - floatingActionButton: AdaptiveLayout.of(context).size == ScreenLayout.single + floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single ? switch (playerState) { VideoPlayerState.minimized => const Padding( padding: EdgeInsets.symmetric(horizontal: 8), @@ -83,9 +85,10 @@ class _NavigationScaffoldState extends ConsumerState { destinations: widget.destinations, currentLocation: currentLocation, ), - bottomNavigationBar: AdaptiveLayout.of(context).layout == LayoutState.phone + bottomNavigationBar: AdaptiveLayout.viewSizeOf(context) == ViewSize.phone ? HideOnScroll( controller: AdaptiveLayout.scrollOf(context), + forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)), child: NestedBottomAppBar( child: Transform.translate( offset: const Offset(0, 8), diff --git a/lib/widgets/shared/hide_on_scroll.dart b/lib/widgets/shared/hide_on_scroll.dart index 5c8469d..e543aa5 100644 --- a/lib/widgets/shared/hide_on_scroll.dart +++ b/lib/widgets/shared/hide_on_scroll.dart @@ -1,20 +1,25 @@ -import 'package:fladder/util/adaptive_layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; +import 'package:fladder/util/adaptive_layout.dart'; + class HideOnScroll extends ConsumerStatefulWidget { final Widget? child; final ScrollController? controller; final double height; final Widget? Function(bool visible)? visibleBuilder; final Duration duration; + final bool forceHide; const HideOnScroll({ this.child, this.controller, this.height = kBottomNavigationBarHeight, this.visibleBuilder, this.duration = const Duration(milliseconds: 200), + this.forceHide = false, super.key, }) : assert(child != null || visibleBuilder != null); @@ -63,12 +68,16 @@ class _HideOnScrollState extends ConsumerState { Widget build(BuildContext context) { if (widget.visibleBuilder != null) return widget.visibleBuilder!(isVisible)!; if (widget.child == null) return const SizedBox(); - if (AdaptiveLayout.of(context).layout == LayoutState.desktop) { + if (AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop) { return widget.child!; } else { return AnimatedAlign( alignment: const Alignment(0, -1), - heightFactor: isVisible ? 1.0 : 0, + heightFactor: widget.forceHide + ? 0 + : isVisible + ? 1.0 + : 0, duration: widget.duration, child: Wrap(children: [widget.child!]), ); diff --git a/lib/widgets/shared/modal_bottom_sheet.dart b/lib/widgets/shared/modal_bottom_sheet.dart index 5403d87..a855643 100644 --- a/lib/widgets/shared/modal_bottom_sheet.dart +++ b/lib/widgets/shared/modal_bottom_sheet.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; @@ -23,7 +24,7 @@ Future showBottomSheetPill({ showDragHandle: true, enableDrag: true, context: context, - constraints: AdaptiveLayout.of(context).layout == LayoutState.phone + constraints: AdaptiveLayout.viewSizeOf(context) == ViewSize.phone ? BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.9) : BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.75, maxHeight: MediaQuery.of(context).size.height * 0.85),