Add pref to control whether to save to server/device

This commit is contained in:
Ming Ming 2022-09-10 15:12:30 +08:00
parent 3df8fbd7ca
commit eda68ff55c
7 changed files with 79 additions and 20 deletions

View file

@ -249,6 +249,15 @@ class Pref {
Future<bool> setMemoriesRange(int value) => _set<int>(PrefKey.memoriesRange, Future<bool> setMemoriesRange(int value) => _set<int>(PrefKey.memoriesRange,
value, (key, value) => provider.setInt(key, value)); value, (key, value) => provider.setInt(key, value));
bool? isSaveEditResultToServer() =>
provider.getBool(PrefKey.saveEditResultToServer);
bool isSaveEditResultToServerOr([bool def = true]) =>
isSaveEditResultToServer() ?? def;
Future<bool> setSaveEditResultToServer(bool value) => _set<bool>(
PrefKey.saveEditResultToServer,
value,
(key, value) => provider.setBool(key, value));
Future<bool> _set<T>(PrefKey key, T value, Future<bool> _set<T>(PrefKey key, T value,
Future<bool> Function(PrefKey key, T value) setFn) async { Future<bool> Function(PrefKey key, T value) setFn) async {
if (await setFn(key, value)) { if (await setFn(key, value)) {
@ -561,6 +570,7 @@ enum PrefKey {
shouldProcessExifWifiOnly, shouldProcessExifWifiOnly,
doubleTapExit, doubleTapExit,
memoriesRange, memoriesRange,
saveEditResultToServer,
// account pref // account pref
isEnableFaceRecognitionApp, isEnableFaceRecognitionApp,
@ -632,6 +642,8 @@ extension on PrefKey {
return "doubleTapExit"; return "doubleTapExit";
case PrefKey.memoriesRange: case PrefKey.memoriesRange:
return "memoriesRange"; return "memoriesRange";
case PrefKey.saveEditResultToServer:
return "saveEditResultToServer";
// account pref // account pref
case PrefKey.isEnableFaceRecognitionApp: case PrefKey.isEnableFaceRecognitionApp:

View file

@ -31,6 +31,7 @@ class EnhanceHandler {
const EnhanceHandler({ const EnhanceHandler({
required this.account, required this.account,
required this.file, required this.file,
required this.isSaveToServer,
}); });
static bool isSupportedFormat(File file) => static bool isSupportedFormat(File file) =>
@ -67,6 +68,7 @@ class EnhanceHandler {
headers: { headers: {
"Authorization": Api.getAuthorizationHeaderValue(account), "Authorization": Api.getAuthorizationHeaderValue(account),
}, },
isSaveToServer: isSaveToServer,
); );
break; break;
@ -80,6 +82,7 @@ class EnhanceHandler {
headers: { headers: {
"Authorization": Api.getAuthorizationHeaderValue(account), "Authorization": Api.getAuthorizationHeaderValue(account),
}, },
isSaveToServer: isSaveToServer,
); );
break; break;
@ -92,6 +95,7 @@ class EnhanceHandler {
headers: { headers: {
"Authorization": Api.getAuthorizationHeaderValue(account), "Authorization": Api.getAuthorizationHeaderValue(account),
}, },
isSaveToServer: isSaveToServer,
); );
break; break;
@ -108,6 +112,7 @@ class EnhanceHandler {
headers: { headers: {
"Authorization": Api.getAuthorizationHeaderValue(account), "Authorization": Api.getAuthorizationHeaderValue(account),
}, },
isSaveToServer: isSaveToServer,
); );
break; break;
} }
@ -353,6 +358,7 @@ class EnhanceHandler {
final Account account; final Account account;
final File file; final File file;
final bool isSaveToServer;
static final _log = Logger("widget.handler.enhance_handler.EnhanceHandler"); static final _log = Logger("widget.handler.enhance_handler.EnhanceHandler");
} }

View file

@ -2,11 +2,13 @@ import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kiwi/kiwi.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/cache_manager_util.dart'; import 'package:nc_photos/cache_manager_util.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/help_utils.dart' as help_util; import 'package:nc_photos/help_utils.dart' as help_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
@ -258,6 +260,7 @@ class _ImageEditorState extends State<ImageEditor> {
} }
Future<void> _onSavePressed(BuildContext context) async { Future<void> _onSavePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
await ImageProcessor.filter( await ImageProcessor.filter(
"${widget.account.url}/${widget.file.path}", "${widget.account.url}/${widget.file.path}",
widget.file.filename, widget.file.filename,
@ -267,6 +270,7 @@ class _ImageEditorState extends State<ImageEditor> {
headers: { headers: {
"Authorization": Api.getAuthorizationHeaderValue(widget.account), "Authorization": Api.getAuthorizationHeaderValue(widget.account),
}, },
isSaveToServer: c.pref.isSaveEditResultToServerOr(),
); );
Navigator.of(context).pop(); Navigator.of(context).pop();
} }

View file

@ -602,11 +602,13 @@ class _ViewerState extends State<Viewer>
_log.shout("[_onEnhancePressed] Video file not supported"); _log.shout("[_onEnhancePressed] Video file not supported");
return; return;
} }
final c = KiwiContainer().resolve<DiContainer>();
_log.info("[_onEnhancePressed] Enhance file: ${file.path}"); _log.info("[_onEnhancePressed] Enhance file: ${file.path}");
EnhanceHandler( EnhanceHandler(
account: widget.account, account: widget.account,
file: file, file: file,
isSaveToServer: c.pref.isSaveEditResultToServerOr(),
)(context); )(context);
} }

View file

@ -28,6 +28,7 @@ class ImageProcessorChannelHandler(context: Context) :
call.argument("filename")!!, call.argument("filename")!!,
call.argument("maxWidth")!!, call.argument("maxWidth")!!,
call.argument("maxHeight")!!, call.argument("maxHeight")!!,
call.argument<Boolean>("isSaveToServer")!!,
call.argument("iteration")!!, call.argument("iteration")!!,
result result
) )
@ -45,6 +46,7 @@ class ImageProcessorChannelHandler(context: Context) :
call.argument("filename")!!, call.argument("filename")!!,
call.argument("maxWidth")!!, call.argument("maxWidth")!!,
call.argument("maxHeight")!!, call.argument("maxHeight")!!,
call.argument<Boolean>("isSaveToServer")!!,
call.argument("radius")!!, call.argument("radius")!!,
result result
) )
@ -62,6 +64,7 @@ class ImageProcessorChannelHandler(context: Context) :
call.argument("filename")!!, call.argument("filename")!!,
call.argument("maxWidth")!!, call.argument("maxWidth")!!,
call.argument("maxHeight")!!, call.argument("maxHeight")!!,
call.argument<Boolean>("isSaveToServer")!!,
result result
) )
} catch (e: Throwable) { } catch (e: Throwable) {
@ -78,6 +81,7 @@ class ImageProcessorChannelHandler(context: Context) :
call.argument("filename")!!, call.argument("filename")!!,
call.argument("maxWidth")!!, call.argument("maxWidth")!!,
call.argument("maxHeight")!!, call.argument("maxHeight")!!,
call.argument<Boolean>("isSaveToServer")!!,
call.argument("styleUri")!!, call.argument("styleUri")!!,
call.argument("weight")!!, call.argument("weight")!!,
result result
@ -96,6 +100,7 @@ class ImageProcessorChannelHandler(context: Context) :
call.argument("filename")!!, call.argument("filename")!!,
call.argument("maxWidth")!!, call.argument("maxWidth")!!,
call.argument("maxHeight")!!, call.argument("maxHeight")!!,
call.argument<Boolean>("isSaveToServer")!!,
call.argument("filters")!!, call.argument("filters")!!,
result result
) )
@ -132,10 +137,10 @@ class ImageProcessorChannelHandler(context: Context) :
private fun zeroDce( private fun zeroDce(
fileUrl: String, headers: Map<String, String>?, filename: String, fileUrl: String, headers: Map<String, String>?, filename: String,
maxWidth: Int, maxHeight: Int, iteration: Int, maxWidth: Int, maxHeight: Int, isSaveToServer: Boolean, iteration: Int,
result: MethodChannel.Result result: MethodChannel.Result
) = method( ) = method(
fileUrl, headers, filename, maxWidth, maxHeight, fileUrl, headers, filename, maxWidth, maxHeight, isSaveToServer,
ImageProcessorService.METHOD_ZERO_DCE, result, onIntent = { ImageProcessorService.METHOD_ZERO_DCE, result, onIntent = {
it.putExtra(ImageProcessorService.EXTRA_ITERATION, iteration) it.putExtra(ImageProcessorService.EXTRA_ITERATION, iteration)
} }
@ -143,9 +148,10 @@ class ImageProcessorChannelHandler(context: Context) :
private fun deepLab3Portrait( private fun deepLab3Portrait(
fileUrl: String, headers: Map<String, String>?, filename: String, fileUrl: String, headers: Map<String, String>?, filename: String,
maxWidth: Int, maxHeight: Int, radius: Int, result: MethodChannel.Result maxWidth: Int, maxHeight: Int, isSaveToServer: Boolean, radius: Int,
result: MethodChannel.Result
) = method( ) = method(
fileUrl, headers, filename, maxWidth, maxHeight, fileUrl, headers, filename, maxWidth, maxHeight, isSaveToServer,
ImageProcessorService.METHOD_DEEP_LAP_PORTRAIT, result, onIntent = { ImageProcessorService.METHOD_DEEP_LAP_PORTRAIT, result, onIntent = {
it.putExtra(ImageProcessorService.EXTRA_RADIUS, radius) it.putExtra(ImageProcessorService.EXTRA_RADIUS, radius)
} }
@ -153,18 +159,19 @@ class ImageProcessorChannelHandler(context: Context) :
private fun esrgan( private fun esrgan(
fileUrl: String, headers: Map<String, String>?, filename: String, fileUrl: String, headers: Map<String, String>?, filename: String,
maxWidth: Int, maxHeight: Int, result: MethodChannel.Result maxWidth: Int, maxHeight: Int, isSaveToServer: Boolean,
result: MethodChannel.Result
) = method( ) = method(
fileUrl, headers, filename, maxWidth, maxHeight, fileUrl, headers, filename, maxWidth, maxHeight, isSaveToServer,
ImageProcessorService.METHOD_ESRGAN, result ImageProcessorService.METHOD_ESRGAN, result
) )
private fun arbitraryStyleTransfer( private fun arbitraryStyleTransfer(
fileUrl: String, headers: Map<String, String>?, filename: String, fileUrl: String, headers: Map<String, String>?, filename: String,
maxWidth: Int, maxHeight: Int, styleUri: String, weight: Float, maxWidth: Int, maxHeight: Int, isSaveToServer: Boolean,
result: MethodChannel.Result styleUri: String, weight: Float, result: MethodChannel.Result
) = method( ) = method(
fileUrl, headers, filename, maxWidth, maxHeight, fileUrl, headers, filename, maxWidth, maxHeight, isSaveToServer,
ImageProcessorService.METHOD_ARBITRARY_STYLE_TRANSFER, result, ImageProcessorService.METHOD_ARBITRARY_STYLE_TRANSFER, result,
onIntent = { onIntent = {
it.putExtra( it.putExtra(
@ -176,14 +183,14 @@ class ImageProcessorChannelHandler(context: Context) :
private fun filter( private fun filter(
fileUrl: String, headers: Map<String, String>?, filename: String, fileUrl: String, headers: Map<String, String>?, filename: String,
maxWidth: Int, maxHeight: Int, filters: List<Map<String, Any>>, maxWidth: Int, maxHeight: Int, isSaveToServer: Boolean,
result: MethodChannel.Result filters: List<Map<String, Any>>, result: MethodChannel.Result
) { ) {
// convert to serializable // convert to serializable
val l = arrayListOf<Serializable>() val l = arrayListOf<Serializable>()
filters.mapTo(l, { HashMap(it) }) filters.mapTo(l, { HashMap(it) })
method( method(
fileUrl, headers, filename, maxWidth, maxHeight, fileUrl, headers, filename, maxWidth, maxHeight, isSaveToServer,
ImageProcessorService.METHOD_FILTER, result, ImageProcessorService.METHOD_FILTER, result,
onIntent = { onIntent = {
it.putExtra(ImageProcessorService.EXTRA_FILTERS, l) it.putExtra(ImageProcessorService.EXTRA_FILTERS, l)
@ -204,7 +211,7 @@ class ImageProcessorChannelHandler(context: Context) :
private fun method( private fun method(
fileUrl: String, headers: Map<String, String>?, filename: String, fileUrl: String, headers: Map<String, String>?, filename: String,
maxWidth: Int, maxHeight: Int, method: String, maxWidth: Int, maxHeight: Int, isSaveToServer: Boolean, method: String,
result: MethodChannel.Result, onIntent: ((Intent) -> Unit)? = null result: MethodChannel.Result, onIntent: ((Intent) -> Unit)? = null
) { ) {
val intent = Intent(context, ImageProcessorService::class.java).apply { val intent = Intent(context, ImageProcessorService::class.java).apply {
@ -216,6 +223,9 @@ class ImageProcessorChannelHandler(context: Context) :
putExtra(ImageProcessorService.EXTRA_FILENAME, filename) putExtra(ImageProcessorService.EXTRA_FILENAME, filename)
putExtra(ImageProcessorService.EXTRA_MAX_WIDTH, maxWidth) putExtra(ImageProcessorService.EXTRA_MAX_WIDTH, maxWidth)
putExtra(ImageProcessorService.EXTRA_MAX_HEIGHT, maxHeight) putExtra(ImageProcessorService.EXTRA_MAX_HEIGHT, maxHeight)
putExtra(
ImageProcessorService.EXTRA_IS_SAVE_TO_SERVER, isSaveToServer
)
onIntent?.invoke(this) onIntent?.invoke(this)
} }
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)

View file

@ -37,6 +37,7 @@ class ImageProcessorService : Service() {
const val EXTRA_FILENAME = "filename" const val EXTRA_FILENAME = "filename"
const val EXTRA_MAX_WIDTH = "maxWidth" const val EXTRA_MAX_WIDTH = "maxWidth"
const val EXTRA_MAX_HEIGHT = "maxHeight" const val EXTRA_MAX_HEIGHT = "maxHeight"
const val EXTRA_IS_SAVE_TO_SERVER = "isSaveToServer"
const val EXTRA_RADIUS = "radius" const val EXTRA_RADIUS = "radius"
const val EXTRA_ITERATION = "iteration" const val EXTRA_ITERATION = "iteration"
const val EXTRA_STYLE_URI = "styleUri" const val EXTRA_STYLE_URI = "styleUri"
@ -134,7 +135,7 @@ class ImageProcessorService : Service() {
// there are commands running in the bg // there are commands running in the bg
addCommand( addCommand(
ImageProcessorEnhanceCommand( ImageProcessorEnhanceCommand(
startId, "null", "", null, "", 0, 0 startId, "null", "", null, "", 0, 0, false
) )
) )
} }
@ -184,10 +185,11 @@ class ImageProcessorService : Service() {
val filename = extras.getString(EXTRA_FILENAME)!! val filename = extras.getString(EXTRA_FILENAME)!!
val maxWidth = extras.getInt(EXTRA_MAX_WIDTH) val maxWidth = extras.getInt(EXTRA_MAX_WIDTH)
val maxHeight = extras.getInt(EXTRA_MAX_HEIGHT) val maxHeight = extras.getInt(EXTRA_MAX_HEIGHT)
val isSaveToServer = extras.getBoolean(EXTRA_IS_SAVE_TO_SERVER)
addCommand( addCommand(
ImageProcessorFilterCommand( ImageProcessorFilterCommand(
startId, fileUrl, headers, filename, maxWidth, startId, fileUrl, headers, filename, maxWidth,
maxHeight, filters maxHeight, isSaveToServer, filters
) )
) )
} }
@ -211,10 +213,11 @@ class ImageProcessorService : Service() {
val filename = extras.getString(EXTRA_FILENAME)!! val filename = extras.getString(EXTRA_FILENAME)!!
val maxWidth = extras.getInt(EXTRA_MAX_WIDTH) val maxWidth = extras.getInt(EXTRA_MAX_WIDTH)
val maxHeight = extras.getInt(EXTRA_MAX_HEIGHT) val maxHeight = extras.getInt(EXTRA_MAX_HEIGHT)
val isSaveToServer = extras.getBoolean(EXTRA_IS_SAVE_TO_SERVER)
addCommand( addCommand(
ImageProcessorEnhanceCommand( ImageProcessorEnhanceCommand(
startId, method, fileUrl, headers, filename, maxWidth, startId, method, fileUrl, headers, filename, maxWidth,
maxHeight, args = args maxHeight, isSaveToServer, args = args
) )
) )
} }
@ -421,6 +424,7 @@ private abstract class ImageProcessorImageCommand(
val filename: String, val filename: String,
val maxWidth: Int, val maxWidth: Int,
val maxHeight: Int, val maxHeight: Int,
val isSaveToServer: Boolean,
) : ImageProcessorCommand { ) : ImageProcessorCommand {
abstract fun apply(context: Context, fileUri: Uri): Bitmap abstract fun apply(context: Context, fileUri: Uri): Bitmap
} }
@ -433,9 +437,11 @@ private class ImageProcessorEnhanceCommand(
filename: String, filename: String,
maxWidth: Int, maxWidth: Int,
maxHeight: Int, maxHeight: Int,
isSaveToServer: Boolean,
val args: Map<String, Any?> = mapOf(), val args: Map<String, Any?> = mapOf(),
) : ImageProcessorImageCommand( ) : ImageProcessorImageCommand(
startId, method, fileUrl, headers, filename, maxWidth, maxHeight startId, method, fileUrl, headers, filename, maxWidth, maxHeight,
isSaveToServer
) { ) {
override fun apply(context: Context, fileUri: Uri): Bitmap { override fun apply(context: Context, fileUri: Uri): Bitmap {
return when (method) { return when (method) {
@ -471,10 +477,11 @@ private class ImageProcessorFilterCommand(
filename: String, filename: String,
maxWidth: Int, maxWidth: Int,
maxHeight: Int, maxHeight: Int,
isSaveToServer: Boolean,
val filters: List<ImageFilter>, val filters: List<ImageFilter>,
) : ImageProcessorImageCommand( ) : ImageProcessorImageCommand(
startId, ImageProcessorService.METHOD_FILTER, fileUrl, headers, filename, startId, ImageProcessorService.METHOD_FILTER, fileUrl, headers, filename,
maxWidth, maxHeight maxWidth, maxHeight, isSaveToServer
) { ) {
override fun apply(context: Context, fileUri: Uri): Bitmap { override fun apply(context: Context, fileUri: Uri): Bitmap {
return ImageFilterProcessor( return ImageFilterProcessor(
@ -687,7 +694,7 @@ private open class ImageProcessorCommandTask(context: Context) :
oExif.saveAttributes() oExif.saveAttributes()
handleCancel() handleCancel()
val persister = EnhancedFileServerPersisterWithFallback(context) val persister = getPersister(cmd.isSaveToServer)
return persister.persist(cmd, outFile) return persister.persist(cmd, outFile)
} finally { } finally {
outFile.delete() outFile.delete()
@ -748,7 +755,7 @@ private open class ImageProcessorCommandTask(context: Context) :
logE(TAG, "[copyExif] Failed while saving EXIF", e) logE(TAG, "[copyExif] Failed while saving EXIF", e)
} }
val persister = EnhancedFileServerPersisterWithFallback(context) val persister = getPersister(cmd.isSaveToServer)
return persister.persist(cmd, outFile) return persister.persist(cmd, outFile)
} finally { } finally {
outFile.delete() outFile.delete()
@ -773,6 +780,14 @@ private open class ImageProcessorCommandTask(context: Context) :
} }
} }
private fun getPersister(isSaveToServer: Boolean): EnhancedFilePersister {
return if (isSaveToServer) {
EnhancedFileServerPersisterWithFallback(context)
} else {
EnhancedFileDevicePersister(context)
}
}
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
private val context = context private val context = context
} }

View file

@ -82,6 +82,7 @@ class ImageProcessor {
int maxHeight, int maxHeight,
int iteration, { int iteration, {
Map<String, String>? headers, Map<String, String>? headers,
required bool isSaveToServer,
}) => }) =>
_methodChannel.invokeMethod("zeroDce", <String, dynamic>{ _methodChannel.invokeMethod("zeroDce", <String, dynamic>{
"fileUrl": fileUrl, "fileUrl": fileUrl,
@ -90,6 +91,7 @@ class ImageProcessor {
"maxWidth": maxWidth, "maxWidth": maxWidth,
"maxHeight": maxHeight, "maxHeight": maxHeight,
"iteration": iteration, "iteration": iteration,
"isSaveToServer": isSaveToServer,
}); });
static Future<void> deepLab3Portrait( static Future<void> deepLab3Portrait(
@ -99,6 +101,7 @@ class ImageProcessor {
int maxHeight, int maxHeight,
int radius, { int radius, {
Map<String, String>? headers, Map<String, String>? headers,
required bool isSaveToServer,
}) => }) =>
_methodChannel.invokeMethod("deepLab3Portrait", <String, dynamic>{ _methodChannel.invokeMethod("deepLab3Portrait", <String, dynamic>{
"fileUrl": fileUrl, "fileUrl": fileUrl,
@ -107,6 +110,7 @@ class ImageProcessor {
"maxWidth": maxWidth, "maxWidth": maxWidth,
"maxHeight": maxHeight, "maxHeight": maxHeight,
"radius": radius, "radius": radius,
"isSaveToServer": isSaveToServer,
}); });
static Future<void> esrgan( static Future<void> esrgan(
@ -115,6 +119,7 @@ class ImageProcessor {
int maxWidth, int maxWidth,
int maxHeight, { int maxHeight, {
Map<String, String>? headers, Map<String, String>? headers,
required bool isSaveToServer,
}) => }) =>
_methodChannel.invokeMethod("esrgan", <String, dynamic>{ _methodChannel.invokeMethod("esrgan", <String, dynamic>{
"fileUrl": fileUrl, "fileUrl": fileUrl,
@ -122,6 +127,7 @@ class ImageProcessor {
"filename": filename, "filename": filename,
"maxWidth": maxWidth, "maxWidth": maxWidth,
"maxHeight": maxHeight, "maxHeight": maxHeight,
"isSaveToServer": isSaveToServer,
}); });
static Future<void> arbitraryStyleTransfer( static Future<void> arbitraryStyleTransfer(
@ -132,6 +138,7 @@ class ImageProcessor {
String styleUri, String styleUri,
double weight, { double weight, {
Map<String, String>? headers, Map<String, String>? headers,
required bool isSaveToServer,
}) => }) =>
_methodChannel.invokeMethod("arbitraryStyleTransfer", <String, dynamic>{ _methodChannel.invokeMethod("arbitraryStyleTransfer", <String, dynamic>{
"fileUrl": fileUrl, "fileUrl": fileUrl,
@ -141,6 +148,7 @@ class ImageProcessor {
"maxHeight": maxHeight, "maxHeight": maxHeight,
"styleUri": styleUri, "styleUri": styleUri,
"weight": weight, "weight": weight,
"isSaveToServer": isSaveToServer,
}); });
static Future<void> filter( static Future<void> filter(
@ -150,6 +158,7 @@ class ImageProcessor {
int maxHeight, int maxHeight,
List<ImageFilter> filters, { List<ImageFilter> filters, {
Map<String, String>? headers, Map<String, String>? headers,
required bool isSaveToServer,
}) => }) =>
_methodChannel.invokeMethod("filter", <String, dynamic>{ _methodChannel.invokeMethod("filter", <String, dynamic>{
"fileUrl": fileUrl, "fileUrl": fileUrl,
@ -158,6 +167,7 @@ class ImageProcessor {
"maxWidth": maxWidth, "maxWidth": maxWidth,
"maxHeight": maxHeight, "maxHeight": maxHeight,
"filters": filters.map((f) => f.toJson()).toList(), "filters": filters.map((f) => f.toJson()).toList(),
"isSaveToServer": isSaveToServer,
}); });
static Future<Rgba8Image> filterPreview( static Future<Rgba8Image> filterPreview(