export: format selection, clean up failed export
This commit is contained in:
parent
2f2a9293bd
commit
b84fde14af
8 changed files with 107 additions and 11 deletions
|
@ -67,7 +67,7 @@ abstract class ImageProvider {
|
||||||
callback: ImageOpCallback,
|
callback: ImageOpCallback,
|
||||||
) {
|
) {
|
||||||
if (!supportedExportMimeTypes.contains(imageExportMimeType)) {
|
if (!supportedExportMimeTypes.contains(imageExportMimeType)) {
|
||||||
throw Exception("unsupported export MIME type=$imageExportMimeType")
|
callback.onFailure(Exception("unsupported export MIME type=$imageExportMimeType"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val destinationDirDocFile = createDirectoryIfAbsent(activity, destinationDir)
|
val destinationDirDocFile = createDirectoryIfAbsent(activity, destinationDir)
|
||||||
|
@ -198,6 +198,12 @@ abstract class ImageProvider {
|
||||||
bitmap.compress(format, quality, output)
|
bitmap.compress(format, quality, output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// remove empty file
|
||||||
|
if (destinationDocFile.exists()) {
|
||||||
|
destinationDocFile.delete()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
Glide.with(activity).clear(target)
|
Glide.with(activity).clear(target)
|
||||||
}
|
}
|
||||||
|
|
|
@ -344,6 +344,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"exportEntryDialogFormat": "Format:",
|
||||||
|
"@exportEntryDialogFormat": {},
|
||||||
|
|
||||||
"renameEntryDialogLabel": "New name",
|
"renameEntryDialogLabel": "New name",
|
||||||
"@renameEntryDialogLabel": {},
|
"@renameEntryDialogLabel": {},
|
||||||
|
|
||||||
|
|
|
@ -156,6 +156,8 @@
|
||||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범의 항목 {count}개를 삭제하시겠습니까?}}",
|
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범의 항목 {count}개를 삭제하시겠습니까?}}",
|
||||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범들의 항목 {count}개를 삭제하시겠습니까?}}",
|
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범들의 항목 {count}개를 삭제하시겠습니까?}}",
|
||||||
|
|
||||||
|
"exportEntryDialogFormat": "형식:",
|
||||||
|
|
||||||
"renameEntryDialogLabel": "이름",
|
"renameEntryDialogLabel": "이름",
|
||||||
|
|
||||||
"editEntryDateDialogTitle": "날짜 및 시간",
|
"editEntryDateDialogTitle": "날짜 및 시간",
|
||||||
|
|
|
@ -254,6 +254,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
|
|
||||||
Future<void> refresh();
|
Future<void> refresh();
|
||||||
|
|
||||||
|
Future<Set<String>> refreshUris(Set<String> changedUris);
|
||||||
|
|
||||||
Future<void> rescan(Set<AvesEntry> entries);
|
Future<void> rescan(Set<AvesEntry> entries);
|
||||||
|
|
||||||
Future<void> refreshMetadata(Set<AvesEntry> entries) async {
|
Future<void> refreshMetadata(Set<AvesEntry> entries) async {
|
||||||
|
|
|
@ -127,6 +127,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
// 2) registered in the Media Store but still being processed by their owner in a temporary location
|
// 2) registered in the Media Store but still being processed by their owner in a temporary location
|
||||||
// For example, when taking a picture with a Galaxy S10e default camera app, querying the Media Store
|
// For example, when taking a picture with a Galaxy S10e default camera app, querying the Media Store
|
||||||
// sometimes yields an entry with its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg`
|
// sometimes yields an entry with its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg`
|
||||||
|
@override
|
||||||
Future<Set<String>> refreshUris(Set<String> changedUris) async {
|
Future<Set<String>> refreshUris(Set<String> changedUris) async {
|
||||||
if (!_initialized || !isMonitoring) return changedUris;
|
if (!_initialized || !isMonitoring) return changedUris;
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,9 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class AvesDialog extends AlertDialog {
|
class AvesDialog extends AlertDialog {
|
||||||
static const contentHorizontalPadding = EdgeInsets.symmetric(horizontal: 24);
|
static const EdgeInsets contentHorizontalPadding = EdgeInsets.symmetric(horizontal: 24);
|
||||||
static const borderWidth = 1.0;
|
static const double controlCaptionPadding = 16;
|
||||||
|
static const double borderWidth = 1.0;
|
||||||
|
|
||||||
AvesDialog({
|
AvesDialog({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
|
70
lib/widgets/dialogs/export_entry_dialog.dart
Normal file
70
lib/widgets/dialogs/export_entry_dialog.dart
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/ref/mime_types.dart';
|
||||||
|
import 'package:aves/utils/mime_utils.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'aves_dialog.dart';
|
||||||
|
|
||||||
|
class ExportEntryDialog extends StatefulWidget {
|
||||||
|
final AvesEntry entry;
|
||||||
|
|
||||||
|
const ExportEntryDialog({
|
||||||
|
Key? key,
|
||||||
|
required this.entry,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ExportEntryDialogState createState() => _ExportEntryDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExportEntryDialogState extends State<ExportEntryDialog> {
|
||||||
|
String _mimeType = MimeTypes.jpeg;
|
||||||
|
|
||||||
|
AvesEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
static const imageExportFormats = [
|
||||||
|
MimeTypes.bmp,
|
||||||
|
MimeTypes.jpeg,
|
||||||
|
MimeTypes.png,
|
||||||
|
MimeTypes.webp,
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AvesDialog(
|
||||||
|
context: context,
|
||||||
|
content: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(context.l10n.exportEntryDialogFormat),
|
||||||
|
const SizedBox(width: AvesDialog.controlCaptionPadding),
|
||||||
|
DropdownButton<String>(
|
||||||
|
items: imageExportFormats.map((mimeType) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: mimeType,
|
||||||
|
child: Text(MimeUtils.displayType(mimeType)),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
value: _mimeType,
|
||||||
|
onChanged: (selected) {
|
||||||
|
if (selected != null) {
|
||||||
|
setState(() => _mimeType = selected);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, _mimeType),
|
||||||
|
child: Text(context.l10n.applyButtonLabel),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,6 @@ import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.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/ref/mime_types.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';
|
||||||
|
@ -20,6 +19,7 @@ import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/size_aware.dart';
|
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/export_entry_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/rename_entry_dialog.dart';
|
import 'package:aves/widgets/dialogs/rename_entry_dialog.dart';
|
||||||
import 'package:aves/widgets/filter_grids/album_pick.dart';
|
import 'package:aves/widgets/filter_grids/album_pick.dart';
|
||||||
import 'package:aves/widgets/viewer/debug/debug_page.dart';
|
import 'package:aves/widgets/viewer/debug/debug_page.dart';
|
||||||
|
@ -185,6 +185,12 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
|
|
||||||
if (!await checkFreeSpaceForMove(context, {entry}, destinationAlbum, MoveType.export)) return;
|
if (!await checkFreeSpaceForMove(context, {entry}, destinationAlbum, MoveType.export)) return;
|
||||||
|
|
||||||
|
final mimeType = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ExportEntryDialog(entry: entry),
|
||||||
|
);
|
||||||
|
if (mimeType == null) return;
|
||||||
|
|
||||||
final selection = <AvesEntry>{};
|
final selection = <AvesEntry>{};
|
||||||
if (entry.isMultiPage) {
|
if (entry.isMultiPage) {
|
||||||
final multiPageInfo = await entry.getMultiPageInfo();
|
final multiPageInfo = await entry.getMultiPageInfo();
|
||||||
|
@ -201,21 +207,26 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
}
|
}
|
||||||
|
|
||||||
final selectionCount = selection.length;
|
final selectionCount = selection.length;
|
||||||
|
source.pauseMonitoring();
|
||||||
showOpReport<ExportOpEvent>(
|
showOpReport<ExportOpEvent>(
|
||||||
context: context,
|
context: context,
|
||||||
// TODO TLAD [SVG] export separately from raster images (sending bytes, like frame captures)
|
// TODO TLAD [SVG] export separately from raster images (sending bytes, like frame captures)
|
||||||
opStream: mediaFileService.export(
|
opStream: mediaFileService.export(
|
||||||
selection,
|
selection,
|
||||||
mimeType: MimeTypes.jpeg,
|
mimeType: mimeType,
|
||||||
destinationAlbum: destinationAlbum,
|
destinationAlbum: destinationAlbum,
|
||||||
nameConflictStrategy: NameConflictStrategy.rename,
|
nameConflictStrategy: NameConflictStrategy.rename,
|
||||||
),
|
),
|
||||||
itemCount: selectionCount,
|
itemCount: selectionCount,
|
||||||
onDone: (processed) {
|
onDone: (processed) {
|
||||||
final movedOps = processed.where((e) => e.success);
|
final exportOps = processed.where((e) => e.success);
|
||||||
final movedCount = movedOps.length;
|
final exportCount = exportOps.length;
|
||||||
final isMainMode = context.read<ValueNotifier<AppMode>>().value == AppMode.main;
|
final isMainMode = context.read<ValueNotifier<AppMode>>().value == AppMode.main;
|
||||||
final showAction = isMainMode && movedCount > 0
|
|
||||||
|
source.resumeMonitoring();
|
||||||
|
source.refreshUris(exportOps.map((v) => v.newFields['uri'] as String?).whereNotNull().toSet());
|
||||||
|
|
||||||
|
final showAction = isMainMode && exportCount > 0
|
||||||
? SnackBarAction(
|
? SnackBarAction(
|
||||||
label: context.l10n.showButtonLabel,
|
label: context.l10n.showButtonLabel,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
@ -236,7 +247,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
));
|
));
|
||||||
final delayDuration = context.read<DurationsData>().staggeredAnimationPageTarget;
|
final delayDuration = context.read<DurationsData>().staggeredAnimationPageTarget;
|
||||||
await Future.delayed(delayDuration + Durations.highlightScrollInitDelay);
|
await Future.delayed(delayDuration + Durations.highlightScrollInitDelay);
|
||||||
final newUris = movedOps.map((v) => v.newFields['uri'] as String?).toSet();
|
final newUris = exportOps.map((v) => v.newFields['uri'] as String?).toSet();
|
||||||
final targetEntry = targetCollection.sortedEntries.firstWhereOrNull((entry) => newUris.contains(entry.uri));
|
final targetEntry = targetCollection.sortedEntries.firstWhereOrNull((entry) => newUris.contains(entry.uri));
|
||||||
if (targetEntry != null) {
|
if (targetEntry != null) {
|
||||||
highlightInfo.trackItem(targetEntry, highlightItem: targetEntry);
|
highlightInfo.trackItem(targetEntry, highlightItem: targetEntry);
|
||||||
|
@ -244,8 +255,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
if (movedCount < selectionCount) {
|
if (exportCount < selectionCount) {
|
||||||
final count = selectionCount - movedCount;
|
final count = selectionCount - exportCount;
|
||||||
showFeedback(
|
showFeedback(
|
||||||
context,
|
context,
|
||||||
context.l10n.collectionExportFailureFeedback(count),
|
context.l10n.collectionExportFailureFeedback(count),
|
||||||
|
|
Loading…
Reference in a new issue