#146 editing orientation/tags automatically sets a metadata date
This commit is contained in:
parent
da7b2ee8c1
commit
445bde2494
12 changed files with 141 additions and 96 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -8,6 +8,11 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
- Info: option to set date from other fields
|
- Info: option to set date from other fields
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- editing an item orientation or tags automatically sets a metadata date (from the file modified
|
||||||
|
date), if it is missing
|
||||||
|
|
||||||
## <a id="v1.5.9"></a>[v1.5.9] - 2021-12-22
|
## <a id="v1.5.9"></a>[v1.5.9] - 2021-12-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -41,7 +46,8 @@ All notable changes to this project will be documented in this file.
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Settings: select hidden path directory with a custom file picker instead of the native SAF one
|
- Settings: select hidden path directory with a custom file picker instead of the native SAF one
|
||||||
- Viewer: video cover (before playing the video) is now loaded at original resolution and can be zoomed
|
- Viewer: video cover (before playing the video) is now loaded at original resolution and can be
|
||||||
|
zoomed
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
@ -79,7 +85,8 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- use build flavors to match distribution channels: `play` (same as original) and `izzy` (no Crashlytics)
|
- use build flavors to match distribution channels: `play` (same as original) and `izzy` (no
|
||||||
|
Crashlytics)
|
||||||
- use 12/24 hour format settings from device to display times
|
- use 12/24 hour format settings from device to display times
|
||||||
- Privacy: consent request on first launch for installed app inventory access
|
- Privacy: consent request on first launch for installed app inventory access
|
||||||
- use File API to rename and delete items, when possible (primary storage, Android <11)
|
- use File API to rename and delete items, when possible (primary storage, Android <11)
|
||||||
|
|
|
@ -648,26 +648,28 @@ class AvesEntry {
|
||||||
await locate(background: background, force: dataTypes.contains(EntryDataType.address), geocoderLocale: geocoderLocale);
|
await locate(background: background, force: dataTypes.contains(EntryDataType.address), geocoderLocale: geocoderLocale);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Set<EntryDataType>> rotate({required bool clockwise, required bool persist}) async {
|
Future<Set<EntryDataType>> _changeOrientation(Future<Map<String, dynamic>> Function() apply) async {
|
||||||
final newFields = await metadataEditService.rotate(this, clockwise: clockwise);
|
final dataTypes = await setMetadataDateIfMissing();
|
||||||
if (newFields.isEmpty) return {};
|
|
||||||
|
|
||||||
await _applyNewFields(newFields, persist: persist);
|
final newFields = await apply();
|
||||||
return {
|
// applying fields is only useful for a smoother visual change,
|
||||||
EntryDataType.basic,
|
// as proper refreshing and persistence happens at the caller level
|
||||||
EntryDataType.catalog,
|
await _applyNewFields(newFields, persist: false);
|
||||||
};
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.addAll({
|
||||||
|
EntryDataType.basic,
|
||||||
|
EntryDataType.catalog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return dataTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Set<EntryDataType>> flip({required bool persist}) async {
|
Future<Set<EntryDataType>> rotate({required bool clockwise}) {
|
||||||
final newFields = await metadataEditService.flip(this);
|
return _changeOrientation(() => metadataEditService.rotate(this, clockwise: clockwise));
|
||||||
if (newFields.isEmpty) return {};
|
}
|
||||||
|
|
||||||
await _applyNewFields(newFields, persist: persist);
|
Future<Set<EntryDataType>> flip() {
|
||||||
return {
|
return _changeOrientation(() => metadataEditService.flip(this));
|
||||||
EntryDataType.basic,
|
|
||||||
EntryDataType.catalog,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Set<EntryDataType>> editDate(DateModifier modifier) async {
|
Future<Set<EntryDataType>> editDate(DateModifier modifier) async {
|
||||||
|
@ -730,6 +732,25 @@ class AvesEntry {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// when editing a file that has no metadata date,
|
||||||
|
// we will set one, using the file modified date, if any
|
||||||
|
Future<Set<EntryDataType>> setMetadataDateIfMissing() async {
|
||||||
|
if (path == null) return {};
|
||||||
|
|
||||||
|
// make sure entry is catalogued before we check whether is has a metadata date
|
||||||
|
if (!isCatalogued) {
|
||||||
|
await catalog(background: false, force: false, persist: true);
|
||||||
|
}
|
||||||
|
final metadataDate = catalogMetadata?.dateMillis;
|
||||||
|
if (metadataDate != null && metadataDate > 0) return {};
|
||||||
|
|
||||||
|
return await editDate(const DateModifier(
|
||||||
|
DateEditAction.set,
|
||||||
|
{MetadataField.exifDateOriginal},
|
||||||
|
setSource: DateSetSource.fileModifiedDate,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Future<Set<EntryDataType>> removeMetadata(Set<MetadataType> types) async {
|
Future<Set<EntryDataType>> removeMetadata(Set<MetadataType> types) async {
|
||||||
final newFields = await metadataEditService.removeTypes(this, types);
|
final newFields = await metadataEditService.removeTypes(this, types);
|
||||||
return newFields.isEmpty
|
return newFields.isEmpty
|
||||||
|
|
|
@ -43,6 +43,8 @@ extension ExtraAvesEntryXmpIptc on AvesEntry {
|
||||||
static String prefixOf(String ns) => nsDefaultPrefixes[ns] ?? '';
|
static String prefixOf(String ns) => nsDefaultPrefixes[ns] ?? '';
|
||||||
|
|
||||||
Future<Set<EntryDataType>> editTags(Set<String> tags) async {
|
Future<Set<EntryDataType>> editTags(Set<String> tags) async {
|
||||||
|
final dataTypes = await setMetadataDateIfMissing();
|
||||||
|
|
||||||
final xmp = await metadataFetchService.getXmp(this);
|
final xmp = await metadataFetchService.getXmp(this);
|
||||||
final extendedXmpString = xmp?.extendedXmpString;
|
final extendedXmpString = xmp?.extendedXmpString;
|
||||||
|
|
||||||
|
@ -118,7 +120,10 @@ extension ExtraAvesEntryXmpIptc on AvesEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
final newFields = await metadataEditService.setXmp(this, editedXmp);
|
final newFields = await metadataEditService.setXmp(this, editedXmp);
|
||||||
return newFields.isEmpty ? {} : {EntryDataType.catalog};
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.add(EntryDataType.catalog);
|
||||||
|
}
|
||||||
|
return dataTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setIptcKeywords(List<Map<String, dynamic>> iptc, Set<String> tags) async {
|
Future<void> _setIptcKeywords(List<Map<String, dynamic>> iptc, Set<String> tags) async {
|
||||||
|
|
|
@ -489,7 +489,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canRotateAndFlip);
|
final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canRotateAndFlip);
|
||||||
if (todoItems == null || todoItems.isEmpty) return;
|
if (todoItems == null || todoItems.isEmpty) return;
|
||||||
|
|
||||||
await _edit(context, selection, todoItems, (entry) => entry.rotate(clockwise: clockwise, persist: true));
|
await _edit(context, selection, todoItems, (entry) => entry.rotate(clockwise: clockwise));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _flip(BuildContext context) async {
|
Future<void> _flip(BuildContext context) async {
|
||||||
|
@ -499,7 +499,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canRotateAndFlip);
|
final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canRotateAndFlip);
|
||||||
if (todoItems == null || todoItems.isEmpty) return;
|
if (todoItems == null || todoItems.isEmpty) return;
|
||||||
|
|
||||||
await _edit(context, selection, todoItems, (entry) => entry.flip(persist: true));
|
await _edit(context, selection, todoItems, (entry) => entry.flip());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _editDate(BuildContext context) async {
|
Future<void> _editDate(BuildContext context) async {
|
||||||
|
|
|
@ -24,20 +24,26 @@ 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/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/debug/debug_page.dart';
|
||||||
import 'package:aves/widgets/viewer/info/notifications.dart';
|
import 'package:aves/widgets/viewer/info/notifications.dart';
|
||||||
import 'package:aves/widgets/viewer/printer.dart';
|
|
||||||
import 'package:aves/widgets/viewer/source_viewer_page.dart';
|
import 'package:aves/widgets/viewer/source_viewer_page.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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 {
|
class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin, SingleEntryEditorMixin {
|
||||||
void onActionSelected(BuildContext context, AvesEntry entry, EntryAction action) {
|
@override
|
||||||
|
final AvesEntry entry;
|
||||||
|
|
||||||
|
EntryActionDelegate(this.entry);
|
||||||
|
|
||||||
|
void onActionSelected(BuildContext context, EntryAction action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case EntryAction.addShortcut:
|
case EntryAction.addShortcut:
|
||||||
_addShortcut(context, entry);
|
_addShortcut(context);
|
||||||
break;
|
break;
|
||||||
case EntryAction.copyToClipboard:
|
case EntryAction.copyToClipboard:
|
||||||
androidAppService.copyToClipboard(entry.uri, entry.bestTitle).then((success) {
|
androidAppService.copyToClipboard(entry.uri, entry.bestTitle).then((success) {
|
||||||
|
@ -45,10 +51,10 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case EntryAction.delete:
|
case EntryAction.delete:
|
||||||
_delete(context, entry);
|
_delete(context);
|
||||||
break;
|
break;
|
||||||
case EntryAction.export:
|
case EntryAction.export:
|
||||||
_export(context, entry);
|
_export(context);
|
||||||
break;
|
break;
|
||||||
case EntryAction.info:
|
case EntryAction.info:
|
||||||
ShowInfoNotification().dispatch(context);
|
ShowInfoNotification().dispatch(context);
|
||||||
|
@ -57,7 +63,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
EntryPrinter(entry).print(context);
|
EntryPrinter(entry).print(context);
|
||||||
break;
|
break;
|
||||||
case EntryAction.rename:
|
case EntryAction.rename:
|
||||||
_rename(context, entry);
|
_rename(context);
|
||||||
break;
|
break;
|
||||||
case EntryAction.share:
|
case EntryAction.share:
|
||||||
androidAppService.shareEntries({entry}).then((success) {
|
androidAppService.shareEntries({entry}).then((success) {
|
||||||
|
@ -69,17 +75,17 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
break;
|
break;
|
||||||
// raster
|
// raster
|
||||||
case EntryAction.rotateCCW:
|
case EntryAction.rotateCCW:
|
||||||
_rotate(context, entry, clockwise: false);
|
_rotate(context, clockwise: false);
|
||||||
break;
|
break;
|
||||||
case EntryAction.rotateCW:
|
case EntryAction.rotateCW:
|
||||||
_rotate(context, entry, clockwise: true);
|
_rotate(context, clockwise: true);
|
||||||
break;
|
break;
|
||||||
case EntryAction.flip:
|
case EntryAction.flip:
|
||||||
_flip(context, entry);
|
_flip(context);
|
||||||
break;
|
break;
|
||||||
// vector
|
// vector
|
||||||
case EntryAction.viewSource:
|
case EntryAction.viewSource:
|
||||||
_goToSourceViewer(context, entry);
|
_goToSourceViewer(context);
|
||||||
break;
|
break;
|
||||||
// external
|
// external
|
||||||
case EntryAction.edit:
|
case EntryAction.edit:
|
||||||
|
@ -108,12 +114,12 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
break;
|
break;
|
||||||
// debug
|
// debug
|
||||||
case EntryAction.debug:
|
case EntryAction.debug:
|
||||||
_goToDebug(context, entry);
|
_goToDebug(context);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addShortcut(BuildContext context, AvesEntry entry) async {
|
Future<void> _addShortcut(BuildContext context) 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(
|
||||||
|
@ -131,18 +137,12 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _flip(BuildContext context, AvesEntry entry) async {
|
Future<void> _flip(BuildContext context) async {
|
||||||
if (!await checkStoragePermission(context, {entry})) return;
|
await edit(context, entry.flip);
|
||||||
|
|
||||||
final dataTypes = await entry.flip(persist: _isMainMode(context));
|
|
||||||
if (dataTypes.isEmpty) showFeedback(context, context.l10n.genericFailureFeedback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _rotate(BuildContext context, AvesEntry entry, {required bool clockwise}) async {
|
Future<void> _rotate(BuildContext context, {required bool clockwise}) async {
|
||||||
if (!await checkStoragePermission(context, {entry})) return;
|
await edit(context, () => entry.rotate(clockwise: clockwise));
|
||||||
|
|
||||||
final dataTypes = await entry.rotate(clockwise: clockwise, persist: _isMainMode(context));
|
|
||||||
if (dataTypes.isEmpty) showFeedback(context, context.l10n.genericFailureFeedback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _rotateScreen(BuildContext context) async {
|
Future<void> _rotateScreen(BuildContext context) async {
|
||||||
|
@ -156,7 +156,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _delete(BuildContext context, AvesEntry entry) async {
|
Future<void> _delete(BuildContext context) async {
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
|
@ -190,7 +190,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _export(BuildContext context, AvesEntry entry) async {
|
Future<void> _export(BuildContext context) async {
|
||||||
final source = context.read<CollectionSource>();
|
final source = context.read<CollectionSource>();
|
||||||
if (!source.initialized) {
|
if (!source.initialized) {
|
||||||
await source.init();
|
await source.init();
|
||||||
|
@ -291,7 +291,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _rename(BuildContext context, AvesEntry entry) async {
|
Future<void> _rename(BuildContext context) async {
|
||||||
final newName = await showDialog<String>(
|
final newName = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => RenameEntryDialog(entry: entry),
|
builder: (context) => RenameEntryDialog(entry: entry),
|
||||||
|
@ -311,7 +311,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
|
|
||||||
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, AvesEntry entry) {
|
void _goToSourceViewer(BuildContext context) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
@ -323,7 +323,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _goToDebug(BuildContext context, AvesEntry entry) {
|
void _goToDebug(BuildContext context) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
|
@ -1,22 +1,18 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/app_mode.dart';
|
|
||||||
import 'package:aves/model/actions/entry_info_actions.dart';
|
import 'package:aves/model/actions/entry_info_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_xmp_iptc.dart';
|
import 'package:aves/model/entry_xmp_iptc.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
|
||||||
import 'package:aves/services/common/services.dart';
|
|
||||||
import 'package:aves/widgets/common/action_mixins/entry_editor.dart';
|
import 'package:aves/widgets/common/action_mixins/entry_editor.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/viewer/action/single_entry_editor.dart';
|
||||||
import 'package:aves/widgets/viewer/embedded/notifications.dart';
|
import 'package:aves/widgets/viewer/embedded/notifications.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class EntryInfoActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwareMixin {
|
class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEditorMixin, SingleEntryEditorMixin {
|
||||||
|
@override
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
|
|
||||||
final StreamController<ActionEvent<EntryInfoAction>> _eventStreamController = StreamController<ActionEvent<EntryInfoAction>>.broadcast();
|
final StreamController<ActionEvent<EntryInfoAction>> _eventStreamController = StreamController<ActionEvent<EntryInfoAction>>.broadcast();
|
||||||
|
@ -74,43 +70,11 @@ class EntryInfoActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAw
|
||||||
_eventStreamController.add(ActionEndedEvent(action));
|
_eventStreamController.add(ActionEndedEvent(action));
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// check before applying, because it relies on provider
|
|
||||||
// but the widget tree may be disposed if the user navigated away
|
|
||||||
final isMainMode = _isMainMode(context);
|
|
||||||
|
|
||||||
final l10n = context.l10n;
|
|
||||||
final source = context.read<CollectionSource?>();
|
|
||||||
source?.pauseMonitoring();
|
|
||||||
|
|
||||||
final dataTypes = await apply();
|
|
||||||
final success = dataTypes.isNotEmpty;
|
|
||||||
try {
|
|
||||||
if (success) {
|
|
||||||
if (isMainMode && source != null) {
|
|
||||||
await source.refreshEntry(entry, dataTypes);
|
|
||||||
} else {
|
|
||||||
await entry.refresh(background: false, persist: false, dataTypes: dataTypes, geocoderLocale: settings.appliedLocale);
|
|
||||||
}
|
|
||||||
showFeedback(context, l10n.genericSuccessFeedback);
|
|
||||||
} else {
|
|
||||||
showFeedback(context, l10n.genericFailureFeedback);
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
|
||||||
await reportService.recordError(e, stack);
|
|
||||||
}
|
|
||||||
source?.resumeMonitoring();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _editDate(BuildContext context) async {
|
Future<void> _editDate(BuildContext context) async {
|
||||||
final modifier = await selectDateModifier(context, {entry});
|
final modifier = await selectDateModifier(context, {entry});
|
||||||
if (modifier == null) return;
|
if (modifier == null) return;
|
||||||
|
|
||||||
await _edit(context, () => entry.editDate(modifier));
|
await edit(context, () => entry.editDate(modifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _editTags(BuildContext context) async {
|
Future<void> _editTags(BuildContext context) async {
|
||||||
|
@ -121,13 +85,13 @@ class EntryInfoActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAw
|
||||||
final currentTags = entry.tags;
|
final currentTags = entry.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, () => entry.editTags(newTags));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _removeMetadata(BuildContext context) async {
|
Future<void> _removeMetadata(BuildContext context) async {
|
||||||
final types = await selectMetadataToRemove(context, {entry});
|
final types = await selectMetadataToRemove(context, {entry});
|
||||||
if (types == null) return;
|
if (types == null) return;
|
||||||
|
|
||||||
await _edit(context, () => entry.removeMetadata(types));
|
await edit(context, () => entry.removeMetadata(types));
|
||||||
}
|
}
|
||||||
}
|
}
|
48
lib/widgets/viewer/action/single_entry_editor.dart
Normal file
48
lib/widgets/viewer/action/single_entry_editor.dart
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
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_source.dart';
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||||
|
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
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;
|
||||||
|
|
||||||
|
// check before applying, because it relies on provider
|
||||||
|
// but the widget tree may be disposed if the user navigated away
|
||||||
|
final isMainMode = _isMainMode(context);
|
||||||
|
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final source = context.read<CollectionSource?>();
|
||||||
|
source?.pauseMonitoring();
|
||||||
|
|
||||||
|
final dataTypes = await apply();
|
||||||
|
final success = dataTypes.isNotEmpty;
|
||||||
|
try {
|
||||||
|
if (success) {
|
||||||
|
if (isMainMode && source != null) {
|
||||||
|
await source.refreshEntry(entry, dataTypes);
|
||||||
|
} else {
|
||||||
|
await entry.refresh(background: false, persist: false, dataTypes: dataTypes, geocoderLocale: settings.appliedLocale);
|
||||||
|
}
|
||||||
|
showFeedback(context, l10n.genericSuccessFeedback);
|
||||||
|
} else {
|
||||||
|
showFeedback(context, l10n.genericFailureFeedback);
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
await reportService.recordError(e, stack);
|
||||||
|
}
|
||||||
|
source?.resumeMonitoring();
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ import 'package:aves/utils/file_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||||
import 'package:aves/widgets/viewer/info/common.dart';
|
import 'package:aves/widgets/viewer/info/common.dart';
|
||||||
import 'package:aves/widgets/viewer/info/entry_info_action_delegate.dart';
|
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart';
|
||||||
import 'package:aves/widgets/viewer/info/owner.dart';
|
import 'package:aves/widgets/viewer/info/owner.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/app_bar_title.dart';
|
import 'package:aves/widgets/common/app_bar_title.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/menu.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/viewer/info/entry_info_action_delegate.dart';
|
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart';
|
||||||
import 'package:aves/widgets/viewer/info/info_search.dart';
|
import 'package:aves/widgets/viewer/info/info_search.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart';
|
import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
|
@ -8,9 +8,9 @@ import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
|
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart';
|
||||||
import 'package:aves/widgets/viewer/embedded/embedded_data_opener.dart';
|
import 'package:aves/widgets/viewer/embedded/embedded_data_opener.dart';
|
||||||
import 'package:aves/widgets/viewer/info/basic_section.dart';
|
import 'package:aves/widgets/viewer/info/basic_section.dart';
|
||||||
import 'package:aves/widgets/viewer/info/entry_info_action_delegate.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/info_app_bar.dart';
|
import 'package:aves/widgets/viewer/info/info_app_bar.dart';
|
||||||
import 'package:aves/widgets/viewer/info/location_section.dart';
|
import 'package:aves/widgets/viewer/info/location_section.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart';
|
import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart';
|
||||||
|
|
|
@ -9,7 +9,7 @@ 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/common/fx/sweeper.dart';
|
import 'package:aves/widgets/common/fx/sweeper.dart';
|
||||||
import 'package:aves/widgets/viewer/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/multipage/conductor.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/minimap.dart';
|
import 'package:aves/widgets/viewer/overlay/minimap.dart';
|
||||||
|
@ -312,7 +312,7 @@ class _TopOverlayRow extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EntryActionDelegate().onActionSelected(context, targetEntry, action);
|
EntryActionDelegate(targetEntry).onActionSelected(context, action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue