diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bdb9329..77a52bc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1289,5 +1289,7 @@ "syncResumeAll": "Resume all", "syncStopAll": "Stop all", "syncDeleteAll": "Delete all files", - "syncAllFiles": "Sync all files" + "syncAllFiles": "Sync all files", + "usePostersForLibraryIconsTitle": "Show posters for library icons", + "usePostersForLibraryIconsDesc": "Show posters instead of icons for libraries" } \ No newline at end of file diff --git a/lib/models/settings/client_settings_model.dart b/lib/models/settings/client_settings_model.dart index f8b9f35..15a2b3d 100644 --- a/lib/models/settings/client_settings_model.dart +++ b/lib/models/settings/client_settings_model.dart @@ -36,6 +36,7 @@ class ClientSettingsModel with _$ClientSettingsModel { @Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant, @Default(true) bool backgroundPosters, @Default(true) bool checkForUpdates, + @Default(false) bool usePosterForLibrary, String? lastViewedUpdate, int? libraryPageSize, }) = _ClientSettingsModel; diff --git a/lib/models/settings/client_settings_model.freezed.dart b/lib/models/settings/client_settings_model.freezed.dart index d506115..ffa166f 100644 --- a/lib/models/settings/client_settings_model.freezed.dart +++ b/lib/models/settings/client_settings_model.freezed.dart @@ -42,6 +42,7 @@ mixin _$ClientSettingsModel { DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError; bool get backgroundPosters => throw _privateConstructorUsedError; bool get checkForUpdates => throw _privateConstructorUsedError; + bool get usePosterForLibrary => throw _privateConstructorUsedError; String? get lastViewedUpdate => throw _privateConstructorUsedError; int? get libraryPageSize => throw _privateConstructorUsedError; @@ -83,6 +84,7 @@ abstract class $ClientSettingsModelCopyWith<$Res> { DynamicSchemeVariant schemeVariant, bool backgroundPosters, bool checkForUpdates, + bool usePosterForLibrary, String? lastViewedUpdate, int? libraryPageSize}); } @@ -123,6 +125,7 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel> Object? schemeVariant = null, Object? backgroundPosters = null, Object? checkForUpdates = null, + Object? usePosterForLibrary = null, Object? lastViewedUpdate = freezed, Object? libraryPageSize = freezed, }) { @@ -211,6 +214,10 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel> ? _value.checkForUpdates : checkForUpdates // ignore: cast_nullable_to_non_nullable as bool, + usePosterForLibrary: null == usePosterForLibrary + ? _value.usePosterForLibrary + : usePosterForLibrary // ignore: cast_nullable_to_non_nullable + as bool, lastViewedUpdate: freezed == lastViewedUpdate ? _value.lastViewedUpdate : lastViewedUpdate // ignore: cast_nullable_to_non_nullable @@ -253,6 +260,7 @@ abstract class _$$ClientSettingsModelImplCopyWith<$Res> DynamicSchemeVariant schemeVariant, bool backgroundPosters, bool checkForUpdates, + bool usePosterForLibrary, String? lastViewedUpdate, int? libraryPageSize}); } @@ -291,6 +299,7 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res> Object? schemeVariant = null, Object? backgroundPosters = null, Object? checkForUpdates = null, + Object? usePosterForLibrary = null, Object? lastViewedUpdate = freezed, Object? libraryPageSize = freezed, }) { @@ -379,6 +388,10 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res> ? _value.checkForUpdates : checkForUpdates // ignore: cast_nullable_to_non_nullable as bool, + usePosterForLibrary: null == usePosterForLibrary + ? _value.usePosterForLibrary + : usePosterForLibrary // ignore: cast_nullable_to_non_nullable + as bool, lastViewedUpdate: freezed == lastViewedUpdate ? _value.lastViewedUpdate : lastViewedUpdate // ignore: cast_nullable_to_non_nullable @@ -417,6 +430,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel this.schemeVariant = DynamicSchemeVariant.rainbow, this.backgroundPosters = true, this.checkForUpdates = true, + this.usePosterForLibrary = false, this.lastViewedUpdate, this.libraryPageSize}) : super._(); @@ -485,13 +499,16 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel @JsonKey() final bool checkForUpdates; @override + @JsonKey() + final bool usePosterForLibrary; + @override final String? lastViewedUpdate; @override final int? libraryPageSize; @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundPosters: $backgroundPosters, checkForUpdates: $checkForUpdates, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize)'; + return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundPosters: $backgroundPosters, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize)'; } @override @@ -522,6 +539,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel ..add(DiagnosticsProperty('schemeVariant', schemeVariant)) ..add(DiagnosticsProperty('backgroundPosters', backgroundPosters)) ..add(DiagnosticsProperty('checkForUpdates', checkForUpdates)) + ..add(DiagnosticsProperty('usePosterForLibrary', usePosterForLibrary)) ..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate)) ..add(DiagnosticsProperty('libraryPageSize', libraryPageSize)); } @@ -566,6 +584,7 @@ abstract class _ClientSettingsModel extends ClientSettingsModel { final DynamicSchemeVariant schemeVariant, final bool backgroundPosters, final bool checkForUpdates, + final bool usePosterForLibrary, final String? lastViewedUpdate, final int? libraryPageSize}) = _$ClientSettingsModelImpl; _ClientSettingsModel._() : super._(); @@ -617,6 +636,8 @@ abstract class _ClientSettingsModel extends ClientSettingsModel { @override bool get checkForUpdates; @override + bool get usePosterForLibrary; + @override String? get lastViewedUpdate; @override int? get libraryPageSize; diff --git a/lib/models/settings/client_settings_model.g.dart b/lib/models/settings/client_settings_model.g.dart index 6c1c6b5..ed42ef6 100644 --- a/lib/models/settings/client_settings_model.g.dart +++ b/lib/models/settings/client_settings_model.g.dart @@ -43,6 +43,7 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson( DynamicSchemeVariant.rainbow, backgroundPosters: json['backgroundPosters'] as bool? ?? true, checkForUpdates: json['checkForUpdates'] as bool? ?? true, + usePosterForLibrary: json['usePosterForLibrary'] as bool? ?? false, lastViewedUpdate: json['lastViewedUpdate'] as String?, libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(), ); @@ -71,6 +72,7 @@ Map _$$ClientSettingsModelImplToJson( 'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!, 'backgroundPosters': instance.backgroundPosters, 'checkForUpdates': instance.checkForUpdates, + 'usePosterForLibrary': instance.usePosterForLibrary, 'lastViewedUpdate': instance.lastViewedUpdate, 'libraryPageSize': instance.libraryPageSize, }; diff --git a/lib/models/view_model.dart b/lib/models/view_model.dart index 53c8d2b..649ecb9 100644 --- a/lib/models/view_model.dart +++ b/lib/models/view_model.dart @@ -113,6 +113,7 @@ class ViewModel { FutureOr Function() action, { FutureOr Function()? onLongPress, List? trailing, + Widget? customIcon, }) { return NavigationButton( label: name, @@ -121,6 +122,7 @@ class ViewModel { onLongPress: onLongPress, horizontal: horizontal, expanded: expanded, + customIcon: customIcon, trailing: trailing ?? [], selectedIcon: Icon(collectionType.icon), icon: Icon(collectionType.iconOutlined), diff --git a/lib/screens/settings/client_sections/client_settings_visual.dart b/lib/screens/settings/client_sections/client_settings_visual.dart index 5666573..64076fe 100644 --- a/lib/screens/settings/client_sections/client_settings_visual.dart +++ b/lib/screens/settings/client_sections/client_settings_visual.dart @@ -99,6 +99,18 @@ List buildClientSettingsVisual( ref.read(clientSettingsProvider.notifier).update((cb) => cb.copyWith(backgroundPosters: value)), ), ), + SettingsListTile( + label: Text(context.localized.usePostersForLibraryIconsTitle), + subLabel: Text(context.localized.usePostersForLibraryIconsDesc), + onTap: () => ref + .read(clientSettingsProvider.notifier) + .update((cb) => cb.copyWith(usePosterForLibrary: !clientSettings.usePosterForLibrary)), + trailing: Switch( + value: clientSettings.usePosterForLibrary, + onChanged: (value) => + ref.read(clientSettingsProvider.notifier).update((cb) => cb.copyWith(usePosterForLibrary: value)), + ), + ), SettingsListTile( label: Text(context.localized.settingsNextUpCutoffDays), trailing: SizedBox( diff --git a/lib/widgets/navigation_scaffold/components/drawer_list_button.dart b/lib/widgets/navigation_scaffold/components/drawer_list_button.dart index 5b5477f..93df459 100644 --- a/lib/widgets/navigation_scaffold/components/drawer_list_button.dart +++ b/lib/widgets/navigation_scaffold/components/drawer_list_button.dart @@ -1,9 +1,11 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; class DrawerListButton extends ConsumerStatefulWidget { final String label; diff --git a/lib/widgets/navigation_scaffold/components/navigation_button.dart b/lib/widgets/navigation_scaffold/components/navigation_button.dart index 004fec5..4b6ca60 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_button.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_button.dart @@ -14,6 +14,7 @@ class NavigationButton extends ConsumerStatefulWidget { final Function()? onPressed; final Function()? onLongPress; final List trailing; + final Widget? customIcon; final bool selected; final Duration duration; const NavigationButton({ @@ -24,6 +25,7 @@ class NavigationButton extends ConsumerStatefulWidget { this.expanded = false, this.onPressed, this.onLongPress, + this.customIcon, this.selected = false, this.trailing = const [], this.duration = const Duration(milliseconds: 125), @@ -64,9 +66,11 @@ class _NavigationButtonState extends ConsumerState { onLongPress: widget.onLongPress, child: widget.horizontal ? Padding( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + padding: widget.customIcon != null + ? EdgeInsetsGeometry.zero + : const EdgeInsets.symmetric(vertical: 4, horizontal: 8), child: SizedBox( - height: 35, + height: widget.customIcon != null ? 60 : 35, child: Row( spacing: 4, mainAxisAlignment: MainAxisAlignment.center, @@ -85,10 +89,11 @@ class _NavigationButtonState extends ConsumerState { .withValues(alpha: widget.selected && !widget.expanded ? 1 : 0), ), ), - AnimatedSwitcher( - duration: widget.duration, - child: widget.selected ? widget.selectedIcon : widget.icon, - ), + widget.customIcon ?? + AnimatedSwitcher( + duration: widget.duration, + child: widget.selected ? widget.selectedIcon : widget.icon, + ), const SizedBox(width: 6), if (widget.horizontal && widget.expanded) ...[ if (widget.label != null) @@ -119,17 +124,18 @@ class _NavigationButtonState extends ConsumerState { ), ) : Padding( - padding: const EdgeInsets.all(8), + padding: widget.customIcon != null ? EdgeInsetsGeometry.zero : const EdgeInsets.all(8), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( spacing: 8, children: [ - AnimatedSwitcher( - duration: widget.duration, - child: widget.selected ? widget.selectedIcon : widget.icon, - ), + widget.customIcon ?? + AnimatedSwitcher( + duration: widget.duration, + child: widget.selected ? widget.selectedIcon : widget.icon, + ), if (widget.label != null && widget.horizontal && widget.expanded) Flexible(child: Text(widget.label!)) ], diff --git a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart index ba5ec29..4fed8c1 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart @@ -7,10 +7,13 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/collection_types.dart'; import 'package:fladder/models/view_model.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/metadata/refresh_metadata.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; +import 'package:fladder/theme.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; @@ -36,6 +39,7 @@ class NestedNavigationDrawer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final useLibraryPosters = ref.watch(clientSettingsProvider.select((value) => value.usePosterForLibrary)); return NavigationDrawer( key: const Key('navigation_drawer'), backgroundColor: isExpanded ? Colors.transparent : null, @@ -91,22 +95,41 @@ class NestedNavigationDrawer extends ConsumerWidget { style: Theme.of(context).textTheme.titleMedium, ), ), - ...views.map((library) => DrawerListButton( - label: library.name, - selected: context.router.currentUrl.contains(library.id), - actions: [ - ItemActionButton( - label: Text(context.localized.scanLibrary), - icon: const Icon(IconsaxPlusLinear.refresh), - action: () => showRefreshPopup(context, library.id, library.name), - ), - ], - onPressed: () { - context.router.push(LibrarySearchRoute(viewModelId: library.id)); - Scaffold.of(context).closeDrawer(); - }, - selectedIcon: Icon(library.collectionType.icon), - icon: Icon(library.collectionType.iconOutlined))), + ...views.map((library) { + var selected = context.router.currentUrl.contains(library.id); + final Widget? posterIcon = useLibraryPosters + ? ClipRRect( + borderRadius: FladderTheme.smallShape.borderRadius, + child: AspectRatio( + aspectRatio: 1.0, + child: FladderImage( + image: library.imageData?.primary, + placeHolder: Card( + child: Icon( + selected ? library.collectionType.icon : library.collectionType.iconOutlined, + ), + ), + ), + ), + ) + : null; + return DrawerListButton( + label: library.name, + selected: selected, + actions: [ + ItemActionButton( + label: Text(context.localized.scanLibrary), + icon: const Icon(IconsaxPlusLinear.refresh), + action: () => showRefreshPopup(context, library.id, library.name), + ), + ], + onPressed: () { + context.router.push(LibrarySearchRoute(viewModelId: library.id)); + Scaffold.of(context).closeDrawer(); + }, + selectedIcon: posterIcon ?? Icon(library.collectionType.icon), + icon: posterIcon ?? Icon(library.collectionType.iconOutlined)); + }), }, const Divider(indent: 28, endIndent: 28), if (isExpanded) diff --git a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart index efb4e87..7797a42 100644 --- a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart +++ b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart @@ -9,11 +9,14 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:overflow_view/overflow_view.dart'; import 'package:fladder/models/collection_types.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/metadata/refresh_metadata.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; +import 'package:fladder/theme.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; @@ -68,14 +71,17 @@ class _SideNavigationBarState extends ConsumerState { @override Widget build(BuildContext context) { final views = ref.watch(viewsProvider.select((value) => value.views)); + final usePostersForLibrary = ref.watch(clientSettingsProvider.select((value) => value.usePosterForLibrary)); + final expandedWidth = 250.0; final padding = MediaQuery.paddingOf(context); - final collapsedWidth = 90.0 + padding.left; + final collapsedWidth = 90 + padding.left; final largeBar = AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; final fullyExpanded = largeBar ? expandedSideBar : false; final shouldExpand = showOnHover || fullyExpanded; final isDesktop = AdaptiveLayout.of(context).isDesktop; + return Stack( children: [ AdaptiveLayoutBuilder( @@ -85,11 +91,7 @@ class _SideNavigationBarState extends ConsumerState { child: (context) => widget.child, ), Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12) - .copyWith(topLeft: const Radius.circular(0), bottomLeft: const Radius.circular(0)), - color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85), - ), + color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85), width: shouldExpand ? expandedWidth : collapsedWidth, child: MouseRegion( onEnter: (value) => startTimer(), @@ -158,6 +160,7 @@ class _SideNavigationBarState extends ConsumerState { spacing: 4, children: views.map( (view) { + final selected = context.router.currentUrl.contains(view.id); final actions = [ ItemActionButton( label: Text(context.localized.scanLibrary), @@ -166,7 +169,7 @@ class _SideNavigationBarState extends ConsumerState { ) ]; return view.toNavigationButton( - context.router.currentUrl.contains(view.id), + selected, true, shouldExpand, () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)), @@ -178,6 +181,24 @@ class _SideNavigationBarState extends ConsumerState { children: actions.listTileItems(context, useIcons: true), ), ), + customIcon: usePostersForLibrary + ? ClipRRect( + borderRadius: FladderTheme.smallShape.borderRadius, + child: SizedBox.square( + dimension: 50, + child: FladderImage( + image: view.imageData?.primary, + placeHolder: Card( + child: Icon( + selected + ? view.collectionType.icon + : view.collectionType.iconOutlined, + ), + ), + ), + ), + ) + : null, trailing: actions, ); }, @@ -191,6 +212,17 @@ class _SideNavigationBarState extends ConsumerState { selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down), icon: const Icon(IconsaxPlusLinear.arrow_square_down), expanded: shouldExpand, + customIcon: usePostersForLibrary + ? ClipRRect( + borderRadius: FladderTheme.smallShape.borderRadius, + child: const SizedBox.square( + dimension: 50, + child: Card( + child: Icon(IconsaxPlusLinear.arrow_square_down), + ), + ), + ) + : null, horizontal: true, ), itemBuilder: (context) => views @@ -201,7 +233,25 @@ class _SideNavigationBarState extends ConsumerState { child: Row( spacing: 8, children: [ - Icon(e.collectionType.iconOutlined), + usePostersForLibrary + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ClipRRect( + borderRadius: FladderTheme.smallShape.borderRadius, + child: SizedBox.square( + dimension: 45, + child: FladderImage( + image: e.imageData?.primary, + placeHolder: Card( + child: Icon( + e.collectionType.iconOutlined, + ), + ), + ), + ), + ), + ) + : Icon(e.collectionType.iconOutlined), Text(e.name), ], ),