#526 collection: bulk converting

This commit is contained in:
Thibault Deckers 2023-02-23 11:46:01 +01:00
parent af6fd8f11b
commit 8f732608d0
16 changed files with 367 additions and 139 deletions

View file

@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- Collection: bulk converting
- Places: page & navigation entry - Places: page & navigation entry
### Fixed ### Fixed

View file

@ -50,7 +50,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
when (op) { when (op) {
"delete" -> ioScope.launch { delete() } "delete" -> ioScope.launch { delete() }
"export" -> ioScope.launch { export() } "convert" -> ioScope.launch { convert() }
"move" -> ioScope.launch { move() } "move" -> ioScope.launch { move() }
"rename" -> ioScope.launch { rename() } "rename" -> ioScope.launch { rename() }
else -> endOfStream() else -> endOfStream()
@ -121,7 +121,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
endOfStream() endOfStream()
} }
private suspend fun export() { private suspend fun convert() {
if (arguments !is Map<*, *> || entryMapList.isEmpty()) { if (arguments !is Map<*, *> || entryMapList.isEmpty()) {
endOfStream() endOfStream()
return return
@ -129,11 +129,12 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
var destinationDir = arguments["destinationPath"] as String? var destinationDir = arguments["destinationPath"] as String?
val mimeType = arguments["mimeType"] as String? val mimeType = arguments["mimeType"] as String?
val lengthUnit = arguments["lengthUnit"] as String?
val width = (arguments["width"] as Number?)?.toInt() val width = (arguments["width"] as Number?)?.toInt()
val height = (arguments["height"] as Number?)?.toInt() val height = (arguments["height"] as Number?)?.toInt()
val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?) val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?)
if (destinationDir == null || mimeType == null || width == null || height == null || nameConflictStrategy == null) { if (destinationDir == null || mimeType == null || lengthUnit == null || width == null || height == null || nameConflictStrategy == null) {
error("export-args", "missing arguments", null) error("convert-args", "missing arguments", null)
return return
} }
@ -141,15 +142,15 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
val firstEntry = entryMapList.first() val firstEntry = entryMapList.first()
val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) } val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) }
if (provider == null) { if (provider == null) {
error("export-provider", "failed to find provider for entry=$firstEntry", null) error("convert-provider", "failed to find provider for entry=$firstEntry", null)
return return
} }
destinationDir = ensureTrailingSeparator(destinationDir) destinationDir = ensureTrailingSeparator(destinationDir)
val entries = entryMapList.map(::AvesEntry) val entries = entryMapList.map(::AvesEntry)
provider.exportMultiple(activity, mimeType, destinationDir, entries, width, height, nameConflictStrategy, object : ImageOpCallback { provider.convertMultiple(activity, mimeType, destinationDir, entries, lengthUnit, width, height, nameConflictStrategy, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = success(fields) override fun onSuccess(fields: FieldMap) = success(fields)
override fun onFailure(throwable: Throwable) = error("export-failure", "failed to export entries", throwable) override fun onFailure(throwable: Throwable) = error("convert-failure", "failed to convert entries", throwable)
}) })
endOfStream() endOfStream()
} }

View file

@ -15,6 +15,15 @@ class AvesEntry(map: FieldMap) {
val trashed = map["trashed"] as Boolean val trashed = map["trashed"] as Boolean
val trashPath = map["trashPath"] as String? val trashPath = map["trashPath"] as String?
private val isRotated: Boolean
get() = rotationDegrees % 180 == 90
val displayWidth: Int
get() = if (isRotated) height else width
val displayHeight: Int
get() = if (isRotated) width else height
companion object { companion object {
// convenience method // convenience method
private fun toLong(o: Any?): Long? = when (o) { private fun toLong(o: Any?): Long? = when (o) {

View file

@ -169,11 +169,12 @@ abstract class ImageProvider {
throw UnsupportedOperationException("`scanPostMetadataEdit` is not supported by this image provider") throw UnsupportedOperationException("`scanPostMetadataEdit` is not supported by this image provider")
} }
suspend fun exportMultiple( suspend fun convertMultiple(
activity: Activity, activity: Activity,
imageExportMimeType: String, imageExportMimeType: String,
targetDir: String, targetDir: String,
entries: List<AvesEntry>, entries: List<AvesEntry>,
lengthUnit: String,
width: Int, width: Int,
height: Int, height: Int,
nameConflictStrategy: NameConflictStrategy, nameConflictStrategy: NameConflictStrategy,
@ -208,6 +209,7 @@ abstract class ImageProvider {
sourceEntry = entry, sourceEntry = entry,
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
lengthUnit = lengthUnit,
width = width, width = width,
height = height, height = height,
nameConflictStrategy = nameConflictStrategy, nameConflictStrategy = nameConflictStrategy,
@ -227,6 +229,7 @@ abstract class ImageProvider {
sourceEntry: AvesEntry, sourceEntry: AvesEntry,
targetDir: String, targetDir: String,
targetDirDocFile: DocumentFileCompat?, targetDirDocFile: DocumentFileCompat?,
lengthUnit: String,
width: Int, width: Int,
height: Int, height: Int,
nameConflictStrategy: NameConflictStrategy, nameConflictStrategy: NameConflictStrategy,
@ -266,6 +269,19 @@ abstract class ImageProvider {
sourceDocFile.copyTo(output) sourceDocFile.copyTo(output)
} }
} else { } else {
val targetWidthPx: Int
val targetHeightPx: Int
when (lengthUnit) {
LENGTH_UNIT_PERCENT -> {
targetWidthPx = sourceEntry.displayWidth * width / 100
targetHeightPx = sourceEntry.displayHeight * height / 100
}
else -> {
targetWidthPx = width
targetHeightPx = height
}
}
val model: Any = if (MimeTypes.isHeic(sourceMimeType) && pageId != null) { val model: Any = if (MimeTypes.isHeic(sourceMimeType) && pageId != null) {
MultiTrackImage(activity, sourceUri, pageId) MultiTrackImage(activity, sourceUri, pageId)
} else if (sourceMimeType == MimeTypes.TIFF) { } else if (sourceMimeType == MimeTypes.TIFF) {
@ -286,7 +302,7 @@ abstract class ImageProvider {
.asBitmap() .asBitmap()
.apply(glideOptions) .apply(glideOptions)
.load(model) .load(model)
.submit(width, height) .submit(targetWidthPx, targetHeightPx)
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
var bitmap = target.get() var bitmap = target.get()
if (MimeTypes.needRotationAfterGlide(sourceMimeType)) { if (MimeTypes.needRotationAfterGlide(sourceMimeType)) {
@ -1209,6 +1225,8 @@ abstract class ImageProvider {
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<ImageProvider>() private val LOG_TAG = LogUtils.createTag<ImageProvider>()
private const val LENGTH_UNIT_PERCENT = "percent"
val supportedExportMimeTypes = listOf(MimeTypes.BMP, MimeTypes.JPEG, MimeTypes.PNG, MimeTypes.WEBP) val supportedExportMimeTypes = listOf(MimeTypes.BMP, MimeTypes.JPEG, MimeTypes.PNG, MimeTypes.WEBP)
// used when skipping a move/creation op because the target file already exists // used when skipping a move/creation op because the target file already exists

View file

@ -200,6 +200,9 @@
"keepScreenOnViewerOnly": "Viewer page only", "keepScreenOnViewerOnly": "Viewer page only",
"keepScreenOnAlways": "Always", "keepScreenOnAlways": "Always",
"lengthUnitPixel": "px",
"lengthUnitPercent": "%",
"mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleNormal": "Google Maps",
"mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleHybrid": "Google Maps (Hybrid)",
"mapStyleGoogleTerrain": "Google Maps (Terrain)", "mapStyleGoogleTerrain": "Google Maps (Terrain)",

View file

@ -25,6 +25,7 @@ enum EntrySetAction {
copy, copy,
move, move,
rename, rename,
convert,
toggleFavourite, toggleFavourite,
rotateCCW, rotateCCW,
rotateCW, rotateCW,
@ -45,13 +46,16 @@ class EntrySetActions {
EntrySetAction.selectNone, EntrySetAction.selectNone,
]; ];
// `null` items are converted to dividers
static const pageBrowsing = [ static const pageBrowsing = [
EntrySetAction.searchCollection, EntrySetAction.searchCollection,
EntrySetAction.toggleTitleSearch, EntrySetAction.toggleTitleSearch,
EntrySetAction.addShortcut, EntrySetAction.addShortcut,
null,
EntrySetAction.map, EntrySetAction.map,
EntrySetAction.slideshow, EntrySetAction.slideshow,
EntrySetAction.stats, EntrySetAction.stats,
null,
EntrySetAction.rescan, EntrySetAction.rescan,
EntrySetAction.emptyBin, EntrySetAction.emptyBin,
]; ];
@ -67,6 +71,7 @@ class EntrySetActions {
EntrySetAction.rescan, EntrySetAction.rescan,
]; ];
// `null` items are converted to dividers
static const pageSelection = [ static const pageSelection = [
EntrySetAction.share, EntrySetAction.share,
EntrySetAction.delete, EntrySetAction.delete,
@ -74,10 +79,13 @@ class EntrySetActions {
EntrySetAction.copy, EntrySetAction.copy,
EntrySetAction.move, EntrySetAction.move,
EntrySetAction.rename, EntrySetAction.rename,
EntrySetAction.convert,
EntrySetAction.toggleFavourite, EntrySetAction.toggleFavourite,
null,
EntrySetAction.map, EntrySetAction.map,
EntrySetAction.slideshow, EntrySetAction.slideshow,
EntrySetAction.stats, EntrySetAction.stats,
null,
EntrySetAction.rescan, EntrySetAction.rescan,
// editing actions are in their subsection // editing actions are in their subsection
]; ];
@ -89,6 +97,7 @@ class EntrySetActions {
EntrySetAction.copy, EntrySetAction.copy,
EntrySetAction.move, EntrySetAction.move,
EntrySetAction.rename, EntrySetAction.rename,
EntrySetAction.convert,
EntrySetAction.toggleFavourite, EntrySetAction.toggleFavourite,
EntrySetAction.map, EntrySetAction.map,
EntrySetAction.slideshow, EntrySetAction.slideshow,
@ -163,6 +172,8 @@ extension ExtraEntrySetAction on EntrySetAction {
return context.l10n.collectionActionMove; return context.l10n.collectionActionMove;
case EntrySetAction.rename: case EntrySetAction.rename:
return context.l10n.entryActionRename; return context.l10n.entryActionRename;
case EntrySetAction.convert:
return context.l10n.entryActionConvert;
case EntrySetAction.toggleFavourite: case EntrySetAction.toggleFavourite:
// different data depending on toggle state // different data depending on toggle state
return context.l10n.entryActionAddFavourite; return context.l10n.entryActionAddFavourite;
@ -232,6 +243,8 @@ extension ExtraEntrySetAction on EntrySetAction {
return AIcons.move; return AIcons.move;
case EntrySetAction.rename: case EntrySetAction.rename:
return AIcons.name; return AIcons.name;
case EntrySetAction.convert:
return AIcons.convert;
case EntrySetAction.toggleFavourite: case EntrySetAction.toggleFavourite:
// different data depending on toggle state // different data depending on toggle state
return AIcons.favourite; return AIcons.favourite;

View file

@ -15,6 +15,8 @@ enum DateFieldSource {
exifGpsDate, exifGpsDate,
} }
enum LengthUnit { px, percent }
enum LocationEditAction { enum LocationEditAction {
chooseOnMap, chooseOnMap,
copyItem, copyItem,

View file

@ -0,0 +1,14 @@
import 'package:aves/model/metadata/enums/enums.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
extension ExtraLengthUnit on LengthUnit {
String getText(BuildContext context) {
switch (this) {
case LengthUnit.px:
return context.l10n.lengthUnitPixel;
case LengthUnit.percent:
return context.l10n.lengthUnitPercent;
}
}
}

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/metadata/enums/enums.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';
import 'package:aves/services/media/enums.dart'; import 'package:aves/services/media/enums.dart';
@ -28,7 +29,7 @@ abstract class MediaEditService {
Stream<ExportOpEvent> export( Stream<ExportOpEvent> export(
Iterable<AvesEntry> entries, { Iterable<AvesEntry> entries, {
required EntryExportOptions options, required EntryConvertOptions options,
required String destinationAlbum, required String destinationAlbum,
required NameConflictStrategy nameConflictStrategy, required NameConflictStrategy nameConflictStrategy,
}); });
@ -113,16 +114,17 @@ class PlatformMediaEditService implements MediaEditService {
@override @override
Stream<ExportOpEvent> export( Stream<ExportOpEvent> export(
Iterable<AvesEntry> entries, { Iterable<AvesEntry> entries, {
required EntryExportOptions options, required EntryConvertOptions options,
required String destinationAlbum, required String destinationAlbum,
required NameConflictStrategy nameConflictStrategy, required NameConflictStrategy nameConflictStrategy,
}) { }) {
try { try {
return _opStream return _opStream
.receiveBroadcastStream(<String, dynamic>{ .receiveBroadcastStream(<String, dynamic>{
'op': 'export', 'op': 'convert',
'entries': entries.map((entry) => entry.toPlatformEntryMap()).toList(), 'entries': entries.map((entry) => entry.toPlatformEntryMap()).toList(),
'mimeType': options.mimeType, 'mimeType': options.mimeType,
'lengthUnit': options.lengthUnit.name,
'width': options.width, 'width': options.width,
'height': options.height, 'height': options.height,
'destinationPath': destinationAlbum, 'destinationPath': destinationAlbum,
@ -183,15 +185,17 @@ class PlatformMediaEditService implements MediaEditService {
} }
@immutable @immutable
class EntryExportOptions extends Equatable { class EntryConvertOptions extends Equatable {
final String mimeType; final String mimeType;
final LengthUnit lengthUnit;
final int width, height; final int width, height;
@override @override
List<Object?> get props => [mimeType, width, height]; List<Object?> get props => [mimeType, lengthUnit, width, height];
const EntryExportOptions({ const EntryConvertOptions({
required this.mimeType, required this.mimeType,
required this.lengthUnit,
required this.width, required this.width,
required this.height, required this.height,
}); });

View file

@ -32,6 +32,7 @@ import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:aves/widgets/dialogs/tile_view_dialog.dart'; import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/search/search_delegate.dart';
import 'package:collection/collection.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';
@ -342,7 +343,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
return [ return [
...EntrySetActions.general, ...EntrySetActions.general,
...isSelecting ? EntrySetActions.pageSelection : EntrySetActions.pageBrowsing, ...isSelecting ? EntrySetActions.pageSelection : EntrySetActions.pageBrowsing,
].where(isVisible).map((action) { ].whereNotNull().where(isVisible).map((action) {
final enabled = canApply(action); final enabled = canApply(action);
return CaptionedButton( return CaptionedButton(
iconButtonBuilder: (context, focusNode) => _buildButtonIcon( iconButtonBuilder: (context, focusNode) => _buildButtonIcon(
@ -388,10 +389,21 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
final browsingMenuActions = EntrySetActions.pageBrowsing.where((v) => !browsingQuickActions.contains(v)); final browsingMenuActions = EntrySetActions.pageBrowsing.where((v) => !browsingQuickActions.contains(v));
final selectionMenuActions = EntrySetActions.pageSelection.where((v) => !selectionQuickActions.contains(v)); final selectionMenuActions = EntrySetActions.pageSelection.where((v) => !selectionQuickActions.contains(v));
final contextualMenuItems = [ final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).fold(<EntrySetAction?>[], (prev, v) {
...(isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map( if (v == null && (prev.isEmpty || prev.last == null)) return prev;
(action) => _toMenuItem(action, enabled: canApply(action), selection: selection), return [...prev, v];
), });
if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) {
contextualMenuActions.removeLast();
}
final contextualMenuItems = <PopupMenuEntry<EntrySetAction>>[
...contextualMenuActions.map(
(action) {
if (action == null) return const PopupMenuDivider();
return _toMenuItem(action, enabled: canApply(action), selection: selection);
},
),
if (isSelecting && !settings.isReadOnly && appMode == AppMode.main && !isTrash) if (isSelecting && !settings.isReadOnly && appMode == AppMode.main && !isTrash)
PopupMenuItem<EntrySetAction>( PopupMenuItem<EntrySetAction>(
enabled: hasSelection, enabled: hasSelection,
@ -630,6 +642,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
case EntrySetAction.copy: case EntrySetAction.copy:
case EntrySetAction.move: case EntrySetAction.move:
case EntrySetAction.rename: case EntrySetAction.rename:
case EntrySetAction.convert:
case EntrySetAction.toggleFavourite: case EntrySetAction.toggleFavourite:
case EntrySetAction.rotateCCW: case EntrySetAction.rotateCCW:
case EntrySetAction.rotateCW: case EntrySetAction.rotateCW:

View file

@ -94,6 +94,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.copy: case EntrySetAction.copy:
case EntrySetAction.move: case EntrySetAction.move:
case EntrySetAction.rename: case EntrySetAction.rename:
case EntrySetAction.convert:
case EntrySetAction.rotateCCW: case EntrySetAction.rotateCCW:
case EntrySetAction.rotateCW: case EntrySetAction.rotateCW:
case EntrySetAction.flip: case EntrySetAction.flip:
@ -145,6 +146,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.copy: case EntrySetAction.copy:
case EntrySetAction.move: case EntrySetAction.move:
case EntrySetAction.rename: case EntrySetAction.rename:
case EntrySetAction.convert:
case EntrySetAction.toggleFavourite: case EntrySetAction.toggleFavourite:
case EntrySetAction.rotateCCW: case EntrySetAction.rotateCCW:
case EntrySetAction.rotateCW: case EntrySetAction.rotateCW:
@ -211,6 +213,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.rename: case EntrySetAction.rename:
_rename(context); _rename(context);
break; break;
case EntrySetAction.convert:
_convert(context);
break;
case EntrySetAction.toggleFavourite: case EntrySetAction.toggleFavourite:
_toggleFavourite(context); _toggleFavourite(context);
break; break;
@ -379,6 +384,13 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
_browse(context); _browse(context);
} }
void _convert(BuildContext context) {
final entries = _getTargetItems(context);
convert(context, entries);
_browse(context);
}
Future<void> _toggleFavourite(BuildContext context) async { Future<void> _toggleFavourite(BuildContext context) async {
final entries = _getTargetItems(context); final entries = _getTargetItems(context);
if (entries.every((entry) => entry.isFavourite)) { if (entries.every((entry) => entry.isFavourite)) {

View file

@ -16,6 +16,7 @@ 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';
import 'package:aves/services/media/enums.dart'; import 'package:aves/services/media/enums.dart';
import 'package:aves/services/media/media_edit_service.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/aves_app.dart';
@ -27,6 +28,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_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/aves_confirmation_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/convert_entry_dialog.dart';
import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart'; import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart';
import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves/widgets/viewer/notifications.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -34,6 +36,100 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
Future<void> convert(BuildContext context, Set<AvesEntry> targetEntries) async {
final options = await showDialog<EntryConvertOptions>(
context: context,
builder: (context) => ConvertEntryDialog(entries: targetEntries),
routeSettings: const RouteSettings(name: ConvertEntryDialog.routeName),
);
if (options == null) return;
final destinationAlbum = await pickAlbum(context: context, moveType: MoveType.export);
if (destinationAlbum == null) return;
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
if (!await checkFreeSpaceForMove(context, targetEntries, destinationAlbum, MoveType.export)) return;
final selection = <AvesEntry>{};
await Future.forEach(targetEntries, (targetEntry) async {
if (targetEntry.isMultiPage) {
final multiPageInfo = await targetEntry.getMultiPageInfo();
if (multiPageInfo != null) {
if (targetEntry.isMotionPhoto) {
await multiPageInfo.extractMotionPhotoVideo();
}
if (multiPageInfo.pageCount > 1) {
selection.addAll(multiPageInfo.exportEntries);
}
}
} else {
selection.add(targetEntry);
}
});
final selectionCount = selection.length;
final source = context.read<CollectionSource>();
source.pauseMonitoring();
await showOpReport<ExportOpEvent>(
context: context,
opStream: mediaEditService.export(
selection,
options: options,
destinationAlbum: destinationAlbum,
nameConflictStrategy: NameConflictStrategy.rename,
),
itemCount: selectionCount,
onDone: (processed) async {
final successOps = processed.where((e) => e.success).toSet();
final exportedOps = successOps.where((e) => !e.skipped).toSet();
final newUris = exportedOps.map((v) => v.newFields['uri'] as String?).whereNotNull().toSet();
final isMainMode = context.read<ValueNotifier<AppMode>>().value == AppMode.main;
source.resumeMonitoring();
unawaited(source.refreshUris(newUris));
final l10n = context.l10n;
final showAction = isMainMode && newUris.isNotEmpty
? SnackBarAction(
label: l10n.showButtonLabel,
onPressed: () {
// local context may be deactivated when action is triggered after navigation
final context = AvesApp.navigatorKey.currentContext;
if (context != null) {
Navigator.maybeOf(context)?.pushAndRemoveUntil(
MaterialPageRoute(
settings: const RouteSettings(name: CollectionPage.routeName),
builder: (context) => CollectionPage(
source: source,
filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))},
highlightTest: (entry) => newUris.contains(entry.uri),
),
),
(route) => false,
);
}
},
)
: null;
final successCount = successOps.length;
if (successCount < selectionCount) {
final count = selectionCount - successCount;
showFeedback(
context,
l10n.collectionExportFailureFeedback(count),
showAction,
);
} else {
showFeedback(
context,
l10n.genericSuccessFeedback,
showAction,
);
}
},
);
}
Future<void> doQuickMove( Future<void> doQuickMove(
BuildContext context, { BuildContext context, {
required MoveType moveType, required MoveType moveType,

View file

@ -1,6 +1,9 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/metadata/enums/enums.dart';
import 'package:aves/model/metadata/enums/length_unit.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/media/media_edit_service.dart'; import 'package:aves/services/media/media_edit_service.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/utils/mime_utils.dart'; import 'package:aves/utils/mime_utils.dart';
import 'package:aves/widgets/common/basic/text_dropdown_button.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
@ -8,26 +11,29 @@ import 'package:flutter/material.dart';
import 'aves_dialog.dart'; import 'aves_dialog.dart';
class ExportEntryDialog extends StatefulWidget { class ConvertEntryDialog extends StatefulWidget {
static const routeName = '/dialog/export_entry'; static const routeName = '/dialog/convert_entry';
final AvesEntry entry; final Set<AvesEntry> entries;
const ExportEntryDialog({ const ConvertEntryDialog({
super.key, super.key,
required this.entry, required this.entries,
}); });
@override @override
State<ExportEntryDialog> createState() => _ExportEntryDialogState(); State<ConvertEntryDialog> createState() => _ConvertEntryDialogState();
} }
class _ExportEntryDialogState extends State<ExportEntryDialog> { class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
final TextEditingController _widthController = TextEditingController(), _heightController = TextEditingController(); final TextEditingController _widthController = TextEditingController(), _heightController = TextEditingController();
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false); final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
String _mimeType = MimeTypes.jpeg; late String _mimeType;
late bool _sameSized;
late List<LengthUnit> _lengthUnitOptions;
late LengthUnit _lengthUnit;
AvesEntry get entry => widget.entry; Set<AvesEntry> get entries => widget.entries;
static const imageExportFormats = [ static const imageExportFormats = [
MimeTypes.bmp, MimeTypes.bmp,
@ -39,11 +45,31 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_widthController.text = '${entry.isRotated ? entry.height : entry.width}'; _mimeType = MimeTypes.jpeg;
_heightController.text = '${entry.isRotated ? entry.width : entry.height}'; _sameSized = entries.map((entry) => entry.displaySize).toSet().length == 1;
_lengthUnitOptions = [
if (_sameSized) LengthUnit.px,
LengthUnit.percent,
];
_lengthUnit = _lengthUnitOptions.first;
_initDimensions();
_validate(); _validate();
} }
void _initDimensions() {
switch (_lengthUnit) {
case LengthUnit.px:
final displaySize = entries.first.displaySize;
_widthController.text = '${displaySize.width.round()}';
_heightController.text = '${displaySize.height.round()}';
break;
case LengthUnit.percent:
_widthController.text = '100';
_heightController.text = '100';
break;
}
}
@override @override
void dispose() { void dispose() {
_widthController.dispose(); _widthController.dispose();
@ -56,6 +82,14 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
final l10n = context.l10n; final l10n = context.l10n;
const contentHorizontalPadding = EdgeInsets.symmetric(horizontal: AvesDialog.defaultHorizontalContentPadding); const contentHorizontalPadding = EdgeInsets.symmetric(horizontal: AvesDialog.defaultHorizontalContentPadding);
// used by the drop down to match input decoration
final textFieldDecorationBorder = Border(
bottom: BorderSide(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.38), //Color(0xFFBDBDBD),
width: 1.0,
),
);
return AvesDialog( return AvesDialog(
scrollableContent: [ scrollableContent: [
const SizedBox(height: 16), const SizedBox(height: 16),
@ -92,12 +126,25 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onChanged: (value) { onChanged: (value) {
final width = int.tryParse(value); final width = int.tryParse(value);
_heightController.text = width != null ? '${(width / entry.displayAspectRatio).round()}' : ''; if (width != null) {
switch (_lengthUnit) {
case LengthUnit.px:
_heightController.text = '${(width / entries.first.displayAspectRatio).round()}';
break;
case LengthUnit.percent:
_heightController.text = '$width';
break;
}
} else {
_heightController.text = '';
}
_validate(); _validate();
}, },
), ),
), ),
const SizedBox(width: 8),
const Text(AvesEntry.resolutionSeparator), const Text(AvesEntry.resolutionSeparator),
const SizedBox(width: 8),
Expanded( Expanded(
child: TextField( child: TextField(
controller: _heightController, controller: _heightController,
@ -105,11 +152,46 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onChanged: (value) { onChanged: (value) {
final height = int.tryParse(value); final height = int.tryParse(value);
_widthController.text = height != null ? '${(height * entry.displayAspectRatio).round()}' : ''; if (height != null) {
switch (_lengthUnit) {
case LengthUnit.px:
_widthController.text = '${(height * entries.first.displayAspectRatio).round()}';
break;
case LengthUnit.percent:
_widthController.text = '$height';
break;
}
} else {
_widthController.text = '';
}
_validate(); _validate();
}, },
), ),
), ),
const SizedBox(width: 16),
TextDropdownButton<LengthUnit>(
values: _lengthUnitOptions,
valueText: (v) => v.getText(context),
value: _lengthUnit,
onChanged: _lengthUnitOptions.length > 1
? (v) {
if (v != null && _lengthUnit != v) {
_lengthUnit = v;
_initDimensions();
_validate();
setState(() {});
}
}
: null,
underline: Container(
height: 1.0,
decoration: BoxDecoration(
border: textFieldDecorationBorder,
),
),
itemHeight: 60,
dropdownColor: Themes.thirdLayerColor(context),
),
], ],
), ),
), ),
@ -126,8 +208,9 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
final width = int.tryParse(_widthController.text); final width = int.tryParse(_widthController.text);
final height = int.tryParse(_heightController.text); final height = int.tryParse(_heightController.text);
final options = (width != null && height != null) final options = (width != null && height != null)
? EntryExportOptions( ? EntryConvertOptions(
mimeType: _mimeType, mimeType: _mimeType,
lengthUnit: _lengthUnit,
width: width, width: width,
height: height, height: height,
) )

View file

@ -327,9 +327,13 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
final browsingMenuActions = ChipSetActions.browsing.where((v) => !browsingQuickActions.contains(v)); final browsingMenuActions = ChipSetActions.browsing.where((v) => !browsingQuickActions.contains(v));
final selectionMenuActions = ChipSetActions.selection.where((v) => !selectionQuickActions.contains(v)); final selectionMenuActions = ChipSetActions.selection.where((v) => !selectionQuickActions.contains(v));
final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).toList(); final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).fold(<ChipSetAction?>[], (prev, v) {
if (contextualMenuActions.isNotEmpty && contextualMenuActions.first == null) contextualMenuActions.removeAt(0); if (v == null && (prev.isEmpty || prev.last == null)) return prev;
if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) contextualMenuActions.removeLast(); return [...prev, v];
});
if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) {
contextualMenuActions.removeLast();
}
return [ return [
...generalMenuItems, ...generalMenuItems,

View file

@ -8,20 +8,14 @@ import 'package:aves/model/actions/share_actions.dart';
import 'package:aves/model/device.dart'; import 'package:aves/model/device.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/entry_metadata_edition.dart'; import 'package:aves/model/entry_metadata_edition.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.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_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/model/vaults/vaults.dart';
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/media_edit_service.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/action_mixins/entry_storage.dart'; import 'package:aves/widgets/common/action_mixins/entry_storage.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';
@ -32,8 +26,6 @@ import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart';
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart'; 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/pick_dialogs/album_pick_page.dart';
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.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';
@ -42,7 +34,6 @@ 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:flutter/foundation.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';
@ -197,7 +188,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
_move(context, targetEntry, moveType: MoveType.fromBin); _move(context, targetEntry, moveType: MoveType.fromBin);
break; break;
case EntryAction.convert: case EntryAction.convert:
_convert(context, targetEntry); convert(context, {targetEntry});
break; break;
case EntryAction.print: case EntryAction.print:
EntryPrinter(targetEntry).print(context); EntryPrinter(targetEntry).print(context);
@ -412,98 +403,6 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
} }
} }
Future<void> _convert(BuildContext context, AvesEntry targetEntry) async {
final options = await showDialog<EntryExportOptions>(
context: context,
builder: (context) => ExportEntryDialog(entry: targetEntry),
routeSettings: const RouteSettings(name: ExportEntryDialog.routeName),
);
if (options == null) return;
final destinationAlbum = await pickAlbum(context: context, moveType: MoveType.export);
if (destinationAlbum == null) return;
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
if (!await checkFreeSpaceForMove(context, {targetEntry}, destinationAlbum, MoveType.export)) return;
final selection = <AvesEntry>{};
if (targetEntry.isMultiPage) {
final multiPageInfo = await targetEntry.getMultiPageInfo();
if (multiPageInfo != null) {
if (targetEntry.isMotionPhoto) {
await multiPageInfo.extractMotionPhotoVideo();
}
if (multiPageInfo.pageCount > 1) {
selection.addAll(multiPageInfo.exportEntries);
}
}
} else {
selection.add(targetEntry);
}
final selectionCount = selection.length;
final source = context.read<CollectionSource>();
source.pauseMonitoring();
await showOpReport<ExportOpEvent>(
context: context,
opStream: mediaEditService.export(
selection,
options: options,
destinationAlbum: destinationAlbum,
nameConflictStrategy: NameConflictStrategy.rename,
),
itemCount: selectionCount,
onDone: (processed) async {
final successOps = processed.where((e) => e.success).toSet();
final exportedOps = successOps.where((e) => !e.skipped).toSet();
final newUris = exportedOps.map((v) => v.newFields['uri'] as String?).whereNotNull().toSet();
final isMainMode = context.read<ValueNotifier<AppMode>>().value == AppMode.main;
source.resumeMonitoring();
unawaited(source.refreshUris(newUris));
final l10n = context.l10n;
final showAction = isMainMode && newUris.isNotEmpty
? SnackBarAction(
label: l10n.showButtonLabel,
onPressed: () {
// local context may be deactivated when action is triggered after navigation
final context = AvesApp.navigatorKey.currentContext;
if (context != null) {
Navigator.maybeOf(context)?.pushAndRemoveUntil(
MaterialPageRoute(
settings: const RouteSettings(name: CollectionPage.routeName),
builder: (context) => CollectionPage(
source: source,
filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))},
highlightTest: (entry) => newUris.contains(entry.uri),
),
),
(route) => false,
);
}
},
)
: null;
final successCount = successOps.length;
if (successCount < selectionCount) {
final count = selectionCount - successCount;
showFeedback(
context,
l10n.collectionExportFailureFeedback(count),
showAction,
);
} else {
showFeedback(
context,
l10n.genericSuccessFeedback,
showAction,
);
}
},
);
}
Future<void> _move(BuildContext context, AvesEntry targetEntry, {required MoveType moveType}) => doMove( Future<void> _move(BuildContext context, AvesEntry targetEntry, {required MoveType moveType}) => doMove(
context, context,
moveType: moveType, moveType: moveType,

View file

@ -116,6 +116,8 @@
"keepScreenOnVideoPlayback", "keepScreenOnVideoPlayback",
"keepScreenOnViewerOnly", "keepScreenOnViewerOnly",
"keepScreenOnAlways", "keepScreenOnAlways",
"lengthUnitPixel",
"lengthUnitPercent",
"mapStyleGoogleNormal", "mapStyleGoogleNormal",
"mapStyleGoogleHybrid", "mapStyleGoogleHybrid",
"mapStyleGoogleTerrain", "mapStyleGoogleTerrain",
@ -600,6 +602,8 @@
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"albumTierVaults", "albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"newVaultWarningDialogMessage", "newVaultWarningDialogMessage",
@ -627,6 +631,8 @@
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"albumTierVaults", "albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"newVaultWarningDialogMessage", "newVaultWarningDialogMessage",
@ -650,6 +656,8 @@
"el": [ "el": [
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"lengthUnitPixel",
"lengthUnitPercent",
"drawerPlacePage", "drawerPlacePage",
"placePageTitle", "placePageTitle",
"placeEmpty" "placeEmpty"
@ -657,6 +665,8 @@
"es": [ "es": [
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"lengthUnitPixel",
"lengthUnitPercent",
"drawerPlacePage", "drawerPlacePage",
"placePageTitle", "placePageTitle",
"placeEmpty" "placeEmpty"
@ -668,6 +678,8 @@
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"albumTierVaults", "albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"newVaultWarningDialogMessage", "newVaultWarningDialogMessage",
@ -725,6 +737,8 @@
"keepScreenOnVideoPlayback", "keepScreenOnVideoPlayback",
"keepScreenOnViewerOnly", "keepScreenOnViewerOnly",
"keepScreenOnAlways", "keepScreenOnAlways",
"lengthUnitPixel",
"lengthUnitPercent",
"nameConflictStrategySkip", "nameConflictStrategySkip",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
@ -1156,6 +1170,8 @@
"fr": [ "fr": [
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"lengthUnitPixel",
"lengthUnitPercent",
"drawerPlacePage", "drawerPlacePage",
"placePageTitle", "placePageTitle",
"placeEmpty" "placeEmpty"
@ -1187,6 +1203,8 @@
"displayRefreshRatePreferHighest", "displayRefreshRatePreferHighest",
"displayRefreshRatePreferLowest", "displayRefreshRatePreferLowest",
"keepScreenOnVideoPlayback", "keepScreenOnVideoPlayback",
"lengthUnitPixel",
"lengthUnitPercent",
"subtitlePositionTop", "subtitlePositionTop",
"subtitlePositionBottom", "subtitlePositionBottom",
"themeBrightnessLight", "themeBrightnessLight",
@ -1793,6 +1811,8 @@
"keepScreenOnVideoPlayback", "keepScreenOnVideoPlayback",
"keepScreenOnViewerOnly", "keepScreenOnViewerOnly",
"keepScreenOnAlways", "keepScreenOnAlways",
"lengthUnitPixel",
"lengthUnitPercent",
"mapStyleGoogleNormal", "mapStyleGoogleNormal",
"mapStyleGoogleHybrid", "mapStyleGoogleHybrid",
"mapStyleGoogleTerrain", "mapStyleGoogleTerrain",
@ -2286,6 +2306,8 @@
"id": [ "id": [
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"lengthUnitPixel",
"lengthUnitPercent",
"drawerPlacePage", "drawerPlacePage",
"placePageTitle", "placePageTitle",
"placeEmpty" "placeEmpty"
@ -2297,6 +2319,8 @@
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"albumTierVaults", "albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"newVaultWarningDialogMessage", "newVaultWarningDialogMessage",
@ -2332,6 +2356,8 @@
"filterTaggedLabel", "filterTaggedLabel",
"albumTierVaults", "albumTierVaults",
"keepScreenOnVideoPlayback", "keepScreenOnVideoPlayback",
"lengthUnitPixel",
"lengthUnitPercent",
"subtitlePositionTop", "subtitlePositionTop",
"subtitlePositionBottom", "subtitlePositionBottom",
"vaultLockTypePin", "vaultLockTypePin",
@ -2364,6 +2390,8 @@
"ko": [ "ko": [
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"lengthUnitPixel",
"lengthUnitPercent",
"drawerPlacePage", "drawerPlacePage",
"placePageTitle", "placePageTitle",
"placeEmpty" "placeEmpty"
@ -2379,6 +2407,8 @@
"filterTaggedLabel", "filterTaggedLabel",
"albumTierVaults", "albumTierVaults",
"keepScreenOnVideoPlayback", "keepScreenOnVideoPlayback",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"newVaultWarningDialogMessage", "newVaultWarningDialogMessage",
@ -2412,6 +2442,8 @@
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"albumTierVaults", "albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"newVaultWarningDialogMessage", "newVaultWarningDialogMessage",
@ -2450,6 +2482,8 @@
"filterTaggedLabel", "filterTaggedLabel",
"albumTierVaults", "albumTierVaults",
"keepScreenOnVideoPlayback", "keepScreenOnVideoPlayback",
"lengthUnitPixel",
"lengthUnitPercent",
"subtitlePositionTop", "subtitlePositionTop",
"subtitlePositionBottom", "subtitlePositionBottom",
"vaultLockTypePin", "vaultLockTypePin",
@ -2500,6 +2534,8 @@
"albumTierVaults", "albumTierVaults",
"displayRefreshRatePreferHighest", "displayRefreshRatePreferHighest",
"displayRefreshRatePreferLowest", "displayRefreshRatePreferLowest",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"wallpaperTargetHome", "wallpaperTargetHome",
@ -2798,6 +2834,8 @@
"pl": [ "pl": [
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"lengthUnitPixel",
"lengthUnitPercent",
"drawerPlacePage", "drawerPlacePage",
"placePageTitle", "placePageTitle",
"placeEmpty" "placeEmpty"
@ -2810,6 +2848,8 @@
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"albumTierVaults", "albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"newVaultWarningDialogMessage", "newVaultWarningDialogMessage",
@ -2835,6 +2875,8 @@
"ro": [ "ro": [
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"lengthUnitPixel",
"lengthUnitPercent",
"drawerPlacePage", "drawerPlacePage",
"placePageTitle", "placePageTitle",
"placeEmpty" "placeEmpty"
@ -2848,6 +2890,8 @@
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"albumTierVaults", "albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"newVaultWarningDialogMessage", "newVaultWarningDialogMessage",
@ -2885,6 +2929,8 @@
"filterNoLocationLabel", "filterNoLocationLabel",
"albumTierVaults", "albumTierVaults",
"coordinateDms", "coordinateDms",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"otherDirectoryDescription", "otherDirectoryDescription",
@ -3300,6 +3346,8 @@
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"albumTierVaults", "albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"newVaultWarningDialogMessage", "newVaultWarningDialogMessage",
@ -3642,6 +3690,8 @@
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"albumTierVaults", "albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"newVaultWarningDialogMessage", "newVaultWarningDialogMessage",
@ -3665,6 +3715,8 @@
"uk": [ "uk": [
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"lengthUnitPixel",
"lengthUnitPercent",
"drawerPlacePage", "drawerPlacePage",
"placePageTitle", "placePageTitle",
"placeEmpty" "placeEmpty"
@ -3678,6 +3730,8 @@
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"albumTierVaults", "albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"newVaultWarningDialogMessage", "newVaultWarningDialogMessage",
@ -3714,6 +3768,8 @@
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"albumTierVaults", "albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
"vaultLockTypePin", "vaultLockTypePin",
"vaultLockTypePassword", "vaultLockTypePassword",
"newVaultWarningDialogMessage", "newVaultWarningDialogMessage",