feature: Rework responsive layout (#217)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-02-07 15:55:01 +01:00 committed by GitHub
parent e07f280124
commit 8012fdcea8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1468 additions and 1040 deletions

View file

@ -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"
} }

View file

@ -18,6 +18,7 @@ import 'package:universal_html/html.dart' as html;
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:fladder/models/account_model.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/models/syncing/i_synced_item.dart';
import 'package:fladder/providers/crash_log_provider.dart'; import 'package:fladder/providers/crash_log_provider.dart';
import 'package:fladder/providers/settings/client_settings_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/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/routes/auto_router.dart';
import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/login/lock_screen.dart'; import 'package:fladder/screens/login/lock_screen.dart';
import 'package:fladder/theme.dart'; import 'package:fladder/theme.dart';
@ -100,11 +102,11 @@ void main() async {
)) ))
], ],
child: AdaptiveLayoutBuilder( child: AdaptiveLayoutBuilder(
fallBack: LayoutState.tablet, fallBack: ViewSize.tablet,
layoutPoints: [ layoutPoints: [
LayoutPoints(start: 0, end: 599, type: LayoutState.phone), LayoutPoints(start: 0, end: 599, type: ViewSize.phone),
LayoutPoints(start: 600, end: 1919, type: LayoutState.tablet), LayoutPoints(start: 600, end: 1919, type: ViewSize.tablet),
LayoutPoints(start: 1920, end: 3180, type: LayoutState.desktop), LayoutPoints(start: 1920, end: 3180, type: ViewSize.desktop),
], ],
child: const Main(), child: const Main(),
), ),
@ -122,6 +124,7 @@ class Main extends ConsumerStatefulWidget with WindowListener {
class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBindingObserver { class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBindingObserver {
DateTime dateTime = DateTime.now(); DateTime dateTime = DateTime.now();
bool hidden = false; bool hidden = false;
late final autoRouter = AutoRouter(ref: ref);
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) async { void didChangeAppLifecycleState(AppLifecycleState state) async {
@ -159,7 +162,7 @@ class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBinding
await ref.read(videoPlayerProvider).pause(); await ref.read(videoPlayerProvider).pause();
if (context.mounted) { if (context.mounted) {
AdaptiveLayout.of(context).router.push(const LockRoute()); autoRouter.push(const LockRoute());
} }
} }
} }
@ -298,7 +301,8 @@ class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBinding
), ),
), ),
themeMode: themeMode, themeMode: themeMode,
routerConfig: AdaptiveLayout.routerOf(context).config(), routerConfig: autoRouter.config(),
// routerConfig: AdaptiveLayout.routerOf(context).config(),
), ),
); );
}), }),

View file

@ -10,6 +10,8 @@ part 'home_settings_model.g.dart';
@freezed @freezed
class HomeSettingsModel with _$HomeSettingsModel { class HomeSettingsModel with _$HomeSettingsModel {
factory HomeSettingsModel({ factory HomeSettingsModel({
@Default({...LayoutMode.values}) Set<LayoutMode> screenLayouts,
@Default({...ViewSize.values}) Set<ViewSize> layoutStates,
@Default(HomeBanner.carousel) HomeBanner homeBanner, @Default(HomeBanner.carousel) HomeBanner homeBanner,
@Default(HomeCarouselSettings.combined) HomeCarouselSettings carouselSettings, @Default(HomeCarouselSettings.combined) HomeCarouselSettings carouselSettings,
@Default(HomeNextUp.separate) HomeNextUp nextUp, @Default(HomeNextUp.separate) HomeNextUp nextUp,
@ -18,6 +20,48 @@ class HomeSettingsModel with _$HomeSettingsModel {
factory HomeSettingsModel.fromJson(Map<String, dynamic> json) => _$HomeSettingsModelFromJson(json); factory HomeSettingsModel.fromJson(Map<String, dynamic> json) => _$HomeSettingsModelFromJson(json);
} }
T selectAvailableOrSmaller<T>(T value, Set<T> availableOptions, List<T> 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 { enum HomeBanner {
hide, hide,
carousel, carousel,

View file

@ -20,6 +20,8 @@ HomeSettingsModel _$HomeSettingsModelFromJson(Map<String, dynamic> json) {
/// @nodoc /// @nodoc
mixin _$HomeSettingsModel { mixin _$HomeSettingsModel {
Set<LayoutMode> get screenLayouts => throw _privateConstructorUsedError;
Set<ViewSize> get layoutStates => throw _privateConstructorUsedError;
HomeBanner get homeBanner => throw _privateConstructorUsedError; HomeBanner get homeBanner => throw _privateConstructorUsedError;
HomeCarouselSettings get carouselSettings => HomeCarouselSettings get carouselSettings =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -42,7 +44,9 @@ abstract class $HomeSettingsModelCopyWith<$Res> {
_$HomeSettingsModelCopyWithImpl<$Res, HomeSettingsModel>; _$HomeSettingsModelCopyWithImpl<$Res, HomeSettingsModel>;
@useResult @useResult
$Res call( $Res call(
{HomeBanner homeBanner, {Set<LayoutMode> screenLayouts,
Set<ViewSize> layoutStates,
HomeBanner homeBanner,
HomeCarouselSettings carouselSettings, HomeCarouselSettings carouselSettings,
HomeNextUp nextUp}); HomeNextUp nextUp});
} }
@ -62,11 +66,21 @@ class _$HomeSettingsModelCopyWithImpl<$Res, $Val extends HomeSettingsModel>
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? screenLayouts = null,
Object? layoutStates = null,
Object? homeBanner = null, Object? homeBanner = null,
Object? carouselSettings = null, Object? carouselSettings = null,
Object? nextUp = null, Object? nextUp = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
screenLayouts: null == screenLayouts
? _value.screenLayouts
: screenLayouts // ignore: cast_nullable_to_non_nullable
as Set<LayoutMode>,
layoutStates: null == layoutStates
? _value.layoutStates
: layoutStates // ignore: cast_nullable_to_non_nullable
as Set<ViewSize>,
homeBanner: null == homeBanner homeBanner: null == homeBanner
? _value.homeBanner ? _value.homeBanner
: homeBanner // ignore: cast_nullable_to_non_nullable : homeBanner // ignore: cast_nullable_to_non_nullable
@ -92,7 +106,9 @@ abstract class _$$HomeSettingsModelImplCopyWith<$Res>
@override @override
@useResult @useResult
$Res call( $Res call(
{HomeBanner homeBanner, {Set<LayoutMode> screenLayouts,
Set<ViewSize> layoutStates,
HomeBanner homeBanner,
HomeCarouselSettings carouselSettings, HomeCarouselSettings carouselSettings,
HomeNextUp nextUp}); HomeNextUp nextUp});
} }
@ -110,11 +126,21 @@ class __$$HomeSettingsModelImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? screenLayouts = null,
Object? layoutStates = null,
Object? homeBanner = null, Object? homeBanner = null,
Object? carouselSettings = null, Object? carouselSettings = null,
Object? nextUp = null, Object? nextUp = null,
}) { }) {
return _then(_$HomeSettingsModelImpl( return _then(_$HomeSettingsModelImpl(
screenLayouts: null == screenLayouts
? _value._screenLayouts
: screenLayouts // ignore: cast_nullable_to_non_nullable
as Set<LayoutMode>,
layoutStates: null == layoutStates
? _value._layoutStates
: layoutStates // ignore: cast_nullable_to_non_nullable
as Set<ViewSize>,
homeBanner: null == homeBanner homeBanner: null == homeBanner
? _value.homeBanner ? _value.homeBanner
: homeBanner // ignore: cast_nullable_to_non_nullable : homeBanner // ignore: cast_nullable_to_non_nullable
@ -135,13 +161,35 @@ class __$$HomeSettingsModelImplCopyWithImpl<$Res>
@JsonSerializable() @JsonSerializable()
class _$HomeSettingsModelImpl implements _HomeSettingsModel { class _$HomeSettingsModelImpl implements _HomeSettingsModel {
_$HomeSettingsModelImpl( _$HomeSettingsModelImpl(
{this.homeBanner = HomeBanner.carousel, {final Set<LayoutMode> screenLayouts = const {...LayoutMode.values},
final Set<ViewSize> layoutStates = const {...ViewSize.values},
this.homeBanner = HomeBanner.carousel,
this.carouselSettings = HomeCarouselSettings.combined, this.carouselSettings = HomeCarouselSettings.combined,
this.nextUp = HomeNextUp.separate}); this.nextUp = HomeNextUp.separate})
: _screenLayouts = screenLayouts,
_layoutStates = layoutStates;
factory _$HomeSettingsModelImpl.fromJson(Map<String, dynamic> json) => factory _$HomeSettingsModelImpl.fromJson(Map<String, dynamic> json) =>
_$$HomeSettingsModelImplFromJson(json); _$$HomeSettingsModelImplFromJson(json);
final Set<LayoutMode> _screenLayouts;
@override
@JsonKey()
Set<LayoutMode> get screenLayouts {
if (_screenLayouts is EqualUnmodifiableSetView) return _screenLayouts;
// ignore: implicit_dynamic_type
return EqualUnmodifiableSetView(_screenLayouts);
}
final Set<ViewSize> _layoutStates;
@override
@JsonKey()
Set<ViewSize> get layoutStates {
if (_layoutStates is EqualUnmodifiableSetView) return _layoutStates;
// ignore: implicit_dynamic_type
return EqualUnmodifiableSetView(_layoutStates);
}
@override @override
@JsonKey() @JsonKey()
final HomeBanner homeBanner; final HomeBanner homeBanner;
@ -154,7 +202,7 @@ class _$HomeSettingsModelImpl implements _HomeSettingsModel {
@override @override
String toString() { String toString() {
return 'HomeSettingsModel(homeBanner: $homeBanner, carouselSettings: $carouselSettings, nextUp: $nextUp)'; return 'HomeSettingsModel(screenLayouts: $screenLayouts, layoutStates: $layoutStates, homeBanner: $homeBanner, carouselSettings: $carouselSettings, nextUp: $nextUp)';
} }
@override @override
@ -162,6 +210,10 @@ class _$HomeSettingsModelImpl implements _HomeSettingsModel {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$HomeSettingsModelImpl && other is _$HomeSettingsModelImpl &&
const DeepCollectionEquality()
.equals(other._screenLayouts, _screenLayouts) &&
const DeepCollectionEquality()
.equals(other._layoutStates, _layoutStates) &&
(identical(other.homeBanner, homeBanner) || (identical(other.homeBanner, homeBanner) ||
other.homeBanner == homeBanner) && other.homeBanner == homeBanner) &&
(identical(other.carouselSettings, carouselSettings) || (identical(other.carouselSettings, carouselSettings) ||
@ -171,8 +223,13 @@ class _$HomeSettingsModelImpl implements _HomeSettingsModel {
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => int get hashCode => Object.hash(
Object.hash(runtimeType, homeBanner, carouselSettings, nextUp); runtimeType,
const DeepCollectionEquality().hash(_screenLayouts),
const DeepCollectionEquality().hash(_layoutStates),
homeBanner,
carouselSettings,
nextUp);
/// Create a copy of HomeSettingsModel /// Create a copy of HomeSettingsModel
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -193,13 +250,19 @@ class _$HomeSettingsModelImpl implements _HomeSettingsModel {
abstract class _HomeSettingsModel implements HomeSettingsModel { abstract class _HomeSettingsModel implements HomeSettingsModel {
factory _HomeSettingsModel( factory _HomeSettingsModel(
{final HomeBanner homeBanner, {final Set<LayoutMode> screenLayouts,
final Set<ViewSize> layoutStates,
final HomeBanner homeBanner,
final HomeCarouselSettings carouselSettings, final HomeCarouselSettings carouselSettings,
final HomeNextUp nextUp}) = _$HomeSettingsModelImpl; final HomeNextUp nextUp}) = _$HomeSettingsModelImpl;
factory _HomeSettingsModel.fromJson(Map<String, dynamic> json) = factory _HomeSettingsModel.fromJson(Map<String, dynamic> json) =
_$HomeSettingsModelImpl.fromJson; _$HomeSettingsModelImpl.fromJson;
@override
Set<LayoutMode> get screenLayouts;
@override
Set<ViewSize> get layoutStates;
@override @override
HomeBanner get homeBanner; HomeBanner get homeBanner;
@override @override

View file

@ -9,6 +9,14 @@ part of 'home_settings_model.dart';
_$HomeSettingsModelImpl _$$HomeSettingsModelImplFromJson( _$HomeSettingsModelImpl _$$HomeSettingsModelImplFromJson(
Map<String, dynamic> json) => Map<String, dynamic> json) =>
_$HomeSettingsModelImpl( _$HomeSettingsModelImpl(
screenLayouts: (json['screenLayouts'] as List<dynamic>?)
?.map((e) => $enumDecode(_$LayoutModeEnumMap, e))
.toSet() ??
const {...LayoutMode.values},
layoutStates: (json['layoutStates'] as List<dynamic>?)
?.map((e) => $enumDecode(_$ViewSizeEnumMap, e))
.toSet() ??
const {...ViewSize.values},
homeBanner: homeBanner:
$enumDecodeNullable(_$HomeBannerEnumMap, json['homeBanner']) ?? $enumDecodeNullable(_$HomeBannerEnumMap, json['homeBanner']) ??
HomeBanner.carousel, HomeBanner.carousel,
@ -22,12 +30,27 @@ _$HomeSettingsModelImpl _$$HomeSettingsModelImplFromJson(
Map<String, dynamic> _$$HomeSettingsModelImplToJson( Map<String, dynamic> _$$HomeSettingsModelImplToJson(
_$HomeSettingsModelImpl instance) => _$HomeSettingsModelImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'screenLayouts':
instance.screenLayouts.map((e) => _$LayoutModeEnumMap[e]!).toList(),
'layoutStates':
instance.layoutStates.map((e) => _$ViewSizeEnumMap[e]!).toList(),
'homeBanner': _$HomeBannerEnumMap[instance.homeBanner]!, 'homeBanner': _$HomeBannerEnumMap[instance.homeBanner]!,
'carouselSettings': 'carouselSettings':
_$HomeCarouselSettingsEnumMap[instance.carouselSettings]!, _$HomeCarouselSettingsEnumMap[instance.carouselSettings]!,
'nextUp': _$HomeNextUpEnumMap[instance.nextUp]!, '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 = { const _$HomeBannerEnumMap = {
HomeBanner.hide: 'hide', HomeBanner.hide: 'hide',
HomeBanner.carousel: 'carousel', HomeBanner.carousel: 'carousel',

View file

@ -6,7 +6,7 @@ part of 'api_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$jellyApiHash() => r'c0cdc4127e7191523b1356e71c54c93f99020c1e'; String _$jellyApiHash() => r'9bc824d28d17f88f40c768cefb637144e0fbf346';
/// See also [JellyApi]. /// See also [JellyApi].
@ProviderFor(JellyApi) @ProviderFor(JellyApi)

View file

@ -19,4 +19,8 @@ class HomeSettingsNotifier extends StateNotifier<HomeSettingsModel> {
} }
HomeSettingsModel update(HomeSettingsModel Function(HomeSettingsModel currentState) value) => state = value(state); HomeSettingsModel update(HomeSettingsModel Function(HomeSettingsModel currentState) value) => state = value(state);
void setLayoutModes(Set<LayoutMode> set) => state = state.copyWith(screenLayouts: set);
void setViewSize(Set<ViewSize> set) => state = state.copyWith(layoutStates: set);
} }

View file

@ -7,7 +7,7 @@ part of 'background_download_provider.dart';
// ************************************************************************** // **************************************************************************
String _$backgroundDownloaderHash() => String _$backgroundDownloaderHash() =>
r'9a9f91504ae4532ab37290ee9372d2e7a18380a9'; r'997d9f4ba79dd0d9d30d5f283b36d5280d10dfaa';
/// See also [backgroundDownloader]. /// See also [backgroundDownloader].
@ProviderFor(backgroundDownloader) @ProviderFor(backgroundDownloader)

View file

@ -7,7 +7,7 @@ part of 'user_provider.dart';
// ************************************************************************** // **************************************************************************
String _$showSyncButtonProviderHash() => String _$showSyncButtonProviderHash() =>
r'3468d7309f3859f7b60b1bd317e306e1f5f00555'; r'c09f42cd6536425bf9417da41c83e15c135d0edb';
/// See also [showSyncButtonProvider]. /// See also [showSyncButtonProvider].
@ProviderFor(showSyncButtonProvider) @ProviderFor(showSyncButtonProvider)
@ -24,7 +24,7 @@ final showSyncButtonProviderProvider = AutoDisposeProvider<bool>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef<bool>; typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef<bool>;
String _$userHash() => r'e83369c0d569d5a862aa1b92f3f0a45a9d1fe446'; String _$userHash() => r'1ab1579051806f114e3f42873a2e100c14115900';
/// See also [User]. /// See also [User].
@ProviderFor(User) @ProviderFor(User)

View file

@ -6,17 +6,16 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/login/lock_screen.dart'; import 'package:fladder/screens/login/lock_screen.dart';
import 'package:fladder/util/adaptive_layout.dart';
const settingsPageRoute = "settings";
@AutoRouterConfig(replaceInRouteName: 'Screen|Page,Route') @AutoRouterConfig(replaceInRouteName: 'Screen|Page,Route')
class AutoRouter extends RootStackRouter { class AutoRouter extends RootStackRouter {
AutoRouter({ AutoRouter({
required this.layout,
required this.ref, required this.ref,
}); });
final WidgetRef ref; final WidgetRef ref;
final ScreenLayout layout;
@override @override
List<AutoRouteGuard> get guards => [...super.guards, AuthGuard(ref: ref)]; List<AutoRouteGuard> get guards => [...super.guards, AuthGuard(ref: ref)];
@ -27,44 +26,33 @@ class AutoRouter extends RootStackRouter {
@override @override
List<AutoRoute> get routes => [ List<AutoRoute> get routes => [
..._defaultRoutes, ..._defaultRoutes,
...(layout == ScreenLayout.dual ? desktopRoutes : mobileRoutes), ...otherRoutes,
]; ];
final List<AutoRoute> mobileRoutes = [ final List<AutoRoute> otherRoutes = [
_homeRoute.copyWith( _homeRoute.copyWith(
children: [ children: [
_dashboardRoute, ...homeRoutes,
_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<AutoRoute> desktopRoutes = [
_homeRoute.copyWith(
children: [
_dashboardRoute,
_favouritesRoute,
_syncedRoute,
AutoRoute(page: DetailsRoute.page, path: 'details', usesPathAsKey: true), AutoRoute(page: DetailsRoute.page, path: 'details', usesPathAsKey: true),
AutoRoute(page: LibrarySearchRoute.page, path: 'library', usesPathAsKey: true), AutoRoute(page: LibrarySearchRoute.page, path: 'library', usesPathAsKey: true),
AutoRoute( CustomRoute(
page: SettingsRoute.page, page: SettingsRoute.page,
path: 'settings', path: settingsPageRoute,
children: _settingsChildren, children: _settingsChildren,
) transitionsBuilder: TransitionsBuilders.fadeIn,
),
], ],
), ),
AutoRoute(page: LockRoute.page, path: '/locked'), AutoRoute(page: LockRoute.page, path: '/locked'),
]; ];
} }
final List<AutoRoute> homeRoutes = [
_dashboardRoute,
_favouritesRoute,
_syncedRoute,
];
final List<AutoRoute> _defaultRoutes = [ final List<AutoRoute> _defaultRoutes = [
AutoRoute(page: SplashRoute.page, path: '/splash'), AutoRoute(page: SplashRoute.page, path: '/splash'),
AutoRoute(page: LoginRoute.page, path: '/login'), AutoRoute(page: LoginRoute.page, path: '/login'),
@ -75,25 +63,25 @@ final AutoRoute _dashboardRoute = CustomRoute(
page: DashboardRoute.page, page: DashboardRoute.page,
transitionsBuilder: TransitionsBuilders.fadeIn, transitionsBuilder: TransitionsBuilders.fadeIn,
initial: true, initial: true,
path: 'dashboard',
maintainState: false, maintainState: false,
path: 'dashboard',
); );
final AutoRoute _favouritesRoute = CustomRoute( final AutoRoute _favouritesRoute = CustomRoute(
page: FavouritesRoute.page, page: FavouritesRoute.page,
transitionsBuilder: TransitionsBuilders.fadeIn, transitionsBuilder: TransitionsBuilders.fadeIn,
path: 'favourites',
maintainState: false, maintainState: false,
path: 'favourites',
); );
final AutoRoute _syncedRoute = CustomRoute( final AutoRoute _syncedRoute = CustomRoute(
page: SyncedRoute.page, page: SyncedRoute.page,
transitionsBuilder: TransitionsBuilders.fadeIn, transitionsBuilder: TransitionsBuilders.fadeIn,
path: 'synced',
maintainState: false, maintainState: false,
path: 'synced',
); );
final List<AutoRoute> _settingsChildren = [ final List<AutoRoute> _settingsChildren = [
CustomRoute( CustomRoute(page: SettingsSelectionRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'list'),
page: ClientSettingsRoute.page, initial: true, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'client'), CustomRoute(page: ClientSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'client'),
CustomRoute(page: SecuritySettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'security'), CustomRoute(page: SecuritySettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'security'),
CustomRoute(page: PlayerSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'player'), CustomRoute(page: PlayerSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'player'),
CustomRoute(page: AboutSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'about'), CustomRoute(page: AboutSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'about'),

View file

@ -8,11 +8,11 @@
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:auto_route/auto_route.dart' as _i15; import 'package:auto_route/auto_route.dart' as _i16;
import 'package:fladder/models/item_base_model.dart' as _i16; import 'package:fladder/models/item_base_model.dart' as _i17;
import 'package:fladder/models/items/photos_model.dart' as _i19; import 'package:fladder/models/items/photos_model.dart' as _i20;
import 'package:fladder/models/library_search/library_search_options.dart' 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/routes/nested_details_screen.dart' as _i4;
import 'package:fladder/screens/dashboard/dashboard_screen.dart' as _i3; import 'package:fladder/screens/dashboard/dashboard_screen.dart' as _i3;
import 'package:fladder/screens/favourites/favourites_screen.dart' as _i5; 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/player_settings_page.dart' as _i10;
import 'package:fladder/screens/settings/security_settings_page.dart' as _i11; 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/settings/settings_screen.dart' as _i12;
import 'package:fladder/screens/splash_screen.dart' as _i13; import 'package:fladder/screens/settings/settings_selection_screen.dart'
import 'package:fladder/screens/syncing/synced_screen.dart' as _i14; as _i13;
import 'package:flutter/foundation.dart' as _i17; import 'package:fladder/screens/splash_screen.dart' as _i14;
import 'package:flutter/material.dart' as _i20; 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 /// generated route for
/// [_i1.AboutSettingsPage] /// [_i1.AboutSettingsPage]
class AboutSettingsRoute extends _i15.PageRouteInfo<void> { class AboutSettingsRoute extends _i16.PageRouteInfo<void> {
const AboutSettingsRoute({List<_i15.PageRouteInfo>? children}) const AboutSettingsRoute({List<_i16.PageRouteInfo>? children})
: super( : super(
AboutSettingsRoute.name, AboutSettingsRoute.name,
initialChildren: children, initialChildren: children,
@ -42,7 +44,7 @@ class AboutSettingsRoute extends _i15.PageRouteInfo<void> {
static const String name = 'AboutSettingsRoute'; static const String name = 'AboutSettingsRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i1.AboutSettingsPage(); return const _i1.AboutSettingsPage();
@ -52,8 +54,8 @@ class AboutSettingsRoute extends _i15.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i2.ClientSettingsPage] /// [_i2.ClientSettingsPage]
class ClientSettingsRoute extends _i15.PageRouteInfo<void> { class ClientSettingsRoute extends _i16.PageRouteInfo<void> {
const ClientSettingsRoute({List<_i15.PageRouteInfo>? children}) const ClientSettingsRoute({List<_i16.PageRouteInfo>? children})
: super( : super(
ClientSettingsRoute.name, ClientSettingsRoute.name,
initialChildren: children, initialChildren: children,
@ -61,7 +63,7 @@ class ClientSettingsRoute extends _i15.PageRouteInfo<void> {
static const String name = 'ClientSettingsRoute'; static const String name = 'ClientSettingsRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i2.ClientSettingsPage(); return const _i2.ClientSettingsPage();
@ -71,8 +73,8 @@ class ClientSettingsRoute extends _i15.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i3.DashboardScreen] /// [_i3.DashboardScreen]
class DashboardRoute extends _i15.PageRouteInfo<void> { class DashboardRoute extends _i16.PageRouteInfo<void> {
const DashboardRoute({List<_i15.PageRouteInfo>? children}) const DashboardRoute({List<_i16.PageRouteInfo>? children})
: super( : super(
DashboardRoute.name, DashboardRoute.name,
initialChildren: children, initialChildren: children,
@ -80,7 +82,7 @@ class DashboardRoute extends _i15.PageRouteInfo<void> {
static const String name = 'DashboardRoute'; static const String name = 'DashboardRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i3.DashboardScreen(); return const _i3.DashboardScreen();
@ -90,12 +92,12 @@ class DashboardRoute extends _i15.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i4.DetailsScreen] /// [_i4.DetailsScreen]
class DetailsRoute extends _i15.PageRouteInfo<DetailsRouteArgs> { class DetailsRoute extends _i16.PageRouteInfo<DetailsRouteArgs> {
DetailsRoute({ DetailsRoute({
String id = '', String id = '',
_i16.ItemBaseModel? item, _i17.ItemBaseModel? item,
_i17.Key? key, _i18.Key? key,
List<_i15.PageRouteInfo>? children, List<_i16.PageRouteInfo>? children,
}) : super( }) : super(
DetailsRoute.name, DetailsRoute.name,
args: DetailsRouteArgs( args: DetailsRouteArgs(
@ -109,7 +111,7 @@ class DetailsRoute extends _i15.PageRouteInfo<DetailsRouteArgs> {
static const String name = 'DetailsRoute'; static const String name = 'DetailsRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
final queryParams = data.queryParams; final queryParams = data.queryParams;
@ -137,9 +139,9 @@ class DetailsRouteArgs {
final String id; final String id;
final _i16.ItemBaseModel? item; final _i17.ItemBaseModel? item;
final _i17.Key? key; final _i18.Key? key;
@override @override
String toString() { String toString() {
@ -149,8 +151,8 @@ class DetailsRouteArgs {
/// generated route for /// generated route for
/// [_i5.FavouritesScreen] /// [_i5.FavouritesScreen]
class FavouritesRoute extends _i15.PageRouteInfo<void> { class FavouritesRoute extends _i16.PageRouteInfo<void> {
const FavouritesRoute({List<_i15.PageRouteInfo>? children}) const FavouritesRoute({List<_i16.PageRouteInfo>? children})
: super( : super(
FavouritesRoute.name, FavouritesRoute.name,
initialChildren: children, initialChildren: children,
@ -158,7 +160,7 @@ class FavouritesRoute extends _i15.PageRouteInfo<void> {
static const String name = 'FavouritesRoute'; static const String name = 'FavouritesRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i5.FavouritesScreen(); return const _i5.FavouritesScreen();
@ -168,8 +170,8 @@ class FavouritesRoute extends _i15.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i6.HomeScreen] /// [_i6.HomeScreen]
class HomeRoute extends _i15.PageRouteInfo<void> { class HomeRoute extends _i16.PageRouteInfo<void> {
const HomeRoute({List<_i15.PageRouteInfo>? children}) const HomeRoute({List<_i16.PageRouteInfo>? children})
: super( : super(
HomeRoute.name, HomeRoute.name,
initialChildren: children, initialChildren: children,
@ -177,7 +179,7 @@ class HomeRoute extends _i15.PageRouteInfo<void> {
static const String name = 'HomeRoute'; static const String name = 'HomeRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i6.HomeScreen(); return const _i6.HomeScreen();
@ -187,16 +189,16 @@ class HomeRoute extends _i15.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i7.LibrarySearchScreen] /// [_i7.LibrarySearchScreen]
class LibrarySearchRoute extends _i15.PageRouteInfo<LibrarySearchRouteArgs> { class LibrarySearchRoute extends _i16.PageRouteInfo<LibrarySearchRouteArgs> {
LibrarySearchRoute({ LibrarySearchRoute({
String? viewModelId, String? viewModelId,
List<String>? folderId, List<String>? folderId,
bool? favourites, bool? favourites,
_i18.SortingOrder? sortOrder, _i19.SortingOrder? sortOrder,
_i18.SortingOptions? sortingOptions, _i19.SortingOptions? sortingOptions,
_i19.PhotoModel? photoToView, _i20.PhotoModel? photoToView,
_i17.Key? key, _i18.Key? key,
List<_i15.PageRouteInfo>? children, List<_i16.PageRouteInfo>? children,
}) : super( }) : super(
LibrarySearchRoute.name, LibrarySearchRoute.name,
args: LibrarySearchRouteArgs( args: LibrarySearchRouteArgs(
@ -220,7 +222,7 @@ class LibrarySearchRoute extends _i15.PageRouteInfo<LibrarySearchRouteArgs> {
static const String name = 'LibrarySearchRoute'; static const String name = 'LibrarySearchRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
final queryParams = data.queryParams; final queryParams = data.queryParams;
@ -262,13 +264,13 @@ class LibrarySearchRouteArgs {
final bool? favourites; 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 @override
String toString() { String toString() {
@ -278,8 +280,8 @@ class LibrarySearchRouteArgs {
/// generated route for /// generated route for
/// [_i8.LockScreen] /// [_i8.LockScreen]
class LockRoute extends _i15.PageRouteInfo<void> { class LockRoute extends _i16.PageRouteInfo<void> {
const LockRoute({List<_i15.PageRouteInfo>? children}) const LockRoute({List<_i16.PageRouteInfo>? children})
: super( : super(
LockRoute.name, LockRoute.name,
initialChildren: children, initialChildren: children,
@ -287,7 +289,7 @@ class LockRoute extends _i15.PageRouteInfo<void> {
static const String name = 'LockRoute'; static const String name = 'LockRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i8.LockScreen(); return const _i8.LockScreen();
@ -297,8 +299,8 @@ class LockRoute extends _i15.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i9.LoginScreen] /// [_i9.LoginScreen]
class LoginRoute extends _i15.PageRouteInfo<void> { class LoginRoute extends _i16.PageRouteInfo<void> {
const LoginRoute({List<_i15.PageRouteInfo>? children}) const LoginRoute({List<_i16.PageRouteInfo>? children})
: super( : super(
LoginRoute.name, LoginRoute.name,
initialChildren: children, initialChildren: children,
@ -306,7 +308,7 @@ class LoginRoute extends _i15.PageRouteInfo<void> {
static const String name = 'LoginRoute'; static const String name = 'LoginRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i9.LoginScreen(); return const _i9.LoginScreen();
@ -316,8 +318,8 @@ class LoginRoute extends _i15.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i10.PlayerSettingsPage] /// [_i10.PlayerSettingsPage]
class PlayerSettingsRoute extends _i15.PageRouteInfo<void> { class PlayerSettingsRoute extends _i16.PageRouteInfo<void> {
const PlayerSettingsRoute({List<_i15.PageRouteInfo>? children}) const PlayerSettingsRoute({List<_i16.PageRouteInfo>? children})
: super( : super(
PlayerSettingsRoute.name, PlayerSettingsRoute.name,
initialChildren: children, initialChildren: children,
@ -325,7 +327,7 @@ class PlayerSettingsRoute extends _i15.PageRouteInfo<void> {
static const String name = 'PlayerSettingsRoute'; static const String name = 'PlayerSettingsRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i10.PlayerSettingsPage(); return const _i10.PlayerSettingsPage();
@ -335,8 +337,8 @@ class PlayerSettingsRoute extends _i15.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i11.SecuritySettingsPage] /// [_i11.SecuritySettingsPage]
class SecuritySettingsRoute extends _i15.PageRouteInfo<void> { class SecuritySettingsRoute extends _i16.PageRouteInfo<void> {
const SecuritySettingsRoute({List<_i15.PageRouteInfo>? children}) const SecuritySettingsRoute({List<_i16.PageRouteInfo>? children})
: super( : super(
SecuritySettingsRoute.name, SecuritySettingsRoute.name,
initialChildren: children, initialChildren: children,
@ -344,7 +346,7 @@ class SecuritySettingsRoute extends _i15.PageRouteInfo<void> {
static const String name = 'SecuritySettingsRoute'; static const String name = 'SecuritySettingsRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i11.SecuritySettingsPage(); return const _i11.SecuritySettingsPage();
@ -354,8 +356,8 @@ class SecuritySettingsRoute extends _i15.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i12.SettingsScreen] /// [_i12.SettingsScreen]
class SettingsRoute extends _i15.PageRouteInfo<void> { class SettingsRoute extends _i16.PageRouteInfo<void> {
const SettingsRoute({List<_i15.PageRouteInfo>? children}) const SettingsRoute({List<_i16.PageRouteInfo>? children})
: super( : super(
SettingsRoute.name, SettingsRoute.name,
initialChildren: children, initialChildren: children,
@ -363,7 +365,7 @@ class SettingsRoute extends _i15.PageRouteInfo<void> {
static const String name = 'SettingsRoute'; static const String name = 'SettingsRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i12.SettingsScreen(); return const _i12.SettingsScreen();
@ -372,12 +374,31 @@ class SettingsRoute extends _i15.PageRouteInfo<void> {
} }
/// generated route for /// generated route for
/// [_i13.SplashScreen] /// [_i13.SettingsSelectionScreen]
class SplashRoute extends _i15.PageRouteInfo<SplashRouteArgs> { class SettingsSelectionRoute extends _i16.PageRouteInfo<void> {
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<SplashRouteArgs> {
SplashRoute({ SplashRoute({
dynamic Function(bool)? loggedIn, dynamic Function(bool)? loggedIn,
_i20.Key? key, _i21.Key? key,
List<_i15.PageRouteInfo>? children, List<_i16.PageRouteInfo>? children,
}) : super( }) : super(
SplashRoute.name, SplashRoute.name,
args: SplashRouteArgs( args: SplashRouteArgs(
@ -389,12 +410,12 @@ class SplashRoute extends _i15.PageRouteInfo<SplashRouteArgs> {
static const String name = 'SplashRoute'; static const String name = 'SplashRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
final args = final args =
data.argsAs<SplashRouteArgs>(orElse: () => const SplashRouteArgs()); data.argsAs<SplashRouteArgs>(orElse: () => const SplashRouteArgs());
return _i13.SplashScreen( return _i14.SplashScreen(
loggedIn: args.loggedIn, loggedIn: args.loggedIn,
key: args.key, key: args.key,
); );
@ -410,7 +431,7 @@ class SplashRouteArgs {
final dynamic Function(bool)? loggedIn; final dynamic Function(bool)? loggedIn;
final _i20.Key? key; final _i21.Key? key;
@override @override
String toString() { String toString() {
@ -419,12 +440,12 @@ class SplashRouteArgs {
} }
/// generated route for /// generated route for
/// [_i14.SyncedScreen] /// [_i15.SyncedScreen]
class SyncedRoute extends _i15.PageRouteInfo<SyncedRouteArgs> { class SyncedRoute extends _i16.PageRouteInfo<SyncedRouteArgs> {
SyncedRoute({ SyncedRoute({
_i20.ScrollController? navigationScrollController, _i21.ScrollController? navigationScrollController,
_i20.Key? key, _i21.Key? key,
List<_i15.PageRouteInfo>? children, List<_i16.PageRouteInfo>? children,
}) : super( }) : super(
SyncedRoute.name, SyncedRoute.name,
args: SyncedRouteArgs( args: SyncedRouteArgs(
@ -436,12 +457,12 @@ class SyncedRoute extends _i15.PageRouteInfo<SyncedRouteArgs> {
static const String name = 'SyncedRoute'; static const String name = 'SyncedRoute';
static _i15.PageInfo page = _i15.PageInfo( static _i16.PageInfo page = _i16.PageInfo(
name, name,
builder: (data) { builder: (data) {
final args = final args =
data.argsAs<SyncedRouteArgs>(orElse: () => const SyncedRouteArgs()); data.argsAs<SyncedRouteArgs>(orElse: () => const SyncedRouteArgs());
return _i14.SyncedScreen( return _i15.SyncedScreen(
navigationScrollController: args.navigationScrollController, navigationScrollController: args.navigationScrollController,
key: args.key, key: args.key,
); );
@ -455,9 +476,9 @@ class SyncedRouteArgs {
this.key, this.key,
}); });
final _i20.ScrollController? navigationScrollController; final _i21.ScrollController? navigationScrollController;
final _i20.Key? key; final _i21.Key? key;
@override @override
String toString() { String toString() {

View file

@ -131,235 +131,243 @@ class _BookViewerControlsState extends ConsumerState<BookViewerControls> {
final previousChapter = details.previousChapter(bookViewerDetails.book); final previousChapter = details.previousChapter(bookViewerDetails.book);
final nextChapter = details.nextChapter(bookViewerDetails.book); final nextChapter = details.nextChapter(bookViewerDetails.book);
return MediaQuery.removePadding( final double leftPadding = MediaQuery.of(context).viewPadding.left;
context: context, final double rightPadding = MediaQuery.of(context).viewPadding.right;
child: InputHandler(
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored, return InputHandler(
child: Stack( onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
children: [ child: Stack(
IgnorePointer( children: [
ignoring: !showControls, IgnorePointer(
child: AnimatedOpacity( ignoring: !showControls,
duration: const Duration(milliseconds: 500), child: AnimatedOpacity(
opacity: showControls ? 1 : 0, duration: const Duration(milliseconds: 500),
child: Stack( opacity: showControls ? 1 : 0,
children: [ child: Stack(
Container( children: [
decoration: BoxDecoration( Container(
gradient: LinearGradient( decoration: BoxDecoration(
begin: Alignment.topCenter, gradient: LinearGradient(
end: Alignment.bottomCenter, begin: Alignment.topCenter,
colors: [ end: Alignment.bottomCenter,
overlayColor.withValues(alpha: 1), colors: [
overlayColor.withValues(alpha: 0.65), overlayColor.withValues(alpha: 1),
overlayColor.withValues(alpha: 0), 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),
],
),
), ),
), ),
if (!bookViewerDetails.loading) ...{ child: Padding(
if (bookViewerDetails.book != null && bookViewerDetails.pages.isNotEmpty) ...{ padding:
Align( EdgeInsets.only(top: topPadding, left: leftPadding, right: rightPadding).copyWith(bottom: 8),
alignment: Alignment.bottomCenter, child: Column(
child: Container( mainAxisSize: MainAxisSize.min,
decoration: BoxDecoration( children: [
gradient: LinearGradient( if (AdaptiveLayout.of(context).isDesktop)
begin: Alignment.topCenter, const Flexible(
end: Alignment.bottomCenter, child: DefaultTitleBar(
colors: [ height: 50,
overlayColor.withValues(alpha: 0), brightness: Brightness.dark,
overlayColor.withValues(alpha: 0.65),
overlayColor.withValues(alpha: 1),
],
), ),
), ),
child: Padding( Row(
padding: EdgeInsets.only(bottom: bottomPadding).copyWith(top: 16, bottom: 16), crossAxisAlignment: CrossAxisAlignment.center,
child: Column( children: [
mainAxisSize: MainAxisSize.min, const BackButton(),
children: [ const SizedBox(
const SizedBox(height: 30), width: 16,
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),
)
],
),
],
), ),
), Flexible(
child: Text(
bookViewerDetails.book?.name ?? "None",
style: Theme.of(context).textTheme.titleLarge,
),
)
],
), ),
), const SizedBox(height: 16),
} 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),
],
), ),
), ),
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),
],
),
), ),
) ),
], )
), ],
), ),
); );
} }

View file

@ -94,7 +94,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
controller: AdaptiveLayout.scrollOf(context), controller: AdaptiveLayout.scrollOf(context),
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
if (AdaptiveLayout.of(context).layout == LayoutState.phone) if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
NestedSliverAppBar( NestedSliverAppBar(
route: LibrarySearchRoute(), route: LibrarySearchRoute(),
parent: context, parent: context,
@ -102,7 +102,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
if (homeBanner && homeCarouselItems.isNotEmpty) ...{ if (homeBanner && homeCarouselItems.isNotEmpty) ...{
SliverToBoxAdapter( SliverToBoxAdapter(
child: Transform.translate( 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), child: HomeBannerWidget(posters: homeCarouselItems),
), ),
), ),

View file

@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/images_models.dart'; import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_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/chip_button.dart';
import 'package:fladder/screens/shared/media/components/media_header.dart'; import 'package:fladder/screens/shared/media/components/media_header.dart';
import 'package:fladder/screens/shared/media/components/small_detail_widgets.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(); (MediaQuery.sizeOf(context).height - (MediaQuery.paddingOf(context).top + 150)).clamp(50, 1250).toDouble();
final crossAlignment = final crossAlignment =
AdaptiveLayout.of(context).layout != LayoutState.phone ? CrossAxisAlignment.start : CrossAxisAlignment.center; AdaptiveLayout.viewSizeOf(context) != ViewSize.phone ? CrossAxisAlignment.start : CrossAxisAlignment.center;
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(

View file

@ -5,6 +5,7 @@ import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.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/items/episode_details_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/details_screens/components/media_stream_information.dart'; import 'package:fladder/screens/details_screens/components/media_stream_information.dart';
@ -43,7 +44,7 @@ class _ItemDetailScreenState extends ConsumerState<EpisodeDetailScreen> {
final seasonDetails = details.series; final seasonDetails = details.series;
final episodeDetails = details.episode; final episodeDetails = details.episode;
final wrapAlignment = final wrapAlignment =
AdaptiveLayout.of(context).layout != LayoutState.phone ? WrapAlignment.start : WrapAlignment.center; AdaptiveLayout.viewSizeOf(context) != ViewSize.phone ? WrapAlignment.start : WrapAlignment.center;
return DetailScaffold( return DetailScaffold(
label: widget.item.name, label: widget.item.name,

View file

@ -1,3 +1,4 @@
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -39,7 +40,7 @@ class _ItemDetailScreenState extends ConsumerState<MovieDetailScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final details = ref.watch(providerInstance); final details = ref.watch(providerInstance);
final wrapAlignment = final wrapAlignment =
AdaptiveLayout.of(context).layout != LayoutState.phone ? WrapAlignment.start : WrapAlignment.center; AdaptiveLayout.viewSizeOf(context) != ViewSize.phone ? WrapAlignment.start : WrapAlignment.center;
return DetailScaffold( return DetailScaffold(
label: widget.item.name, label: widget.item.name,

View file

@ -1,3 +1,4 @@
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -56,7 +57,7 @@ class _PersonDetailScreenState extends ConsumerState<PersonDetailScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15), 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
: MediaQuery.of(context).size.width / 3.5, : MediaQuery.of(context).size.width / 3.5,
child: AspectRatio( child: AspectRatio(

View file

@ -1,3 +1,4 @@
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@ -43,7 +44,7 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final details = ref.watch(providerId); final details = ref.watch(providerId);
final wrapAlignment = final wrapAlignment =
AdaptiveLayout.of(context).layout != LayoutState.phone ? WrapAlignment.start : WrapAlignment.center; AdaptiveLayout.viewSizeOf(context) != ViewSize.phone ? WrapAlignment.start : WrapAlignment.center;
return DetailScaffold( return DetailScaffold(
label: details?.name ?? "", label: details?.name ?? "",

View file

@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; 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/providers/settings/client_settings_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/shared/nested_scaffold.dart'; import 'package:fladder/screens/shared/nested_scaffold.dart';
@ -32,7 +33,7 @@ class FavouritesScreen extends ConsumerWidget {
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
controller: AdaptiveLayout.scrollOf(context), controller: AdaptiveLayout.scrollOf(context),
slivers: [ slivers: [
if (AdaptiveLayout.of(context).layout == LayoutState.phone) if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
NestedSliverAppBar( NestedSliverAppBar(
searchTitle: "${context.localized.search} ${context.localized.favorites.toLowerCase()}...", searchTitle: "${context.localized.search} ${context.localized.favorites.toLowerCase()}...",
parent: context, parent: context,

View file

@ -25,51 +25,54 @@ class HomeScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final canDownload = ref.watch(showSyncButtonProviderProvider); final canDownload = ref.watch(showSyncButtonProviderProvider);
final destinations = HomeTabs.values.map((e) { final destinations = HomeTabs.values
switch (e) { .map((e) {
case HomeTabs.dashboard: switch (e) {
return DestinationModel( case HomeTabs.dashboard:
label: context.localized.navigationDashboard, return DestinationModel(
icon: const Icon(IconsaxOutline.home), label: context.localized.navigationDashboard,
selectedIcon: const Icon(IconsaxBold.home), icon: const Icon(IconsaxOutline.home),
route: const DashboardRoute(), selectedIcon: const Icon(IconsaxBold.home),
action: () => context.router.navigate(const DashboardRoute()), route: const DashboardRoute(),
floatingActionButton: AdaptiveFab( action: () => context.router.navigate(const DashboardRoute()),
context: context, floatingActionButton: AdaptiveFab(
title: context.localized.search, context: context,
key: Key(e.name.capitalize()), title: context.localized.search,
onPressed: () => context.router.navigate(LibrarySearchRoute()), key: Key(e.name.capitalize()),
child: const Icon(IconsaxOutline.search_normal_1), onPressed: () => context.router.navigate(LibrarySearchRoute()),
), child: const Icon(IconsaxOutline.search_normal_1),
); ),
case HomeTabs.favorites: );
return DestinationModel( case HomeTabs.favorites:
label: context.localized.navigationFavorites, return DestinationModel(
icon: const Icon(IconsaxOutline.heart), label: context.localized.navigationFavorites,
selectedIcon: const Icon(IconsaxBold.heart), icon: const Icon(IconsaxOutline.heart),
route: const FavouritesRoute(), selectedIcon: const Icon(IconsaxBold.heart),
floatingActionButton: AdaptiveFab( route: const FavouritesRoute(),
context: context, floatingActionButton: AdaptiveFab(
title: context.localized.filter(0), context: context,
key: Key(e.name.capitalize()), title: context.localized.filter(0),
onPressed: () => context.router.navigate(LibrarySearchRoute(favourites: true)), key: Key(e.name.capitalize()),
child: const Icon(IconsaxOutline.heart_search), onPressed: () => context.router.navigate(LibrarySearchRoute(favourites: true)),
), child: const Icon(IconsaxOutline.heart_search),
action: () => context.router.navigate(const FavouritesRoute()), ),
); action: () => context.router.navigate(const FavouritesRoute()),
case HomeTabs.sync: );
if (canDownload) { case HomeTabs.sync:
return DestinationModel( if (canDownload) {
label: context.localized.navigationSync, return DestinationModel(
icon: const Icon(IconsaxOutline.cloud), label: context.localized.navigationSync,
selectedIcon: const Icon(IconsaxBold.cloud), icon: const Icon(IconsaxOutline.cloud),
route: SyncedRoute(), selectedIcon: const Icon(IconsaxBold.cloud),
action: () => context.router.navigate(SyncedRoute()), route: SyncedRoute(),
); action: () => context.router.navigate(SyncedRoute()),
);
}
return null;
} }
return null; })
} .nonNulls
}); .toList();
return HeroControllerScope( return HeroControllerScope(
controller: HeroController(), controller: HeroController(),
child: AutoRouter( child: AutoRouter(

View file

@ -1,4 +1,5 @@
import 'package:fladder/models/items/photos_model.dart'; 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/models/view_model.dart';
import 'package:fladder/providers/library_provider.dart'; import 'package:fladder/providers/library_provider.dart';
import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart'; import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
@ -27,7 +28,7 @@ class TimelineTab extends ConsumerStatefulWidget {
class _TimelineTabState extends ConsumerState<TimelineTab> with AutomaticKeepAliveClientMixin { class _TimelineTabState extends ConsumerState<TimelineTab> with AutomaticKeepAliveClientMixin {
final itemScrollController = ItemScrollController(); final itemScrollController = ItemScrollController();
double get posterCount { double get posterCount {
if (AdaptiveLayout.of(context).layout == LayoutState.desktop) { if (AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop) {
return 200; return 200;
} }
return 125; return 125;

View file

@ -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/library_search/library_search_options.dart';
import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/playlist_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/library_search_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/providers/video_player_provider.dart';
@ -227,7 +228,7 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
child: MediaQuery.removeViewInsets( child: MediaQuery.removeViewInsets(
context: context, context: context,
child: ClipRRect( child: ClipRRect(
borderRadius: AdaptiveLayout.of(context).layout == LayoutState.desktop borderRadius: AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop
? BorderRadius.circular(15) ? BorderRadius.circular(15)
: BorderRadius.circular(0), : BorderRadius.circular(0),
child: FladderScrollbar( child: FladderScrollbar(
@ -419,7 +420,7 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
), ),
); );
}), }),
if (AdaptiveLayout.of(context).layout == LayoutState.phone) ...[ if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) ...[
const SizedBox(width: 6), const SizedBox(width: 6),
const SizedBox.square(dimension: 46, child: SettingsUserIcon()), const SizedBox.square(dimension: 46, child: SettingsUserIcon()),
], ],

View file

@ -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<Widget> 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<ViewSize>(
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<LayoutMode>(
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(', ')),
),
),
),
];
}

View file

@ -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<Widget> 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(),
];
}

View file

@ -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<Widget> 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(),
],
];
}

View file

@ -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<Widget> 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<ThemeMode>(
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<ColorThemes?>(
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<ColorThemes?>(
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>[
Color(0xFF4285F4), // blue
Color(0xFF34A853), // green
Color(0xFFFBBC05), // yellow
Color(0xFFEA4335), // red
Color(0xFF4285F4), // blue again to seamlessly transition to the start
],
stops: <double>[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<DynamicSchemeVariant>(
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<DynamicSchemeVariant>(
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(),
];
}

View file

@ -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<Widget> 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(),
];
}

View file

@ -2,33 +2,23 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.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:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_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/client_settings_provider.dart';
import 'package:fladder/providers/settings/home_settings_provider.dart';
import 'package:fladder/providers/shared_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/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_list_tile.dart';
import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.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/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/localization_helper.dart';
import 'package:fladder/util/option_dialogue.dart';
import 'package:fladder/util/simple_duration_picker.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() @RoutePage()
class ClientSettingsPage extends ConsumerStatefulWidget { class ClientSettingsPage extends ConsumerStatefulWidget {
@ -48,114 +38,15 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final clientSettings = ref.watch(clientSettingsProvider); final clientSettings = ref.watch(clientSettingsProvider);
final showBackground = AdaptiveLayout.of(context).layout != LayoutState.phone && final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone &&
AdaptiveLayout.of(context).size != ScreenLayout.single; AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
final currentFolder = ref.watch(syncProvider.notifier).savePath;
Locale currentLocale = WidgetsBinding.instance.platformDispatcher.locale;
final canSync = ref.watch(userProvider.select((value) => value?.canDownload ?? false));
return Card( return Card(
elevation: showBackground ? 2 : 0, elevation: showBackground ? 2 : 0,
child: SettingsScaffold( child: SettingsScaffold(
label: "Fladder", label: "Fladder",
items: [ items: [
if (canSync && !kIsWeb) ...[ ...buildClientSettingsDownload(context, ref, setState),
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(),
],
SettingsLabelDivider(label: context.localized.lockscreen), SettingsLabelDivider(label: context.localized.lockscreen),
SettingsListTile( SettingsListTile(
label: Text(context.localized.timeOut), label: Text(context.localized.timeOut),
@ -166,329 +57,18 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
initialValue: clientSettings.timeOut ?? const Duration(), 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) ? Duration(minutes: timePicker.inMinutes, seconds: timePicker.inSeconds % 60)
: null); : null);
}, },
), ),
const Divider(), const Divider(),
SettingsLabelDivider(label: context.localized.dashboard), ...buildClientSettingsDashboard(context, ref),
SettingsListTile( ...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController),
label: Text(context.localized.settingsHomeBannerTitle), ...buildClientSettingsTheme(context, ref),
subLabel: Text(context.localized.settingsHomeBannerDescription), if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[
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<ColorThemes>(
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>[
Color(0xFF4285F4), // blue
Color(0xFF34A853), // green
Color(0xFFFBBC05), // yellow
Color(0xFFEA4335), // red
Color(0xFF4285F4), // blue again to seamlessly transition to the start
],
stops: <double>[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<DynamicSchemeVariant>(
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(),
SettingsLabelDivider(label: context.localized.controls), SettingsLabelDivider(label: context.localized.controls),
SettingsListTile( SettingsListTile(
label: Text(context.localized.mouseDragSupport), label: Text(context.localized.mouseDragSupport),
@ -499,11 +79,13 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
trailing: Switch( trailing: Switch(
value: clientSettings.mouseDragSupport, value: clientSettings.mouseDragSupport,
onChanged: (value) => ref onChanged: (value) => ref
.read(clientSettingsProvider.notifier) .read(clientSettingsProvider.notifier)
.update((current) => current.copyWith(mouseDragSupport: !clientSettings.mouseDragSupport)), .update((current) => current.copyWith(mouseDragSupport: !clientSettings.mouseDragSupport)),
), ),
), ),
], ],
const Divider(),
...buildClientSettingsAdvanced(context, ref),
if (kDebugMode) ...[ if (kDebugMode) ...[
const SizedBox(height: 64), const SizedBox(height: 64),
SettingsListTile( SettingsListTile(
@ -554,7 +136,6 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
}, },
), ),
], ],
const SizedBox(height: 16),
], ],
), ),
); );

View file

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart';
@ -34,8 +35,8 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final videoSettings = ref.watch(videoPlayerSettingsProvider); final videoSettings = ref.watch(videoPlayerSettingsProvider);
final provider = ref.read(videoPlayerSettingsProvider.notifier); final provider = ref.read(videoPlayerSettingsProvider.notifier);
final showBackground = AdaptiveLayout.of(context).layout != LayoutState.phone && final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone &&
AdaptiveLayout.of(context).size != ScreenLayout.single; AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
return Card( return Card(
elevation: showBackground ? 2 : 0, elevation: showBackground ? 2 : 0,
child: SettingsScaffold( child: SettingsScaffold(
@ -63,20 +64,19 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
SettingsListTile( SettingsListTile(
label: Text(context.localized.videoScalingFillScreenTitle), label: Text(context.localized.videoScalingFillScreenTitle),
subLabel: Text(videoSettings.videoFit.label(context)), subLabel: Text(videoSettings.videoFit.label(context)),
onTap: () => openOptionDialogue( onTap: () => openMultiSelectOptions(
context, context,
label: context.localized.videoScalingFillScreenTitle, label: context.localized.videoScalingFillScreenTitle,
items: BoxFit.values, items: BoxFit.values,
itemBuilder: (type) => RadioListTile( selected: [ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit))],
title: Text(type?.label(context) ?? ""), 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, value: type,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
groupValue: ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit)), onChanged: (value) => tap(),
onChanged: (value) {
provider.setFitType(value);
Navigator.pop(context);
},
), ),
), ),
), ),

View file

@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; 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/providers/user_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart';
@ -21,8 +22,8 @@ class _UserSettingsPageState extends ConsumerState<SecuritySettingsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = ref.watch(userProvider); final user = ref.watch(userProvider);
final showBackground = AdaptiveLayout.of(context).layout != LayoutState.phone && final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone &&
AdaptiveLayout.of(context).size != ScreenLayout.single; AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
return Card( return Card(
elevation: showBackground ? 2 : 0, elevation: showBackground ? 2 : 0,
child: SettingsScaffold( child: SettingsScaffold(

View file

@ -1,8 +1,10 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/user_icon.dart'; import 'package:fladder/screens/shared/user_icon.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
@ -10,26 +12,29 @@ import 'package:fladder/util/router_extension.dart';
class SettingsScaffold extends ConsumerWidget { class SettingsScaffold extends ConsumerWidget {
final String label; final String label;
final bool showUserIcon;
final ScrollController? scrollController; final ScrollController? scrollController;
final List<Widget> items; final List<Widget> items;
final List<Widget> bottomActions; final List<Widget> bottomActions;
final bool showUserIcon;
final bool showBackButtonNested;
final Widget? floatingActionButton; final Widget? floatingActionButton;
const SettingsScaffold({ const SettingsScaffold({
required this.label, required this.label,
this.showUserIcon = false,
this.scrollController, this.scrollController,
required this.items, required this.items,
this.bottomActions = const [], this.bottomActions = const [],
this.floatingActionButton, this.floatingActionButton,
this.showUserIcon = false,
this.showBackButtonNested = false,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final padding = MediaQuery.of(context).padding; final padding = MediaQuery.of(context).padding;
final singleLayout = AdaptiveLayout.layoutModeOf(context) == LayoutMode.single;
return Scaffold( return Scaffold(
backgroundColor: AdaptiveLayout.of(context).size == ScreenLayout.dual ? Colors.transparent : null, backgroundColor: AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual ? Colors.transparent : null,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: floatingActionButton, floatingActionButton: floatingActionButton,
body: Column( body: Column(
@ -38,10 +43,11 @@ class SettingsScaffold extends ConsumerWidget {
child: CustomScrollView( child: CustomScrollView(
controller: scrollController, controller: scrollController,
slivers: [ slivers: [
if (AdaptiveLayout.of(context).size == ScreenLayout.single) if (singleLayout)
SliverAppBar.large( SliverAppBar.large(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, leading: BackButton(
leading: context.router.backButton(), onPressed: () => backAction(context),
),
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
titlePadding: const EdgeInsets.symmetric(horizontal: 16) titlePadding: const EdgeInsets.symmetric(horizontal: 16)
.add(EdgeInsets.only(left: padding.left, right: padding.right, bottom: 4)), .add(EdgeInsets.only(left: padding.left, right: padding.right, bottom: 4)),
@ -51,11 +57,12 @@ class SettingsScaffold extends ConsumerWidget {
const Spacer(), const Spacer(),
if (showUserIcon) if (showUserIcon)
SizedBox.fromSize( SizedBox.fromSize(
size: const Size.fromRadius(14), size: const Size.fromRadius(14),
child: UserIcon( child: UserIcon(
user: ref.watch(userProvider), user: ref.watch(userProvider),
cornerRadius: 200, cornerRadius: 200,
)) ),
)
], ],
), ),
expandedTitleScale: 1.2, expandedTitleScale: 1.2,
@ -68,9 +75,15 @@ class SettingsScaffold extends ConsumerWidget {
else else
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32), padding: MediaQuery.paddingOf(context),
child: Text(AdaptiveLayout.of(context).size == ScreenLayout.single ? label : "", child: Row(
style: Theme.of(context).textTheme.headlineLarge), children: [
if (showBackButtonNested)
BackButton(
onPressed: () => backAction(context),
)
],
),
), ),
), ),
SliverPadding( 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();
}
}
} }

View file

@ -4,6 +4,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:ficonsax/ficonsax.dart'; import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/auth_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/routes/auto_router.gr.dart';
@ -13,7 +14,6 @@ import 'package:fladder/screens/settings/settings_scaffold.dart';
import 'package:fladder/screens/shared/fladder_icon.dart'; import 'package:fladder/screens/shared/fladder_icon.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/router_extension.dart';
import 'package:fladder/util/theme_extensions.dart'; import 'package:fladder/util/theme_extensions.dart';
@RoutePage() @RoutePage()
@ -27,84 +27,84 @@ class SettingsScreen extends ConsumerStatefulWidget {
class _SettingsScreenState extends ConsumerState<SettingsScreen> { class _SettingsScreenState extends ConsumerState<SettingsScreen> {
final scrollController = ScrollController(); final scrollController = ScrollController();
final minVerticalPadding = 20.0; final minVerticalPadding = 20.0;
late LayoutMode lastAdaptiveLayout = AdaptiveLayout.layoutModeOf(context);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (AdaptiveLayout.of(context).size == ScreenLayout.single) { return AutoTabsRouter(
return Card( builder: (context, content) {
elevation: 0, checkForNullIndex(context);
child: _leftPane(context), return PopScope(
); canPop: context.tabsRouter.activeIndex == 0 || AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual,
} else { onPopInvokedWithResult: (didPop, result) {
return AutoRouter( if (!didPop) {
builder: (context, content) { context.tabsRouter.setActiveIndex(0);
return Row( }
mainAxisSize: MainAxisSize.max, },
crossAxisAlignment: CrossAxisAlignment.stretch, child: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single
children: [ ? Card(
Expanded(flex: 1, child: _leftPane(context)), elevation: 0,
Expanded( child: Stack(
flex: 2, children: [_leftPane(context), content],
child: 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 { IconData get deviceIcon {
if (AdaptiveLayout.of(context).isDesktop) { if (AdaptiveLayout.of(context).isDesktop) {
return IconsaxOutline.monitor; return IconsaxOutline.monitor;
} }
switch (AdaptiveLayout.of(context).layout) { switch (AdaptiveLayout.viewSizeOf(context)) {
case LayoutState.phone: case ViewSize.phone:
return IconsaxOutline.mobile; return IconsaxOutline.mobile;
case LayoutState.tablet: case ViewSize.tablet:
return IconsaxOutline.monitor; return IconsaxOutline.monitor;
case LayoutState.desktop: case ViewSize.desktop:
return IconsaxOutline.monitor; return IconsaxOutline.monitor;
} }
} }
Widget _leftPane(BuildContext context) { Widget _leftPane(BuildContext context) {
void navigateTo(PageRouteInfo route) { void navigateTo(PageRouteInfo route) => context.tabsRouter.navigate(route);
AdaptiveLayout.of(context).size == ScreenLayout.single
? context.router.navigate(route)
: context.router.replace(route);
}
bool containsRoute(PageRouteInfo route) { bool containsRoute(PageRouteInfo route) =>
return context.router.current.name == route.routeName; AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual && context.tabsRouter.current.name == route.routeName;
}
final quickConnectAvailable = final quickConnectAvailable =
ref.watch(userProvider.select((value) => value?.serverConfiguration?.quickConnectAvailable ?? false)); ref.watch(userProvider.select((value) => value?.serverConfiguration?.quickConnectAvailable ?? false));
return Container( return Container(
color: context.colors.surface, color: context.colors.surface,
child: SettingsScaffold( child: SettingsScaffold(
label: context.localized.settings, label: context.localized.settings,
scrollController: scrollController, scrollController: scrollController,
showBackButtonNested: true,
showUserIcon: true, showUserIcon: true,
items: [ 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( SettingsListTile(
label: Text(context.localized.settingsClientTitle), label: Text(context.localized.settingsClientTitle),
subLabel: Text(context.localized.settingsClientDesc), subLabel: Text(context.localized.settingsClientDesc),

View file

@ -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();
}
}

View file

@ -1,12 +1,15 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:ficonsax/ficonsax.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/adaptive_layout.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/map_bool_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_bottom_sheet.dart';
import 'package:fladder/widgets/shared/modal_side_sheet.dart'; import 'package:fladder/widgets/shared/modal_side_sheet.dart';
import 'package:flutter/material.dart';
class CategoryChip<T> extends StatelessWidget { class CategoryChip<T> extends StatelessWidget {
final Map<T, bool> items; final Map<T, bool> items;
@ -126,7 +129,7 @@ class CategoryChip<T> extends StatelessWidget {
].addInBetween(const SizedBox(width: 6)), ].addInBetween(const SizedBox(width: 6)),
); );
if (AdaptiveLayout.of(context).layout != LayoutState.phone) { if (AdaptiveLayout.viewSizeOf(context) != ViewSize.phone) {
await showModalSideSheet( await showModalSideSheet(
context, context,
addDivider: true, addDivider: true,

View file

@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/images_models.dart'; import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/media_playback_model.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/video_player_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/theme.dart'; import 'package:fladder/theme.dart';
@ -242,7 +243,8 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
), ),
), ),
), ),
if (AdaptiveLayout.of(context).size == ScreenLayout.single) if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single ||
AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
Container( Container(
margin: const EdgeInsets.symmetric(horizontal: 6), margin: const EdgeInsets.symmetric(horizontal: 6),
child: const SizedBox( child: const SizedBox(

View file

@ -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/item_base_model.dart';
import 'package:fladder/models/items/item_shared_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/poster_image.dart'; import 'package:fladder/screens/shared/media/components/poster_image.dart';
import 'package:fladder/util/adaptive_layout.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/item_base_model_extensions.dart';
import 'package:fladder/util/item_base_model/play_item_helpers.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/clickable_text.dart';
import 'package:fladder/widgets/shared/item_actions.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 { class PosterWidget extends ConsumerWidget {
final ItemBaseModel poster; final ItemBaseModel poster;
@ -69,7 +72,7 @@ class PosterWidget extends ConsumerWidget {
children: [ children: [
Flexible( Flexible(
child: ClickableText( child: ClickableText(
onTap: AdaptiveLayout.of(context).layout != LayoutState.phone onTap: AdaptiveLayout.viewSizeOf(context) != ViewSize.phone
? () => poster.parentBaseModel.navigateTo(context) ? () => poster.parentBaseModel.navigateTo(context)
: null, : null,
text: poster.title, text: poster.title,

View file

@ -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/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/video_player_provider.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.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 { class NestedScaffold extends ConsumerWidget {
final Widget body; final Widget body;
@ -18,7 +21,7 @@ class NestedScaffold extends ConsumerWidget {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: switch (AdaptiveLayout.layoutOf(context)) { floatingActionButton: switch (AdaptiveLayout.layoutOf(context)) {
LayoutState.phone => null, ViewSize.phone => null,
_ => switch (playerState) { _ => switch (playerState) {
VideoPlayerState.minimized => const Padding( VideoPlayerState.minimized => const Padding(
padding: EdgeInsets.symmetric(horizontal: 8), padding: EdgeInsets.symmetric(horizontal: 8),

View file

@ -1,19 +1,21 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:ficonsax/ficonsax.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/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/shared/nested_scaffold.dart'; import 'package:fladder/screens/shared/nested_scaffold.dart';
import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
import 'package:fladder/screens/syncing/sync_list_item.dart'; import 'package:fladder/screens/syncing/sync_list_item.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.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/pinch_poster_zoom.dart';
import 'package:fladder/widgets/shared/pull_to_refresh.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() @RoutePage()
class SyncedScreen extends ConsumerStatefulWidget { class SyncedScreen extends ConsumerStatefulWidget {
@ -40,7 +42,7 @@ class _SyncedScreenState extends ConsumerState<SyncedScreen> {
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
controller: widget.navigationScrollController, controller: widget.navigationScrollController,
slivers: [ slivers: [
if (AdaptiveLayout.of(context).layout == LayoutState.phone) if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
NestedSliverAppBar( NestedSliverAppBar(
searchTitle: "${context.localized.search} ...", searchTitle: "${context.localized.search} ...",
parent: context, parent: context,

View file

@ -12,6 +12,7 @@ import 'package:screen_brightness/screen_brightness.dart';
import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/playback/playback_model.dart'; import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/providers/video_player_provider.dart';
@ -300,7 +301,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
IconButton( IconButton(
onPressed: () => showVideoPlayerOptions(context, () => minimizePlayer(context)), onPressed: () => showVideoPlayerOptions(context, () => minimizePlayer(context)),
icon: const Icon(IconsaxOutline.more)), icon: const Icon(IconsaxOutline.more)),
if (AdaptiveLayout.layoutOf(context) == LayoutState.tablet) ...[ if (AdaptiveLayout.layoutOf(context) == ViewSize.tablet) ...[
IconButton( IconButton(
onPressed: () => showSubSelection(context), onPressed: () => showSubSelection(context),
icon: const Icon(IconsaxOutline.subtitle), icon: const Icon(IconsaxOutline.subtitle),
@ -310,7 +311,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
icon: const Icon(IconsaxOutline.audio_square), icon: const Icon(IconsaxOutline.audio_square),
), ),
], ],
if (AdaptiveLayout.layoutOf(context) == LayoutState.desktop) ...[ if (AdaptiveLayout.layoutOf(context) == ViewSize.desktop) ...[
Flexible( Flexible(
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () => showSubSelection(context), onPressed: () => showSubSelection(context),

View file

@ -3,21 +3,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/debug_banner.dart';
import 'package:fladder/util/poster_defaults.dart'; import 'package:fladder/util/poster_defaults.dart';
enum LayoutState {
phone,
tablet,
desktop,
}
enum ScreenLayout {
single,
dual,
}
enum InputDevice { enum InputDevice {
touch, touch,
pointer, pointer,
@ -26,7 +16,7 @@ enum InputDevice {
class LayoutPoints { class LayoutPoints {
final double start; final double start;
final double end; final double end;
final LayoutState type; final ViewSize type;
LayoutPoints({ LayoutPoints({
required this.start, required this.start,
required this.end, required this.end,
@ -36,7 +26,7 @@ class LayoutPoints {
LayoutPoints copyWith({ LayoutPoints copyWith({
double? start, double? start,
double? end, double? end,
LayoutState? type, ViewSize? type,
}) { }) {
return LayoutPoints( return LayoutPoints(
start: start ?? this.start, start: start ?? this.start,
@ -60,23 +50,21 @@ class LayoutPoints {
} }
class AdaptiveLayout extends InheritedWidget { class AdaptiveLayout extends InheritedWidget {
final LayoutState layout; final ViewSize viewSize;
final ScreenLayout size; final LayoutMode layoutMode;
final InputDevice inputDevice; final InputDevice inputDevice;
final TargetPlatform platform; final TargetPlatform platform;
final bool isDesktop; final bool isDesktop;
final AutoRouter router;
final PosterDefaults posterDefaults; final PosterDefaults posterDefaults;
final ScrollController controller; final ScrollController controller;
const AdaptiveLayout({ const AdaptiveLayout({
super.key, super.key,
required this.layout, required this.viewSize,
required this.size, required this.layoutMode,
required this.inputDevice, required this.inputDevice,
required this.platform, required this.platform,
required this.isDesktop, required this.isDesktop,
required this.router,
required this.posterDefaults, required this.posterDefaults,
required this.controller, required this.controller,
required super.child, required super.child,
@ -86,9 +74,9 @@ class AdaptiveLayout extends InheritedWidget {
return context.dependOnInheritedWidgetOfExactType<AdaptiveLayout>(); return context.dependOnInheritedWidgetOfExactType<AdaptiveLayout>();
} }
static LayoutState layoutOf(BuildContext context) { static ViewSize layoutOf(BuildContext context) {
final AdaptiveLayout? result = maybeOf(context); final AdaptiveLayout? result = maybeOf(context);
return result!.layout; return result!.viewSize;
} }
static PosterDefaults poster(BuildContext context) { static PosterDefaults poster(BuildContext context) {
@ -96,11 +84,6 @@ class AdaptiveLayout extends InheritedWidget {
return result!.posterDefaults; return result!.posterDefaults;
} }
static AutoRouter routerOf(BuildContext context) {
final AdaptiveLayout? result = maybeOf(context);
return result!.router;
}
static AdaptiveLayout of(BuildContext context) { static AdaptiveLayout of(BuildContext context) {
final AdaptiveLayout? result = maybeOf(context); final AdaptiveLayout? result = maybeOf(context);
return result!; return result!;
@ -111,14 +94,18 @@ class AdaptiveLayout extends InheritedWidget {
return result!.controller; 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 @override
bool updateShouldNotify(AdaptiveLayout oldWidget) { bool updateShouldNotify(AdaptiveLayout oldWidget) {
return layout != oldWidget.layout || return viewSize != oldWidget.viewSize ||
size != oldWidget.size || layoutMode != oldWidget.layoutMode ||
platform != oldWidget.platform || platform != oldWidget.platform ||
inputDevice != oldWidget.inputDevice || inputDevice != oldWidget.inputDevice ||
isDesktop != oldWidget.isDesktop || isDesktop != oldWidget.isDesktop;
router != oldWidget.router;
} }
} }
@ -126,7 +113,7 @@ const defaultTitleBarHeight = 35.0;
class AdaptiveLayoutBuilder extends ConsumerStatefulWidget { class AdaptiveLayoutBuilder extends ConsumerStatefulWidget {
final List<LayoutPoints> layoutPoints; final List<LayoutPoints> layoutPoints;
final LayoutState fallBack; final ViewSize fallBack;
final Widget child; final Widget child;
const AdaptiveLayoutBuilder({required this.layoutPoints, required this.child, required this.fallBack, super.key}); 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<AdaptiveLayoutBuilder> { class _AdaptiveLayoutBuilderState extends ConsumerState<AdaptiveLayoutBuilder> {
late LayoutState layout = widget.fallBack; late ViewSize viewSize = widget.fallBack;
late ScreenLayout size = ScreenLayout.single; late LayoutMode layoutMode = LayoutMode.single;
AutoRouter? router;
late TargetPlatform currentPlatform = defaultTargetPlatform; late TargetPlatform currentPlatform = defaultTargetPlatform;
late ScrollController controller = ScrollController(); late ScrollController controller = ScrollController();
@ -158,47 +144,45 @@ class _AdaptiveLayoutBuilderState extends ConsumerState<AdaptiveLayoutBuilder> {
} }
void calculateLayout() { void calculateLayout() {
LayoutState? newType; ViewSize? newType;
for (var element in widget.layoutPoints) { for (var element in widget.layoutPoints) {
if (MediaQuery.of(context).size.width > element.start && MediaQuery.of(context).size.width < element.end) { if (MediaQuery.of(context).size.width > element.start && MediaQuery.of(context).size.width < element.end) {
newType = element.type; newType = element.type;
} }
} }
if (newType == LayoutState.phone && isDesktop) { viewSize = newType ?? widget.fallBack;
newType = LayoutState.tablet;
}
layout = newType ?? widget.fallBack;
} }
void calculateSize() { void calculateSize() {
ScreenLayout newSize; LayoutMode newSize;
if (MediaQuery.of(context).size.width > 0 && MediaQuery.of(context).size.width < 960 && !isDesktop) { if (MediaQuery.of(context).size.width > 0 && MediaQuery.of(context).size.width < 960) {
newSize = ScreenLayout.single; newSize = LayoutMode.single;
} else { } else {
newSize = ScreenLayout.dual; newSize = LayoutMode.dual;
} }
size = newSize; layoutMode = newSize;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final acceptedLayouts = ref.watch(homeSettingsProvider.select((value) => value.screenLayouts));
final acceptedViewSizes = ref.watch(homeSettingsProvider.select((value) => value.layoutStates));
return MediaQuery( return MediaQuery(
data: MediaQuery.of(context).copyWith( data: MediaQuery.of(context).copyWith(
padding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, padding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null,
viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null,
), ),
child: AdaptiveLayout( child: AdaptiveLayout(
layout: layout, viewSize: selectAvailableOrSmaller<ViewSize>(viewSize, acceptedViewSizes, ViewSize.values),
controller: controller, controller: controller,
size: size, layoutMode: selectAvailableOrSmaller<LayoutMode>(layoutMode, acceptedLayouts, LayoutMode.values),
inputDevice: (isDesktop || kIsWeb) ? InputDevice.pointer : InputDevice.touch, inputDevice: (isDesktop || kIsWeb) ? InputDevice.pointer : InputDevice.touch,
platform: currentPlatform, platform: currentPlatform,
isDesktop: isDesktop, isDesktop: isDesktop,
router: router ??= AutoRouter(layout: size, ref: ref), posterDefaults: switch (viewSize) {
posterDefaults: switch (layout) { ViewSize.phone => const PosterDefaults(size: 300, ratio: 0.55),
LayoutState.phone => const PosterDefaults(size: 300, ratio: 0.55), ViewSize.tablet => const PosterDefaults(size: 350, ratio: 0.55),
LayoutState.tablet => const PosterDefaults(size: 350, ratio: 0.55), ViewSize.desktop => const PosterDefaults(size: 400, ratio: 0.55),
LayoutState.desktop => const PosterDefaults(size: 400, ratio: 0.55),
}, },
child: DebugBanner(child: widget.child), child: DebugBanner(child: widget.child),
), ),

View file

@ -1,31 +1,53 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
Future<void> openOptionDialogue<T>( Future<List<T>> openMultiSelectOptions<T>(
BuildContext context, { BuildContext context, {
required String label, required String label,
bool allowMultiSelection = false,
bool forceAtleastOne = true,
required List<T> selected,
required List<T> items, required List<T> items,
bool isNullable = false, Function(List<T> values)? onChanged,
required Widget Function(T? type) itemBuilder, required Widget Function(T type, bool selected, Function onTap) itemBuilder,
}) { }) async {
return showDialog( Set<T> currentSelection = selected.toSet();
await showDialog(
context: context, context: context,
builder: (context) { builder: (context) => StatefulBuilder(
return AlertDialog( builder: (context, setState) => AlertDialog(
title: Text(label), title: Text(label),
content: SizedBox( content: SizedBox(
width: MediaQuery.of(context).size.width * 0.65, width: MediaQuery.of(context).size.width * 0.65,
child: ListView( child: ListView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
children: [ children: items.map((item) {
if (isNullable) itemBuilder(null), bool isSelected = currentSelection.contains(item);
...items.map( return itemBuilder(
(e) => itemBuilder(e), 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();
} }

View file

@ -149,7 +149,7 @@ class SimpleDurationPicker extends ConsumerWidget {
children: [ children: [
if (showNever) ...{ if (showNever) ...{
TextButton( TextButton(
onPressed: () => onChanged(null), onPressed: () => onChanged(Duration.zero),
child: Text(context.localized.never), child: Text(context.localized.never),
), ),
const Spacer(), const Spacer(),

View file

@ -1,12 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:ficonsax/ficonsax.dart'; import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/client_settings_provider.dart';
import 'package:fladder/providers/views_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/routes/auto_router.gr.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
@ -50,6 +53,8 @@ class _NavigationBodyState extends ConsumerState<NavigationBody> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final views = ref.watch(viewsProvider.select((value) => value.views)); 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( ref.listen(
clientSettingsProvider, clientSettingsProvider,
(previous, next) { (previous, next) {
@ -62,48 +67,66 @@ class _NavigationBodyState extends ConsumerState<NavigationBody> {
); );
return switch (AdaptiveLayout.layoutOf(context)) { return switch (AdaptiveLayout.layoutOf(context)) {
LayoutState.phone => MediaQuery.removePadding( ViewSize.phone => MediaQuery.removePadding(
context: widget.parentContext, context: widget.parentContext,
child: widget.child, child: widget.child,
), ),
LayoutState.tablet => Row( ViewSize.tablet => Row(
children: [ children: [
navigationRail(context), AnimatedFadeSize(
duration: const Duration(milliseconds: 250),
child: hasOverlay ? navigationRail(context) : const SizedBox(),
),
Flexible( Flexible(
child: widget.child, child: MediaQuery(
data: semiNestedPadding(context, hasOverlay),
child: widget.child,
),
) )
], ],
), ),
LayoutState.desktop => Row( ViewSize.desktop => Row(
children: [ children: [
AnimatedFadeSize( AnimatedFadeSize(
duration: const Duration(milliseconds: 125), duration: const Duration(milliseconds: 125),
child: expandedSideBar child: hasOverlay
? MediaQuery.removePadding( ? expandedSideBar
context: widget.parentContext, ? MediaQuery.removePadding(
child: NestedNavigationDrawer( context: widget.parentContext,
isExpanded: expandedSideBar, child: NestedNavigationDrawer(
actionButton: actionButton(), isExpanded: expandedSideBar,
toggleExpanded: (value) { actionButton: actionButton(),
setState(() { toggleExpanded: (value) {
expandedSideBar = value; setState(() {
}); expandedSideBar = value;
}, });
views: views, },
destinations: widget.destinations, views: views,
currentLocation: widget.currentLocation, destinations: widget.destinations,
), currentLocation: widget.currentLocation,
) ),
: navigationRail(context), )
: navigationRail(context)
: const SizedBox(),
), ),
Flexible( 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() { AdaptiveFab? actionButton() {
return (widget.currentIndex >= 0 && widget.currentIndex < widget.destinations.length) return (widget.currentIndex >= 0 && widget.currentIndex < widget.destinations.length)
? widget.destinations[widget.currentIndex].floatingActionButton ? widget.destinations[widget.currentIndex].floatingActionButton
@ -120,7 +143,8 @@ class _NavigationBodyState extends ConsumerState<NavigationBody> {
style: Theme.of(context).textTheme.titleSmall, 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( Flexible(
child: Padding( child: Padding(
key: const Key('navigation_rail'), key: const Key('navigation_rail'),
@ -130,7 +154,7 @@ class _NavigationBodyState extends ConsumerState<NavigationBody> {
children: [ children: [
IconButton( IconButton(
onPressed: () { onPressed: () {
if (AdaptiveLayout.layoutOf(context) != LayoutState.desktop) { if (AdaptiveLayout.layoutOf(context) != ViewSize.desktop) {
widget.drawerKey.currentState?.openDrawer(); widget.drawerKey.currentState?.openDrawer();
} else { } else {
setState(() { setState(() {
@ -140,7 +164,7 @@ class _NavigationBodyState extends ConsumerState<NavigationBody> {
}, },
icon: const Icon(IconsaxBold.menu), icon: const Icon(IconsaxBold.menu),
), ),
if (AdaptiveLayout.of(context).size == ScreenLayout.dual) ...[ if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
AnimatedFadeSize( AnimatedFadeSize(
child: AnimatedSwitcher( child: AnimatedSwitcher(

View file

@ -6,6 +6,7 @@ import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/collection_types.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/models/view_model.dart';
import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/metadata/refresh_metadata.dart'; import 'package:fladder/screens/metadata/refresh_metadata.dart';
@ -116,11 +117,11 @@ class NestedNavigationDrawer extends ConsumerWidget {
selected: currentLocation.contains(const SettingsRoute().routeName), selected: currentLocation.contains(const SettingsRoute().routeName),
icon: const SizedBox(width: 35, height: 35, child: SettingsUserIcon()), icon: const SizedBox(width: 35, height: 35, child: SettingsUserIcon()),
onPressed: () { onPressed: () {
switch (AdaptiveLayout.of(context).size) { switch (AdaptiveLayout.layoutModeOf(context)) {
case ScreenLayout.single: case LayoutMode.single:
const SettingsRoute().push(context); const SettingsRoute().push(context);
break; break;
case ScreenLayout.dual: case LayoutMode.dual:
context.router.push(const ClientSettingsRoute()); context.router.push(const ClientSettingsRoute());
break; break;
} }
@ -135,11 +136,11 @@ class NestedNavigationDrawer extends ConsumerWidget {
icon: const Icon(IconsaxOutline.setting_2), icon: const Icon(IconsaxOutline.setting_2),
selected: currentLocation.contains(const SettingsRoute().routeName), selected: currentLocation.contains(const SettingsRoute().routeName),
onPressed: () { onPressed: () {
switch (AdaptiveLayout.of(context).size) { switch (AdaptiveLayout.layoutModeOf(context)) {
case ScreenLayout.single: case LayoutMode.single:
const SettingsRoute().push(context); const SettingsRoute().push(context);
break; break;
case ScreenLayout.dual: case LayoutMode.dual:
context.router.push(const ClientSettingsRoute()); context.router.push(const ClientSettingsRoute());
break; break;
} }

View file

@ -1,10 +1,14 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/shared/user_icon.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:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SettingsUserIcon extends ConsumerWidget { class SettingsUserIcon extends ConsumerWidget {
const SettingsUserIcon({super.key}); const SettingsUserIcon({super.key});
@ -19,7 +23,13 @@ class SettingsUserIcon extends ConsumerWidget {
user: users, user: users,
cornerRadius: 200, cornerRadius: 200,
onLongPress: () => context.router.push(const LockRoute()), 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());
}
},
), ),
); );
} }

View file

@ -3,8 +3,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/media_playback_model.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/video_player_provider.dart';
import 'package:fladder/providers/views_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/screens/shared/nested_bottom_appbar.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
@ -65,7 +67,7 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
extendBody: true, extendBody: true,
floatingActionButtonLocation: floatingActionButtonLocation:
playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null, playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null,
floatingActionButton: AdaptiveLayout.of(context).size == ScreenLayout.single floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single
? switch (playerState) { ? switch (playerState) {
VideoPlayerState.minimized => const Padding( VideoPlayerState.minimized => const Padding(
padding: EdgeInsets.symmetric(horizontal: 8), padding: EdgeInsets.symmetric(horizontal: 8),
@ -83,9 +85,10 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
destinations: widget.destinations, destinations: widget.destinations,
currentLocation: currentLocation, currentLocation: currentLocation,
), ),
bottomNavigationBar: AdaptiveLayout.of(context).layout == LayoutState.phone bottomNavigationBar: AdaptiveLayout.viewSizeOf(context) == ViewSize.phone
? HideOnScroll( ? HideOnScroll(
controller: AdaptiveLayout.scrollOf(context), controller: AdaptiveLayout.scrollOf(context),
forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)),
child: NestedBottomAppBar( child: NestedBottomAppBar(
child: Transform.translate( child: Transform.translate(
offset: const Offset(0, 8), offset: const Offset(0, 8),

View file

@ -1,20 +1,25 @@
import 'package:fladder/util/adaptive_layout.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_riverpod/flutter_riverpod.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 { class HideOnScroll extends ConsumerStatefulWidget {
final Widget? child; final Widget? child;
final ScrollController? controller; final ScrollController? controller;
final double height; final double height;
final Widget? Function(bool visible)? visibleBuilder; final Widget? Function(bool visible)? visibleBuilder;
final Duration duration; final Duration duration;
final bool forceHide;
const HideOnScroll({ const HideOnScroll({
this.child, this.child,
this.controller, this.controller,
this.height = kBottomNavigationBarHeight, this.height = kBottomNavigationBarHeight,
this.visibleBuilder, this.visibleBuilder,
this.duration = const Duration(milliseconds: 200), this.duration = const Duration(milliseconds: 200),
this.forceHide = false,
super.key, super.key,
}) : assert(child != null || visibleBuilder != null); }) : assert(child != null || visibleBuilder != null);
@ -63,12 +68,16 @@ class _HideOnScrollState extends ConsumerState<HideOnScroll> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.visibleBuilder != null) return widget.visibleBuilder!(isVisible)!; if (widget.visibleBuilder != null) return widget.visibleBuilder!(isVisible)!;
if (widget.child == null) return const SizedBox(); if (widget.child == null) return const SizedBox();
if (AdaptiveLayout.of(context).layout == LayoutState.desktop) { if (AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop) {
return widget.child!; return widget.child!;
} else { } else {
return AnimatedAlign( return AnimatedAlign(
alignment: const Alignment(0, -1), alignment: const Alignment(0, -1),
heightFactor: isVisible ? 1.0 : 0, heightFactor: widget.forceHide
? 0
: isVisible
? 1.0
: 0,
duration: widget.duration, duration: widget.duration,
child: Wrap(children: [widget.child!]), child: Wrap(children: [widget.child!]),
); );

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/item_base_model.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/adaptive_layout.dart';
import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/fladder_image.dart';
@ -23,7 +24,7 @@ Future<void> showBottomSheetPill({
showDragHandle: true, showDragHandle: true,
enableDrag: true, enableDrag: true,
context: context, 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(maxHeight: MediaQuery.of(context).size.height * 0.9)
: BoxConstraints( : BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.75, maxHeight: MediaQuery.of(context).size.height * 0.85), maxWidth: MediaQuery.of(context).size.width * 0.75, maxHeight: MediaQuery.of(context).size.height * 0.85),