diff --git a/app/lib/controller/pref_controller.dart b/app/lib/controller/pref_controller.dart index 7d34fa81..585e4fca 100644 --- a/app/lib/controller/pref_controller.dart +++ b/app/lib/controller/pref_controller.dart @@ -12,6 +12,7 @@ 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/viewer.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_common/object_util.dart'; import 'package:np_gps_map/np_gps_map.dart'; @@ -234,6 +235,21 @@ class PrefController { value: value, ); + Future setViewerAppBarButtons(List value) => + _set>( + controller: _viewerAppBarButtonsController, + setter: (pref, value) => pref.setViewerAppBarButtons(value), + value: value, + ); + + Future setViewerBottomAppBarButtons( + List value) => + _set>( + controller: _viewerBottomAppBarButtonsController, + setter: (pref, value) => pref.setViewerBottomAppBarButtons(value), + value: value, + ); + Future _set({ required BehaviorSubject controller, required Future Function(Pref pref, T value) setter, @@ -373,6 +389,23 @@ class PrefController { @npSubjectAccessor late final _isSlideshowReverseController = BehaviorSubject.seeded(pref.isSlideshowReverse() ?? false); + @npSubjectAccessor + late final _viewerAppBarButtonsController = + BehaviorSubject.seeded(pref.getViewerAppBarButtons() ?? + const [ + ViewerAppBarButtonType.livePhoto, + ViewerAppBarButtonType.favorite, + ]); + @npSubjectAccessor + late final _viewerBottomAppBarButtonsController = + BehaviorSubject.seeded(pref.getViewerBottomAppBarButtons() ?? + const [ + ViewerAppBarButtonType.share, + ViewerAppBarButtonType.edit, + ViewerAppBarButtonType.enhance, + ViewerAppBarButtonType.download, + ViewerAppBarButtonType.delete, + ]); } extension PrefControllerExtension on PrefController { diff --git a/app/lib/controller/pref_controller.g.dart b/app/lib/controller/pref_controller.g.dart index 1d32089e..9b1c5bbb 100644 --- a/app/lib/controller/pref_controller.g.dart +++ b/app/lib/controller/pref_controller.g.dart @@ -234,6 +234,24 @@ extension $PrefControllerNpSubjectAccessor on PrefController { Stream get isSlideshowReverseChange => isSlideshowReverse.distinct().skip(1); bool get isSlideshowReverseValue => _isSlideshowReverseController.value; +// _viewerAppBarButtonsController + ValueStream> get viewerAppBarButtons => + _viewerAppBarButtonsController.stream; + Stream> get viewerAppBarButtonsNew => + viewerAppBarButtons.skip(1); + Stream> get viewerAppBarButtonsChange => + viewerAppBarButtons.distinct().skip(1); + List get viewerAppBarButtonsValue => + _viewerAppBarButtonsController.value; +// _viewerBottomAppBarButtonsController + ValueStream> get viewerBottomAppBarButtons => + _viewerBottomAppBarButtonsController.stream; + Stream> get viewerBottomAppBarButtonsNew => + viewerBottomAppBarButtons.skip(1); + Stream> get viewerBottomAppBarButtonsChange => + viewerBottomAppBarButtons.distinct().skip(1); + List get viewerBottomAppBarButtonsValue => + _viewerBottomAppBarButtonsController.value; } extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController { diff --git a/app/lib/controller/pref_controller/util.dart b/app/lib/controller/pref_controller/util.dart index 703992e8..3034abdc 100644 --- a/app/lib/controller/pref_controller/util.dart +++ b/app/lib/controller/pref_controller/util.dart @@ -149,6 +149,23 @@ extension on Pref { bool? isSlideshowReverse() => provider.getBool(PrefKey.isSlideshowReverse); Future setSlideshowReverse(bool value) => provider.setBool(PrefKey.isSlideshowReverse, value); + + List? getViewerAppBarButtons() => provider + .getIntList(PrefKey.viewerAppBarButtons) + ?.map(ViewerAppBarButtonType.fromValue) + .toList(); + Future setViewerAppBarButtons(List value) => + provider.setIntList( + PrefKey.viewerAppBarButtons, value.map((e) => e.index).toList()); + + List? getViewerBottomAppBarButtons() => provider + .getIntList(PrefKey.viewerBottomAppBarButtons) + ?.map(ViewerAppBarButtonType.fromValue) + .toList(); + Future setViewerBottomAppBarButtons( + List value) => + provider.setIntList(PrefKey.viewerBottomAppBarButtons, + value.map((e) => e.index).toList()); } MapCoord? _tryMapCoordFromJson(dynamic json) { diff --git a/app/lib/entity/pref.dart b/app/lib/entity/pref.dart index ae4b253a..43360785 100644 --- a/app/lib/entity/pref.dart +++ b/app/lib/entity/pref.dart @@ -117,6 +117,8 @@ enum PrefKey implements PrefKeyInterface { isNewHttpEngine, mapDefaultRangeType, mapDefaultCustomRange, + viewerAppBarButtons, + viewerBottomAppBarButtons, ; @override @@ -211,6 +213,10 @@ enum PrefKey implements PrefKeyInterface { return "mapDefaultRangeType"; case PrefKey.mapDefaultCustomRange: return "mapDefaultCustomRange"; + case PrefKey.viewerAppBarButtons: + return "viewerAppBarButtons"; + case PrefKey.viewerBottomAppBarButtons: + return "viewerBottomAppBarButtons"; } } } @@ -260,6 +266,9 @@ abstract class PrefProvider { List? getStringList(PrefKeyInterface key); Future setStringList(PrefKeyInterface key, List value); + List? getIntList(PrefKeyInterface key); + Future setIntList(PrefKeyInterface key, List value); + Future remove(PrefKeyInterface key); Future clear(); } diff --git a/app/lib/entity/pref/provider/memory.dart b/app/lib/entity/pref/provider/memory.dart index c45c8ed4..c8a97eda 100644 --- a/app/lib/entity/pref/provider/memory.dart +++ b/app/lib/entity/pref/provider/memory.dart @@ -31,6 +31,12 @@ class PrefMemoryProvider extends PrefProvider { Future setStringList(PrefKeyInterface key, List value) => _set(key, value); + @override + List? getIntList(PrefKeyInterface key) => _get>(key); + @override + Future setIntList(PrefKeyInterface key, List value) => + _set(key, value); + @override Future remove(PrefKeyInterface key) async { _data.remove(key.toStringKey()); diff --git a/app/lib/entity/pref/provider/secure_storage.dart b/app/lib/entity/pref/provider/secure_storage.dart index 6ae48eae..5f72b3cf 100644 --- a/app/lib/entity/pref/provider/secure_storage.dart +++ b/app/lib/entity/pref/provider/secure_storage.dart @@ -71,6 +71,16 @@ class PrefSecureStorageProvider implements PrefProvider { Future setStringList(PrefKeyInterface key, List value) => setString(key, jsonEncode(value)); + @override + List? getIntList(PrefKeyInterface key) { + final value = _rawData[key.toStringKey()]; + return (value?.let(jsonDecode) as List).cast(); + } + + @override + Future setIntList(PrefKeyInterface key, List value) => + setString(key, jsonEncode(value)); + @override Future remove(PrefKeyInterface key) async { try { diff --git a/app/lib/entity/pref/provider/shared_preferences.dart b/app/lib/entity/pref/provider/shared_preferences.dart index eea95058..8c8aca1e 100644 --- a/app/lib/entity/pref/provider/shared_preferences.dart +++ b/app/lib/entity/pref/provider/shared_preferences.dart @@ -47,6 +47,15 @@ class PrefSharedPreferencesProvider extends PrefProvider { Future setStringList(PrefKeyInterface key, List value) => _pref.setStringList(key.toStringKey(), value); + @override + List? getIntList(PrefKeyInterface key) => + _pref.getStringList(key.toStringKey())?.map(int.parse).toList(); + + @override + Future setIntList(PrefKeyInterface key, List value) => + _pref.setStringList( + key.toStringKey(), value.map((e) => e.toString()).toList()); + @override Future remove(PrefKeyInterface key) => _pref.remove(key.toStringKey()); diff --git a/app/lib/entity/pref/provider/universal_storage.dart b/app/lib/entity/pref/provider/universal_storage.dart index 7962deb2..ecdc3ce0 100644 --- a/app/lib/entity/pref/provider/universal_storage.dart +++ b/app/lib/entity/pref/provider/universal_storage.dart @@ -36,6 +36,12 @@ class PrefUniversalStorageProvider extends PrefProvider { Future setStringList(PrefKeyInterface key, List value) => _set(key, value); + @override + List? getIntList(PrefKeyInterface key) => _get>(key); + @override + Future setIntList(PrefKeyInterface key, List value) => + _set(key, value); + @override Future remove(PrefKeyInterface key) async { final newData = Map.of(_data)..remove(key.toStringKey()); diff --git a/app/lib/widget/viewer.g.dart b/app/lib/widget/viewer.g.dart index 0991f7ae..296409be 100644 --- a/app/lib/widget/viewer.g.dart +++ b/app/lib/widget/viewer.g.dart @@ -29,8 +29,10 @@ abstract class $_StateCopyWithWorker { Unique<_OpenDetailPaneRequest>? openDetailPaneRequest, Unique? closeDetailPane, bool? isZoomed, - bool? isShowAppBar, bool? isInitialLoad, + bool? isShowAppBar, + List? appBarButtons, + List? bottomAppBarButtons, Unique? pendingRemovePage, Unique? imageEditorRequest, Unique? imageEnhancerRequest, @@ -59,8 +61,10 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { dynamic openDetailPaneRequest, dynamic closeDetailPane, dynamic isZoomed, - dynamic isShowAppBar, dynamic isInitialLoad, + dynamic isShowAppBar, + dynamic appBarButtons, + dynamic bottomAppBarButtons, dynamic pendingRemovePage, dynamic imageEditorRequest, dynamic imageEnhancerRequest, @@ -98,8 +102,13 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { closeDetailPane: closeDetailPane as Unique? ?? that.closeDetailPane, isZoomed: isZoomed as bool? ?? that.isZoomed, - isShowAppBar: isShowAppBar as bool? ?? that.isShowAppBar, isInitialLoad: isInitialLoad as bool? ?? that.isInitialLoad, + isShowAppBar: isShowAppBar as bool? ?? that.isShowAppBar, + appBarButtons: appBarButtons as List? ?? + that.appBarButtons, + bottomAppBarButtons: + bottomAppBarButtons as List? ?? + that.bottomAppBarButtons, pendingRemovePage: pendingRemovePage as Unique? ?? that.pendingRemovePage, imageEditorRequest: @@ -193,7 +202,7 @@ extension _$_PageViewStateNpLog on _PageViewState { extension _$_StateToString on _State { String _$toString() { // ignore: unnecessary_string_interpolations - return "_State {fileIdOrders: $fileIdOrders, files: {length: ${files.length}}, fileStates: {length: ${fileStates.length}}, index: $index, currentFile: ${currentFile == null ? null : "${currentFile!.fdPath}"}, currentFileState: $currentFileState, collection: $collection, collectionItemsController: $collectionItemsController, collectionItems: ${collectionItems == null ? null : "{length: ${collectionItems!.length}}"}, isShowDetailPane: $isShowDetailPane, isClosingDetailPane: $isClosingDetailPane, isDetailPaneActive: $isDetailPaneActive, openDetailPaneRequest: $openDetailPaneRequest, closeDetailPane: $closeDetailPane, isZoomed: $isZoomed, isShowAppBar: $isShowAppBar, isInitialLoad: $isInitialLoad, pendingRemovePage: $pendingRemovePage, imageEditorRequest: $imageEditorRequest, imageEnhancerRequest: $imageEnhancerRequest, shareRequest: $shareRequest, slideshowRequest: $slideshowRequest, error: $error}"; + return "_State {fileIdOrders: $fileIdOrders, files: {length: ${files.length}}, fileStates: {length: ${fileStates.length}}, index: $index, currentFile: ${currentFile == null ? null : "${currentFile!.fdPath}"}, currentFileState: $currentFileState, collection: $collection, collectionItemsController: $collectionItemsController, collectionItems: ${collectionItems == null ? null : "{length: ${collectionItems!.length}}"}, isShowDetailPane: $isShowDetailPane, isClosingDetailPane: $isClosingDetailPane, isDetailPaneActive: $isDetailPaneActive, openDetailPaneRequest: $openDetailPaneRequest, closeDetailPane: $closeDetailPane, isZoomed: $isZoomed, isInitialLoad: $isInitialLoad, isShowAppBar: $isShowAppBar, appBarButtons: [length: ${appBarButtons.length}], bottomAppBarButtons: [length: ${bottomAppBarButtons.length}], pendingRemovePage: $pendingRemovePage, imageEditorRequest: $imageEditorRequest, imageEnhancerRequest: $imageEnhancerRequest, shareRequest: $shareRequest, slideshowRequest: $slideshowRequest, error: $error}"; } } @@ -260,6 +269,20 @@ extension _$_HideAppBarToString on _HideAppBar { } } +extension _$_SetAppBarButtonsToString on _SetAppBarButtons { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetAppBarButtons {value: [length: ${value.length}]}"; + } +} + +extension _$_SetBottomAppBarButtonsToString on _SetBottomAppBarButtons { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetBottomAppBarButtons {value: [length: ${value.length}]}"; + } +} + extension _$_PauseLivePhotoToString on _PauseLivePhoto { String _$toString() { // ignore: unnecessary_string_interpolations diff --git a/app/lib/widget/viewer/app_bar.dart b/app/lib/widget/viewer/app_bar.dart index 199ea5aa..28bee1f6 100644 --- a/app/lib/widget/viewer/app_bar.dart +++ b/app/lib/widget/viewer/app_bar.dart @@ -9,7 +9,10 @@ class _AppBar extends StatelessWidget { return _BlocBuilder( buildWhen: (previous, current) => previous.isDetailPaneActive != current.isDetailPaneActive || - previous.isZoomed != current.isZoomed, + previous.isZoomed != current.isZoomed || + previous.currentFile != current.currentFile || + previous.collection != current.collection || + previous.appBarButtons != current.appBarButtons, builder: (context, state) => AppBar( backgroundColor: Colors.transparent, elevation: 0, @@ -29,8 +32,13 @@ class _AppBar extends StatelessWidget { centerTitle: isTitleCentered, actions: !state.isDetailPaneActive && !state.isZoomed ? [ - const _AppBarLivePhotoButton(), - const _AppBarFavoriteButton(), + ...state.appBarButtons + .map((e) => _buildAppBarButton( + e, + currentFile: state.currentFile, + collection: state.collection, + )) + .nonNulls, IconButton( icon: const Icon(Icons.more_vert), tooltip: L10n.global().detailsTooltip, @@ -100,21 +108,18 @@ class _BottomAppBar extends StatelessWidget { child: _BlocBuilder( buildWhen: (previous, current) => previous.currentFile != current.currentFile || - previous.collection != current.collection, + previous.collection != current.collection || + previous.bottomAppBarButtons != current.bottomAppBarButtons, builder: (context, state) => Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, - children: [ - const _AppBarShareButton(), - if (features.isSupportEnhancement && - state.currentFile?.let(ImageEnhancer.isSupportedFormat) == - true) ...[ - const _AppBarEditButton(), - const _AppBarEnhanceButton(), - ], - const _AppBarDownloadButton(), - if (state.collection == null) const _AppBarDeleteButton(), - ] + children: state.bottomAppBarButtons + .map((e) => _buildAppBarButton( + e, + currentFile: state.currentFile, + collection: state.collection, + )) + .nonNulls .map((e) => Expanded( flex: 1, child: e, @@ -125,3 +130,36 @@ class _BottomAppBar extends StatelessWidget { ); } } + +/// Build app bar buttons based on [type]. May return null if this button type +/// is not supported in the current context +Widget? _buildAppBarButton( + ViewerAppBarButtonType type, { + required FileDescriptor? currentFile, + required Collection? collection, +}) { + switch (type) { + case ViewerAppBarButtonType.livePhoto: + return currentFile?.let(getLivePhotoTypeFromFile) != null + ? const _AppBarLivePhotoButton() + : null; + case ViewerAppBarButtonType.favorite: + return const _AppBarFavoriteButton(); + case ViewerAppBarButtonType.share: + return const _AppBarShareButton(); + case ViewerAppBarButtonType.edit: + return features.isSupportEnhancement && + currentFile?.let(ImageEnhancer.isSupportedFormat) == true + ? const _AppBarEditButton() + : null; + case ViewerAppBarButtonType.enhance: + return features.isSupportEnhancement && + currentFile?.let(ImageEnhancer.isSupportedFormat) == true + ? const _AppBarEnhanceButton() + : null; + case ViewerAppBarButtonType.download: + return const _AppBarDownloadButton(); + case ViewerAppBarButtonType.delete: + return collection == null ? const _AppBarDeleteButton() : null; + } +} diff --git a/app/lib/widget/viewer/app_bar_buttons.dart b/app/lib/widget/viewer/app_bar_buttons.dart index 716da687..63aeacd4 100644 --- a/app/lib/widget/viewer/app_bar_buttons.dart +++ b/app/lib/widget/viewer/app_bar_buttons.dart @@ -1,5 +1,20 @@ part of '../viewer.dart'; +enum ViewerAppBarButtonType { + // the order must not be changed + livePhoto, + favorite, + share, + edit, + enhance, + download, + delete, + ; + + static ViewerAppBarButtonType fromValue(int value) => + ViewerAppBarButtonType.values[value]; +} + class _AppBarLivePhotoButton extends StatelessWidget { const _AppBarLivePhotoButton(); diff --git a/app/lib/widget/viewer/bloc.dart b/app/lib/widget/viewer/bloc.dart index fdb432c8..b97abfb1 100644 --- a/app/lib/widget/viewer/bloc.dart +++ b/app/lib/widget/viewer/bloc.dart @@ -18,6 +18,8 @@ class _Bloc extends Bloc<_Event, _State> index: startIndex, currentFile: filesController.stream.value.dataMap[fileIds[startIndex]]!, + appBarButtons: prefController.viewerAppBarButtonsValue, + bottomAppBarButtons: prefController.viewerBottomAppBarButtonsValue, )) { on<_Init>(_onInit); on<_SetIndex>(_onSetIndex); @@ -28,6 +30,8 @@ class _Bloc extends Bloc<_Event, _State> on<_ToggleAppBar>(_onToggleAppBar); on<_ShowAppBar>(_onShowAppBar); on<_HideAppBar>(_onHideAppBar); + on<_SetAppBarButtons>(_onAppBarButtons); + on<_SetBottomAppBarButtons>(_onBottomAppBarButtons); on<_PauseLivePhoto>(_onPauseLivePhoto); on<_PlayLivePhoto>(_onPlayLivePhoto); on<_Unfavorite>(_onUnfavorite); @@ -74,6 +78,13 @@ class _Bloc extends Bloc<_Event, _State> _collectionItemsSubscription?.cancel(); })); } + _subscriptions.add(prefController.viewerAppBarButtonsChange.listen((event) { + add(_SetAppBarButtons(event)); + })); + _subscriptions + .add(prefController.viewerBottomAppBarButtonsChange.listen((event) { + add(_SetBottomAppBarButtons(event)); + })); add(_SetIndex(startIndex)); } @@ -189,6 +200,16 @@ class _Bloc extends Bloc<_Event, _State> SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); } + void _onAppBarButtons(_SetAppBarButtons ev, _Emitter emit) { + _log.info(ev); + emit(state.copyWith(appBarButtons: ev.value)); + } + + void _onBottomAppBarButtons(_SetBottomAppBarButtons ev, _Emitter emit) { + _log.info(ev); + emit(state.copyWith(bottomAppBarButtons: ev.value)); + } + void _onPauseLivePhoto(_PauseLivePhoto ev, _Emitter emit) { _log.info(ev); _updateFileState(ev.fileId, emit, shouldPlayLivePhoto: false); diff --git a/app/lib/widget/viewer/state_event.dart b/app/lib/widget/viewer/state_event.dart index 90c3bfc5..9cb64fe0 100644 --- a/app/lib/widget/viewer/state_event.dart +++ b/app/lib/widget/viewer/state_event.dart @@ -19,8 +19,10 @@ class _State { required this.openDetailPaneRequest, required this.closeDetailPane, required this.isZoomed, - required this.isShowAppBar, required this.isInitialLoad, + required this.isShowAppBar, + required this.appBarButtons, + required this.bottomAppBarButtons, required this.pendingRemovePage, required this.imageEditorRequest, required this.imageEnhancerRequest, @@ -33,6 +35,8 @@ class _State { required List fileIds, required int index, required FileDescriptor currentFile, + required List appBarButtons, + required List bottomAppBarButtons, }) => _State( fileIdOrders: fileIds, @@ -46,8 +50,10 @@ class _State { openDetailPaneRequest: Unique(const _OpenDetailPaneRequest(false)), closeDetailPane: Unique(false), isZoomed: false, - isShowAppBar: true, isInitialLoad: true, + isShowAppBar: true, + appBarButtons: appBarButtons, + bottomAppBarButtons: bottomAppBarButtons, pendingRemovePage: Unique(null), imageEditorRequest: Unique(null), imageEnhancerRequest: Unique(null), @@ -76,9 +82,12 @@ class _State { final Unique<_OpenDetailPaneRequest> openDetailPaneRequest; final Unique closeDetailPane; final bool isZoomed; - final bool isShowAppBar; final bool isInitialLoad; + final bool isShowAppBar; + final List appBarButtons; + final List bottomAppBarButtons; + final Unique pendingRemovePage; final Unique imageEditorRequest; @@ -189,6 +198,26 @@ class _HideAppBar implements _Event { String toString() => _$toString(); } +@toString +class _SetAppBarButtons implements _Event { + const _SetAppBarButtons(this.value); + + @override + String toString() => _$toString(); + + final List value; +} + +@toString +class _SetBottomAppBarButtons implements _Event { + const _SetBottomAppBarButtons(this.value); + + @override + String toString() => _$toString(); + + final List value; +} + @toString class _PauseLivePhoto implements _Event { const _PauseLivePhoto(this.fileId);