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