diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index fda262d88..be8b3564d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -335,6 +335,7 @@ "editEntryDateDialogTitle": "Date & Time", "editEntryDateDialogSetCustom": "Set custom date", "editEntryDateDialogCopyField": "Copy from other date", + "editEntryDateDialogCopyItem": "Copy from other item", "editEntryDateDialogExtractFromTitle": "Extract from title", "editEntryDateDialogShift": "Shift", "editEntryDateDialogSourceFileModifiedDate": "File modified date", diff --git a/lib/model/entry_metadata_edition.dart b/lib/model/entry_metadata_edition.dart index 2b2ee6054..8a9856aa7 100644 --- a/lib/model/entry_metadata_edition.dart +++ b/lib/model/entry_metadata_edition.dart @@ -42,6 +42,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { switch (appliedModifier.action) { case DateEditAction.setCustom: case DateEditAction.copyField: + case DateEditAction.copyItem: case DateEditAction.extractFromTitle: editCreateDateXmp(descriptions, appliedModifier.setDateTime); break; @@ -319,6 +320,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { final date = parseUnknownDateFormat(bestTitle); return date != null ? DateModifier.setCustom(mainMetadataDate(), date) : null; case DateEditAction.setCustom: + case DateEditAction.copyItem: return DateModifier.setCustom(mainMetadataDate(), modifier.setDateTime!); case DateEditAction.shift: case DateEditAction.remove: diff --git a/lib/model/metadata/date_modifier.dart b/lib/model/metadata/date_modifier.dart index 73d648463..3e0c20f94 100644 --- a/lib/model/metadata/date_modifier.dart +++ b/lib/model/metadata/date_modifier.dart @@ -24,30 +24,30 @@ class DateModifier extends Equatable { List get props => [action, fields, setDateTime, copyFieldSource, shiftMinutes]; const DateModifier._private( - this.action, - this.fields, { + this.action, { + this.fields = const {}, this.setDateTime, this.copyFieldSource, this.shiftMinutes, }); factory DateModifier.setCustom(Set fields, DateTime dateTime) { - return DateModifier._private(DateEditAction.setCustom, fields, setDateTime: dateTime); + return DateModifier._private(DateEditAction.setCustom, fields: fields, setDateTime: dateTime); } - factory DateModifier.copyField(Set fields, DateFieldSource copyFieldSource) { - return DateModifier._private(DateEditAction.copyField, fields, copyFieldSource: copyFieldSource); + factory DateModifier.copyField(DateFieldSource copyFieldSource) { + return DateModifier._private(DateEditAction.copyField, copyFieldSource: copyFieldSource); } - factory DateModifier.extractFromTitle(Set fields) { - return DateModifier._private(DateEditAction.extractFromTitle, fields); + factory DateModifier.extractFromTitle() { + return const DateModifier._private(DateEditAction.extractFromTitle); } factory DateModifier.shift(Set fields, int shiftMinutes) { - return DateModifier._private(DateEditAction.shift, fields, shiftMinutes: shiftMinutes); + return DateModifier._private(DateEditAction.shift, fields: fields, shiftMinutes: shiftMinutes); } factory DateModifier.remove(Set fields) { - return DateModifier._private(DateEditAction.remove, fields); + return DateModifier._private(DateEditAction.remove, fields: fields); } } diff --git a/lib/model/metadata/enums.dart b/lib/model/metadata/enums.dart index 086f35894..e35f8c743 100644 --- a/lib/model/metadata/enums.dart +++ b/lib/model/metadata/enums.dart @@ -3,6 +3,7 @@ import 'package:aves/model/metadata/fields.dart'; enum DateEditAction { setCustom, copyField, + copyItem, extractFromTitle, shift, remove, diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 6c6f8766b..b8147e4cf 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -427,7 +427,8 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final todoItems = await _getEditableTargetItems(context, canEdit: (entry) => entry.canEditDate); if (todoItems == null || todoItems.isEmpty) return; - final modifier = await selectDateModifier(context, todoItems); + final collection = context.read(); + final modifier = await selectDateModifier(context, todoItems, collection); if (modifier == null) return; await _edit(context, todoItems, (entry) => entry.editDate(modifier)); diff --git a/lib/widgets/common/action_mixins/entry_editor.dart b/lib/widgets/common/action_mixins/entry_editor.dart index 6e67e4865..9b0de9185 100644 --- a/lib/widgets/common/action_mixins/entry_editor.dart +++ b/lib/widgets/common/action_mixins/entry_editor.dart @@ -14,13 +14,14 @@ import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; mixin EntryEditorMixin { - Future selectDateModifier(BuildContext context, Set entries) async { + Future selectDateModifier(BuildContext context, Set entries, CollectionLens? collection) async { if (entries.isEmpty) return null; final modifier = await showDialog( context: context, builder: (context) => EditEntryDateDialog( entry: entries.first, + collection: collection, ), ); return modifier; diff --git a/lib/widgets/common/grid/overlay.dart b/lib/widgets/common/grid/overlay.dart index 0bae6317b..4af0f83d2 100644 --- a/lib/widgets/common/grid/overlay.dart +++ b/lib/widgets/common/grid/overlay.dart @@ -56,7 +56,7 @@ class GridItemSelectionOverlay extends StatelessWidget { return child; }, ) - : const SizedBox.shrink(); + : const SizedBox(); return AnimatedSwitcher( duration: duration, child: child, diff --git a/lib/widgets/dialogs/add_shortcut_dialog.dart b/lib/widgets/dialogs/add_shortcut_dialog.dart index 480430a31..799e2200e 100644 --- a/lib/widgets/dialogs/add_shortcut_dialog.dart +++ b/lib/widgets/dialogs/add_shortcut_dialog.dart @@ -2,10 +2,9 @@ import 'package:aves/model/covers.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; -import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/dialogs/item_pick_dialog.dart'; +import 'package:aves/widgets/dialogs/item_picker.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -66,7 +65,11 @@ class _AddShortcutDialogState extends State { Container( alignment: Alignment.center, padding: const EdgeInsets.only(top: 16), - child: _buildCover(_coverEntry!, extent), + child: ItemPicker( + extent: extent, + entry: _coverEntry!, + onTap: _pickEntry, + ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24), @@ -103,29 +106,6 @@ class _AddShortcutDialogState extends State { ); } - Widget _buildCover(AvesEntry entry, double extent) { - return GestureDetector( - onTap: _pickEntry, - child: Container( - decoration: BoxDecoration( - border: AvesBorder.border(context), - borderRadius: const BorderRadius.all(Radius.circular(32)), - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(32)), - child: SizedBox( - width: extent, - height: extent, - child: ThumbnailImage( - entry: entry, - extent: extent, - ), - ), - ), - ), - ); - } - Future _pickEntry() async { final _collection = widget.collection; if (_collection == null) return; diff --git a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart index 6c3418078..3b4c70f47 100644 --- a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart @@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/metadata/date_modifier.dart'; import 'package:aves/model/metadata/enums.dart'; import 'package:aves/model/metadata/fields.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/format.dart'; import 'package:aves/theme/icons.dart'; @@ -10,15 +11,19 @@ import 'package:aves/widgets/common/basic/wheel.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves/widgets/dialogs/item_pick_dialog.dart'; +import 'package:aves/widgets/dialogs/item_picker.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class EditEntryDateDialog extends StatefulWidget { final AvesEntry entry; + final CollectionLens? collection; const EditEntryDateDialog({ Key? key, required this.entry, + this.collection, }) : super(key: key); @override @@ -28,16 +33,20 @@ class EditEntryDateDialog extends StatefulWidget { class _EditEntryDateDialogState extends State { DateEditAction _action = DateEditAction.setCustom; DateFieldSource _copyFieldSource = DateFieldSource.fileModifiedDate; + late AvesEntry _copyItemSource; late DateTime _setDateTime; late ValueNotifier _shiftHour, _shiftMinute; late ValueNotifier _shiftSign; bool _showOptions = false; final Set _fields = {...DateModifier.writableDateFields}; + DateTime get copyItemDate => _copyItemSource.bestDate ?? DateTime.now(); + @override void initState() { super.initState(); _initSet(); + _initCopyItem(); _initShift(60); } @@ -45,6 +54,10 @@ class _EditEntryDateDialogState extends State { _setDateTime = widget.entry.bestDate ?? DateTime.now(); } + void _initCopyItem() { + _copyItemSource = widget.entry; + } + void _initShift(int initialMinutes) { final abs = initialMinutes.abs(); _shiftHour = ValueNotifier(abs ~/ 60); @@ -91,6 +104,7 @@ class _EditEntryDateDialogState extends State { children: [ if (_action == DateEditAction.setCustom) _buildSetCustomContent(context), if (_action == DateEditAction.copyField) _buildCopyFieldContent(context), + if (_action == DateEditAction.copyItem) _buildCopyItemContent(context), if (_action == DateEditAction.shift) _buildShiftContent(context), (_action == DateEditAction.shift || _action == DateEditAction.remove) ? _buildDestinationFields(context) : const SizedBox(height: 8), ], @@ -170,6 +184,27 @@ class _EditEntryDateDialogState extends State { ); } + Widget _buildCopyItemContent(BuildContext context) { + final l10n = context.l10n; + final locale = l10n.localeName; + final use24hour = context.select((v) => v.alwaysUse24HourFormat); + + return Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 8), + child: Row( + children: [ + Expanded(child: Text(formatDateTime(copyItemDate, locale, use24hour))), + const SizedBox(width: 8), + ItemPicker( + extent: 48, + entry: _copyItemSource, + onTap: _pickCopyItemSource, + ), + ], + ), + ); + } + Widget _buildShiftContent(BuildContext context) { const textStyle = TextStyle(fontSize: 34); return Center( @@ -268,6 +303,8 @@ class _EditEntryDateDialogState extends State { return l10n.editEntryDateDialogSetCustom; case DateEditAction.copyField: return l10n.editEntryDateDialogCopyField; + case DateEditAction.copyItem: + return l10n.editEntryDateDialogCopyItem; case DateEditAction.extractFromTitle: return l10n.editEntryDateDialogExtractFromTitle; case DateEditAction.shift: @@ -335,6 +372,27 @@ class _EditEntryDateDialogState extends State { )); } + Future _pickCopyItemSource() async { + final _collection = widget.collection; + if (_collection == null) return; + + final entry = await Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: ItemPickDialog.routeName), + builder: (context) => ItemPickDialog( + collection: CollectionLens( + source: _collection.source, + ), + ), + fullscreenDialog: true, + ), + ); + if (entry != null) { + setState(() => _copyItemSource = entry); + } + } + DateModifier _getModifier() { // fields to modify are only set for the `shift` and `remove` actions, // as the effective fields for the other actions will depend on @@ -343,9 +401,11 @@ class _EditEntryDateDialogState extends State { case DateEditAction.setCustom: return DateModifier.setCustom(const {}, _setDateTime); case DateEditAction.copyField: - return DateModifier.copyField(const {}, _copyFieldSource); + return DateModifier.copyField(_copyFieldSource); + case DateEditAction.copyItem: + return DateModifier.setCustom(const {}, copyItemDate); case DateEditAction.extractFromTitle: - return DateModifier.extractFromTitle(const {}); + return DateModifier.extractFromTitle(); case DateEditAction.shift: final shiftTotalMinutes = (_shiftHour.value * 60 + _shiftMinute.value) * (_shiftSign.value == '+' ? 1 : -1); return DateModifier.shift(_fields, shiftTotalMinutes); diff --git a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart index 2b861a791..e81574f0a 100644 --- a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart @@ -2,11 +2,11 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/item_pick_dialog.dart'; +import 'package:aves/widgets/dialogs/item_picker.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -72,11 +72,12 @@ class _CoverSelectionDialogState extends State { children: [ title, const Spacer(), - IconButton( - icon: const Icon(AIcons.setCover), - onPressed: _isCustom ? _pickEntry : null, - tooltip: context.l10n.changeTooltip, - ), + if (_customEntry != null) + ItemPicker( + extent: 46, + entry: _customEntry!, + onTap: _pickEntry, + ), ], ) : title, diff --git a/lib/widgets/dialogs/item_picker.dart b/lib/widgets/dialogs/item_picker.dart new file mode 100644 index 000000000..b70d7ecc9 --- /dev/null +++ b/lib/widgets/dialogs/item_picker.dart @@ -0,0 +1,72 @@ +import 'dart:math'; + +import 'package:aves/model/entry.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/fx/borders.dart'; +import 'package:aves/widgets/common/thumbnail/image.dart'; +import 'package:flutter/material.dart'; + +class ItemPicker extends StatelessWidget { + final double extent; + final AvesEntry entry; + final GestureTapCallback? onTap; + + const ItemPicker({ + Key? key, + required this.extent, + required this.entry, + this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final imageBorderRadius = BorderRadius.all(Radius.circular(extent * .25)); + final actionBoxDimension = min(40.0, extent * .4); + final actionBoxBorderRadius = BorderRadiusDirectional.only(topStart: Radius.circular(actionBoxDimension * .6)); + return Tooltip( + message: context.l10n.changeTooltip, + child: GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + border: AvesBorder.border(context), + borderRadius: imageBorderRadius, + ), + child: ClipRRect( + borderRadius: imageBorderRadius, + child: SizedBox( + width: extent, + height: extent, + child: Stack( + children: [ + ThumbnailImage( + entry: entry, + extent: extent, + ), + PositionedDirectional( + end: -1, + bottom: -1, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark ? const Color(0xAA000000) : const Color(0xCCFFFFFF), + border: AvesBorder.border(context), + borderRadius: actionBoxBorderRadius, + ), + width: actionBoxDimension, + height: actionBoxDimension, + child: Icon( + AIcons.edit, + size: actionBoxDimension * .6, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/viewer/action/entry_info_action_delegate.dart b/lib/widgets/viewer/action/entry_info_action_delegate.dart index 727065f34..1c3f06e4e 100644 --- a/lib/widgets/viewer/action/entry_info_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_info_action_delegate.dart @@ -97,7 +97,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi } Future _editDate(BuildContext context) async { - final modifier = await selectDateModifier(context, {entry}); + final modifier = await selectDateModifier(context, {entry}, collection); if (modifier == null) return; await edit(context, () => entry.editDate(modifier)); diff --git a/untranslated.json b/untranslated.json index 112e127e1..3ad3eea16 100644 --- a/untranslated.json +++ b/untranslated.json @@ -3,6 +3,7 @@ "themeBrightnessLight", "themeBrightnessDark", "themeBrightnessBlack", + "editEntryDateDialogCopyItem", "settingsSectionDisplay", "settingsThemeBrightness", "settingsThemeColorful" @@ -12,6 +13,7 @@ "themeBrightnessLight", "themeBrightnessDark", "themeBrightnessBlack", + "editEntryDateDialogCopyItem", "settingsSectionDisplay", "settingsThemeBrightness", "settingsThemeColorful" @@ -21,6 +23,7 @@ "themeBrightnessLight", "themeBrightnessDark", "themeBrightnessBlack", + "editEntryDateDialogCopyItem", "settingsSectionDisplay", "settingsThemeBrightness", "settingsThemeColorful" @@ -36,6 +39,7 @@ "themeBrightnessLight", "themeBrightnessDark", "themeBrightnessBlack", + "editEntryDateDialogCopyItem", "settingsViewerShowOverlayThumbnails", "settingsVideoControlsTile", "settingsVideoControlsTitle", @@ -52,6 +56,7 @@ "themeBrightnessLight", "themeBrightnessDark", "themeBrightnessBlack", + "editEntryDateDialogCopyItem", "settingsSectionDisplay", "settingsThemeBrightness", "settingsThemeColorful" @@ -61,6 +66,7 @@ "themeBrightnessLight", "themeBrightnessDark", "themeBrightnessBlack", + "editEntryDateDialogCopyItem", "settingsSectionDisplay", "settingsThemeBrightness", "settingsThemeColorful" @@ -70,6 +76,7 @@ "themeBrightnessLight", "themeBrightnessDark", "themeBrightnessBlack", + "editEntryDateDialogCopyItem", "settingsSectionDisplay", "settingsThemeBrightness", "settingsThemeColorful" @@ -79,6 +86,7 @@ "themeBrightnessLight", "themeBrightnessDark", "themeBrightnessBlack", + "editEntryDateDialogCopyItem", "settingsSectionDisplay", "settingsThemeBrightness", "settingsThemeColorful"