#160 export: fixed svg, added size parameter
This commit is contained in:
parent
f7183156bf
commit
e548134d30
12 changed files with 176 additions and 40 deletions
|
@ -13,7 +13,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.signature.ObjectKey
|
||||
import deckers.thibault.aves.decoder.MultiTrackImage
|
||||
import deckers.thibault.aves.decoder.SvgThumbnail
|
||||
import deckers.thibault.aves.decoder.SvgImage
|
||||
import deckers.thibault.aves.decoder.TiffImage
|
||||
import deckers.thibault.aves.decoder.VideoThumbnail
|
||||
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
|
||||
|
@ -128,7 +128,7 @@ class ThumbnailFetcher internal constructor(
|
|||
.submit(width, height)
|
||||
} else {
|
||||
val model: Any = when {
|
||||
svgFetch -> SvgThumbnail(context, uri)
|
||||
svgFetch -> SvgImage(context, uri)
|
||||
tiffFetch -> TiffImage(context, uri, pageId)
|
||||
multiTrackFetch -> MultiTrackImage(context, uri, pageId)
|
||||
else -> StorageUtils.getGlideSafeUri(uri, mimeType)
|
||||
|
|
|
@ -134,8 +134,10 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
|
||||
var destinationDir = arguments["destinationPath"] as String?
|
||||
val mimeType = arguments["mimeType"] as String?
|
||||
val width = arguments["width"] as Int?
|
||||
val height = arguments["height"] as Int?
|
||||
val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?)
|
||||
if (destinationDir == null || mimeType == null || nameConflictStrategy == null) {
|
||||
if (destinationDir == null || mimeType == null || width == null || height == null || nameConflictStrategy == null) {
|
||||
error("export-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
@ -150,7 +152,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
|
||||
destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir)
|
||||
val entries = entryMapList.map(::AvesEntry)
|
||||
provider.exportMultiple(activity, mimeType, destinationDir, entries, nameConflictStrategy, object : ImageOpCallback {
|
||||
provider.exportMultiple(activity, mimeType, destinationDir, entries, 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)
|
||||
})
|
||||
|
|
|
@ -25,27 +25,27 @@ import kotlin.math.ceil
|
|||
@GlideModule
|
||||
class SvgGlideModule : LibraryGlideModule() {
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
registry.append(SvgThumbnail::class.java, Bitmap::class.java, SvgLoader.Factory())
|
||||
registry.append(SvgImage::class.java, Bitmap::class.java, SvgLoader.Factory())
|
||||
}
|
||||
}
|
||||
|
||||
class SvgThumbnail(val context: Context, val uri: Uri)
|
||||
class SvgImage(val context: Context, val uri: Uri)
|
||||
|
||||
internal class SvgLoader : ModelLoader<SvgThumbnail, Bitmap> {
|
||||
override fun buildLoadData(model: SvgThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
|
||||
internal class SvgLoader : ModelLoader<SvgImage, Bitmap> {
|
||||
override fun buildLoadData(model: SvgImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
|
||||
return ModelLoader.LoadData(ObjectKey(model.uri), SvgFetcher(model, width, height))
|
||||
}
|
||||
|
||||
override fun handles(model: SvgThumbnail): Boolean = true
|
||||
override fun handles(model: SvgImage): Boolean = true
|
||||
|
||||
internal class Factory : ModelLoaderFactory<SvgThumbnail, Bitmap> {
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<SvgThumbnail, Bitmap> = SvgLoader()
|
||||
internal class Factory : ModelLoaderFactory<SvgImage, Bitmap> {
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<SvgImage, Bitmap> = SvgLoader()
|
||||
|
||||
override fun teardown() {}
|
||||
}
|
||||
}
|
||||
|
||||
internal class SvgFetcher(val model: SvgThumbnail, val width: Int, val height: Int) : DataFetcher<Bitmap> {
|
||||
internal class SvgFetcher(val model: SvgImage, val width: Int, val height: Int) : DataFetcher<Bitmap> {
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in Bitmap>) {
|
||||
val context = model.context
|
||||
val uri = model.uri
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.commonsware.cwac.document.DocumentFileCompat
|
||||
import deckers.thibault.aves.decoder.MultiTrackImage
|
||||
import deckers.thibault.aves.decoder.SvgImage
|
||||
import deckers.thibault.aves.decoder.TiffImage
|
||||
import deckers.thibault.aves.metadata.*
|
||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
|
||||
|
@ -82,6 +83,8 @@ abstract class ImageProvider {
|
|||
imageExportMimeType: String,
|
||||
targetDir: String,
|
||||
entries: List<AvesEntry>,
|
||||
width: Int,
|
||||
height: Int,
|
||||
nameConflictStrategy: NameConflictStrategy,
|
||||
callback: ImageOpCallback,
|
||||
) {
|
||||
|
@ -120,6 +123,8 @@ abstract class ImageProvider {
|
|||
sourceEntry = entry,
|
||||
targetDir = targetDir,
|
||||
targetDirDocFile = targetDirDocFile,
|
||||
width = width,
|
||||
height = height,
|
||||
nameConflictStrategy = nameConflictStrategy,
|
||||
exportMimeType = exportMimeType,
|
||||
)
|
||||
|
@ -138,6 +143,8 @@ abstract class ImageProvider {
|
|||
sourceEntry: AvesEntry,
|
||||
targetDir: String,
|
||||
targetDirDocFile: DocumentFileCompat,
|
||||
width: Int,
|
||||
height: Int,
|
||||
nameConflictStrategy: NameConflictStrategy,
|
||||
exportMimeType: String,
|
||||
): FieldMap {
|
||||
|
@ -178,6 +185,8 @@ abstract class ImageProvider {
|
|||
MultiTrackImage(activity, sourceUri, pageId)
|
||||
} else if (sourceMimeType == MimeTypes.TIFF) {
|
||||
TiffImage(activity, sourceUri, pageId)
|
||||
} else if (sourceMimeType == MimeTypes.SVG) {
|
||||
SvgImage(activity, sourceUri)
|
||||
} else {
|
||||
StorageUtils.getGlideSafeUri(sourceUri, sourceMimeType)
|
||||
}
|
||||
|
@ -192,7 +201,7 @@ abstract class ImageProvider {
|
|||
.asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(model)
|
||||
.submit()
|
||||
.submit(width, height)
|
||||
try {
|
||||
var bitmap = target.get()
|
||||
if (MimeTypes.needRotationAfterGlide(sourceMimeType)) {
|
||||
|
|
|
@ -291,6 +291,8 @@
|
|||
},
|
||||
|
||||
"exportEntryDialogFormat": "Format:",
|
||||
"exportEntryDialogWidth": "Width",
|
||||
"exportEntryDialogHeight": "Height",
|
||||
|
||||
"renameEntryDialogLabel": "New name",
|
||||
|
||||
|
|
|
@ -179,6 +179,8 @@
|
|||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Voulez-vous vraiment supprimer ces albums et leur élément ?} other{Voulez-vous vraiment supprimer ces albums et leurs {count} éléments ?}}",
|
||||
|
||||
"exportEntryDialogFormat": "Format :",
|
||||
"exportEntryDialogWidth": "Largeur",
|
||||
"exportEntryDialogHeight": "Hauteur",
|
||||
|
||||
"renameEntryDialogLabel": "Nouveau nom",
|
||||
|
||||
|
|
|
@ -179,6 +179,8 @@
|
|||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범들의 항목 {count}개를 삭제하시겠습니까?}}",
|
||||
|
||||
"exportEntryDialogFormat": "형식:",
|
||||
"exportEntryDialogWidth": "가로",
|
||||
"exportEntryDialogHeight": "세로",
|
||||
|
||||
"renameEntryDialogLabel": "이름",
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:aves/services/common/output_buffer.dart';
|
|||
import 'package:aves/services/common/service_policy.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/services/media/enums.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:streams_channel/streams_channel.dart';
|
||||
|
@ -87,7 +88,7 @@ abstract class MediaFileService {
|
|||
|
||||
Stream<ExportOpEvent> export(
|
||||
Iterable<AvesEntry> entries, {
|
||||
required String mimeType,
|
||||
required EntryExportOptions options,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
});
|
||||
|
@ -368,7 +369,7 @@ class PlatformMediaFileService implements MediaFileService {
|
|||
@override
|
||||
Stream<ExportOpEvent> export(
|
||||
Iterable<AvesEntry> entries, {
|
||||
required String mimeType,
|
||||
required EntryExportOptions options,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
}) {
|
||||
|
@ -377,7 +378,9 @@ class PlatformMediaFileService implements MediaFileService {
|
|||
.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'export',
|
||||
'entries': entries.map(_toPlatformEntryMap).toList(),
|
||||
'mimeType': mimeType,
|
||||
'mimeType': options.mimeType,
|
||||
'width': options.width,
|
||||
'height': options.height,
|
||||
'destinationPath': destinationAlbum,
|
||||
'nameConflictStrategy': nameConflictStrategy.toPlatform(),
|
||||
})
|
||||
|
@ -434,3 +437,18 @@ class PlatformMediaFileService implements MediaFileService {
|
|||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class EntryExportOptions extends Equatable {
|
||||
final String mimeType;
|
||||
final int width, height;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [mimeType, width, height];
|
||||
|
||||
const EntryExportOptions({
|
||||
required this.mimeType,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -293,7 +293,10 @@ class _GeoMapState extends State<GeoMap> {
|
|||
// node size: 64 by default, higher means faster indexing but slower search
|
||||
nodeSize: nodeSize,
|
||||
points: markers,
|
||||
createCluster: GeoEntry.createCluster,
|
||||
// use lambda instead of tear-off because of runtime exception when using
|
||||
// `T Function(BaseCluster, double, double)` for `T Function(BaseCluster?, double?, double?)`
|
||||
// ignore: unnecessary_lambdas
|
||||
createCluster: (base, lng, lat) => GeoEntry.createCluster(base, lng, lat),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/services/media/media_file_service.dart';
|
||||
import 'package:aves/utils/mime_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -19,6 +20,8 @@ class ExportEntryDialog extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ExportEntryDialogState extends State<ExportEntryDialog> {
|
||||
final TextEditingController _widthController = TextEditingController(), _heightController = TextEditingController();
|
||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||
String _mimeType = MimeTypes.jpeg;
|
||||
|
||||
AvesEntry get entry => widget.entry;
|
||||
|
@ -30,13 +33,33 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
|
|||
MimeTypes.webp,
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_widthController.text = '${entry.isRotated ? entry.height : entry.width}';
|
||||
_heightController.text = '${entry.isRotated ? entry.width : entry.height}';
|
||||
_validate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_widthController.dispose();
|
||||
_heightController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return AvesDialog(
|
||||
content: Row(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(context.l10n.exportEntryDialogFormat),
|
||||
Text(l10n.exportEntryDialogFormat),
|
||||
const SizedBox(width: AvesDialog.controlCaptionPadding),
|
||||
DropdownButton<String>(
|
||||
items: imageExportFormats.map((mimeType) {
|
||||
|
@ -54,16 +77,73 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
|
|||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _widthController,
|
||||
decoration: InputDecoration(labelText: l10n.exportEntryDialogWidth),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
final width = int.tryParse(value);
|
||||
_heightController.text = width != null ? '${(width / entry.displayAspectRatio).round()}' : '';
|
||||
_validate();
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text(AvesEntry.resolutionSeparator),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _heightController,
|
||||
decoration: InputDecoration(labelText: l10n.exportEntryDialogHeight),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
final height = int.tryParse(value);
|
||||
_widthController.text = height != null ? '${(height * entry.displayAspectRatio).round()}' : '';
|
||||
_validate();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, _mimeType),
|
||||
child: Text(context.l10n.applyButtonLabel),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _isValidNotifier,
|
||||
builder: (context, isValid, child) {
|
||||
return TextButton(
|
||||
onPressed: isValid
|
||||
? () {
|
||||
final width = int.tryParse(_widthController.text);
|
||||
final height = int.tryParse(_heightController.text);
|
||||
final options = (width != null && height != null)
|
||||
? EntryExportOptions(
|
||||
mimeType: _mimeType,
|
||||
width: width,
|
||||
height: height,
|
||||
)
|
||||
: null;
|
||||
Navigator.pop(context, options);
|
||||
}
|
||||
: null,
|
||||
child: Text(l10n.applyButtonLabel),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _validate() async {
|
||||
final width = int.tryParse(_widthController.text);
|
||||
final height = int.tryParse(_heightController.text);
|
||||
_isValidNotifier.value = (width ?? 0) > 0 && (height ?? 0) > 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,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_file_service.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||
|
@ -203,11 +204,11 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
|
||||
if (!await checkFreeSpaceForMove(context, {entry}, destinationAlbum, MoveType.export)) return;
|
||||
|
||||
final mimeType = await showDialog<String>(
|
||||
final options = await showDialog<EntryExportOptions>(
|
||||
context: context,
|
||||
builder: (context) => ExportEntryDialog(entry: entry),
|
||||
);
|
||||
if (mimeType == null) return;
|
||||
if (options == null) return;
|
||||
|
||||
final selection = <AvesEntry>{};
|
||||
if (entry.isMultiPage) {
|
||||
|
@ -231,7 +232,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
// TODO TLAD [SVG] export separately from raster images (sending bytes, like frame captures)
|
||||
opStream: mediaFileService.export(
|
||||
selection,
|
||||
mimeType: mimeType,
|
||||
options: options,
|
||||
destinationAlbum: destinationAlbum,
|
||||
nameConflictStrategy: NameConflictStrategy.rename,
|
||||
),
|
||||
|
|
|
@ -1,5 +1,22 @@
|
|||
{
|
||||
"de": [
|
||||
"exportEntryDialogWidth",
|
||||
"exportEntryDialogHeight"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"exportEntryDialogWidth",
|
||||
"exportEntryDialogHeight"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"exportEntryDialogWidth",
|
||||
"exportEntryDialogHeight"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"exportEntryDialogWidth",
|
||||
"exportEntryDialogHeight",
|
||||
"appExportCovers",
|
||||
"settingsThumbnailShowFavouriteIcon"
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue