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,
|
||||
) {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -344,6 +344,9 @@
|
|||
}
|
||||
},
|
||||
|
||||
"exportEntryDialogFormat": "Format:",
|
||||
"@exportEntryDialogFormat": {},
|
||||
|
||||
"renameEntryDialogLabel": "New name",
|
||||
"@renameEntryDialogLabel": {},
|
||||
|
||||
|
|
|
@ -156,6 +156,8 @@
|
|||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범의 항목 {count}개를 삭제하시겠습니까?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범들의 항목 {count}개를 삭제하시겠습니까?}}",
|
||||
|
||||
"exportEntryDialogFormat": "형식:",
|
||||
|
||||
"renameEntryDialogLabel": "이름",
|
||||
|
||||
"editEntryDateDialogTitle": "날짜 및 시간",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
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/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),
|
||||
|
|
Loading…
Reference in a new issue