#390 viewer: metadata editing actions available as quick actions
This commit is contained in:
parent
093b967e26
commit
1e624aebae
14 changed files with 404 additions and 417 deletions
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,6 +30,7 @@ class ViewerActionEditorPage extends StatelessWidget {
|
|||
EntryAction.videoSetSpeed,
|
||||
EntryAction.videoSelectStreams,
|
||||
],
|
||||
EntryActions.commonMetadataActions,
|
||||
];
|
||||
|
||||
@override
|
||||
|
|
|
@ -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<MultiPageConductor>().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<VideoConductor>().getController(entry);
|
||||
final controller = context.read<VideoConductor>().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<void> _addShortcut(BuildContext context) async {
|
||||
Future<void> _addShortcut(BuildContext context, AvesEntry targetEntry) async {
|
||||
final result = await showDialog<Tuple2<AvesEntry?, String>>(
|
||||
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<void> _flip(BuildContext context) async {
|
||||
await edit(context, entry.flip);
|
||||
Future<void> _flip(BuildContext context, AvesEntry targetEntry) async {
|
||||
await edit(context, targetEntry, targetEntry.flip);
|
||||
}
|
||||
|
||||
Future<void> _rotate(BuildContext context, {required bool clockwise}) async {
|
||||
await edit(context, () => entry.rotate(clockwise: clockwise));
|
||||
Future<void> _rotate(BuildContext context, AvesEntry targetEntry, {required bool clockwise}) async {
|
||||
await edit(context, targetEntry, () => targetEntry.rotate(clockwise: clockwise));
|
||||
}
|
||||
|
||||
Future<void> _rotateScreen(BuildContext context) async {
|
||||
|
@ -185,9 +311,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _delete(BuildContext context) async {
|
||||
if (settings.enableBin && !entry.trashed) {
|
||||
await _move(context, moveType: MoveType.toBin);
|
||||
Future<void> _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<CollectionSource>();
|
||||
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<void> _convert(BuildContext context) async {
|
||||
Future<void> _convert(BuildContext context, AvesEntry targetEntry) async {
|
||||
final options = await showDialog<EntryExportOptions>(
|
||||
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 = <AvesEntry>{};
|
||||
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<void> _move(BuildContext context, {required MoveType moveType}) => move(
|
||||
Future<void> _move(BuildContext context, AvesEntry targetEntry, {required MoveType moveType}) => move(
|
||||
context,
|
||||
moveType: moveType,
|
||||
entries: {entry},
|
||||
entries: {targetEntry},
|
||||
);
|
||||
|
||||
Future<void> _rename(BuildContext context) async {
|
||||
Future<void> _rename(BuildContext context, AvesEntry targetEntry) async {
|
||||
final newName = await showDialog<String>(
|
||||
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<ValueNotifier<AppMode>>().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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<ActionEvent<EntryAction>> _eventStreamController = StreamController.broadcast();
|
||||
|
||||
final StreamController<ActionEvent<EntryInfoAction>> _eventStreamController = StreamController.broadcast();
|
||||
Stream<ActionEvent<EntryAction>> get eventStream => _eventStreamController.stream;
|
||||
|
||||
Stream<ActionEvent<EntryInfoAction>> 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<void> _editDate(BuildContext context) async {
|
||||
final modifier = await selectDateModifier(context, {entry}, collection);
|
||||
Future<void> _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<void> _editLocation(BuildContext context) async {
|
||||
final location = await selectLocation(context, {entry}, collection);
|
||||
Future<void> _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<void> _editTitleDescription(BuildContext context) async {
|
||||
final modifier = await selectTitleDescriptionModifier(context, {entry});
|
||||
Future<void> _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<void> _editRating(BuildContext context) async {
|
||||
final rating = await selectRating(context, {entry});
|
||||
Future<void> _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<void> _editTags(BuildContext context) async {
|
||||
final newTagsByEntry = await selectTags(context, {entry});
|
||||
Future<void> _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<void> _removeMetadata(BuildContext context) async {
|
||||
final types = await selectMetadataToRemove(context, {entry});
|
||||
Future<void> _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<void> _exportMetadata(BuildContext context) async {
|
||||
Future<void> _exportMetadata(BuildContext context, AvesEntry targetEntry) async {
|
||||
final lines = <String>[];
|
||||
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<void> _convertMotionPhotoToStillImage(BuildContext context) async {
|
||||
Future<void> _convertMotionPhotoToStillImage(BuildContext context, AvesEntry targetEntry) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
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<void> _showGeoTiffOnMap(BuildContext context) async {
|
||||
final info = await metadataFetchService.getGeoTiffInfo(entry);
|
||||
Future<void> _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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ValueNotifier<AppMode>>().value == AppMode.main;
|
||||
|
||||
Future<void> edit(BuildContext context, Future<Set<EntryDataType>> Function() apply) async {
|
||||
if (!await checkStoragePermission(context, {entry})) return;
|
||||
Future<void> edit(BuildContext context, AvesEntry targetEntry, Future<Set<EntryDataType>> 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<String> obsoleteTags = entry.tags;
|
||||
String? obsoleteCountryCode = entry.addressDetails?.countryCode;
|
||||
Set<String> 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 {
|
||||
|
|
|
@ -440,7 +440,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
ViewerBottomOverlay(
|
||||
entries: entries,
|
||||
index: _currentEntryIndex,
|
||||
hasCollection: hasCollection,
|
||||
collection: collection,
|
||||
animationController: _overlayAnimationController,
|
||||
viewInsets: _frozenViewInsets,
|
||||
viewPadding: _frozenViewPadding,
|
||||
|
|
|
@ -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<EntryInfoAction?> isEditingMetadataNotifier;
|
||||
final ValueNotifier<EntryAction?> 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<EntryInfoAction?>(
|
||||
Widget _buildEditMetadataButton(BuildContext context, EntryAction action) {
|
||||
return ValueListenableBuilder<EntryAction?>(
|
||||
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),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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<Map<String, MetadataDirectory>> 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<EntryInfoAction>(
|
||||
child: PopupMenuButton<EntryAction>(
|
||||
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<EntryInfoAction> _toMenuItem(BuildContext context, EntryInfoAction action, {required bool enabled}) {
|
||||
PopupMenuItem<EntryAction> _toMenuItem(BuildContext context, EntryAction action, {required bool enabled}) {
|
||||
return PopupMenuItem(
|
||||
value: action,
|
||||
enabled: enabled,
|
||||
|
|
|
@ -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<StreamSubscription> _subscriptions = [];
|
||||
late EntryInfoActionDelegate _actionDelegate;
|
||||
final ValueNotifier<Map<String, MetadataDirectory>> _metadataNotifier = ValueNotifier({});
|
||||
final ValueNotifier<EntryInfoAction?> _isEditingMetadataNotifier = ValueNotifier(null);
|
||||
final ValueNotifier<EntryAction?> _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<EntryInfoAction> event) {
|
||||
void _onActionDelegateEvent(ActionEvent<EntryAction> event) {
|
||||
Future.delayed(Durations.dialogTransitionAnimation).then((_) {
|
||||
if (event is ActionStartedEvent) {
|
||||
_isEditingMetadataNotifier.value = event.action;
|
||||
|
|
|
@ -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<AvesEntry> 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<ViewerBottomOverlay> {
|
|||
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<AvesEntry> 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,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
@ -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<double> 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<Settings, bool>(
|
||||
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<EntryAction> quickActions, topLevelActions, exportActions, videoActions;
|
||||
final Animation<double> scale;
|
||||
final AvesEntry mainEntry, pageEntry;
|
||||
final CollectionLens? collection;
|
||||
final ValueNotifier<String?> _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<EntryAction> _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<MultiPageConductor>().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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -211,7 +211,7 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
|
|||
ViewerBottomOverlay(
|
||||
entries: [widget.entry],
|
||||
index: 0,
|
||||
hasCollection: false,
|
||||
collection: null,
|
||||
animationController: _overlayAnimationController,
|
||||
viewInsets: _frozenViewInsets,
|
||||
viewPadding: _frozenViewPadding,
|
||||
|
|
Loading…
Reference in a new issue