#117 collection: edit date in bulk
This commit is contained in:
parent
eb299c2feb
commit
0bf238245f
10 changed files with 273 additions and 59 deletions
|
@ -289,6 +289,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"unsupportedTypeDialogTitle": "Unsupported Types",
|
||||||
|
"@unsupportedTypeDialogTitle": {},
|
||||||
|
"unsupportedTypeDialogMessage": "{count, plural, =1{This operation is not supported for items of the following type: {types}.} other{This operation is not supported for items of the following types: {types}.}}",
|
||||||
|
"@unsupportedTypeDialogMessage": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {},
|
||||||
|
"types": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"nameConflictDialogSingleSourceMessage": "Some files in the destination folder have the same name.",
|
"nameConflictDialogSingleSourceMessage": "Some files in the destination folder have the same name.",
|
||||||
"@nameConflictDialogSingleSourceMessage": {},
|
"@nameConflictDialogSingleSourceMessage": {},
|
||||||
"nameConflictDialogMultipleSourceMessage": "Some files have the same name.",
|
"nameConflictDialogMultipleSourceMessage": "Some files have the same name.",
|
||||||
|
@ -360,8 +372,8 @@
|
||||||
"@editEntryDateDialogSet": {},
|
"@editEntryDateDialogSet": {},
|
||||||
"editEntryDateDialogShift": "Shift",
|
"editEntryDateDialogShift": "Shift",
|
||||||
"@editEntryDateDialogShift": {},
|
"@editEntryDateDialogShift": {},
|
||||||
"editEntryDateDialogFromTitle": "From title",
|
"editEntryDateDialogExtractFromTitle": "Extract from title",
|
||||||
"@editEntryDateDialogFromTitle": {},
|
"@editEntryDateDialogExtractFromTitle": {},
|
||||||
"editEntryDateDialogClear": "Clear",
|
"editEntryDateDialogClear": "Clear",
|
||||||
"@editEntryDateDialogClear": {},
|
"@editEntryDateDialogClear": {},
|
||||||
"editEntryDateDialogFieldSelection": "Field selection",
|
"editEntryDateDialogFieldSelection": "Field selection",
|
||||||
|
@ -493,6 +505,8 @@
|
||||||
"@collectionActionMove": {},
|
"@collectionActionMove": {},
|
||||||
"collectionActionRescan": "Rescan",
|
"collectionActionRescan": "Rescan",
|
||||||
"@collectionActionRescan": {},
|
"@collectionActionRescan": {},
|
||||||
|
"collectionActionEdit": "Edit",
|
||||||
|
"@collectionActionEdit": {},
|
||||||
|
|
||||||
"collectionSortTitle": "Sort",
|
"collectionSortTitle": "Sort",
|
||||||
"@collectionSortTitle": {},
|
"@collectionSortTitle": {},
|
||||||
|
@ -540,6 +554,12 @@
|
||||||
"count": {}
|
"count": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"collectionEditFailureFeedback": "{count, plural, =1{Failed to edit 1 item} other{Failed to edit {count} items}}",
|
||||||
|
"@collectionEditFailureFeedback": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"collectionExportFailureFeedback": "{count, plural, =1{Failed to export 1 page} other{Failed to export {count} pages}}",
|
"collectionExportFailureFeedback": "{count, plural, =1{Failed to export 1 page} other{Failed to export {count} pages}}",
|
||||||
"@collectionExportFailureFeedback": {
|
"@collectionExportFailureFeedback": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
@ -558,6 +578,12 @@
|
||||||
"count": {}
|
"count": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"collectionEditSuccessFeedback": "{count, plural, =1{Edited 1 item} other{Edited {count} items}}",
|
||||||
|
"@collectionEditSuccessFeedback": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"collectionEmptyFavourites": "No favourites",
|
"collectionEmptyFavourites": "No favourites",
|
||||||
"@collectionEmptyFavourites": {},
|
"@collectionEmptyFavourites": {},
|
||||||
|
|
|
@ -139,7 +139,7 @@
|
||||||
"noMatchingAppDialogTitle": "Нет подходящего приложения",
|
"noMatchingAppDialogTitle": "Нет подходящего приложения",
|
||||||
"noMatchingAppDialogMessage": "Нет приложений, которые могли бы с этим справиться.",
|
"noMatchingAppDialogMessage": "Нет приложений, которые могли бы с этим справиться.",
|
||||||
|
|
||||||
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Вы уверены, что хотите удалить этот объект?} few{Вы уверены, что хотите удалить эти {count} объекта?} other{Вы уверены, что хотите удалить эти {count} объектов)?}}",
|
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Вы уверены, что хотите удалить этот объект?} few{Вы уверены, что хотите удалить эти {count} объекта?} other{Вы уверены, что хотите удалить эти {count} объектов?}}",
|
||||||
|
|
||||||
"setCoverDialogTitle": "Установить обложку",
|
"setCoverDialogTitle": "Установить обложку",
|
||||||
"setCoverDialogLatest": "Последний объект",
|
"setCoverDialogLatest": "Последний объект",
|
||||||
|
@ -228,7 +228,7 @@
|
||||||
|
|
||||||
"collectionPageTitle": "Коллекция",
|
"collectionPageTitle": "Коллекция",
|
||||||
"collectionPickPageTitle": "Выбрать",
|
"collectionPickPageTitle": "Выбрать",
|
||||||
"collectionSelectionPageTitle": "{count, plural, =0{Выберите объекты} =1{1 объект} few{{count} объекта} other{{count} объектов)}}",
|
"collectionSelectionPageTitle": "{count, plural, =0{Выберите объекты} =1{1 объект} few{{count} объекта} other{{count} объектов}}",
|
||||||
|
|
||||||
"collectionActionAddShortcut": "Добавить ярлык",
|
"collectionActionAddShortcut": "Добавить ярлык",
|
||||||
"collectionActionCopy": "Скопировать в альбом",
|
"collectionActionCopy": "Скопировать в альбом",
|
||||||
|
|
|
@ -20,6 +20,7 @@ enum EntrySetAction {
|
||||||
copy,
|
copy,
|
||||||
move,
|
move,
|
||||||
rescan,
|
rescan,
|
||||||
|
editDate,
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntrySetActions {
|
class EntrySetActions {
|
||||||
|
@ -67,6 +68,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
||||||
return context.l10n.collectionActionMove;
|
return context.l10n.collectionActionMove;
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
return context.l10n.collectionActionRescan;
|
return context.l10n.collectionActionRescan;
|
||||||
|
case EntrySetAction.editDate:
|
||||||
|
return context.l10n.entryInfoActionEditDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,6 +109,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
||||||
return AIcons.move;
|
return AIcons.move;
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
return AIcons.refresh;
|
return AIcons.refresh;
|
||||||
|
case EntrySetAction.editDate:
|
||||||
|
return AIcons.date;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -636,7 +636,7 @@ class AvesEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> editDate(DateModifier modifier) async {
|
Future<bool> editDate(DateModifier modifier) async {
|
||||||
if (modifier.action == DateEditAction.fromTitle) {
|
if (modifier.action == DateEditAction.extractFromTitle) {
|
||||||
final _title = bestTitle;
|
final _title = bestTitle;
|
||||||
if (_title == null) return false;
|
if (_title == null) return false;
|
||||||
final date = parseUnknownDateFormat(_title);
|
final date = parseUnknownDateFormat(_title);
|
||||||
|
|
|
@ -8,7 +8,7 @@ enum MetadataField {
|
||||||
enum DateEditAction {
|
enum DateEditAction {
|
||||||
set,
|
set,
|
||||||
shift,
|
shift,
|
||||||
fromTitle,
|
extractFromTitle,
|
||||||
clear,
|
clear,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/enums.dart';
|
import 'package:aves/model/source/enums.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
|
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
|
||||||
import 'package:aves/widgets/collection/filter_bar.dart';
|
import 'package:aves/widgets/collection/filter_bar.dart';
|
||||||
import 'package:aves/widgets/common/app_bar_subtitle.dart';
|
import 'package:aves/widgets/common/app_bar_subtitle.dart';
|
||||||
|
@ -143,14 +144,16 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget? _buildAppBarTitle(bool isSelecting) {
|
Widget? _buildAppBarTitle(bool isSelecting) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
|
||||||
if (isSelecting) {
|
if (isSelecting) {
|
||||||
return Selector<Selection<AvesEntry>, int>(
|
return Selector<Selection<AvesEntry>, int>(
|
||||||
selector: (context, selection) => selection.selectedItems.length,
|
selector: (context, selection) => selection.selectedItems.length,
|
||||||
builder: (context, count, child) => Text(context.l10n.collectionSelectionPageTitle(count)),
|
builder: (context, count, child) => Text(l10n.collectionSelectionPageTitle(count)),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
Widget title = Text(appMode.isPicking ? context.l10n.collectionPickPageTitle : context.l10n.collectionPageTitle);
|
Widget title = Text(appMode.isPicking ? l10n.collectionPickPageTitle : l10n.collectionPageTitle);
|
||||||
if (appMode == AppMode.main) {
|
if (appMode == AppMode.main) {
|
||||||
title = SourceStateAwareAppBarTitle(
|
title = SourceStateAwareAppBarTitle(
|
||||||
title: title,
|
title: title,
|
||||||
|
@ -209,7 +212,21 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
enabled: hasItems,
|
enabled: hasItems,
|
||||||
),
|
),
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
if (isSelecting) ...EntrySetActions.selection.where((v) => !selectionQuickActions.contains(v)).map((v) => _toMenuItem(v, enabled: hasSelection)),
|
if (isSelecting) ...[
|
||||||
|
...EntrySetActions.selection.where((v) => !selectionQuickActions.contains(v)).map((v) => _toMenuItem(v, enabled: hasSelection)),
|
||||||
|
PopupMenuItem(
|
||||||
|
enabled: hasSelection,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
child: PopupMenuItemExpansionPanel<EntrySetAction>(
|
||||||
|
enabled: hasSelection,
|
||||||
|
icon: AIcons.edit,
|
||||||
|
title: context.l10n.collectionActionEdit,
|
||||||
|
items: [
|
||||||
|
_toMenuItem(EntrySetAction.editDate, enabled: hasSelection),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
if (!isSelecting)
|
if (!isSelecting)
|
||||||
...[
|
...[
|
||||||
EntrySetAction.map,
|
EntrySetAction.map,
|
||||||
|
@ -285,6 +302,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
case EntrySetAction.map:
|
case EntrySetAction.map:
|
||||||
case EntrySetAction.stats:
|
case EntrySetAction.stats:
|
||||||
|
case EntrySetAction.editDate:
|
||||||
_actionDelegate.onActionSelected(context, action);
|
_actionDelegate.onActionSelected(context, action);
|
||||||
break;
|
break;
|
||||||
case EntrySetAction.select:
|
case EntrySetAction.select:
|
||||||
|
@ -302,16 +320,19 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
case EntrySetAction.group:
|
case EntrySetAction.group:
|
||||||
final value = await showDialog<EntryGroupFactor>(
|
final value = await showDialog<EntryGroupFactor>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AvesSelectionDialog<EntryGroupFactor>(
|
builder: (context) {
|
||||||
initialValue: settings.collectionSectionFactor,
|
final l10n = context.l10n;
|
||||||
options: {
|
return AvesSelectionDialog<EntryGroupFactor>(
|
||||||
EntryGroupFactor.album: context.l10n.collectionGroupAlbum,
|
initialValue: settings.collectionSectionFactor,
|
||||||
EntryGroupFactor.month: context.l10n.collectionGroupMonth,
|
options: {
|
||||||
EntryGroupFactor.day: context.l10n.collectionGroupDay,
|
EntryGroupFactor.album: l10n.collectionGroupAlbum,
|
||||||
EntryGroupFactor.none: context.l10n.collectionGroupNone,
|
EntryGroupFactor.month: l10n.collectionGroupMonth,
|
||||||
},
|
EntryGroupFactor.day: l10n.collectionGroupDay,
|
||||||
title: context.l10n.collectionGroupTitle,
|
EntryGroupFactor.none: l10n.collectionGroupNone,
|
||||||
),
|
},
|
||||||
|
title: l10n.collectionGroupTitle,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
// 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);
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:aves/model/actions/move_type.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
|
import 'package:aves/model/metadata/date_modifier.dart';
|
||||||
import 'package:aves/model/selection.dart';
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/source/analysis_controller.dart';
|
import 'package:aves/model/source/analysis_controller.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
|
@ -14,6 +15,7 @@ import 'package:aves/services/common/image_op_events.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/services/media/enums.dart';
|
import 'package:aves/services/media/enums.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/utils/mime_utils.dart';
|
||||||
import 'package:aves/widgets/collection/collection_page.dart';
|
import 'package:aves/widgets/collection/collection_page.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';
|
||||||
|
@ -21,6 +23,7 @@ import 'package:aves/widgets/common/action_mixins/size_aware.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
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/dialogs/aves_selection_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||||
|
import 'package:aves/widgets/dialogs/edit_entry_date_dialog.dart';
|
||||||
import 'package:aves/widgets/filter_grids/album_pick.dart';
|
import 'package:aves/widgets/filter_grids/album_pick.dart';
|
||||||
import 'package:aves/widgets/map/map_page.dart';
|
import 'package:aves/widgets/map/map_page.dart';
|
||||||
import 'package:aves/widgets/stats/stats_page.dart';
|
import 'package:aves/widgets/stats/stats_page.dart';
|
||||||
|
@ -47,6 +50,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
_rescan(context);
|
_rescan(context);
|
||||||
break;
|
break;
|
||||||
|
case EntrySetAction.editDate:
|
||||||
|
_editDate(context);
|
||||||
|
break;
|
||||||
case EntrySetAction.map:
|
case EntrySetAction.map:
|
||||||
_goToMap(context);
|
_goToMap(context);
|
||||||
break;
|
break;
|
||||||
|
@ -81,6 +87,59 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
selection.browse();
|
selection.browse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showDeleteDialog(BuildContext context) async {
|
||||||
|
final source = context.read<CollectionSource>();
|
||||||
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
|
final selectedItems = _getExpandedSelectedItems(selection);
|
||||||
|
final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet();
|
||||||
|
final todoCount = selectedItems.length;
|
||||||
|
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AvesDialog(
|
||||||
|
context: context,
|
||||||
|
content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(todoCount)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: Text(context.l10n.deleteButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (confirmed == null || !confirmed) return;
|
||||||
|
|
||||||
|
if (!await checkStoragePermissionForAlbums(context, selectionDirs, entries: selectedItems)) return;
|
||||||
|
|
||||||
|
source.pauseMonitoring();
|
||||||
|
showOpReport<ImageOpEvent>(
|
||||||
|
context: context,
|
||||||
|
opStream: mediaFileService.delete(selectedItems),
|
||||||
|
itemCount: todoCount,
|
||||||
|
onDone: (processed) async {
|
||||||
|
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();
|
||||||
|
await source.removeEntries(deletedUris);
|
||||||
|
selection.browse();
|
||||||
|
source.resumeMonitoring();
|
||||||
|
|
||||||
|
final deletedCount = deletedUris.length;
|
||||||
|
if (deletedCount < todoCount) {
|
||||||
|
final count = todoCount - deletedCount;
|
||||||
|
showFeedback(context, context.l10n.collectionDeleteFailureFeedback(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
await storageService.deleteEmptyDirectories(selectionDirs);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _moveSelection(BuildContext context, {required MoveType moveType}) async {
|
Future<void> _moveSelection(BuildContext context, {required MoveType moveType}) async {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final source = context.read<CollectionSource>();
|
final source = context.read<CollectionSource>();
|
||||||
|
@ -213,55 +272,74 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showDeleteDialog(BuildContext context) async {
|
Future<void> _editDate(BuildContext context) async {
|
||||||
|
final l10n = context.l10n;
|
||||||
final source = context.read<CollectionSource>();
|
final source = context.read<CollectionSource>();
|
||||||
final selection = context.read<Selection<AvesEntry>>();
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
final selectedItems = _getExpandedSelectedItems(selection);
|
final selectedItems = _getExpandedSelectedItems(selection);
|
||||||
final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet();
|
|
||||||
final todoCount = selectedItems.length;
|
|
||||||
|
|
||||||
final confirmed = await showDialog<bool>(
|
final bySupported = groupBy<AvesEntry, bool>(selectedItems, (entry) => entry.canEditExif);
|
||||||
|
final todoEntries = (bySupported[true] ?? []).toSet();
|
||||||
|
final unsupportedItems = (bySupported[false] ?? []);
|
||||||
|
if (unsupportedItems.isNotEmpty) {
|
||||||
|
final unsupportedTypes = unsupportedItems.map((entry) => entry.mimeType).toSet().map(MimeUtils.displayType).toList()..sort();
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AvesDialog(
|
||||||
|
context: context,
|
||||||
|
title: l10n.unsupportedTypeDialogTitle,
|
||||||
|
content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||||
|
),
|
||||||
|
if (todoEntries.isNotEmpty)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: Text(l10n.continueButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (confirmed == null || !confirmed) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectionDirs = todoEntries.map((e) => e.directory).whereNotNull().toSet();
|
||||||
|
final todoCount = todoEntries.length;
|
||||||
|
|
||||||
|
final modifier = await showDialog<DateModifier>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) => EditEntryDateDialog(entry: todoEntries.first),
|
||||||
return AvesDialog(
|
|
||||||
context: context,
|
|
||||||
content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(todoCount)),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
child: Text(context.l10n.deleteButtonLabel),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
if (confirmed == null || !confirmed) return;
|
if (modifier == null) return;
|
||||||
|
|
||||||
if (!await checkStoragePermissionForAlbums(context, selectionDirs, entries: selectedItems)) return;
|
if (!await checkStoragePermissionForAlbums(context, selectionDirs, entries: todoEntries)) return;
|
||||||
|
|
||||||
source.pauseMonitoring();
|
source.pauseMonitoring();
|
||||||
showOpReport<ImageOpEvent>(
|
showOpReport<ImageOpEvent>(
|
||||||
context: context,
|
context: context,
|
||||||
opStream: mediaFileService.delete(selectedItems),
|
opStream: Stream.fromIterable(todoEntries).asyncMap((entry) async {
|
||||||
|
final success = await entry.editDate(modifier);
|
||||||
|
return ImageOpEvent(success: success, uri: entry.uri);
|
||||||
|
}).asBroadcastStream(),
|
||||||
itemCount: todoCount,
|
itemCount: todoCount,
|
||||||
onDone: (processed) async {
|
onDone: (processed) async {
|
||||||
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();
|
final successOps = processed.where((e) => e.success).toSet();
|
||||||
await source.removeEntries(deletedUris);
|
|
||||||
selection.browse();
|
selection.browse();
|
||||||
source.resumeMonitoring();
|
source.resumeMonitoring();
|
||||||
|
unawaited(source.refreshUris(successOps.map((v) => v.uri).toSet()));
|
||||||
|
|
||||||
final deletedCount = deletedUris.length;
|
final successCount = successOps.length;
|
||||||
if (deletedCount < todoCount) {
|
if (successCount < todoCount) {
|
||||||
final count = todoCount - deletedCount;
|
final count = todoCount - successCount;
|
||||||
showFeedback(context, context.l10n.collectionDeleteFailureFeedback(count));
|
showFeedback(context, l10n.collectionEditFailureFeedback(count));
|
||||||
|
} else {
|
||||||
|
final count = successCount;
|
||||||
|
showFeedback(context, l10n.collectionEditSuccessFeedback(count));
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup
|
|
||||||
await storageService.deleteEmptyDirectories(selectionDirs);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class MenuRow extends StatelessWidget {
|
class MenuRow extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
|
@ -45,3 +47,75 @@ class MenuIconTheme extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PopupMenuItemExpansionPanel<T> extends StatefulWidget {
|
||||||
|
final bool enabled;
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final List<PopupMenuItem<T>> items;
|
||||||
|
|
||||||
|
const PopupMenuItemExpansionPanel({
|
||||||
|
Key? key,
|
||||||
|
this.enabled = true,
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.items,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_PopupMenuItemExpansionPanelState createState() => _PopupMenuItemExpansionPanelState<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PopupMenuItemExpansionPanelState<T> extends State<PopupMenuItemExpansionPanel<T>> {
|
||||||
|
bool _isExpanded = false;
|
||||||
|
|
||||||
|
// ref `_kMenuHorizontalPadding` used in `PopupMenuItem`
|
||||||
|
static const double _horizontalPadding = 16;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
var style = PopupMenuTheme.of(context).textStyle ?? theme.textTheme.subtitle1!;
|
||||||
|
if (!widget.enabled) {
|
||||||
|
style = style.copyWith(color: theme.disabledColor);
|
||||||
|
}
|
||||||
|
final animationDuration = context.select<DurationsData, Duration>((v) => v.expansionTileAnimation);
|
||||||
|
|
||||||
|
Widget child = ExpansionPanelList(
|
||||||
|
expansionCallback: (index, isExpanded) {
|
||||||
|
setState(() => _isExpanded = !isExpanded);
|
||||||
|
},
|
||||||
|
animationDuration: animationDuration,
|
||||||
|
expandedHeaderPadding: EdgeInsets.zero,
|
||||||
|
elevation: 0,
|
||||||
|
children: [
|
||||||
|
ExpansionPanel(
|
||||||
|
headerBuilder: (context, isExpanded) => DefaultTextStyle(
|
||||||
|
style: style,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: _horizontalPadding),
|
||||||
|
child: MenuRow(
|
||||||
|
text: widget.title,
|
||||||
|
icon: Icon(widget.icon),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const PopupMenuDivider(height: 0),
|
||||||
|
...widget.items,
|
||||||
|
const PopupMenuDivider(height: 0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
isExpanded: _isExpanded,
|
||||||
|
canTapOnHeader: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (!widget.enabled) {
|
||||||
|
child = IgnorePointer(child: child);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -106,11 +106,11 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
final fromTitleTile = RadioListTile<DateEditAction>(
|
final extractFromTitleTile = RadioListTile<DateEditAction>(
|
||||||
value: DateEditAction.fromTitle,
|
value: DateEditAction.extractFromTitle,
|
||||||
groupValue: _action,
|
groupValue: _action,
|
||||||
onChanged: _updateAction,
|
onChanged: _updateAction,
|
||||||
title: _tileText(l10n.editEntryDateDialogFromTitle),
|
title: _tileText(l10n.editEntryDateDialogExtractFromTitle),
|
||||||
);
|
);
|
||||||
final clearTile = RadioListTile<DateEditAction>(
|
final clearTile = RadioListTile<DateEditAction>(
|
||||||
value: DateEditAction.clear,
|
value: DateEditAction.clear,
|
||||||
|
@ -134,7 +134,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
scrollableContent: [
|
scrollableContent: [
|
||||||
setTile,
|
setTile,
|
||||||
shiftTile,
|
shiftTile,
|
||||||
fromTitleTile,
|
extractFromTitleTile,
|
||||||
clearTile,
|
clearTile,
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 1),
|
padding: const EdgeInsets.only(bottom: 1),
|
||||||
|
@ -250,7 +250,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
case DateEditAction.shift:
|
case DateEditAction.shift:
|
||||||
modifier = DateModifier(_action, _fields, shiftMinutes: _shiftMinutes);
|
modifier = DateModifier(_action, _fields, shiftMinutes: _shiftMinutes);
|
||||||
break;
|
break;
|
||||||
case DateEditAction.fromTitle:
|
case DateEditAction.extractFromTitle:
|
||||||
case DateEditAction.clear:
|
case DateEditAction.clear:
|
||||||
modifier = DateModifier(_action, _fields);
|
modifier = DateModifier(_action, _fields);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
{
|
{
|
||||||
"ko": [
|
"ko": [
|
||||||
"editEntryDateDialogFromTitle"
|
"unsupportedTypeDialogTitle",
|
||||||
|
"unsupportedTypeDialogMessage",
|
||||||
|
"editEntryDateDialogExtractFromTitle",
|
||||||
|
"collectionActionEdit",
|
||||||
|
"collectionEditFailureFeedback",
|
||||||
|
"collectionEditSuccessFeedback"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
"editEntryDateDialogFromTitle",
|
"unsupportedTypeDialogTitle",
|
||||||
"aboutCreditsTranslators"
|
"unsupportedTypeDialogMessage",
|
||||||
|
"editEntryDateDialogExtractFromTitle",
|
||||||
|
"aboutCreditsTranslators",
|
||||||
|
"collectionActionEdit",
|
||||||
|
"collectionEditFailureFeedback",
|
||||||
|
"collectionEditSuccessFeedback"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue