export: format selection, clean up failed export

This commit is contained in:
Thibault Deckers 2021-10-09 19:07:09 +09:00
parent 2f2a9293bd
commit b84fde14af
8 changed files with 107 additions and 11 deletions

View file

@ -67,7 +67,7 @@ abstract class ImageProvider {
callback: ImageOpCallback,
) {
if (!supportedExportMimeTypes.contains(imageExportMimeType)) {
throw Exception("unsupported export MIME type=$imageExportMimeType")
callback.onFailure(Exception("unsupported export MIME type=$imageExportMimeType"))
}
val destinationDirDocFile = createDirectoryIfAbsent(activity, destinationDir)
@ -198,6 +198,12 @@ abstract class ImageProvider {
bitmap.compress(format, quality, output)
}
}
} catch (e: Exception) {
// remove empty file
if (destinationDocFile.exists()) {
destinationDocFile.delete()
}
throw e
} finally {
Glide.with(activity).clear(target)
}

View file

@ -344,6 +344,9 @@
}
},
"exportEntryDialogFormat": "Format:",
"@exportEntryDialogFormat": {},
"renameEntryDialogLabel": "New name",
"@renameEntryDialogLabel": {},

View file

@ -156,6 +156,8 @@
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범의 항목 {count}개를 삭제하시겠습니까?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범들의 항목 {count}개를 삭제하시겠습니까?}}",
"exportEntryDialogFormat": "형식:",
"renameEntryDialogLabel": "이름",
"editEntryDateDialogTitle": "날짜 및 시간",

View file

@ -254,6 +254,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
Future<void> refresh();
Future<Set<String>> refreshUris(Set<String> changedUris);
Future<void> rescan(Set<AvesEntry> entries);
Future<void> refreshMetadata(Set<AvesEntry> entries) async {

View file

@ -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
// 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`
@override
Future<Set<String>> refreshUris(Set<String> changedUris) async {
if (!_initialized || !isMonitoring) return changedUris;

View file

@ -5,8 +5,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class AvesDialog extends AlertDialog {
static const contentHorizontalPadding = EdgeInsets.symmetric(horizontal: 24);
static const borderWidth = 1.0;
static const EdgeInsets contentHorizontalPadding = EdgeInsets.symmetric(horizontal: 24);
static const double controlCaptionPadding = 16;
static const double borderWidth = 1.0;
AvesDialog({
Key? key,

View 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),
)
],
);
}
}

View file

@ -9,7 +9,6 @@ import 'package:aves/model/filters/album.dart';
import 'package:aves/model/highlight.dart';
import 'package:aves/model/source/collection_lens.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/services.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/extensions/build_context.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/filter_grids/album_pick.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;
final mimeType = await showDialog<String>(
context: context,
builder: (context) => ExportEntryDialog(entry: entry),
);
if (mimeType == null) return;
final selection = <AvesEntry>{};
if (entry.isMultiPage) {
final multiPageInfo = await entry.getMultiPageInfo();
@ -201,21 +207,26 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
}
final selectionCount = selection.length;
source.pauseMonitoring();
showOpReport<ExportOpEvent>(
context: context,
// TODO TLAD [SVG] export separately from raster images (sending bytes, like frame captures)
opStream: mediaFileService.export(
selection,
mimeType: MimeTypes.jpeg,
mimeType: mimeType,
destinationAlbum: destinationAlbum,
nameConflictStrategy: NameConflictStrategy.rename,
),
itemCount: selectionCount,
onDone: (processed) {
final movedOps = processed.where((e) => e.success);
final movedCount = movedOps.length;
final exportOps = processed.where((e) => e.success);
final exportCount = exportOps.length;
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(
label: context.l10n.showButtonLabel,
onPressed: () async {
@ -236,7 +247,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
));
final delayDuration = context.read<DurationsData>().staggeredAnimationPageTarget;
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));
if (targetEntry != null) {
highlightInfo.trackItem(targetEntry, highlightItem: targetEntry);
@ -244,8 +255,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
},
)
: null;
if (movedCount < selectionCount) {
final count = selectionCount - movedCount;
if (exportCount < selectionCount) {
final count = selectionCount - exportCount;
showFeedback(
context,
context.l10n.collectionExportFailureFeedback(count),