From 121f611c7f4aef00b3e6913a5b3114eb068b3403 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 26 Oct 2024 21:35:48 +0800 Subject: [PATCH] New settings page to configure nav bar in collections tab --- app/lib/controller/pref_controller.dart | 39 +++ app/lib/controller/pref_controller.g.dart | 12 + app/lib/controller/pref_controller/type.dart | 21 ++ app/lib/controller/pref_controller/util.dart | 10 + app/lib/entity/pref.dart | 3 + app/lib/l10n/app_en.arb | 5 + app/lib/l10n/untranslated-messages.txt | 56 +++- app/lib/widget/home_collections.dart | 2 + app/lib/widget/home_collections.g.dart | 13 +- app/lib/widget/home_collections/bloc.dart | 12 + .../home_collections/nav_bar_buttons.dart | 241 ++++++++++++++++++ .../home_collections/navigation_bar.dart | 224 ++-------------- .../widget/home_collections/state_event.dart | 15 ++ .../widget/settings/collection_settings.dart | 12 + .../settings/collections_nav_bar/bloc.dart | 122 +++++++++ .../settings/collections_nav_bar/buttons.dart | 108 ++++++++ .../collections_nav_bar/state_event.dart | 106 ++++++++ .../settings/collections_nav_bar/view.dart | 199 +++++++++++++++ .../collections_nav_bar_settings.dart | 120 +++++++++ .../collections_nav_bar_settings.g.dart | 110 ++++++++ np_collection/lib/src/list_extension.dart | 3 + 21 files changed, 1220 insertions(+), 213 deletions(-) create mode 100644 app/lib/widget/home_collections/nav_bar_buttons.dart create mode 100644 app/lib/widget/settings/collections_nav_bar/bloc.dart create mode 100644 app/lib/widget/settings/collections_nav_bar/buttons.dart create mode 100644 app/lib/widget/settings/collections_nav_bar/state_event.dart create mode 100644 app/lib/widget/settings/collections_nav_bar/view.dart create mode 100644 app/lib/widget/settings/collections_nav_bar_settings.dart create mode 100644 app/lib/widget/settings/collections_nav_bar_settings.g.dart diff --git a/app/lib/controller/pref_controller.dart b/app/lib/controller/pref_controller.dart index feaae446..5eca835b 100644 --- a/app/lib/controller/pref_controller.dart +++ b/app/lib/controller/pref_controller.dart @@ -12,9 +12,11 @@ import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/language_util.dart'; import 'package:nc_photos/protected_page_handler.dart'; import 'package:nc_photos/size.dart'; +import 'package:nc_photos/widget/home_collections.dart'; import 'package:nc_photos/widget/viewer.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_common/object_util.dart'; +import 'package:np_common/type.dart'; import 'package:np_gps_map/np_gps_map.dart'; import 'package:np_string/np_string.dart'; import 'package:rxdart/rxdart.dart'; @@ -261,6 +263,17 @@ class PrefController { defaultValue: _viewerBottomAppBarButtonsDefault, ); + Future setHomeCollectionsNavBarButtons( + List? value) => + _setOrRemove( + controller: _homeCollectionsNavBarButtonsController, + setter: (pref, value) => pref.setHomeCollectionsNavBarButtonsJson( + jsonEncode(value.map((e) => e.toJson()).toList())), + remover: (pref) => pref.setHomeCollectionsNavBarButtonsJson(null), + value: value, + defaultValue: _homeCollectionsNavBarButtonsDefault, + ); + Future _set({ required BehaviorSubject controller, required Future Function(Pref pref, T value) setter, @@ -406,6 +419,14 @@ class PrefController { @npSubjectAccessor late final _viewerBottomAppBarButtonsController = BehaviorSubject.seeded( pref.getViewerBottomAppBarButtons() ?? _viewerBottomAppBarButtonsDefault); + @npSubjectAccessor + late final _homeCollectionsNavBarButtonsController = BehaviorSubject.seeded( + pref.getHomeCollectionsNavBarButtonsJson()?.let((s) => + (jsonDecode(s) as List) + .cast() + .map(PrefHomeCollectionsNavButton.fromJson) + .toList()) ?? + _homeCollectionsNavBarButtonsDefault); } extension PrefControllerExtension on PrefController { @@ -561,6 +582,24 @@ const _viewerBottomAppBarButtonsDefault = [ ViewerAppBarButtonType.download, ViewerAppBarButtonType.delete, ]; +const _homeCollectionsNavBarButtonsDefault = [ + PrefHomeCollectionsNavButton( + type: HomeCollectionsNavBarButtonType.sharing, + isMinimized: false, + ), + PrefHomeCollectionsNavButton( + type: HomeCollectionsNavBarButtonType.edited, + isMinimized: false, + ), + PrefHomeCollectionsNavButton( + type: HomeCollectionsNavBarButtonType.archive, + isMinimized: false, + ), + PrefHomeCollectionsNavButton( + type: HomeCollectionsNavBarButtonType.trash, + isMinimized: false, + ), +]; @npLog // ignore: camel_case_types diff --git a/app/lib/controller/pref_controller.g.dart b/app/lib/controller/pref_controller.g.dart index 9b1c5bbb..f2e92c47 100644 --- a/app/lib/controller/pref_controller.g.dart +++ b/app/lib/controller/pref_controller.g.dart @@ -252,6 +252,18 @@ extension $PrefControllerNpSubjectAccessor on PrefController { viewerBottomAppBarButtons.distinct().skip(1); List get viewerBottomAppBarButtonsValue => _viewerBottomAppBarButtonsController.value; +// _homeCollectionsNavBarButtonsController + ValueStream> + get homeCollectionsNavBarButtons => + _homeCollectionsNavBarButtonsController.stream; + Stream> + get homeCollectionsNavBarButtonsNew => + homeCollectionsNavBarButtons.skip(1); + Stream> + get homeCollectionsNavBarButtonsChange => + homeCollectionsNavBarButtons.distinct().skip(1); + List get homeCollectionsNavBarButtonsValue => + _homeCollectionsNavBarButtonsController.value; } extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController { diff --git a/app/lib/controller/pref_controller/type.dart b/app/lib/controller/pref_controller/type.dart index 43c41786..25c93d2e 100644 --- a/app/lib/controller/pref_controller/type.dart +++ b/app/lib/controller/pref_controller/type.dart @@ -14,3 +14,24 @@ enum PrefMapDefaultRangeType { final int value; } + +class PrefHomeCollectionsNavButton { + const PrefHomeCollectionsNavButton({ + required this.type, + required this.isMinimized, + }); + + static PrefHomeCollectionsNavButton fromJson(JsonObj json) => + PrefHomeCollectionsNavButton( + type: HomeCollectionsNavBarButtonType.fromValue(json["type"]), + isMinimized: json["isMinimized"], + ); + + JsonObj toJson() => { + "type": type.index, + "isMinimized": isMinimized, + }; + + final HomeCollectionsNavBarButtonType type; + final bool isMinimized; +} diff --git a/app/lib/controller/pref_controller/util.dart b/app/lib/controller/pref_controller/util.dart index 491f0e09..ad3c6f2e 100644 --- a/app/lib/controller/pref_controller/util.dart +++ b/app/lib/controller/pref_controller/util.dart @@ -185,6 +185,16 @@ extension on Pref { value.map((e) => e.index).toList()); } } + + String? getHomeCollectionsNavBarButtonsJson() => + provider.getString(PrefKey.homeCollectionsNavBarButtons); + Future setHomeCollectionsNavBarButtonsJson(String? value) { + if (value == null) { + return provider.remove(PrefKey.homeCollectionsNavBarButtons); + } else { + return provider.setString(PrefKey.homeCollectionsNavBarButtons, value); + } + } } MapCoord? _tryMapCoordFromJson(dynamic json) { diff --git a/app/lib/entity/pref.dart b/app/lib/entity/pref.dart index 57e8b603..5cfb6a8b 100644 --- a/app/lib/entity/pref.dart +++ b/app/lib/entity/pref.dart @@ -117,6 +117,7 @@ enum PrefKey implements PrefKeyInterface { mapDefaultCustomRange, viewerAppBarButtons, viewerBottomAppBarButtons, + homeCollectionsNavBarButtons, ; @override @@ -215,6 +216,8 @@ enum PrefKey implements PrefKeyInterface { return "viewerAppBarButtons"; case PrefKey.viewerBottomAppBarButtons: return "viewerBottomAppBarButtons"; + case PrefKey.homeCollectionsNavBarButtons: + return "homeCollectionsNavBarButtons"; } } } diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 03387093..f02db856 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -360,6 +360,7 @@ "settingsViewerCustomizeBottomAppBarTitle": "Customize bottom app bar", "settingsShowDateInAlbumTitle": "Group photos by date", "settingsShowDateInAlbumDescription": "Apply only when the album is sorted by time", + "settingsCollectionsCustomizeNavigationBarTitle": "Customize navigation bar", "settingsImageEditTitle": "Editor", "@settingsImageEditTitle": { "description": "Include settings for image enhancements and the image editor" @@ -1512,6 +1513,10 @@ "@dragAndDropRearrangeButtons": { "description": "Instruction to customize buttons layout" }, + "customizeCollectionsNavBarDescription": "Drag and drop to rearrange buttons, tap the buttons above to minimize them", + "@customizeCollectionsNavBarDescription": { + "description": "Instruction to customize navigation bar buttons in the Collections page" + }, "errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues", "@errorUnauthenticated": { diff --git a/app/lib/l10n/untranslated-messages.txt b/app/lib/l10n/untranslated-messages.txt index d42861b6..e0acea0d 100644 --- a/app/lib/l10n/untranslated-messages.txt +++ b/app/lib/l10n/untranslated-messages.txt @@ -18,6 +18,7 @@ "settingsViewerCustomizeBottomAppBarTitle", "settingsShowDateInAlbumTitle", "settingsShowDateInAlbumDescription", + "settingsCollectionsCustomizeNavigationBarTitle", "settingsImageEditTitle", "settingsImageEditDescription", "settingsEnhanceMaxResolutionTitle2", @@ -265,6 +266,7 @@ "alternativeSignIn", "livePhotoTooltip", "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -278,17 +280,21 @@ "cs": [ "settingsViewerCustomizeAppBarTitle", "settingsViewerCustomizeBottomAppBarTitle", + "settingsCollectionsCustomizeNavigationBarTitle", "alternativeSignIn", "livePhotoTooltip", - "dragAndDropRearrangeButtons" + "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription" ], "de": [ "settingsViewerCustomizeAppBarTitle", "settingsViewerCustomizeBottomAppBarTitle", + "settingsCollectionsCustomizeNavigationBarTitle", "alternativeSignIn", "livePhotoTooltip", - "dragAndDropRearrangeButtons" + "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription" ], "el": [ @@ -308,6 +314,7 @@ "settingsMemoriesRangeValueText", "settingsViewerCustomizeAppBarTitle", "settingsViewerCustomizeBottomAppBarTitle", + "settingsCollectionsCustomizeNavigationBarTitle", "settingsImageEditTitle", "settingsImageEditDescription", "settingsEnhanceMaxResolutionTitle2", @@ -441,20 +448,24 @@ "todayText", "alternativeSignIn", "livePhotoTooltip", - "dragAndDropRearrangeButtons" + "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription" ], "es": [ "settingsViewerCustomizeAppBarTitle", "settingsViewerCustomizeBottomAppBarTitle", + "settingsCollectionsCustomizeNavigationBarTitle", "alternativeSignIn", "livePhotoTooltip", - "dragAndDropRearrangeButtons" + "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription" ], "fi": [ "settingsViewerCustomizeAppBarTitle", "settingsViewerCustomizeBottomAppBarTitle", + "settingsCollectionsCustomizeNavigationBarTitle", "settingsThemePrimaryColor", "settingsThemeSecondaryColor", "settingsThemePresets", @@ -493,12 +504,14 @@ "todayText", "alternativeSignIn", "livePhotoTooltip", - "dragAndDropRearrangeButtons" + "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription" ], "fr": [ "settingsViewerCustomizeAppBarTitle", "settingsViewerCustomizeBottomAppBarTitle", + "settingsCollectionsCustomizeNavigationBarTitle", "settingsThemePrimaryColor", "settingsThemeSecondaryColor", "settingsThemePresets", @@ -537,13 +550,15 @@ "todayText", "alternativeSignIn", "livePhotoTooltip", - "dragAndDropRearrangeButtons" + "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription" ], "it": [ "settingsPersonProviderTitle", "settingsViewerCustomizeAppBarTitle", "settingsViewerCustomizeBottomAppBarTitle", + "settingsCollectionsCustomizeNavigationBarTitle", "settingsImageEditTitle", "settingsThemePrimaryColor", "settingsThemeSecondaryColor", @@ -586,7 +601,8 @@ "todayText", "alternativeSignIn", "livePhotoTooltip", - "dragAndDropRearrangeButtons" + "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription" ], "nl": [ @@ -647,6 +663,7 @@ "settingsViewerCustomizeBottomAppBarTitle", "settingsShowDateInAlbumTitle", "settingsShowDateInAlbumDescription", + "settingsCollectionsCustomizeNavigationBarTitle", "settingsImageEditTitle", "settingsImageEditDescription", "settingsEnhanceMaxResolutionTitle2", @@ -972,6 +989,7 @@ "alternativeSignIn", "livePhotoTooltip", "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -986,6 +1004,7 @@ "settingsMemoriesRangeValueText", "settingsViewerCustomizeAppBarTitle", "settingsViewerCustomizeBottomAppBarTitle", + "settingsCollectionsCustomizeNavigationBarTitle", "settingsThemePrimaryColor", "settingsThemeSecondaryColor", "settingsThemePresets", @@ -1027,7 +1046,8 @@ "todayText", "alternativeSignIn", "livePhotoTooltip", - "dragAndDropRearrangeButtons" + "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription" ], "pt": [ @@ -1036,6 +1056,7 @@ "settingsPersonProviderTitle", "settingsViewerCustomizeAppBarTitle", "settingsViewerCustomizeBottomAppBarTitle", + "settingsCollectionsCustomizeNavigationBarTitle", "settingsSeedColorSystemColorDescription", "settingsThemePrimaryColor", "settingsThemeSecondaryColor", @@ -1091,12 +1112,14 @@ "todayText", "alternativeSignIn", "livePhotoTooltip", - "dragAndDropRearrangeButtons" + "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription" ], "ru": [ "settingsViewerCustomizeAppBarTitle", "settingsViewerCustomizeBottomAppBarTitle", + "settingsCollectionsCustomizeNavigationBarTitle", "settingsThemePrimaryColor", "settingsThemeSecondaryColor", "settingsThemePresets", @@ -1135,15 +1158,18 @@ "todayText", "alternativeSignIn", "livePhotoTooltip", - "dragAndDropRearrangeButtons" + "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription" ], "tr": [ "settingsViewerCustomizeAppBarTitle", "settingsViewerCustomizeBottomAppBarTitle", + "settingsCollectionsCustomizeNavigationBarTitle", "alternativeSignIn", "livePhotoTooltip", - "dragAndDropRearrangeButtons" + "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription" ], "zh": [ @@ -1152,6 +1178,7 @@ "settingsMemoriesRangeValueText", "settingsViewerCustomizeAppBarTitle", "settingsViewerCustomizeBottomAppBarTitle", + "settingsCollectionsCustomizeNavigationBarTitle", "settingsSeedColorDescription", "settingsSeedColorSystemColorDescription", "settingsThemePrimaryColor", @@ -1218,7 +1245,8 @@ "todayText", "alternativeSignIn", "livePhotoTooltip", - "dragAndDropRearrangeButtons" + "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription" ], "zh_Hant": [ @@ -1238,6 +1266,7 @@ "settingsMemoriesRangeValueText", "settingsViewerCustomizeAppBarTitle", "settingsViewerCustomizeBottomAppBarTitle", + "settingsCollectionsCustomizeNavigationBarTitle", "settingsImageEditTitle", "settingsImageEditDescription", "settingsEnhanceMaxResolutionTitle2", @@ -1387,6 +1416,7 @@ "todayText", "alternativeSignIn", "livePhotoTooltip", - "dragAndDropRearrangeButtons" + "dragAndDropRearrangeButtons", + "customizeCollectionsNavBarDescription" ] } diff --git a/app/lib/widget/home_collections.dart b/app/lib/widget/home_collections.dart index 63a4c47a..7d8276f6 100644 --- a/app/lib/widget/home_collections.dart +++ b/app/lib/widget/home_collections.dart @@ -51,6 +51,7 @@ import 'package:to_string/to_string.dart'; part 'home_collections.g.dart'; part 'home_collections/app_bar.dart'; part 'home_collections/bloc.dart'; +part 'home_collections/nav_bar_buttons.dart'; part 'home_collections/navigation_bar.dart'; part 'home_collections/state_event.dart'; part 'home_collections/type.dart'; @@ -226,6 +227,7 @@ typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; typedef _BlocListener = BlocListener<_Bloc, _State>; // typedef _BlocListenerT = BlocListenerT<_Bloc, _State, T>; typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; +typedef _Emitter = Emitter<_State>; extension on BuildContext { _Bloc get bloc => read<_Bloc>(); diff --git a/app/lib/widget/home_collections.g.dart b/app/lib/widget/home_collections.g.dart index 31d70f16..a77dda91 100644 --- a/app/lib/widget/home_collections.g.dart +++ b/app/lib/widget/home_collections.g.dart @@ -20,6 +20,7 @@ abstract class $_StateCopyWithWorker { List<_Item>? transformedItems, Set<_Item>? selectedItems, Map? itemCounts, + List? navBarButtons, ExceptionEvent? error, ExceptionEvent? removeError}); } @@ -35,6 +36,7 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { dynamic transformedItems, dynamic selectedItems, dynamic itemCounts, + dynamic navBarButtons, dynamic error = copyWithNull, dynamic removeError = copyWithNull}) { return _State( @@ -45,6 +47,8 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { transformedItems as List<_Item>? ?? that.transformedItems, selectedItems: selectedItems as Set<_Item>? ?? that.selectedItems, itemCounts: itemCounts as Map? ?? that.itemCounts, + navBarButtons: navBarButtons as List? ?? + that.navBarButtons, error: error == copyWithNull ? that.error : error as ExceptionEvent?, removeError: removeError == copyWithNull ? that.removeError @@ -106,7 +110,7 @@ extension _$_ItemNpLog on _Item { extension _$_StateToString on _State { String _$toString() { // ignore: unnecessary_string_interpolations - return "_State {collections: [length: ${collections.length}], sort: ${sort.name}, isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, itemCounts: {length: ${itemCounts.length}}, error: $error, removeError: $removeError}"; + return "_State {collections: [length: ${collections.length}], sort: ${sort.name}, isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, itemCounts: {length: ${itemCounts.length}}, navBarButtons: [length: ${navBarButtons.length}], error: $error, removeError: $removeError}"; } } @@ -166,6 +170,13 @@ extension _$_SetItemCountToString on _SetItemCount { } } +extension _$_SetNavBarButtonsToString on _SetNavBarButtons { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetNavBarButtons {value: [length: ${value.length}]}"; + } +} + extension _$_SetErrorToString on _SetError { String _$toString() { // ignore: unnecessary_string_interpolations diff --git a/app/lib/widget/home_collections/bloc.dart b/app/lib/widget/home_collections/bloc.dart index a9817df3..fe3971ba 100644 --- a/app/lib/widget/home_collections/bloc.dart +++ b/app/lib/widget/home_collections/bloc.dart @@ -9,6 +9,7 @@ class _Bloc extends Bloc<_Event, _State> required this.prefController, }) : super(_State.init( sort: prefController.homeAlbumsSortValue, + navBarButtons: prefController.homeCollectionsNavBarButtonsValue, )) { on<_LoadCollections>(_onLoad); on<_ReloadCollections>(_onReload); @@ -21,6 +22,8 @@ class _Bloc extends Bloc<_Event, _State> on<_SetCollectionSort>(_onSetCollectionSort); on<_SetItemCount>(_onSetItemCount); + on<_SetNavBarButtons>(_onSetNavBarButtons); + on<_SetError>(_onSetError); _subscriptions.add(prefController.homeAlbumsSortChange.listen((event) { @@ -39,6 +42,10 @@ class _Bloc extends Bloc<_Event, _State> })); } })); + _subscriptions + .add(prefController.homeCollectionsNavBarButtonsChange.listen((event) { + add(_SetNavBarButtons(event)); + })); } @override @@ -134,6 +141,11 @@ class _Bloc extends Bloc<_Event, _State> emit(state.copyWith(itemCounts: next)); } + void _onSetNavBarButtons(_SetNavBarButtons ev, _Emitter emit) { + _log.info(ev); + emit(state.copyWith(navBarButtons: ev.value)); + } + void _onSetError(_SetError ev, Emitter<_State> emit) { _log.info(ev); emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); diff --git a/app/lib/widget/home_collections/nav_bar_buttons.dart b/app/lib/widget/home_collections/nav_bar_buttons.dart new file mode 100644 index 00000000..324c65cd --- /dev/null +++ b/app/lib/widget/home_collections/nav_bar_buttons.dart @@ -0,0 +1,241 @@ +part of '../home_collections.dart'; + +class HomeCollectionsNavBarButton extends StatelessWidget { + const HomeCollectionsNavBarButton({ + super.key, + required this.icon, + required this.label, + required this.isMinimized, + this.isShowIndicator = false, + this.isEnabled = true, + this.isUseTooltipWhenMinimized = true, + this.onPressed, + }); + + @override + Widget build(BuildContext context) { + if (isMinimized) { + return IconButtonTheme( + data: const IconButtonThemeData( + style: ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + child: IconButton.outlined( + icon: Stack( + children: [ + IconTheme( + data: IconThemeData( + size: 18, + color: Theme.of(context).colorScheme.secondary, + ), + child: icon, + ), + if (isShowIndicator) + const Positioned( + right: 2, + top: 2, + child: _NavBarButtonIndicator(), + ), + ], + ), + tooltip: isUseTooltipWhenMinimized ? label : null, + onPressed: isEnabled ? onPressed : null, + ), + ); + } else { + return ActionChip( + avatar: icon, + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(label), + if (isShowIndicator) ...const [ + SizedBox(width: 4), + _NavBarButtonIndicator(), + ], + ], + ), + onPressed: isEnabled ? onPressed : null, + ); + } + } + + final Widget icon; + final String label; + final bool isMinimized; + final bool isShowIndicator; + final bool isEnabled; + final bool isUseTooltipWhenMinimized; + final VoidCallback? onPressed; +} + +class _NavBarButton extends StatelessWidget { + const _NavBarButton({ + required this.icon, + required this.label, + required this.isMinimized, + this.isShowIndicator = false, + this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return _BlocSelector( + selector: (state) => state.selectedItems.isEmpty, + builder: (context, isEnabled) => HomeCollectionsNavBarButton( + icon: icon, + label: label, + isMinimized: isMinimized, + isShowIndicator: isShowIndicator, + isEnabled: isEnabled, + onPressed: onPressed, + ), + ); + } + + final Widget icon; + final String label; + final bool isMinimized; + final bool isShowIndicator; + final VoidCallback? onPressed; +} + +@npLog +class _NavBarNewButton extends StatelessWidget { + const _NavBarNewButton(); + + @override + Widget build(BuildContext context) { + return _NavBarButton( + icon: const Icon(Icons.add_outlined), + label: L10n.global().createCollectionTooltip, + isMinimized: true, + onPressed: () async { + try { + final collection = await showDialog( + context: context, + builder: (_) => NewCollectionDialog( + account: context.bloc.account, + ), + ); + if (collection == null) { + return; + } + // Right now we don't have a way to add photos inside the + // CollectionBrowser, eventually we should add that and remove this + // branching + if (collection.isDynamicCollection) { + // open the newly created collection + unawaited(Navigator.of(context).pushNamed( + CollectionBrowser.routeName, + arguments: CollectionBrowserArguments(collection), + )); + } + } catch (e, stacktrace) { + _log.shout("[build] Uncaught exception", e, stacktrace); + context.addEvent(_SetError(AppMessageException( + L10n.global().createCollectionFailureNotification))); + } + }, + ); + } +} + +class _NavBarSharingButton extends StatelessWidget { + const _NavBarSharingButton({ + required this.isMinimized, + }); + + @override + Widget build(BuildContext context) { + return ValueStreamBuilderEx( + stream: context + .read() + .accountPrefController + .hasNewSharedAlbum, + builder: StreamWidgetBuilder.value( + (context, hasNewSharedAlbum) => _NavBarButton( + icon: const Icon(Icons.share_outlined), + label: L10n.global().collectionSharingLabel, + isMinimized: isMinimized, + isShowIndicator: hasNewSharedAlbum, + onPressed: () { + Navigator.of(context).pushNamed( + SharingBrowser.routeName, + arguments: SharingBrowserArguments(context.bloc.account), + ); + }, + ), + ), + ); + } + + final bool isMinimized; +} + +class _NavBarEditedButton extends StatelessWidget { + const _NavBarEditedButton({ + required this.isMinimized, + }); + + @override + Widget build(BuildContext context) { + return _NavBarButton( + icon: const Icon(Icons.auto_fix_high_outlined), + label: L10n.global().collectionEditedPhotosLabel, + isMinimized: isMinimized, + onPressed: () { + Navigator.of(context).pushNamed( + EnhancedPhotoBrowser.routeName, + arguments: const EnhancedPhotoBrowserArguments(null), + ); + }, + ); + } + + final bool isMinimized; +} + +class _NavBarArchiveButton extends StatelessWidget { + const _NavBarArchiveButton({ + required this.isMinimized, + }); + + @override + Widget build(BuildContext context) { + return _NavBarButton( + icon: const Icon(Icons.archive_outlined), + label: L10n.global().albumArchiveLabel, + isMinimized: isMinimized, + onPressed: () { + Navigator.of(context).pushNamed(ArchiveBrowser.routeName); + }, + ); + } + + final bool isMinimized; +} + +class _NavBarTrashButton extends StatelessWidget { + const _NavBarTrashButton({ + required this.isMinimized, + }); + + @override + Widget build(BuildContext context) { + return _NavBarButton( + icon: const Icon(Icons.delete_outlined), + label: L10n.global().albumTrashLabel, + isMinimized: isMinimized, + onPressed: () { + Navigator.of(context).pushNamed( + TrashbinBrowser.routeName, + arguments: TrashbinBrowserArguments(context.bloc.account), + ); + }, + ); + } + + final bool isMinimized; +} diff --git a/app/lib/widget/home_collections/navigation_bar.dart b/app/lib/widget/home_collections/navigation_bar.dart index 3f608f08..999ee7b1 100644 --- a/app/lib/widget/home_collections/navigation_bar.dart +++ b/app/lib/widget/home_collections/navigation_bar.dart @@ -37,8 +37,6 @@ class _NavigationBarState extends State<_NavigationBar> { @override Widget build(BuildContext context) { - final buttons = - _buttons.map((e) => _buildButton(context, e)).nonNulls.toList(); return SliverToBoxAdapter( child: SizedBox( height: 48, @@ -47,13 +45,25 @@ class _NavigationBarState extends State<_NavigationBar> { Expanded( child: Stack( children: [ - ListView.separated( - controller: _scrollController, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.only(left: 16), - itemCount: buttons.length, - itemBuilder: (context, i) => buttons[i], - separatorBuilder: (context, _) => const SizedBox(width: 16), + _BlocSelector( + selector: (state) => state.navBarButtons, + builder: (context, navBarButtons) { + final buttons = navBarButtons + .map((e) => _buildButton(context, e)) + .nonNulls + .toList(); + return ListView.separated( + controller: _scrollController, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.only(left: 16), + itemCount: buttons.length, + itemBuilder: (context, i) => Center( + child: buttons[i], + ), + separatorBuilder: (context, _) => + const SizedBox(width: 12), + ); + }, ), if (_hasLeftContent) Positioned( @@ -113,19 +123,18 @@ class _NavigationBarState extends State<_NavigationBar> { ); } - Widget? _buildButton( - BuildContext context, HomeCollectionsNavBarButtonType type) { - switch (type) { + Widget? _buildButton(BuildContext context, PrefHomeCollectionsNavButton btn) { + switch (btn.type) { case HomeCollectionsNavBarButtonType.sharing: - return const _NavBarSharingButton(); + return _NavBarSharingButton(isMinimized: btn.isMinimized); case HomeCollectionsNavBarButtonType.edited: return features.isSupportEnhancement - ? const _NavBarEditedButton() + ? _NavBarEditedButton(isMinimized: btn.isMinimized) : null; case HomeCollectionsNavBarButtonType.archive: - return const _NavBarArchiveButton(); + return _NavBarArchiveButton(isMinimized: btn.isMinimized); case HomeCollectionsNavBarButtonType.trash: - return const _NavBarTrashButton(); + return _NavBarTrashButton(isMinimized: btn.isMinimized); } } @@ -175,13 +184,6 @@ class _NavigationBarState extends State<_NavigationBar> { Timer(const Duration(milliseconds: 100), _ensureUpdateButtonScroll); } - static const _buttons = [ - HomeCollectionsNavBarButtonType.sharing, - HomeCollectionsNavBarButtonType.edited, - HomeCollectionsNavBarButtonType.archive, - HomeCollectionsNavBarButtonType.trash, - ]; - late final ScrollController _scrollController; var _hasFirstScrollUpdate = false; var _hasLeftContent = false; @@ -202,179 +204,3 @@ class _NavBarButtonIndicator extends StatelessWidget { ); } } - -class _NavBarButton extends StatelessWidget { - const _NavBarButton({ - required this.icon, - required this.label, - required this.isMinimized, - this.isShowIndicator = false, - required this.onPressed, - }); - - @override - Widget build(BuildContext context) { - return _BlocSelector( - selector: (state) => state.selectedItems.isEmpty, - builder: (context, isEnabled) => isMinimized - ? IconButton.outlined( - icon: Stack( - children: [ - icon, - if (isShowIndicator) - const Positioned( - right: 2, - top: 2, - child: _NavBarButtonIndicator(), - ), - ], - ), - tooltip: label, - onPressed: isEnabled ? onPressed : null, - ) - : ActionChip( - avatar: icon, - label: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(label), - if (isShowIndicator) ...const [ - SizedBox(width: 4), - _NavBarButtonIndicator(), - ], - ], - ), - onPressed: isEnabled ? onPressed : null, - ), - ); - } - - final Widget icon; - final String label; - final bool isMinimized; - final bool isShowIndicator; - final VoidCallback onPressed; -} - -@npLog -class _NavBarNewButton extends StatelessWidget { - const _NavBarNewButton(); - - @override - Widget build(BuildContext context) { - return _NavBarButton( - icon: const Icon(Icons.add_outlined), - label: L10n.global().createCollectionTooltip, - isMinimized: true, - onPressed: () async { - try { - final collection = await showDialog( - context: context, - builder: (_) => NewCollectionDialog( - account: context.bloc.account, - ), - ); - if (collection == null) { - return; - } - // Right now we don't have a way to add photos inside the - // CollectionBrowser, eventually we should add that and remove this - // branching - if (collection.isDynamicCollection) { - // open the newly created collection - unawaited(Navigator.of(context).pushNamed( - CollectionBrowser.routeName, - arguments: CollectionBrowserArguments(collection), - )); - } - } catch (e, stacktrace) { - _log.shout("[build] Uncaught exception", e, stacktrace); - context.addEvent(_SetError(AppMessageException( - L10n.global().createCollectionFailureNotification))); - } - }, - ); - } -} - -class _NavBarSharingButton extends StatelessWidget { - const _NavBarSharingButton(); - - @override - Widget build(BuildContext context) { - return ValueStreamBuilderEx( - stream: context - .read() - .accountPrefController - .hasNewSharedAlbum, - builder: StreamWidgetBuilder.value( - (context, hasNewSharedAlbum) => _NavBarButton( - icon: const Icon(Icons.share_outlined), - label: L10n.global().collectionSharingLabel, - isMinimized: false, - isShowIndicator: hasNewSharedAlbum, - onPressed: () { - Navigator.of(context).pushNamed( - SharingBrowser.routeName, - arguments: SharingBrowserArguments(context.bloc.account), - ); - }, - ), - ), - ); - } -} - -class _NavBarEditedButton extends StatelessWidget { - const _NavBarEditedButton(); - - @override - Widget build(BuildContext context) { - return _NavBarButton( - icon: const Icon(Icons.auto_fix_high_outlined), - label: L10n.global().collectionEditedPhotosLabel, - isMinimized: false, - onPressed: () { - Navigator.of(context).pushNamed( - EnhancedPhotoBrowser.routeName, - arguments: const EnhancedPhotoBrowserArguments(null), - ); - }, - ); - } -} - -class _NavBarArchiveButton extends StatelessWidget { - const _NavBarArchiveButton(); - - @override - Widget build(BuildContext context) { - return _NavBarButton( - icon: const Icon(Icons.archive_outlined), - label: L10n.global().albumArchiveLabel, - isMinimized: false, - onPressed: () { - Navigator.of(context).pushNamed(ArchiveBrowser.routeName); - }, - ); - } -} - -class _NavBarTrashButton extends StatelessWidget { - const _NavBarTrashButton(); - - @override - Widget build(BuildContext context) { - return _NavBarButton( - icon: const Icon(Icons.delete_outlined), - label: L10n.global().albumTrashLabel, - isMinimized: false, - onPressed: () { - Navigator.of(context).pushNamed( - TrashbinBrowser.routeName, - arguments: TrashbinBrowserArguments(context.bloc.account), - ); - }, - ); - } -} diff --git a/app/lib/widget/home_collections/state_event.dart b/app/lib/widget/home_collections/state_event.dart index fc35c3ed..245ea086 100644 --- a/app/lib/widget/home_collections/state_event.dart +++ b/app/lib/widget/home_collections/state_event.dart @@ -10,12 +10,14 @@ class _State { required this.transformedItems, required this.selectedItems, required this.itemCounts, + required this.navBarButtons, this.error, required this.removeError, }); factory _State.init({ required collection_util.CollectionSort sort, + required List navBarButtons, }) { return _State( collections: [], @@ -24,6 +26,7 @@ class _State { transformedItems: [], selectedItems: {}, itemCounts: {}, + navBarButtons: navBarButtons, removeError: null, ); } @@ -38,6 +41,8 @@ class _State { final Set<_Item> selectedItems; final Map itemCounts; + final List navBarButtons; + final ExceptionEvent? error; final ExceptionEvent? removeError; } @@ -128,6 +133,16 @@ class _SetItemCount implements _Event { final int value; } +@toString +class _SetNavBarButtons implements _Event { + const _SetNavBarButtons(this.value); + + @override + String toString() => _$toString(); + + final List value; +} + @toString class _SetError implements _Event { const _SetError(this.error, [this.stackTrace]); diff --git a/app/lib/widget/settings/collection_settings.dart b/app/lib/widget/settings/collection_settings.dart index 076d4f35..3c518a25 100644 --- a/app/lib/widget/settings/collection_settings.dart +++ b/app/lib/widget/settings/collection_settings.dart @@ -8,6 +8,7 @@ import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/widget/page_visibility_mixin.dart'; +import 'package:nc_photos/widget/settings/collections_nav_bar_settings.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:to_string/to_string.dart'; @@ -85,6 +86,17 @@ class _WrappedAlbumSettingsState extends State<_WrappedAlbumSettings> ); }, ), + ListTile( + title: Text(L10n.global() + .settingsCollectionsCustomizeNavigationBarTitle), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const CollectionsNavBarSettings(), + ), + ); + }, + ), ], ), ), diff --git a/app/lib/widget/settings/collections_nav_bar/bloc.dart b/app/lib/widget/settings/collections_nav_bar/bloc.dart new file mode 100644 index 00000000..9032195f --- /dev/null +++ b/app/lib/widget/settings/collections_nav_bar/bloc.dart @@ -0,0 +1,122 @@ +part of '../collections_nav_bar_settings.dart'; + +@npLog +class _Bloc extends Bloc<_Event, _State> + with BlocLogger, BlocForEachMixin<_Event, _State> { + _Bloc({ + required this.prefController, + required this.isBottom, + }) : super(_State.init( + buttons: prefController.homeCollectionsNavBarButtonsValue, + )) { + on<_MoveButton>(_onMoveButton); + on<_RemoveButton>(_onRemoveButton); + on<_ToggleMinimized>(_onToggleMinimized); + on<_RevertDefault>(_onRevertDefault); + + on<_SetError>(_onSetError); + } + + @override + Future close() { + for (final s in _subscriptions) { + s.cancel(); + } + return super.close(); + } + + @override + String get tag => _log.fullName; + + @override + void onError(Object error, StackTrace stackTrace) { + // we need this to prevent onError being triggered recursively + if (!isClosed && !_isHandlingError) { + _isHandlingError = true; + try { + add(_SetError(error, stackTrace)); + } catch (_) {} + _isHandlingError = false; + } + super.onError(error, stackTrace); + } + + void _onMoveButton(_MoveButton ev, _Emitter emit) { + _log.info(ev); + final pos = state.buttons.indexWhere((e) => e.type == ev.which); + final found = pos >= 0 ? state.buttons[pos] : null; + final insert = found ?? + PrefHomeCollectionsNavButton(type: ev.which, isMinimized: false); + var result = + pos >= 0 ? state.buttons.removedAt(pos) : List.of(state.buttons); + if (ev.before == null && ev.after == null) { + // add at the beginning + emit(state.copyWith(buttons: result..insert(0, insert))); + return; + } + + final target = (ev.before ?? ev.after)!; + if (ev.which == target) { + // dropping on itself, do nothing + return; + } + final targetPos = result.indexWhere((e) => e.type == target); + if (targetPos == -1) { + _log.severe("[_onMoveButton] Target not found: $target"); + return; + } + if (ev.before != null) { + // insert before + result.insert(targetPos, insert); + } else { + // insert after + result.insert(targetPos + 1, insert); + } + _log.fine( + "[_onMoveButton] From ${state.buttons.toReadableString()} -> ${result.toReadableString()}"); + emit(state.copyWith(buttons: result)); + } + + void _onRemoveButton(_RemoveButton ev, _Emitter emit) { + _log.info(ev); + emit(state.copyWith( + buttons: state.buttons.removedWhere((e) => e.type == ev.value), + )); + } + + void _onToggleMinimized(_ToggleMinimized ev, _Emitter emit) { + _log.info(ev); + final result = List.of(state.buttons); + final pos = result.indexWhere((e) => e.type == ev.value); + if (pos == -1) { + // button not enabled + _log.severe( + "[_onToggleMinimized] Type not found in buttons: ${ev.value}"); + return; + } + result[pos] = PrefHomeCollectionsNavButton( + type: ev.value, + isMinimized: !result[pos].isMinimized, + ); + emit(state.copyWith(buttons: result)); + } + + Future _onRevertDefault(_RevertDefault ev, _Emitter emit) async { + _log.info(ev); + await prefController.setHomeCollectionsNavBarButtons(null); + emit(state.copyWith( + buttons: prefController.homeCollectionsNavBarButtonsValue, + )); + } + + void _onSetError(_SetError ev, _Emitter emit) { + _log.info(ev); + emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); + } + + final PrefController prefController; + final bool isBottom; + + final _subscriptions = []; + var _isHandlingError = false; +} diff --git a/app/lib/widget/settings/collections_nav_bar/buttons.dart b/app/lib/widget/settings/collections_nav_bar/buttons.dart new file mode 100644 index 00000000..ca01c239 --- /dev/null +++ b/app/lib/widget/settings/collections_nav_bar/buttons.dart @@ -0,0 +1,108 @@ +part of '../collections_nav_bar_settings.dart'; + +class _NewButton extends StatelessWidget { + const _NewButton(); + + @override + Widget build(BuildContext context) { + return HomeCollectionsNavBarButton( + icon: const Icon(Icons.add_outlined), + label: L10n.global().createCollectionTooltip, + isMinimized: true, + isUseTooltipWhenMinimized: false, + onPressed: () {}, + ); + } +} + +class _SharingButton extends StatelessWidget { + const _SharingButton({ + required this.isMinimized, + this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return HomeCollectionsNavBarButton( + icon: const Icon(Icons.share_outlined), + label: L10n.global().collectionSharingLabel, + isMinimized: isMinimized, + isUseTooltipWhenMinimized: false, + onPressed: () { + onPressed?.call(); + }, + ); + } + + final bool isMinimized; + final VoidCallback? onPressed; +} + +class _EditedButton extends StatelessWidget { + const _EditedButton({ + required this.isMinimized, + this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return HomeCollectionsNavBarButton( + icon: const Icon(Icons.auto_fix_high_outlined), + label: L10n.global().collectionEditedPhotosLabel, + isMinimized: isMinimized, + isUseTooltipWhenMinimized: false, + onPressed: () { + onPressed?.call(); + }, + ); + } + + final bool isMinimized; + final VoidCallback? onPressed; +} + +class _ArchiveButton extends StatelessWidget { + const _ArchiveButton({ + required this.isMinimized, + this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return HomeCollectionsNavBarButton( + icon: const Icon(Icons.archive_outlined), + label: L10n.global().albumArchiveLabel, + isMinimized: isMinimized, + isUseTooltipWhenMinimized: false, + onPressed: () { + onPressed?.call(); + }, + ); + } + + final bool isMinimized; + final VoidCallback? onPressed; +} + +class _TrashButton extends StatelessWidget { + const _TrashButton({ + required this.isMinimized, + this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return HomeCollectionsNavBarButton( + icon: const Icon(Icons.delete_outlined), + label: L10n.global().albumTrashLabel, + isMinimized: isMinimized, + isUseTooltipWhenMinimized: false, + onPressed: () { + onPressed?.call(); + }, + ); + } + + final bool isMinimized; + final VoidCallback? onPressed; +} diff --git a/app/lib/widget/settings/collections_nav_bar/state_event.dart b/app/lib/widget/settings/collections_nav_bar/state_event.dart new file mode 100644 index 00000000..eec93ee3 --- /dev/null +++ b/app/lib/widget/settings/collections_nav_bar/state_event.dart @@ -0,0 +1,106 @@ +part of '../collections_nav_bar_settings.dart'; + +@genCopyWith +@toString +class _State { + const _State({ + required this.buttons, + this.error, + }); + + factory _State.init({ + required List buttons, + }) { + return _State( + buttons: buttons, + ); + } + + @override + String toString() => _$toString(); + + final List buttons; + + final ExceptionEvent? error; +} + +abstract class _Event { + const _Event(); +} + +@toString +class _Init implements _Event { + const _Init(); + + @override + String toString() => _$toString(); +} + +@toString +class _MoveButton implements _Event { + const _MoveButton._({ + required this.which, + this.before, + this.after, + }); + + const _MoveButton.first({ + required HomeCollectionsNavBarButtonType which, + }) : this._(which: which); + + const _MoveButton.before({ + required HomeCollectionsNavBarButtonType which, + required HomeCollectionsNavBarButtonType target, + }) : this._(which: which, before: target); + + const _MoveButton.after({ + required HomeCollectionsNavBarButtonType which, + required HomeCollectionsNavBarButtonType target, + }) : this._(which: which, after: target); + + @override + String toString() => _$toString(); + + final HomeCollectionsNavBarButtonType which; + final HomeCollectionsNavBarButtonType? before; + final HomeCollectionsNavBarButtonType? after; +} + +@toString +class _RemoveButton implements _Event { + const _RemoveButton(this.value); + + @override + String toString() => _$toString(); + + final HomeCollectionsNavBarButtonType value; +} + +@toString +class _ToggleMinimized implements _Event { + const _ToggleMinimized(this.value); + + @override + String toString() => _$toString(); + + final HomeCollectionsNavBarButtonType value; +} + +@toString +class _RevertDefault implements _Event { + const _RevertDefault(); + + @override + String toString() => _$toString(); +} + +@toString +class _SetError implements _Event { + const _SetError(this.error, [this.stackTrace]); + + @override + String toString() => _$toString(); + + final Object error; + final StackTrace? stackTrace; +} diff --git a/app/lib/widget/settings/collections_nav_bar/view.dart b/app/lib/widget/settings/collections_nav_bar/view.dart new file mode 100644 index 00000000..fe8d7aed --- /dev/null +++ b/app/lib/widget/settings/collections_nav_bar/view.dart @@ -0,0 +1,199 @@ +part of '../collections_nav_bar_settings.dart'; + +class _DemoView extends StatelessWidget { + const _DemoView(); + + @override + Widget build(BuildContext context) { + return _BlocSelector( + selector: (state) => state.buttons, + builder: (context, buttons) { + final navBar = SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.only(left: 16 - 6), + itemCount: buttons.length, + itemBuilder: (context, i) { + final btn = buttons[i]; + return my.Draggable( + data: btn.type, + feedback: _CandidateButtonDelegate(btn.type), + onDropBefore: (data) { + context.addEvent(_MoveButton.before( + which: data, + target: btn.type, + )); + }, + onDropAfter: (data) { + context.addEvent(_MoveButton.after( + which: data, + target: btn.type, + )); + }, + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: _DemoButtonDelegate( + btn.type, + isMinimized: btn.isMinimized, + ), + ), + ), + ); + }, + ), + ), + const SizedBox(width: 8), + const _NewButton(), + const SizedBox(width: 16), + ], + ), + ); + if (buttons.isEmpty) { + return DragTarget( + builder: (context, candidateData, rejectedData) => SizedBox( + height: 48, + child: Stack( + children: [ + navBar, + IgnorePointer( + child: Opacity( + opacity: candidateData.isNotEmpty ? .35 : 0, + child: Container( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + onAcceptWithDetails: (details) { + context.addEvent(_MoveButton.first(which: details.data)); + }, + ); + } else { + return navBar; + } + }, + ); + } +} + +class _DemoButtonDelegate extends StatelessWidget { + const _DemoButtonDelegate( + this.type, { + required this.isMinimized, + }); + + @override + Widget build(BuildContext context) { + switch (type) { + case HomeCollectionsNavBarButtonType.sharing: + return _SharingButton( + isMinimized: isMinimized, + onPressed: () { + context.addEvent(_ToggleMinimized(type)); + }, + ); + case HomeCollectionsNavBarButtonType.edited: + return _EditedButton( + isMinimized: isMinimized, + onPressed: () { + context.addEvent(_ToggleMinimized(type)); + }, + ); + case HomeCollectionsNavBarButtonType.archive: + return _ArchiveButton( + isMinimized: isMinimized, + onPressed: () { + context.addEvent(_ToggleMinimized(type)); + }, + ); + case HomeCollectionsNavBarButtonType.trash: + return _TrashButton( + isMinimized: isMinimized, + onPressed: () { + context.addEvent(_ToggleMinimized(type)); + }, + ); + } + } + + final HomeCollectionsNavBarButtonType type; + final bool isMinimized; +} + +class _CandidateGrid extends StatelessWidget { + const _CandidateGrid(); + + @override + Widget build(BuildContext context) { + return DragTarget( + builder: (context, candidateData, rejectedData) => Stack( + children: [ + _BlocSelector( + selector: (state) => state.buttons, + builder: (context, buttons) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Wrap( + direction: Axis.horizontal, + spacing: 16, + runSpacing: 8, + children: HomeCollectionsNavBarButtonType.values + .where((e) => !buttons.any((b) => b.type == e)) + .map((e) => my.Draggable( + data: e, + feedback: _CandidateButtonDelegate(e), + child: _CandidateButtonDelegate(e), + )) + .toList(), + ), + ), + ), + IgnorePointer( + child: Opacity( + opacity: candidateData.isNotEmpty ? .1 : 0, + child: Container( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + onAcceptWithDetails: (details) { + context.addEvent(_RemoveButton(details.data)); + }, + onWillAcceptWithDetails: (details) { + // moving down + return context.state.buttons.any((e) => e.type == details.data); + }, + ); + } +} + +class _CandidateButtonDelegate extends StatelessWidget { + const _CandidateButtonDelegate(this.type); + + @override + Widget build(BuildContext context) { + switch (type) { + case HomeCollectionsNavBarButtonType.sharing: + return const _SharingButton(isMinimized: false); + case HomeCollectionsNavBarButtonType.edited: + return const _EditedButton(isMinimized: false); + case HomeCollectionsNavBarButtonType.archive: + return const _ArchiveButton(isMinimized: false); + case HomeCollectionsNavBarButtonType.trash: + return const _TrashButton(isMinimized: false); + } + } + + final HomeCollectionsNavBarButtonType type; +} diff --git a/app/lib/widget/settings/collections_nav_bar_settings.dart b/app/lib/widget/settings/collections_nav_bar_settings.dart new file mode 100644 index 00000000..75094383 --- /dev/null +++ b/app/lib/widget/settings/collections_nav_bar_settings.dart @@ -0,0 +1,120 @@ +import 'dart:async'; + +import 'package:copy_with/copy_with.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/bloc_util.dart'; +import 'package:nc_photos/controller/pref_controller.dart'; +import 'package:nc_photos/exception_event.dart'; +import 'package:nc_photos/snack_bar_manager.dart'; +import 'package:nc_photos/widget/draggable.dart' as my; +import 'package:nc_photos/widget/home_collections.dart'; +import 'package:nc_photos/widget/page_visibility_mixin.dart'; +import 'package:np_codegen/np_codegen.dart'; +import 'package:np_collection/np_collection.dart'; +import 'package:to_string/to_string.dart'; + +part 'collections_nav_bar/bloc.dart'; +part 'collections_nav_bar/buttons.dart'; +part 'collections_nav_bar/state_event.dart'; +part 'collections_nav_bar/view.dart'; +part 'collections_nav_bar_settings.g.dart'; + +class CollectionsNavBarSettings extends StatelessWidget { + const CollectionsNavBarSettings({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => _Bloc( + isBottom: true, + prefController: context.read(), + ), + child: const _WrappedCollectionsNavBarSettings(), + ); + } +} + +class _WrappedCollectionsNavBarSettings extends StatefulWidget { + const _WrappedCollectionsNavBarSettings(); + + @override + State createState() => + _WrappedCollectionsNavBarSettingsState(); +} + +@npLog +class _WrappedCollectionsNavBarSettingsState + extends State<_WrappedCollectionsNavBarSettings> + with RouteAware, PageVisibilityMixin { + @override + Widget build(BuildContext context) { + return PopScope( + canPop: true, + onPopInvoked: (_) { + final prefController = context.bloc.prefController; + final from = prefController.homeCollectionsNavBarButtonsValue; + final to = context.state.buttons; + if (!listEquals(from, to)) { + _log.info("[build] Updated: ${to.toReadableString()}"); + prefController.setHomeCollectionsNavBarButtons(to); + } + }, + child: Scaffold( + appBar: AppBar( + title: Text( + L10n.global().settingsCollectionsCustomizeNavigationBarTitle), + actions: [ + TextButton( + onPressed: () { + context.addEvent(const _RevertDefault()); + }, + child: Text(L10n.global().defaultButtonLabel), + ), + ], + ), + body: MultiBlocListener( + listeners: [ + _BlocListener( + listenWhen: (previous, current) => + previous.error != current.error, + listener: (context, state) { + if (state.error != null && isPageVisible()) { + SnackBarManager() + .showSnackBarForException(state.error!.error); + } + }, + ), + ], + child: Column( + children: [ + const _DemoView(), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: + Text(L10n.global().customizeCollectionsNavBarDescription), + ), + const Expanded(child: _CandidateGrid()), + ], + ), + ), + ), + ); + } +} + +// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; +typedef _BlocListener = BlocListener<_Bloc, _State>; +// typedef _BlocListenerT = BlocListenerT<_Bloc, _State, T>; +typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; +typedef _Emitter = Emitter<_State>; + +extension on BuildContext { + _Bloc get bloc => read<_Bloc>(); + _State get state => bloc.state; + void addEvent(_Event event) => bloc.add(event); +} diff --git a/app/lib/widget/settings/collections_nav_bar_settings.g.dart b/app/lib/widget/settings/collections_nav_bar_settings.g.dart new file mode 100644 index 00000000..c3409f05 --- /dev/null +++ b/app/lib/widget/settings/collections_nav_bar_settings.g.dart @@ -0,0 +1,110 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'collections_nav_bar_settings.dart'; + +// ************************************************************************** +// CopyWithLintRuleGenerator +// ************************************************************************** + +// ignore_for_file: library_private_types_in_public_api, duplicate_ignore + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class $_StateCopyWithWorker { + _State call( + {List? buttons, ExceptionEvent? error}); +} + +class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { + _$_StateCopyWithWorkerImpl(this.that); + + @override + _State call({dynamic buttons, dynamic error = copyWithNull}) { + return _State( + buttons: buttons as List? ?? that.buttons, + error: error == copyWithNull ? that.error : error as ExceptionEvent?); + } + + final _State that; +} + +extension $_StateCopyWith on _State { + $_StateCopyWithWorker get copyWith => _$copyWith; + $_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this); +} + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$_WrappedCollectionsNavBarSettingsStateNpLog + on _WrappedCollectionsNavBarSettingsState { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger( + "widget.settings.collections_nav_bar_settings._WrappedCollectionsNavBarSettingsState"); +} + +extension _$_BlocNpLog on _Bloc { + // ignore: unused_element + Logger get _log => log; + + static final log = + Logger("widget.settings.collections_nav_bar_settings._Bloc"); +} + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$_StateToString on _State { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_State {buttons: [length: ${buttons.length}], error: $error}"; + } +} + +extension _$_InitToString on _Init { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_Init {}"; + } +} + +extension _$_MoveButtonToString on _MoveButton { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_MoveButton {which: ${which.name}, before: ${before == null ? null : "${before!.name}"}, after: ${after == null ? null : "${after!.name}"}}"; + } +} + +extension _$_RemoveButtonToString on _RemoveButton { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_RemoveButton {value: ${value.name}}"; + } +} + +extension _$_ToggleMinimizedToString on _ToggleMinimized { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_ToggleMinimized {value: ${value.name}}"; + } +} + +extension _$_RevertDefaultToString on _RevertDefault { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_RevertDefault {}"; + } +} + +extension _$_SetErrorToString on _SetError { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetError {error: $error, stackTrace: $stackTrace}"; + } +} diff --git a/np_collection/lib/src/list_extension.dart b/np_collection/lib/src/list_extension.dart index addc165f..558661eb 100644 --- a/np_collection/lib/src/list_extension.dart +++ b/np_collection/lib/src/list_extension.dart @@ -88,4 +88,7 @@ extension ListExtension on List { List removed(T value) => toList()..remove(value); List removedAt(int index) => toList()..removeAt(index); + + List removedWhere(bool Function(T element) test) => + toList()..removeWhere(test); }