#526 collection: bulk converting
This commit is contained in:
parent
af6fd8f11b
commit
8f732608d0
16 changed files with 367 additions and 139 deletions
|
@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
### Added
|
||||
|
||||
- Collection: bulk converting
|
||||
- Places: page & navigation entry
|
||||
|
||||
### Fixed
|
||||
|
|
|
@ -50,7 +50,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
|
||||
when (op) {
|
||||
"delete" -> ioScope.launch { delete() }
|
||||
"export" -> ioScope.launch { export() }
|
||||
"convert" -> ioScope.launch { convert() }
|
||||
"move" -> ioScope.launch { move() }
|
||||
"rename" -> ioScope.launch { rename() }
|
||||
else -> endOfStream()
|
||||
|
@ -121,7 +121,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
endOfStream()
|
||||
}
|
||||
|
||||
private suspend fun export() {
|
||||
private suspend fun convert() {
|
||||
if (arguments !is Map<*, *> || entryMapList.isEmpty()) {
|
||||
endOfStream()
|
||||
return
|
||||
|
@ -129,11 +129,12 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
|
||||
var destinationDir = arguments["destinationPath"] as String?
|
||||
val mimeType = arguments["mimeType"] as String?
|
||||
val lengthUnit = arguments["lengthUnit"] as String?
|
||||
val width = (arguments["width"] as Number?)?.toInt()
|
||||
val height = (arguments["height"] as Number?)?.toInt()
|
||||
val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?)
|
||||
if (destinationDir == null || mimeType == null || width == null || height == null || nameConflictStrategy == null) {
|
||||
error("export-args", "missing arguments", null)
|
||||
if (destinationDir == null || mimeType == null || lengthUnit == null || width == null || height == null || nameConflictStrategy == null) {
|
||||
error("convert-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -141,15 +142,15 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
val firstEntry = entryMapList.first()
|
||||
val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) }
|
||||
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
|
||||
}
|
||||
|
||||
destinationDir = ensureTrailingSeparator(destinationDir)
|
||||
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 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()
|
||||
}
|
||||
|
|
|
@ -15,6 +15,15 @@ class AvesEntry(map: FieldMap) {
|
|||
val trashed = map["trashed"] as Boolean
|
||||
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 {
|
||||
// convenience method
|
||||
private fun toLong(o: Any?): Long? = when (o) {
|
||||
|
|
|
@ -169,11 +169,12 @@ abstract class ImageProvider {
|
|||
throw UnsupportedOperationException("`scanPostMetadataEdit` is not supported by this image provider")
|
||||
}
|
||||
|
||||
suspend fun exportMultiple(
|
||||
suspend fun convertMultiple(
|
||||
activity: Activity,
|
||||
imageExportMimeType: String,
|
||||
targetDir: String,
|
||||
entries: List<AvesEntry>,
|
||||
lengthUnit: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
nameConflictStrategy: NameConflictStrategy,
|
||||
|
@ -208,6 +209,7 @@ abstract class ImageProvider {
|
|||
sourceEntry = entry,
|
||||
targetDir = targetDir,
|
||||
targetDirDocFile = targetDirDocFile,
|
||||
lengthUnit = lengthUnit,
|
||||
width = width,
|
||||
height = height,
|
||||
nameConflictStrategy = nameConflictStrategy,
|
||||
|
@ -227,6 +229,7 @@ abstract class ImageProvider {
|
|||
sourceEntry: AvesEntry,
|
||||
targetDir: String,
|
||||
targetDirDocFile: DocumentFileCompat?,
|
||||
lengthUnit: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
nameConflictStrategy: NameConflictStrategy,
|
||||
|
@ -266,6 +269,19 @@ abstract class ImageProvider {
|
|||
sourceDocFile.copyTo(output)
|
||||
}
|
||||
} 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) {
|
||||
MultiTrackImage(activity, sourceUri, pageId)
|
||||
} else if (sourceMimeType == MimeTypes.TIFF) {
|
||||
|
@ -286,7 +302,7 @@ abstract class ImageProvider {
|
|||
.asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(model)
|
||||
.submit(width, height)
|
||||
.submit(targetWidthPx, targetHeightPx)
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
var bitmap = target.get()
|
||||
if (MimeTypes.needRotationAfterGlide(sourceMimeType)) {
|
||||
|
@ -1209,6 +1225,8 @@ abstract class ImageProvider {
|
|||
companion object {
|
||||
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)
|
||||
|
||||
// used when skipping a move/creation op because the target file already exists
|
||||
|
|
|
@ -200,6 +200,9 @@
|
|||
"keepScreenOnViewerOnly": "Viewer page only",
|
||||
"keepScreenOnAlways": "Always",
|
||||
|
||||
"lengthUnitPixel": "px",
|
||||
"lengthUnitPercent": "%",
|
||||
|
||||
"mapStyleGoogleNormal": "Google Maps",
|
||||
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
||||
"mapStyleGoogleTerrain": "Google Maps (Terrain)",
|
||||
|
|
|
@ -25,6 +25,7 @@ enum EntrySetAction {
|
|||
copy,
|
||||
move,
|
||||
rename,
|
||||
convert,
|
||||
toggleFavourite,
|
||||
rotateCCW,
|
||||
rotateCW,
|
||||
|
@ -45,13 +46,16 @@ class EntrySetActions {
|
|||
EntrySetAction.selectNone,
|
||||
];
|
||||
|
||||
// `null` items are converted to dividers
|
||||
static const pageBrowsing = [
|
||||
EntrySetAction.searchCollection,
|
||||
EntrySetAction.toggleTitleSearch,
|
||||
EntrySetAction.addShortcut,
|
||||
null,
|
||||
EntrySetAction.map,
|
||||
EntrySetAction.slideshow,
|
||||
EntrySetAction.stats,
|
||||
null,
|
||||
EntrySetAction.rescan,
|
||||
EntrySetAction.emptyBin,
|
||||
];
|
||||
|
@ -67,6 +71,7 @@ class EntrySetActions {
|
|||
EntrySetAction.rescan,
|
||||
];
|
||||
|
||||
// `null` items are converted to dividers
|
||||
static const pageSelection = [
|
||||
EntrySetAction.share,
|
||||
EntrySetAction.delete,
|
||||
|
@ -74,10 +79,13 @@ class EntrySetActions {
|
|||
EntrySetAction.copy,
|
||||
EntrySetAction.move,
|
||||
EntrySetAction.rename,
|
||||
EntrySetAction.convert,
|
||||
EntrySetAction.toggleFavourite,
|
||||
null,
|
||||
EntrySetAction.map,
|
||||
EntrySetAction.slideshow,
|
||||
EntrySetAction.stats,
|
||||
null,
|
||||
EntrySetAction.rescan,
|
||||
// editing actions are in their subsection
|
||||
];
|
||||
|
@ -89,6 +97,7 @@ class EntrySetActions {
|
|||
EntrySetAction.copy,
|
||||
EntrySetAction.move,
|
||||
EntrySetAction.rename,
|
||||
EntrySetAction.convert,
|
||||
EntrySetAction.toggleFavourite,
|
||||
EntrySetAction.map,
|
||||
EntrySetAction.slideshow,
|
||||
|
@ -163,6 +172,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
return context.l10n.collectionActionMove;
|
||||
case EntrySetAction.rename:
|
||||
return context.l10n.entryActionRename;
|
||||
case EntrySetAction.convert:
|
||||
return context.l10n.entryActionConvert;
|
||||
case EntrySetAction.toggleFavourite:
|
||||
// different data depending on toggle state
|
||||
return context.l10n.entryActionAddFavourite;
|
||||
|
@ -232,6 +243,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
return AIcons.move;
|
||||
case EntrySetAction.rename:
|
||||
return AIcons.name;
|
||||
case EntrySetAction.convert:
|
||||
return AIcons.convert;
|
||||
case EntrySetAction.toggleFavourite:
|
||||
// different data depending on toggle state
|
||||
return AIcons.favourite;
|
||||
|
|
|
@ -15,6 +15,8 @@ enum DateFieldSource {
|
|||
exifGpsDate,
|
||||
}
|
||||
|
||||
enum LengthUnit { px, percent }
|
||||
|
||||
enum LocationEditAction {
|
||||
chooseOnMap,
|
||||
copyItem,
|
||||
|
|
14
lib/model/metadata/enums/length_unit.dart
Normal file
14
lib/model/metadata/enums/length_unit.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
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/services.dart';
|
||||
import 'package:aves/services/media/enums.dart';
|
||||
|
@ -28,7 +29,7 @@ abstract class MediaEditService {
|
|||
|
||||
Stream<ExportOpEvent> export(
|
||||
Iterable<AvesEntry> entries, {
|
||||
required EntryExportOptions options,
|
||||
required EntryConvertOptions options,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
});
|
||||
|
@ -113,16 +114,17 @@ class PlatformMediaEditService implements MediaEditService {
|
|||
@override
|
||||
Stream<ExportOpEvent> export(
|
||||
Iterable<AvesEntry> entries, {
|
||||
required EntryExportOptions options,
|
||||
required EntryConvertOptions options,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
}) {
|
||||
try {
|
||||
return _opStream
|
||||
.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'export',
|
||||
'op': 'convert',
|
||||
'entries': entries.map((entry) => entry.toPlatformEntryMap()).toList(),
|
||||
'mimeType': options.mimeType,
|
||||
'lengthUnit': options.lengthUnit.name,
|
||||
'width': options.width,
|
||||
'height': options.height,
|
||||
'destinationPath': destinationAlbum,
|
||||
|
@ -183,15 +185,17 @@ class PlatformMediaEditService implements MediaEditService {
|
|||
}
|
||||
|
||||
@immutable
|
||||
class EntryExportOptions extends Equatable {
|
||||
class EntryConvertOptions extends Equatable {
|
||||
final String mimeType;
|
||||
final LengthUnit lengthUnit;
|
||||
final int width, height;
|
||||
|
||||
@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.lengthUnit,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
|
|
|
@ -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/filter_grids/common/action_delegates/chip.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -342,7 +343,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
return [
|
||||
...EntrySetActions.general,
|
||||
...isSelecting ? EntrySetActions.pageSelection : EntrySetActions.pageBrowsing,
|
||||
].where(isVisible).map((action) {
|
||||
].whereNotNull().where(isVisible).map((action) {
|
||||
final enabled = canApply(action);
|
||||
return CaptionedButton(
|
||||
iconButtonBuilder: (context, focusNode) => _buildButtonIcon(
|
||||
|
@ -388,9 +389,20 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
|
||||
final browsingMenuActions = EntrySetActions.pageBrowsing.where((v) => !browsingQuickActions.contains(v));
|
||||
final selectionMenuActions = EntrySetActions.pageSelection.where((v) => !selectionQuickActions.contains(v));
|
||||
final contextualMenuItems = [
|
||||
...(isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map(
|
||||
(action) => _toMenuItem(action, enabled: canApply(action), selection: selection),
|
||||
final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).fold(<EntrySetAction?>[], (prev, v) {
|
||||
if (v == null && (prev.isEmpty || prev.last == null)) return prev;
|
||||
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)
|
||||
PopupMenuItem<EntrySetAction>(
|
||||
|
@ -630,6 +642,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
case EntrySetAction.copy:
|
||||
case EntrySetAction.move:
|
||||
case EntrySetAction.rename:
|
||||
case EntrySetAction.convert:
|
||||
case EntrySetAction.toggleFavourite:
|
||||
case EntrySetAction.rotateCCW:
|
||||
case EntrySetAction.rotateCW:
|
||||
|
|
|
@ -94,6 +94,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.copy:
|
||||
case EntrySetAction.move:
|
||||
case EntrySetAction.rename:
|
||||
case EntrySetAction.convert:
|
||||
case EntrySetAction.rotateCCW:
|
||||
case EntrySetAction.rotateCW:
|
||||
case EntrySetAction.flip:
|
||||
|
@ -145,6 +146,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.copy:
|
||||
case EntrySetAction.move:
|
||||
case EntrySetAction.rename:
|
||||
case EntrySetAction.convert:
|
||||
case EntrySetAction.toggleFavourite:
|
||||
case EntrySetAction.rotateCCW:
|
||||
case EntrySetAction.rotateCW:
|
||||
|
@ -211,6 +213,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.rename:
|
||||
_rename(context);
|
||||
break;
|
||||
case EntrySetAction.convert:
|
||||
_convert(context);
|
||||
break;
|
||||
case EntrySetAction.toggleFavourite:
|
||||
_toggleFavourite(context);
|
||||
break;
|
||||
|
@ -379,6 +384,13 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
_browse(context);
|
||||
}
|
||||
|
||||
void _convert(BuildContext context) {
|
||||
final entries = _getTargetItems(context);
|
||||
convert(context, entries);
|
||||
|
||||
_browse(context);
|
||||
}
|
||||
|
||||
Future<void> _toggleFavourite(BuildContext context) async {
|
||||
final entries = _getTargetItems(context);
|
||||
if (entries.every((entry) => entry.isFavourite)) {
|
||||
|
|
|
@ -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/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/utils/android_file_utils.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/dialogs/aves_confirmation_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/viewer/notifications.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -34,6 +36,100 @@ import 'package:flutter/material.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
|
||||
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(
|
||||
BuildContext context, {
|
||||
required MoveType moveType,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
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/services/media/media_edit_service.dart';
|
||||
import 'package:aves/theme/themes.dart';
|
||||
import 'package:aves/utils/mime_utils.dart';
|
||||
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
|
@ -8,26 +11,29 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'aves_dialog.dart';
|
||||
|
||||
class ExportEntryDialog extends StatefulWidget {
|
||||
static const routeName = '/dialog/export_entry';
|
||||
class ConvertEntryDialog extends StatefulWidget {
|
||||
static const routeName = '/dialog/convert_entry';
|
||||
|
||||
final AvesEntry entry;
|
||||
final Set<AvesEntry> entries;
|
||||
|
||||
const ExportEntryDialog({
|
||||
const ConvertEntryDialog({
|
||||
super.key,
|
||||
required this.entry,
|
||||
required this.entries,
|
||||
});
|
||||
|
||||
@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 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 = [
|
||||
MimeTypes.bmp,
|
||||
|
@ -39,11 +45,31 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_widthController.text = '${entry.isRotated ? entry.height : entry.width}';
|
||||
_heightController.text = '${entry.isRotated ? entry.width : entry.height}';
|
||||
_mimeType = MimeTypes.jpeg;
|
||||
_sameSized = entries.map((entry) => entry.displaySize).toSet().length == 1;
|
||||
_lengthUnitOptions = [
|
||||
if (_sameSized) LengthUnit.px,
|
||||
LengthUnit.percent,
|
||||
];
|
||||
_lengthUnit = _lengthUnitOptions.first;
|
||||
_initDimensions();
|
||||
_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
|
||||
void dispose() {
|
||||
_widthController.dispose();
|
||||
|
@ -56,6 +82,14 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
|
|||
final l10n = context.l10n;
|
||||
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(
|
||||
scrollableContent: [
|
||||
const SizedBox(height: 16),
|
||||
|
@ -92,12 +126,25 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
|
|||
keyboardType: TextInputType.number,
|
||||
onChanged: (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();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(AvesEntry.resolutionSeparator),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _heightController,
|
||||
|
@ -105,11 +152,46 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
|
|||
keyboardType: TextInputType.number,
|
||||
onChanged: (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();
|
||||
},
|
||||
),
|
||||
),
|
||||
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 height = int.tryParse(_heightController.text);
|
||||
final options = (width != null && height != null)
|
||||
? EntryExportOptions(
|
||||
? EntryConvertOptions(
|
||||
mimeType: _mimeType,
|
||||
lengthUnit: _lengthUnit,
|
||||
width: width,
|
||||
height: height,
|
||||
)
|
|
@ -327,9 +327,13 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
|
|||
|
||||
final browsingMenuActions = ChipSetActions.browsing.where((v) => !browsingQuickActions.contains(v));
|
||||
final selectionMenuActions = ChipSetActions.selection.where((v) => !selectionQuickActions.contains(v));
|
||||
final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).toList();
|
||||
if (contextualMenuActions.isNotEmpty && contextualMenuActions.first == null) contextualMenuActions.removeAt(0);
|
||||
if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) contextualMenuActions.removeLast();
|
||||
final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).fold(<ChipSetAction?>[], (prev, v) {
|
||||
if (v == null && (prev.isEmpty || prev.last == null)) return prev;
|
||||
return [...prev, v];
|
||||
});
|
||||
if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) {
|
||||
contextualMenuActions.removeLast();
|
||||
}
|
||||
|
||||
return [
|
||||
...generalMenuItems,
|
||||
|
|
|
@ -8,20 +8,14 @@ import 'package:aves/model/actions/share_actions.dart';
|
|||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/entry.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/settings/enums/enums.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
import 'package:aves/services/common/image_op_events.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/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/feedback.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_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/printer.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/source_viewer_page.dart';
|
||||
import 'package:aves/widgets/viewer/video/conductor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
@ -197,7 +188,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
_move(context, targetEntry, moveType: MoveType.fromBin);
|
||||
break;
|
||||
case EntryAction.convert:
|
||||
_convert(context, targetEntry);
|
||||
convert(context, {targetEntry});
|
||||
break;
|
||||
case EntryAction.print:
|
||||
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(
|
||||
context,
|
||||
moveType: moveType,
|
||||
|
|
|
@ -116,6 +116,8 @@
|
|||
"keepScreenOnVideoPlayback",
|
||||
"keepScreenOnViewerOnly",
|
||||
"keepScreenOnAlways",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"mapStyleGoogleNormal",
|
||||
"mapStyleGoogleHybrid",
|
||||
"mapStyleGoogleTerrain",
|
||||
|
@ -600,6 +602,8 @@
|
|||
"chipActionCreateVault",
|
||||
"chipActionConfigureVault",
|
||||
"albumTierVaults",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"newVaultWarningDialogMessage",
|
||||
|
@ -627,6 +631,8 @@
|
|||
"chipActionCreateVault",
|
||||
"chipActionConfigureVault",
|
||||
"albumTierVaults",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"newVaultWarningDialogMessage",
|
||||
|
@ -650,6 +656,8 @@
|
|||
|
||||
"el": [
|
||||
"chipActionGoToPlacePage",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
|
@ -657,6 +665,8 @@
|
|||
|
||||
"es": [
|
||||
"chipActionGoToPlacePage",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
|
@ -668,6 +678,8 @@
|
|||
"chipActionCreateVault",
|
||||
"chipActionConfigureVault",
|
||||
"albumTierVaults",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"newVaultWarningDialogMessage",
|
||||
|
@ -725,6 +737,8 @@
|
|||
"keepScreenOnVideoPlayback",
|
||||
"keepScreenOnViewerOnly",
|
||||
"keepScreenOnAlways",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"nameConflictStrategySkip",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
|
@ -1156,6 +1170,8 @@
|
|||
|
||||
"fr": [
|
||||
"chipActionGoToPlacePage",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
|
@ -1187,6 +1203,8 @@
|
|||
"displayRefreshRatePreferHighest",
|
||||
"displayRefreshRatePreferLowest",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"subtitlePositionTop",
|
||||
"subtitlePositionBottom",
|
||||
"themeBrightnessLight",
|
||||
|
@ -1793,6 +1811,8 @@
|
|||
"keepScreenOnVideoPlayback",
|
||||
"keepScreenOnViewerOnly",
|
||||
"keepScreenOnAlways",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"mapStyleGoogleNormal",
|
||||
"mapStyleGoogleHybrid",
|
||||
"mapStyleGoogleTerrain",
|
||||
|
@ -2286,6 +2306,8 @@
|
|||
|
||||
"id": [
|
||||
"chipActionGoToPlacePage",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
|
@ -2297,6 +2319,8 @@
|
|||
"chipActionCreateVault",
|
||||
"chipActionConfigureVault",
|
||||
"albumTierVaults",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"newVaultWarningDialogMessage",
|
||||
|
@ -2332,6 +2356,8 @@
|
|||
"filterTaggedLabel",
|
||||
"albumTierVaults",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"subtitlePositionTop",
|
||||
"subtitlePositionBottom",
|
||||
"vaultLockTypePin",
|
||||
|
@ -2364,6 +2390,8 @@
|
|||
|
||||
"ko": [
|
||||
"chipActionGoToPlacePage",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
|
@ -2379,6 +2407,8 @@
|
|||
"filterTaggedLabel",
|
||||
"albumTierVaults",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"newVaultWarningDialogMessage",
|
||||
|
@ -2412,6 +2442,8 @@
|
|||
"chipActionCreateVault",
|
||||
"chipActionConfigureVault",
|
||||
"albumTierVaults",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"newVaultWarningDialogMessage",
|
||||
|
@ -2450,6 +2482,8 @@
|
|||
"filterTaggedLabel",
|
||||
"albumTierVaults",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"subtitlePositionTop",
|
||||
"subtitlePositionBottom",
|
||||
"vaultLockTypePin",
|
||||
|
@ -2500,6 +2534,8 @@
|
|||
"albumTierVaults",
|
||||
"displayRefreshRatePreferHighest",
|
||||
"displayRefreshRatePreferLowest",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"wallpaperTargetHome",
|
||||
|
@ -2798,6 +2834,8 @@
|
|||
|
||||
"pl": [
|
||||
"chipActionGoToPlacePage",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
|
@ -2810,6 +2848,8 @@
|
|||
"chipActionCreateVault",
|
||||
"chipActionConfigureVault",
|
||||
"albumTierVaults",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"newVaultWarningDialogMessage",
|
||||
|
@ -2835,6 +2875,8 @@
|
|||
|
||||
"ro": [
|
||||
"chipActionGoToPlacePage",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
|
@ -2848,6 +2890,8 @@
|
|||
"filterLocatedLabel",
|
||||
"filterTaggedLabel",
|
||||
"albumTierVaults",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"newVaultWarningDialogMessage",
|
||||
|
@ -2885,6 +2929,8 @@
|
|||
"filterNoLocationLabel",
|
||||
"albumTierVaults",
|
||||
"coordinateDms",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"otherDirectoryDescription",
|
||||
|
@ -3300,6 +3346,8 @@
|
|||
"chipActionCreateVault",
|
||||
"chipActionConfigureVault",
|
||||
"albumTierVaults",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"newVaultWarningDialogMessage",
|
||||
|
@ -3642,6 +3690,8 @@
|
|||
"chipActionCreateVault",
|
||||
"chipActionConfigureVault",
|
||||
"albumTierVaults",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"newVaultWarningDialogMessage",
|
||||
|
@ -3665,6 +3715,8 @@
|
|||
|
||||
"uk": [
|
||||
"chipActionGoToPlacePage",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
|
@ -3678,6 +3730,8 @@
|
|||
"filterLocatedLabel",
|
||||
"filterTaggedLabel",
|
||||
"albumTierVaults",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"newVaultWarningDialogMessage",
|
||||
|
@ -3714,6 +3768,8 @@
|
|||
"filterLocatedLabel",
|
||||
"filterTaggedLabel",
|
||||
"albumTierVaults",
|
||||
"lengthUnitPixel",
|
||||
"lengthUnitPercent",
|
||||
"vaultLockTypePin",
|
||||
"vaultLockTypePassword",
|
||||
"newVaultWarningDialogMessage",
|
||||
|
|
Loading…
Reference in a new issue