diff --git a/CHANGELOG.md b/CHANGELOG.md index b2ce2dc98..70c6e1416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added +- Viewer: Info page editing actions available as quick actions - Info: export metadata to text file - Accessibility: apply bold font system setting - `libre` app flavor (no mobile service maps, no Crashlytics) diff --git a/lib/model/actions/entry_actions.dart b/lib/model/actions/entry_actions.dart index 3a8f1fff5..cb6dabebb 100644 --- a/lib/model/actions/entry_actions.dart +++ b/lib/model/actions/entry_actions.dart @@ -38,6 +38,19 @@ enum EntryAction { setAs, // platform rotateScreen, + // metadata + editDate, + editLocation, + editTitleDescription, + editRating, + editTags, + removeMetadata, + exportMetadata, + // metadata / GeoTIFF + showGeoTiffOnMap, + // metadata / motion photo + convertMotionPhotoToStillImage, + viewMotionPhotoVideo, // debug debug, } @@ -99,6 +112,22 @@ class EntryActions { EntryAction.videoSelectStreams, EntryAction.videoSettings, ]; + + static const commonMetadataActions = [ + EntryAction.editDate, + EntryAction.editLocation, + EntryAction.editTitleDescription, + EntryAction.editRating, + EntryAction.editTags, + EntryAction.removeMetadata, + EntryAction.exportMetadata, + ]; + + static const formatSpecificMetadataActions = [ + EntryAction.showGeoTiffOnMap, + EntryAction.convertMotionPhotoToStillImage, + EntryAction.viewMotionPhotoVideo, + ]; } extension ExtraEntryAction on EntryAction { @@ -170,6 +199,29 @@ extension ExtraEntryAction on EntryAction { // platform case EntryAction.rotateScreen: return context.l10n.entryActionRotateScreen; + // metadata + case EntryAction.editDate: + return context.l10n.entryInfoActionEditDate; + case EntryAction.editLocation: + return context.l10n.entryInfoActionEditLocation; + case EntryAction.editTitleDescription: + return context.l10n.entryInfoActionEditTitleDescription; + case EntryAction.editRating: + return context.l10n.entryInfoActionEditRating; + case EntryAction.editTags: + return context.l10n.entryInfoActionEditTags; + case EntryAction.removeMetadata: + return context.l10n.entryInfoActionRemoveMetadata; + case EntryAction.exportMetadata: + return context.l10n.entryInfoActionExportMetadata; + // metadata / GeoTIFF + case EntryAction.showGeoTiffOnMap: + return context.l10n.entryActionShowGeoTiffOnMap; + // metadata / motion photo + case EntryAction.convertMotionPhotoToStillImage: + return context.l10n.entryActionConvertMotionPhotoToStillImage; + case EntryAction.viewMotionPhotoVideo: + return context.l10n.entryActionViewMotionPhotoVideo; // debug case EntryAction.debug: return 'Debug'; @@ -258,6 +310,29 @@ extension ExtraEntryAction on EntryAction { // platform case EntryAction.rotateScreen: return AIcons.rotateScreen; + // metadata + case EntryAction.editDate: + return AIcons.date; + case EntryAction.editLocation: + return AIcons.location; + case EntryAction.editTitleDescription: + return AIcons.description; + case EntryAction.editRating: + return AIcons.editRating; + case EntryAction.editTags: + return AIcons.editTags; + case EntryAction.removeMetadata: + return AIcons.clear; + case EntryAction.exportMetadata: + return AIcons.fileExport; + // metadata / GeoTIFF + case EntryAction.showGeoTiffOnMap: + return AIcons.map; + // metadata / motion photo + case EntryAction.convertMotionPhotoToStillImage: + return AIcons.convertToStillImage; + case EntryAction.viewMotionPhotoVideo: + return AIcons.openVideo; // debug case EntryAction.debug: return AIcons.debug; diff --git a/lib/model/actions/entry_info_actions.dart b/lib/model/actions/entry_info_actions.dart deleted file mode 100644 index c0857c536..000000000 --- a/lib/model/actions/entry_info_actions.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:aves/theme/colors.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:flutter/widgets.dart'; - -enum EntryInfoAction { - // general - editDate, - editLocation, - editTitleDescription, - editRating, - editTags, - removeMetadata, - exportMetadata, - // GeoTIFF - showGeoTiffOnMap, - // motion photo - convertMotionPhotoToStillImage, - viewMotionPhotoVideo, - // debug - debug, -} - -class EntryInfoActions { - static const common = [ - EntryInfoAction.editDate, - EntryInfoAction.editLocation, - EntryInfoAction.editTitleDescription, - EntryInfoAction.editRating, - EntryInfoAction.editTags, - EntryInfoAction.removeMetadata, - EntryInfoAction.exportMetadata, - ]; - - static const formatSpecific = [ - EntryInfoAction.showGeoTiffOnMap, - EntryInfoAction.convertMotionPhotoToStillImage, - EntryInfoAction.viewMotionPhotoVideo, - ]; -} - -extension ExtraEntryInfoAction on EntryInfoAction { - String getText(BuildContext context) { - switch (this) { - // general - case EntryInfoAction.editDate: - return context.l10n.entryInfoActionEditDate; - case EntryInfoAction.editLocation: - return context.l10n.entryInfoActionEditLocation; - case EntryInfoAction.editTitleDescription: - return context.l10n.entryInfoActionEditTitleDescription; - case EntryInfoAction.editRating: - return context.l10n.entryInfoActionEditRating; - case EntryInfoAction.editTags: - return context.l10n.entryInfoActionEditTags; - case EntryInfoAction.removeMetadata: - return context.l10n.entryInfoActionRemoveMetadata; - case EntryInfoAction.exportMetadata: - return context.l10n.entryInfoActionExportMetadata; - // GeoTIFF - case EntryInfoAction.showGeoTiffOnMap: - return context.l10n.entryActionShowGeoTiffOnMap; - // motion photo - case EntryInfoAction.convertMotionPhotoToStillImage: - return context.l10n.entryActionConvertMotionPhotoToStillImage; - case EntryInfoAction.viewMotionPhotoVideo: - return context.l10n.entryActionViewMotionPhotoVideo; - // debug - case EntryInfoAction.debug: - return 'Debug'; - } - } - - Widget getIcon() { - final child = Icon(_getIconData()); - switch (this) { - case EntryInfoAction.debug: - return ShaderMask( - shaderCallback: AvesColorsData.debugGradient.createShader, - blendMode: BlendMode.srcIn, - child: child, - ); - default: - return child; - } - } - - IconData _getIconData() { - switch (this) { - // general - case EntryInfoAction.editDate: - return AIcons.date; - case EntryInfoAction.editLocation: - return AIcons.location; - case EntryInfoAction.editTitleDescription: - return AIcons.description; - case EntryInfoAction.editRating: - return AIcons.editRating; - case EntryInfoAction.editTags: - return AIcons.editTags; - case EntryInfoAction.removeMetadata: - return AIcons.clear; - case EntryInfoAction.exportMetadata: - return AIcons.fileExport; - // GeoTIFF - case EntryInfoAction.showGeoTiffOnMap: - return AIcons.map; - // motion photo - case EntryInfoAction.convertMotionPhotoToStillImage: - return AIcons.convertToStillImage; - case EntryInfoAction.viewMotionPhotoVideo: - return AIcons.openVideo; - // debug - case EntryInfoAction.debug: - return AIcons.debug; - } - } -} diff --git a/lib/widgets/settings/viewer/viewer_actions_editor.dart b/lib/widgets/settings/viewer/viewer_actions_editor.dart index 6c18e529d..8b63bf131 100644 --- a/lib/widgets/settings/viewer/viewer_actions_editor.dart +++ b/lib/widgets/settings/viewer/viewer_actions_editor.dart @@ -30,6 +30,7 @@ class ViewerActionEditorPage extends StatelessWidget { EntryAction.videoSetSpeed, EntryAction.videoSelectStreams, ], + EntryActions.commonMetadataActions, ]; @override diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 439c06405..9311227db 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -10,6 +10,7 @@ import 'package:aves/model/entry_metadata_edition.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; @@ -29,79 +30,191 @@ import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/entry_editors/rename_entry_dialog.dart'; import 'package:aves/widgets/dialogs/export_entry_dialog.dart'; import 'package:aves/widgets/filter_grids/album_pick.dart'; +import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; import 'package:aves/widgets/viewer/action/printer.dart'; import 'package:aves/widgets/viewer/action/single_entry_editor.dart'; import 'package:aves/widgets/viewer/debug/debug_page.dart'; +import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves/widgets/viewer/source_viewer_page.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin, SingleEntryEditorMixin, EntryStorageMixin { - @override - final AvesEntry entry; + final AvesEntry mainEntry, pageEntry; + final CollectionLens? collection; + final EntryInfoActionDelegate _metadataActionDelegate = EntryInfoActionDelegate(); - EntryActionDelegate(this.entry); + EntryActionDelegate(this.mainEntry, this.pageEntry, this.collection); + + bool isVisible(EntryAction action) { + if (mainEntry.trashed) { + switch (action) { + case EntryAction.delete: + case EntryAction.restore: + return true; + case EntryAction.debug: + return kDebugMode; + default: + return false; + } + } else { + final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry; + switch (action) { + case EntryAction.toggleFavourite: + return collection != null; + case EntryAction.delete: + case EntryAction.rename: + case EntryAction.copy: + case EntryAction.move: + return targetEntry.canEdit; + case EntryAction.rotateCCW: + case EntryAction.rotateCW: + return targetEntry.canRotate; + case EntryAction.flip: + return targetEntry.canFlip; + case EntryAction.convert: + case EntryAction.print: + return !targetEntry.isVideo && device.canPrint; + case EntryAction.openMap: + return targetEntry.hasGps; + case EntryAction.viewSource: + return targetEntry.isSvg; + case EntryAction.videoCaptureFrame: + case EntryAction.videoToggleMute: + case EntryAction.videoSelectStreams: + case EntryAction.videoSetSpeed: + case EntryAction.videoSettings: + case EntryAction.videoTogglePlay: + case EntryAction.videoReplay10: + case EntryAction.videoSkip10: + return targetEntry.isVideo; + case EntryAction.rotateScreen: + return settings.isRotationLocked; + case EntryAction.addShortcut: + return device.canPinShortcut; + case EntryAction.info: + case EntryAction.copyToClipboard: + case EntryAction.edit: + case EntryAction.open: + case EntryAction.setAs: + case EntryAction.share: + return true; + case EntryAction.restore: + return false; + case EntryAction.editDate: + case EntryAction.editLocation: + case EntryAction.editTitleDescription: + case EntryAction.editRating: + case EntryAction.editTags: + case EntryAction.removeMetadata: + case EntryAction.exportMetadata: + case EntryAction.showGeoTiffOnMap: + case EntryAction.convertMotionPhotoToStillImage: + case EntryAction.viewMotionPhotoVideo: + return _metadataActionDelegate.isVisible(targetEntry, action); + case EntryAction.debug: + return kDebugMode; + } + } + } + + bool canApply(EntryAction action) { + final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry; + switch (action) { + case EntryAction.rotateCCW: + case EntryAction.rotateCW: + return targetEntry.canRotate; + case EntryAction.flip: + return targetEntry.canFlip; + case EntryAction.editDate: + case EntryAction.editLocation: + case EntryAction.editTitleDescription: + case EntryAction.editRating: + case EntryAction.editTags: + case EntryAction.removeMetadata: + case EntryAction.exportMetadata: + case EntryAction.showGeoTiffOnMap: + case EntryAction.convertMotionPhotoToStillImage: + case EntryAction.viewMotionPhotoVideo: + return _metadataActionDelegate.canApply(targetEntry, action); + default: + return true; + } + } void onActionSelected(BuildContext context, EntryAction action) { + var targetEntry = mainEntry; + if (mainEntry.isMultiPage && (mainEntry.isBurst || EntryActions.pageActions.contains(action))) { + final multiPageController = context.read().getController(mainEntry); + if (multiPageController != null) { + final multiPageInfo = multiPageController.info; + final pageEntry = multiPageInfo?.getPageEntryByIndex(multiPageController.page); + if (pageEntry != null) { + targetEntry = pageEntry; + } + } + } + switch (action) { case EntryAction.info: ShowInfoNotification().dispatch(context); break; case EntryAction.addShortcut: - _addShortcut(context); + _addShortcut(context, targetEntry); break; case EntryAction.copyToClipboard: - androidAppService.copyToClipboard(entry.uri, entry.bestTitle).then((success) { + androidAppService.copyToClipboard(targetEntry.uri, targetEntry.bestTitle).then((success) { showFeedback(context, success ? context.l10n.genericSuccessFeedback : context.l10n.genericFailureFeedback); }); break; case EntryAction.delete: - _delete(context); + _delete(context, targetEntry); break; case EntryAction.restore: - _move(context, moveType: MoveType.fromBin); + _move(context, targetEntry, moveType: MoveType.fromBin); break; case EntryAction.convert: - _convert(context); + _convert(context, targetEntry); break; case EntryAction.print: - EntryPrinter(entry).print(context); + EntryPrinter(targetEntry).print(context); break; case EntryAction.rename: - _rename(context); + _rename(context, targetEntry); break; case EntryAction.copy: - _move(context, moveType: MoveType.copy); + _move(context, targetEntry, moveType: MoveType.copy); break; case EntryAction.move: - _move(context, moveType: MoveType.move); + _move(context, targetEntry, moveType: MoveType.move); break; case EntryAction.share: - androidAppService.shareEntries({entry}).then((success) { + androidAppService.shareEntries({targetEntry}).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; case EntryAction.toggleFavourite: - entry.toggleFavourite(); + targetEntry.toggleFavourite(); break; // raster case EntryAction.rotateCCW: - _rotate(context, clockwise: false); + _rotate(context, targetEntry, clockwise: false); break; case EntryAction.rotateCW: - _rotate(context, clockwise: true); + _rotate(context, targetEntry, clockwise: true); break; case EntryAction.flip: - _flip(context); + _flip(context, targetEntry); break; // vector case EntryAction.viewSource: - _goToSourceViewer(context); + _goToSourceViewer(context, targetEntry); break; // video case EntryAction.videoCaptureFrame: @@ -112,28 +225,28 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.videoTogglePlay: case EntryAction.videoReplay10: case EntryAction.videoSkip10: - final controller = context.read().getController(entry); + final controller = context.read().getController(targetEntry); if (controller != null) { VideoActionNotification(controller: controller, action: action).dispatch(context); } break; case EntryAction.edit: - androidAppService.edit(entry.uri, entry.mimeType).then((success) { + androidAppService.edit(targetEntry.uri, targetEntry.mimeType).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; case EntryAction.open: - androidAppService.open(entry.uri, entry.mimeTypeAnySubtype).then((success) { + androidAppService.open(targetEntry.uri, targetEntry.mimeTypeAnySubtype).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; case EntryAction.openMap: - androidAppService.openMap(entry.latLng!).then((success) { + androidAppService.openMap(targetEntry.latLng!).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; case EntryAction.setAs: - androidAppService.setAs(entry.uri, entry.mimeType).then((success) { + androidAppService.setAs(targetEntry.uri, targetEntry.mimeType).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; @@ -141,18 +254,31 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.rotateScreen: _rotateScreen(context); break; + // metadata + case EntryAction.editDate: + case EntryAction.editLocation: + case EntryAction.editTitleDescription: + case EntryAction.editRating: + case EntryAction.editTags: + case EntryAction.removeMetadata: + case EntryAction.exportMetadata: + case EntryAction.showGeoTiffOnMap: + case EntryAction.convertMotionPhotoToStillImage: + case EntryAction.viewMotionPhotoVideo: + _metadataActionDelegate.onActionSelected(context, targetEntry, collection, action); + break; // debug case EntryAction.debug: - _goToDebug(context); + _goToDebug(context, targetEntry); break; } } - Future _addShortcut(BuildContext context) async { + Future _addShortcut(BuildContext context, AvesEntry targetEntry) async { final result = await showDialog>( context: context, builder: (context) => AddShortcutDialog( - defaultName: entry.bestTitle ?? '', + defaultName: targetEntry.bestTitle ?? '', ), ); if (result == null) return; @@ -160,18 +286,18 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final name = result.item2; if (name.isEmpty) return; - await androidAppService.pinToHomeScreen(name, entry, uri: entry.uri); + await androidAppService.pinToHomeScreen(name, targetEntry, uri: targetEntry.uri); if (!device.showPinShortcutFeedback) { showFeedback(context, context.l10n.genericSuccessFeedback); } } - Future _flip(BuildContext context) async { - await edit(context, entry.flip); + Future _flip(BuildContext context, AvesEntry targetEntry) async { + await edit(context, targetEntry, targetEntry.flip); } - Future _rotate(BuildContext context, {required bool clockwise}) async { - await edit(context, () => entry.rotate(clockwise: clockwise)); + Future _rotate(BuildContext context, AvesEntry targetEntry, {required bool clockwise}) async { + await edit(context, targetEntry, () => targetEntry.rotate(clockwise: clockwise)); } Future _rotateScreen(BuildContext context) async { @@ -185,9 +311,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } } - Future _delete(BuildContext context) async { - if (settings.enableBin && !entry.trashed) { - await _move(context, moveType: MoveType.toBin); + Future _delete(BuildContext context, AvesEntry targetEntry) async { + if (settings.enableBin && !targetEntry.trashed) { + await _move(context, targetEntry, moveType: MoveType.toBin); return; } @@ -199,23 +325,23 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix confirmationButtonLabel: l10n.deleteButtonLabel, )) return; - if (!await checkStoragePermission(context, {entry})) return; + if (!await checkStoragePermission(context, {targetEntry})) return; - if (!await entry.delete()) { + if (!await targetEntry.delete()) { showFeedback(context, l10n.genericFailureFeedback); } else { final source = context.read(); if (source.initState != SourceInitializationState.none) { - await source.removeEntries({entry.uri}, includeTrash: true); + await source.removeEntries({targetEntry.uri}, includeTrash: true); } - EntryDeletedNotification({entry}).dispatch(context); + EntryDeletedNotification({targetEntry}).dispatch(context); } } - Future _convert(BuildContext context) async { + Future _convert(BuildContext context, AvesEntry targetEntry) async { final options = await showDialog( context: context, - builder: (context) => ExportEntryDialog(entry: entry), + builder: (context) => ExportEntryDialog(entry: targetEntry), ); if (options == null) return; @@ -223,13 +349,13 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (destinationAlbum == null) return; if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; - if (!await checkFreeSpaceForMove(context, {entry}, destinationAlbum, MoveType.export)) return; + if (!await checkFreeSpaceForMove(context, {targetEntry}, destinationAlbum, MoveType.export)) return; final selection = {}; - if (entry.isMultiPage) { - final multiPageInfo = await entry.getMultiPageInfo(); + if (targetEntry.isMultiPage) { + final multiPageInfo = await targetEntry.getMultiPageInfo(); if (multiPageInfo != null) { - if (entry.isMotionPhoto) { + if (targetEntry.isMotionPhoto) { await multiPageInfo.extractMotionPhotoVideo(); } if (multiPageInfo.pageCount > 1) { @@ -237,7 +363,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } } } else { - selection.add(entry); + selection.add(targetEntry); } final selectionCount = selection.length; @@ -304,32 +430,32 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix ); } - Future _move(BuildContext context, {required MoveType moveType}) => move( + Future _move(BuildContext context, AvesEntry targetEntry, {required MoveType moveType}) => move( context, moveType: moveType, - entries: {entry}, + entries: {targetEntry}, ); - Future _rename(BuildContext context) async { + Future _rename(BuildContext context, AvesEntry targetEntry) async { final newName = await showDialog( context: context, - builder: (context) => RenameEntryDialog(entry: entry), + builder: (context) => RenameEntryDialog(entry: targetEntry), ); - if (newName == null || newName.isEmpty || newName == entry.filenameWithoutExtension) return; + if (newName == null || newName.isEmpty || newName == targetEntry.filenameWithoutExtension) return; // wait for the dialog to hide as applying the change may block the UI await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); await rename( context, - entriesToNewName: {entry: '$newName${entry.extension}'}, + entriesToNewName: {targetEntry: '$newName${targetEntry.extension}'}, persist: _isMainMode(context), - onSuccess: entry.metadataChangeNotifier.notify, + onSuccess: targetEntry.metadataChangeNotifier.notify, ); } bool _isMainMode(BuildContext context) => context.read>().value == AppMode.main; - void _goToSourceViewer(BuildContext context) { + void _goToSourceViewer(BuildContext context, AvesEntry targetEntry) { Navigator.push( context, MaterialPageRoute( @@ -337,9 +463,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix builder: (context) => SourceViewerPage( loader: () async { final data = await mediaFetchService.getSvg( - entry.uri, - entry.mimeType, - sizeBytes: entry.sizeBytes, + targetEntry.uri, + targetEntry.mimeType, + sizeBytes: targetEntry.sizeBytes, ); return utf8.decode(data); }, @@ -348,12 +474,12 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix ); } - void _goToDebug(BuildContext context) { + void _goToDebug(BuildContext context, AvesEntry targetEntry) { Navigator.push( context, MaterialPageRoute( settings: const RouteSettings(name: ViewerDebugPage.routeName), - builder: (context) => ViewerDebugPage(entry: entry), + builder: (context) => ViewerDebugPage(entry: targetEntry), ), ); } diff --git a/lib/widgets/viewer/action/entry_info_action_delegate.dart b/lib/widgets/viewer/action/entry_info_action_delegate.dart index 77c7c4f0a..4a01160ae 100644 --- a/lib/widgets/viewer/action/entry_info_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_info_action_delegate.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; -import 'package:aves/model/actions/entry_info_actions.dart'; +import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/events.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_info.dart'; @@ -17,171 +17,160 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/viewer/action/single_entry_editor.dart'; -import 'package:aves/widgets/viewer/debug/debug_page.dart'; import 'package:aves/widgets/viewer/embedded/notifications.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEditorMixin, SingleEntryEditorMixin { - @override - final AvesEntry entry; - final CollectionLens? collection; + final StreamController> _eventStreamController = StreamController.broadcast(); - final StreamController> _eventStreamController = StreamController.broadcast(); + Stream> get eventStream => _eventStreamController.stream; - Stream> get eventStream => _eventStreamController.stream; - - EntryInfoActionDelegate(this.entry, this.collection); - - bool isVisible(EntryInfoAction action) { + bool isVisible(AvesEntry targetEntry, EntryAction action) { switch (action) { // general - case EntryInfoAction.editDate: - case EntryInfoAction.editLocation: - case EntryInfoAction.editTitleDescription: - case EntryInfoAction.editRating: - case EntryInfoAction.editTags: - case EntryInfoAction.removeMetadata: - case EntryInfoAction.exportMetadata: + case EntryAction.editDate: + case EntryAction.editLocation: + case EntryAction.editTitleDescription: + case EntryAction.editRating: + case EntryAction.editTags: + case EntryAction.removeMetadata: + case EntryAction.exportMetadata: return true; // GeoTIFF - case EntryInfoAction.showGeoTiffOnMap: - return entry.isGeotiff; + case EntryAction.showGeoTiffOnMap: + return targetEntry.isGeotiff; // motion photo - case EntryInfoAction.convertMotionPhotoToStillImage: - case EntryInfoAction.viewMotionPhotoVideo: - return entry.isMotionPhoto; - // debug - case EntryInfoAction.debug: - return kDebugMode; + case EntryAction.convertMotionPhotoToStillImage: + case EntryAction.viewMotionPhotoVideo: + return targetEntry.isMotionPhoto; + default: + return false; } } - bool canApply(EntryInfoAction action) { + bool canApply(AvesEntry targetEntry, EntryAction action) { switch (action) { // general - case EntryInfoAction.editDate: - return entry.canEditDate; - case EntryInfoAction.editLocation: - return entry.canEditLocation; - case EntryInfoAction.editTitleDescription: - return entry.canEditTitleDescription; - case EntryInfoAction.editRating: - return entry.canEditRating; - case EntryInfoAction.editTags: - return entry.canEditTags; - case EntryInfoAction.removeMetadata: - return entry.canRemoveMetadata; - case EntryInfoAction.exportMetadata: + case EntryAction.editDate: + return targetEntry.canEditDate; + case EntryAction.editLocation: + return targetEntry.canEditLocation; + case EntryAction.editTitleDescription: + return targetEntry.canEditTitleDescription; + case EntryAction.editRating: + return targetEntry.canEditRating; + case EntryAction.editTags: + return targetEntry.canEditTags; + case EntryAction.removeMetadata: + return targetEntry.canRemoveMetadata; + case EntryAction.exportMetadata: return true; // GeoTIFF - case EntryInfoAction.showGeoTiffOnMap: + case EntryAction.showGeoTiffOnMap: return true; // motion photo - case EntryInfoAction.convertMotionPhotoToStillImage: - return entry.canEditXmp; - case EntryInfoAction.viewMotionPhotoVideo: - return true; - // debug - case EntryInfoAction.debug: + case EntryAction.convertMotionPhotoToStillImage: + return targetEntry.canEditXmp; + case EntryAction.viewMotionPhotoVideo: return true; + default: + return false; } } - void onActionSelected(BuildContext context, EntryInfoAction action) async { + void onActionSelected(BuildContext context, AvesEntry targetEntry, CollectionLens? collection, EntryAction action) async { _eventStreamController.add(ActionStartedEvent(action)); switch (action) { // general - case EntryInfoAction.editDate: - await _editDate(context); + case EntryAction.editDate: + await _editDate(context, targetEntry, collection); break; - case EntryInfoAction.editLocation: - await _editLocation(context); + case EntryAction.editLocation: + await _editLocation(context, targetEntry, collection); break; - case EntryInfoAction.editTitleDescription: - await _editTitleDescription(context); + case EntryAction.editTitleDescription: + await _editTitleDescription(context, targetEntry); break; - case EntryInfoAction.editRating: - await _editRating(context); + case EntryAction.editRating: + await _editRating(context, targetEntry); break; - case EntryInfoAction.editTags: - await _editTags(context); + case EntryAction.editTags: + await _editTags(context, targetEntry); break; - case EntryInfoAction.removeMetadata: - await _removeMetadata(context); + case EntryAction.removeMetadata: + await _removeMetadata(context, targetEntry); break; - case EntryInfoAction.exportMetadata: - await _exportMetadata(context); + case EntryAction.exportMetadata: + await _exportMetadata(context, targetEntry); break; // GeoTIFF - case EntryInfoAction.showGeoTiffOnMap: - await _showGeoTiffOnMap(context); + case EntryAction.showGeoTiffOnMap: + await _showGeoTiffOnMap(context, targetEntry, collection); break; // motion photo - case EntryInfoAction.convertMotionPhotoToStillImage: - await _convertMotionPhotoToStillImage(context); + case EntryAction.convertMotionPhotoToStillImage: + await _convertMotionPhotoToStillImage(context, targetEntry); break; - case EntryInfoAction.viewMotionPhotoVideo: + case EntryAction.viewMotionPhotoVideo: OpenEmbeddedDataNotification.motionPhotoVideo().dispatch(context); break; - // debug - case EntryInfoAction.debug: - _goToDebug(context); + default: break; } _eventStreamController.add(ActionEndedEvent(action)); } - Future _editDate(BuildContext context) async { - final modifier = await selectDateModifier(context, {entry}, collection); + Future _editDate(BuildContext context, AvesEntry targetEntry, CollectionLens? collection) async { + final modifier = await selectDateModifier(context, {targetEntry}, collection); if (modifier == null) return; - await edit(context, () => entry.editDate(modifier)); + await edit(context, targetEntry, () => targetEntry.editDate(modifier)); } - Future _editLocation(BuildContext context) async { - final location = await selectLocation(context, {entry}, collection); + Future _editLocation(BuildContext context, AvesEntry targetEntry, CollectionLens? collection) async { + final location = await selectLocation(context, {targetEntry}, collection); if (location == null) return; - await edit(context, () => entry.editLocation(location)); + await edit(context, targetEntry, () => targetEntry.editLocation(location)); } - Future _editTitleDescription(BuildContext context) async { - final modifier = await selectTitleDescriptionModifier(context, {entry}); + Future _editTitleDescription(BuildContext context, AvesEntry targetEntry) async { + final modifier = await selectTitleDescriptionModifier(context, {targetEntry}); if (modifier == null) return; - await edit(context, () => entry.editTitleDescription(modifier)); + await edit(context, targetEntry, () => targetEntry.editTitleDescription(modifier)); } - Future _editRating(BuildContext context) async { - final rating = await selectRating(context, {entry}); + Future _editRating(BuildContext context, AvesEntry targetEntry) async { + final rating = await selectRating(context, {targetEntry}); if (rating == null) return; - await edit(context, () => entry.editRating(rating)); + await edit(context, targetEntry, () => targetEntry.editRating(rating)); } - Future _editTags(BuildContext context) async { - final newTagsByEntry = await selectTags(context, {entry}); + Future _editTags(BuildContext context, AvesEntry targetEntry) async { + final newTagsByEntry = await selectTags(context, {targetEntry}); if (newTagsByEntry == null) return; - final newTags = newTagsByEntry[entry] ?? entry.tags; - final currentTags = entry.tags; + final newTags = newTagsByEntry[targetEntry] ?? targetEntry.tags; + final currentTags = targetEntry.tags; if (newTags.length == currentTags.length && newTags.every(currentTags.contains)) return; - await edit(context, () => entry.editTags(newTags)); + await edit(context, targetEntry, () => targetEntry.editTags(newTags)); } - Future _removeMetadata(BuildContext context) async { - final types = await selectMetadataToRemove(context, {entry}); + Future _removeMetadata(BuildContext context, AvesEntry targetEntry) async { + final types = await selectMetadataToRemove(context, {targetEntry}); if (types == null) return; - await edit(context, () => entry.removeMetadata(types)); + await edit(context, targetEntry, () => targetEntry.removeMetadata(types)); } - Future _exportMetadata(BuildContext context) async { + Future _exportMetadata(BuildContext context, AvesEntry targetEntry) async { final lines = []; final padding = ' ' * 2; - final titledDirectories = await entry.getMetadataDirectories(context); + final titledDirectories = await targetEntry.getMetadataDirectories(context); titledDirectories.forEach((kv) { final title = kv.key; final dir = kv.value; @@ -197,7 +186,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi final metadataString = lines.join('\n'); final success = await storageService.createFile( - '${entry.filenameWithoutExtension}-metadata.txt', + '${targetEntry.filenameWithoutExtension}-metadata.txt', MimeTypes.plainText, Uint8List.fromList(utf8.encode(metadataString)), ); @@ -210,7 +199,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi } } - Future _convertMotionPhotoToStillImage(BuildContext context) async { + Future _convertMotionPhotoToStillImage(BuildContext context, AvesEntry targetEntry) async { final confirmed = await showDialog( context: context, builder: (context) { @@ -231,16 +220,16 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi ); if (confirmed == null || !confirmed) return; - await edit(context, entry.removeTrailerVideo); + await edit(context, targetEntry, targetEntry.removeTrailerVideo); } - Future _showGeoTiffOnMap(BuildContext context) async { - final info = await metadataFetchService.getGeoTiffInfo(entry); + Future _showGeoTiffOnMap(BuildContext context, AvesEntry targetEntry, CollectionLens? collection) async { + final info = await metadataFetchService.getGeoTiffInfo(targetEntry); if (info == null) return; final mappedGeoTiff = MappedGeoTiff( info: info, - entry: entry, + entry: targetEntry, ); if (!mappedGeoTiff.canOverlay) return; @@ -255,7 +244,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi return MapPage( collection: baseCollection.copyWith( listenToSource: true, - fixedSelection: baseCollection.sortedEntries.where((entry) => entry.hasGps).where((entry) => entry != this.entry).toList(), + fixedSelection: baseCollection.sortedEntries.where((entry) => entry.hasGps).where((entry) => entry != targetEntry).toList(), ), overlayEntry: mappedGeoTiff, ); @@ -263,14 +252,4 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi ), ); } - - void _goToDebug(BuildContext context) { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: ViewerDebugPage.routeName), - builder: (context) => ViewerDebugPage(entry: entry), - ), - ); - } } diff --git a/lib/widgets/viewer/action/single_entry_editor.dart b/lib/widgets/viewer/action/single_entry_editor.dart index 5cdfdadc4..0209c1652 100644 --- a/lib/widgets/viewer/action/single_entry_editor.dart +++ b/lib/widgets/viewer/action/single_entry_editor.dart @@ -12,12 +12,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; mixin SingleEntryEditorMixin on FeedbackMixin, PermissionAwareMixin { - AvesEntry get entry; - bool _isMainMode(BuildContext context) => context.read>().value == AppMode.main; - Future edit(BuildContext context, Future> Function() apply) async { - if (!await checkStoragePermission(context, {entry})) return; + Future edit(BuildContext context, AvesEntry targetEntry, Future> Function() apply) async { + if (!await checkStoragePermission(context, {targetEntry})) return; // check before applying, because it relies on provider // but the widget tree may be disposed if the user navigated away @@ -32,10 +30,10 @@ mixin SingleEntryEditorMixin on FeedbackMixin, PermissionAwareMixin { try { if (success) { if (isMainMode && source != null) { - Set obsoleteTags = entry.tags; - String? obsoleteCountryCode = entry.addressDetails?.countryCode; + Set obsoleteTags = targetEntry.tags; + String? obsoleteCountryCode = targetEntry.addressDetails?.countryCode; - await source.refreshEntry(entry, dataTypes); + await source.refreshEntry(targetEntry, dataTypes); // invalidate filters derived from values before edition // this invalidation must happen after the source is refreshed, @@ -47,7 +45,7 @@ mixin SingleEntryEditorMixin on FeedbackMixin, PermissionAwareMixin { source.invalidateTagFilterSummary(tags: obsoleteTags); } } else { - await entry.refresh(background: false, persist: false, dataTypes: dataTypes, geocoderLocale: settings.appliedLocale); + await targetEntry.refresh(background: false, persist: false, dataTypes: dataTypes, geocoderLocale: settings.appliedLocale); } showFeedback(context, l10n.genericSuccessFeedback); } else { diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 14f8918b4..8635e5678 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -440,7 +440,7 @@ class _EntryViewerStackState extends State with EntryViewContr ViewerBottomOverlay( entries: entries, index: _currentEntryIndex, - hasCollection: hasCollection, + collection: collection, animationController: _overlayAnimationController, viewInsets: _frozenViewInsets, viewPadding: _frozenViewPadding, diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index b5b61a750..5f9e672c0 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -1,6 +1,6 @@ import 'package:aves/app_mode.dart'; import 'package:aves/image_providers/app_icon_image_provider.dart'; -import 'package:aves/model/actions/entry_info_actions.dart'; +import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/album.dart'; @@ -29,7 +29,7 @@ class BasicSection extends StatelessWidget { final AvesEntry entry; final CollectionLens? collection; final EntryInfoActionDelegate actionDelegate; - final ValueNotifier isEditingMetadataNotifier; + final ValueNotifier isEditingMetadataNotifier; final FilterCallback onFilter; const BasicSection({ @@ -100,9 +100,9 @@ class BasicSection extends StatelessWidget { Widget _buildEditButtons(BuildContext context) { final children = [ - EntryInfoAction.editRating, - EntryInfoAction.editTags, - ].where(actionDelegate.canApply).map((v) => _buildEditMetadataButton(context, v)).toList(); + EntryAction.editRating, + EntryAction.editTags, + ].where((v) => actionDelegate.canApply(entry, v)).map((v) => _buildEditMetadataButton(context, v)).toList(); return children.isEmpty ? const SizedBox() @@ -121,8 +121,8 @@ class BasicSection extends StatelessWidget { ); } - Widget _buildEditMetadataButton(BuildContext context, EntryInfoAction action) { - return ValueListenableBuilder( + Widget _buildEditMetadataButton(BuildContext context, EntryAction action) { + return ValueListenableBuilder( valueListenable: isEditingMetadataNotifier, builder: (context, editingAction, child) { final isEditing = editingAction != null; @@ -138,7 +138,7 @@ class BasicSection extends StatelessWidget { ), child: IconButton( icon: action.getIcon(), - onPressed: isEditing ? null : () => actionDelegate.onActionSelected(context, action), + onPressed: isEditing ? null : () => actionDelegate.onActionSelected(context, entry, collection, action), tooltip: action.getText(context), ), ), diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index b28fbbace..c7dfee805 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -1,5 +1,6 @@ -import 'package:aves/model/actions/entry_info_actions.dart'; +import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/app_bar/app_bar_title.dart'; @@ -15,6 +16,7 @@ import 'package:flutter/scheduler.dart'; class InfoAppBar extends StatelessWidget { final AvesEntry entry; + final CollectionLens? collection; final EntryInfoActionDelegate actionDelegate; final ValueNotifier> metadataNotifier; final VoidCallback onBackPressed; @@ -22,6 +24,7 @@ class InfoAppBar extends StatelessWidget { const InfoAppBar({ super.key, required this.entry, + required this.collection, required this.actionDelegate, required this.metadataNotifier, required this.onBackPressed, @@ -29,8 +32,8 @@ class InfoAppBar extends StatelessWidget { @override Widget build(BuildContext context) { - final commonActions = EntryInfoActions.common.where(actionDelegate.isVisible); - final formatSpecificActions = EntryInfoActions.formatSpecific.where(actionDelegate.isVisible); + final commonActions = EntryActions.commonMetadataActions.where((v) => actionDelegate.isVisible(entry, v)); + final formatSpecificActions = EntryActions.formatSpecificMetadataActions.where((v) => actionDelegate.isVisible(entry, v)); return SliverAppBar( leading: IconButton( @@ -54,22 +57,22 @@ class InfoAppBar extends StatelessWidget { ), if (entry.canEdit) MenuIconTheme( - child: PopupMenuButton( + child: PopupMenuButton( itemBuilder: (context) => [ - ...commonActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(action))), + ...commonActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(entry, action))), if (formatSpecificActions.isNotEmpty) ...[ const PopupMenuDivider(), - ...formatSpecificActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(action))), + ...formatSpecificActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(entry, action))), ], if (!kReleaseMode) ...[ const PopupMenuDivider(), - _toMenuItem(context, EntryInfoAction.debug, enabled: true), + _toMenuItem(context, EntryAction.debug, enabled: true), ] ], onSelected: (action) async { // wait for the popup menu to hide before proceeding with the action await Future.delayed(Durations.popupMenuAnimation * timeDilation); - actionDelegate.onActionSelected(context, action); + actionDelegate.onActionSelected(context, entry, collection, action); }, ), ), @@ -78,7 +81,7 @@ class InfoAppBar extends StatelessWidget { ); } - PopupMenuItem _toMenuItem(BuildContext context, EntryInfoAction action, {required bool enabled}) { + PopupMenuItem _toMenuItem(BuildContext context, EntryAction action, {required bool enabled}) { return PopupMenuItem( value: action, enabled: enabled, diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 5bb4f6384..18cc43c7a 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:aves/model/actions/entry_info_actions.dart'; +import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/events.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; @@ -150,7 +150,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { final List _subscriptions = []; late EntryInfoActionDelegate _actionDelegate; final ValueNotifier> _metadataNotifier = ValueNotifier({}); - final ValueNotifier _isEditingMetadataNotifier = ValueNotifier(null); + final ValueNotifier _isEditingMetadataNotifier = ValueNotifier(null); static const horizontalPadding = EdgeInsets.symmetric(horizontal: 8); @@ -181,7 +181,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { } void _registerWidget(_InfoPageContent widget) { - _actionDelegate = EntryInfoActionDelegate(widget.entry, collection); + _actionDelegate = EntryInfoActionDelegate(); _subscriptions.add(_actionDelegate.eventStream.listen(_onActionDelegateEvent)); } @@ -242,6 +242,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { slivers: [ InfoAppBar( entry: entry, + collection: collection, actionDelegate: _actionDelegate, metadataNotifier: _metadataNotifier, onBackPressed: widget.goToViewer, @@ -260,7 +261,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { ); } - void _onActionDelegateEvent(ActionEvent event) { + void _onActionDelegateEvent(ActionEvent event) { Future.delayed(Durations.dialogTransitionAnimation).then((_) { if (event is ActionStartedEvent) { _isEditingMetadataNotifier.value = event.action; diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index 41caf8f84..0dcab4ebe 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:aves/app_mode.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/overlay/multipage.dart'; @@ -17,7 +18,7 @@ import 'package:tuple/tuple.dart'; class ViewerBottomOverlay extends StatefulWidget { final List entries; final int index; - final bool hasCollection; + final CollectionLens? collection; final AnimationController animationController; final EdgeInsets? viewInsets, viewPadding; final MultiPageController? multiPageController; @@ -26,7 +27,7 @@ class ViewerBottomOverlay extends StatefulWidget { super.key, required this.entries, required this.index, - required this.hasCollection, + required this.collection, required this.animationController, this.viewInsets, this.viewPadding, @@ -65,7 +66,7 @@ class _ViewerBottomOverlayState extends State { index: widget.index, mainEntry: mainEntry, pageEntry: pageEntry ?? mainEntry, - hasCollection: widget.hasCollection, + collection: widget.collection, viewInsets: widget.viewInsets, viewPadding: widget.viewPadding, multiPageController: multiPageController, @@ -96,7 +97,7 @@ class _BottomOverlayContent extends StatefulWidget { final List entries; final int index; final AvesEntry mainEntry, pageEntry; - final bool hasCollection; + final CollectionLens? collection; final EdgeInsets? viewInsets, viewPadding; final MultiPageController? multiPageController; final AnimationController animationController; @@ -106,7 +107,7 @@ class _BottomOverlayContent extends StatefulWidget { required this.index, required this.mainEntry, required this.pageEntry, - required this.hasCollection, + required this.collection, required this.viewInsets, required this.viewPadding, required this.multiPageController, @@ -167,8 +168,8 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> { : ViewerButtons( mainEntry: mainEntry, pageEntry: pageEntry, + collection: widget.collection, scale: _buttonScale, - canToggleFavourite: widget.hasCollection, ), ); diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index aef53584e..c2e589e59 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -1,7 +1,7 @@ import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/app_bar/favourite_toggler.dart'; @@ -9,7 +9,6 @@ import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup_menu_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/action/entry_action_delegate.dart'; -import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/overlay/video/mute_toggler.dart'; @@ -23,10 +22,9 @@ import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; class ViewerButtons extends StatelessWidget { - final AvesEntry mainEntry; - final AvesEntry pageEntry; + final AvesEntry mainEntry, pageEntry; + final CollectionLens? collection; final Animation scale; - final bool canToggleFavourite; static const double outerPadding = 8; static const double innerPadding = 8; @@ -39,75 +37,15 @@ class ViewerButtons extends StatelessWidget { super.key, required this.mainEntry, required this.pageEntry, + required this.collection, required this.scale, - required this.canToggleFavourite, }); @override Widget build(BuildContext context) { + final actionDelegate = EntryActionDelegate(mainEntry, pageEntry, collection); final trashed = mainEntry.trashed; - bool _isVisible(EntryAction action) { - if (trashed) { - switch (action) { - case EntryAction.delete: - case EntryAction.restore: - return true; - case EntryAction.debug: - return kDebugMode; - default: - return false; - } - } else { - final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry; - switch (action) { - case EntryAction.toggleFavourite: - return canToggleFavourite; - case EntryAction.delete: - case EntryAction.rename: - case EntryAction.copy: - case EntryAction.move: - return targetEntry.canEdit; - case EntryAction.rotateCCW: - case EntryAction.rotateCW: - return targetEntry.canRotate; - case EntryAction.flip: - return targetEntry.canFlip; - case EntryAction.convert: - case EntryAction.print: - return !targetEntry.isVideo && device.canPrint; - case EntryAction.openMap: - return targetEntry.hasGps; - case EntryAction.viewSource: - return targetEntry.isSvg; - case EntryAction.videoCaptureFrame: - case EntryAction.videoToggleMute: - case EntryAction.videoSelectStreams: - case EntryAction.videoSetSpeed: - case EntryAction.videoSettings: - case EntryAction.videoTogglePlay: - case EntryAction.videoReplay10: - case EntryAction.videoSkip10: - return targetEntry.isVideo; - case EntryAction.rotateScreen: - return settings.isRotationLocked; - case EntryAction.addShortcut: - return device.canPinShortcut; - case EntryAction.info: - case EntryAction.copyToClipboard: - case EntryAction.edit: - case EntryAction.open: - case EntryAction.setAs: - case EntryAction.share: - return true; - case EntryAction.restore: - return false; - case EntryAction.debug: - return kDebugMode; - } - } - } - return SafeArea( top: false, bottom: false, @@ -118,10 +56,10 @@ class ViewerButtons extends StatelessWidget { return Selector( selector: (context, s) => s.isRotationLocked, builder: (context, s, child) { - final quickActions = (trashed ? EntryActions.trashed : settings.viewerQuickActions).where(_isVisible).take(availableCount - 1).toList(); - final topLevelActions = EntryActions.topLevel.where((action) => !quickActions.contains(action)).where(_isVisible).toList(); - final exportActions = EntryActions.export.where((action) => !quickActions.contains(action)).where(_isVisible).toList(); - final videoActions = EntryActions.video.where((action) => !quickActions.contains(action)).where(_isVisible).toList(); + final quickActions = (trashed ? EntryActions.trashed : settings.viewerQuickActions).where(actionDelegate.isVisible).where(actionDelegate.canApply).take(availableCount - 1).toList(); + final topLevelActions = EntryActions.topLevel.where((action) => !quickActions.contains(action)).where(actionDelegate.isVisible).toList(); + final exportActions = EntryActions.export.where((action) => !quickActions.contains(action)).where(actionDelegate.isVisible).toList(); + final videoActions = EntryActions.video.where((action) => !quickActions.contains(action)).where(actionDelegate.isVisible).toList(); return ViewerButtonRowContent( quickActions: quickActions, topLevelActions: topLevelActions, @@ -130,6 +68,7 @@ class ViewerButtons extends StatelessWidget { scale: scale, mainEntry: mainEntry, pageEntry: pageEntry, + collection: collection, ); }, ); @@ -143,6 +82,7 @@ class ViewerButtonRowContent extends StatelessWidget { final List quickActions, topLevelActions, exportActions, videoActions; final Animation scale; final AvesEntry mainEntry, pageEntry; + final CollectionLens? collection; final ValueNotifier _popupExpandedNotifier = ValueNotifier(null); AvesEntry get favouriteTargetEntry => mainEntry.isBurst ? pageEntry : mainEntry; @@ -158,6 +98,7 @@ class ViewerButtonRowContent extends StatelessWidget { required this.scale, required this.mainEntry, required this.pageEntry, + required this.collection, }); @override @@ -358,17 +299,7 @@ class ViewerButtonRowContent extends StatelessWidget { } PopupMenuItem _buildRotateAndFlipMenuItems(BuildContext context) { - bool canApply(EntryAction action) { - switch (action) { - case EntryAction.rotateCCW: - case EntryAction.rotateCW: - return pageEntry.canRotate; - case EntryAction.flip: - return pageEntry.canFlip; - default: - return true; - } - } + final actionDelegate = EntryActionDelegate(mainEntry, pageEntry, collection); Widget buildDivider() => const SizedBox( height: 16, @@ -386,7 +317,7 @@ class ViewerButtonRowContent extends StatelessWidget { clipBehavior: Clip.antiAlias, child: PopupMenuItem( value: action, - enabled: canApply(action), + enabled: actionDelegate.canApply(action), child: Tooltip( message: action.getText(context), child: Center(child: action.getIcon()), @@ -423,17 +354,6 @@ class ViewerButtonRowContent extends StatelessWidget { } void _onActionSelected(BuildContext context, EntryAction action) { - var targetEntry = mainEntry; - if (mainEntry.isMultiPage && (mainEntry.isBurst || EntryActions.pageActions.contains(action))) { - final multiPageController = context.read().getController(mainEntry); - if (multiPageController != null) { - final multiPageInfo = multiPageController.info; - final pageEntry = multiPageInfo?.getPageEntryByIndex(multiPageController.page); - if (pageEntry != null) { - targetEntry = pageEntry; - } - } - } - EntryActionDelegate(targetEntry).onActionSelected(context, action); + EntryActionDelegate(mainEntry, pageEntry, collection).onActionSelected(context, action); } } diff --git a/lib/widgets/wallpaper_page.dart b/lib/widgets/wallpaper_page.dart index ef7b4b61e..93ee8db9e 100644 --- a/lib/widgets/wallpaper_page.dart +++ b/lib/widgets/wallpaper_page.dart @@ -211,7 +211,7 @@ class _EntryEditorState extends State with EntryViewControllerMixin ViewerBottomOverlay( entries: [widget.entry], index: 0, - hasCollection: false, + collection: null, animationController: _overlayAnimationController, viewInsets: _frozenViewInsets, viewPadding: _frozenViewPadding,