Merge branch 'upload-edit-results-to-server' into dev

This commit is contained in:
Ming Ming 2022-09-10 15:23:27 +08:00
commit 51a2597ef4
18 changed files with 556 additions and 101 deletions

View file

@ -107,14 +107,20 @@ class MainActivity : FlutterActivity(), MethodChannel.MethodCallHandler,
logE(TAG, "Image result uri == null")
return null
}
return if (resultUri.scheme?.startsWith("http") == true) {
// remote uri
val encodedUrl = URLEncoder.encode(resultUri.toString(), "utf-8")
"/result-viewer?url=$encodedUrl"
} else {
val filename = UriUtil.resolveFilename(this, resultUri)?.let {
URLEncoder.encode(it, Charsets.UTF_8.toString())
}
return StringBuilder().apply {
StringBuilder().apply {
append("/enhanced-photo-browser?")
if (filename != null) append("filename=$filename")
}.toString()
}
}
private var _initialRoute: String? = null
private var _isNewGMapsRenderer = false

View file

@ -121,6 +121,7 @@ class ScanAccountDirBloc
_accountPrefUpdatedEventListener.begin();
_nativeFileExifUpdatedListener?.begin();
_imageProcessorUploadSuccessListener?.begin();
on<ScanAccountDirBlocEvent>(_onEvent, transformer: ((events, mapper) {
return events.asyncExpand(mapper).distinct((a, b) {
@ -164,6 +165,7 @@ class ScanAccountDirBloc
_accountPrefUpdatedEventListener.end();
_nativeFileExifUpdatedListener?.end();
_imageProcessorUploadSuccessListener?.end();
_refreshThrottler.clear();
return super.close();
@ -333,6 +335,14 @@ class ScanAccountDirBloc
);
}
void _onImageProcessorUploadSuccessEvent(
ImageProcessorUploadSuccessEvent ev) {
_refreshThrottler.trigger(
maxResponceTime: const Duration(seconds: 3),
maxPendingCount: 10,
);
}
/// Query a small amount of files to give an illusion of quick startup
Future<List<File>> _queryOfflineMini(ScanAccountDirBlocQueryBase ev) async {
return await ScanDirOfflineMini(_c)(
@ -540,6 +550,10 @@ class ScanAccountDirBloc
late final _nativeFileExifUpdatedListener = platform_k.isWeb
? null
: NativeEventListener<FileExifUpdatedEvent>(_onNativeFileExifUpdated);
late final _imageProcessorUploadSuccessListener = platform_k.isWeb
? null
: NativeEventListener<ImageProcessorUploadSuccessEvent>(
_onImageProcessorUploadSuccessEvent);
late final _refreshThrottler = Throttler(
onTriggered: (_) {

View file

@ -31,6 +31,9 @@ class NativeEventListener<T> {
case FileExifUpdatedEvent._id:
return FileExifUpdatedEvent.fromEvent(ev);
case ImageProcessorUploadSuccessEvent._id:
return ImageProcessorUploadSuccessEvent.fromEvent(ev);
default:
throw ArgumentError("Invalid event: ${ev.event}");
}
@ -63,3 +66,15 @@ class FileExifUpdatedEvent {
final List<int> fileIds;
}
class ImageProcessorUploadSuccessEvent {
const ImageProcessorUploadSuccessEvent();
factory ImageProcessorUploadSuccessEvent.fromEvent(NativeEventObject ev) {
assert(ev.event == _id);
assert(ev.data == null);
return const ImageProcessorUploadSuccessEvent();
}
static const _id = "ImageProcessorUploadSuccessEvent";
}

View file

@ -403,13 +403,19 @@
},
"settingsShowDateInAlbumTitle": "Group photos by date",
"settingsShowDateInAlbumDescription": "Apply only when the album is sorted by time",
"settingsPhotoEnhancementTitle": "Photo enhancement",
"settingsPhotoEnhancementPageTitle": "Photo enhancement settings",
"@settingsPhotoEnhancementPageTitle": {
"description": "Dedicated page for photo enhancement settings"
"settingsImageEditTitle": "Editor",
"@settingsImageEditTitle": {
"description": "Include settings for image enhancements and the image editor"
},
"settingsEnhanceMaxResolutionTitle": "Max output resolution",
"settingsImageEditDescription": "Customize image enhancements and the image editor",
"settingsEnhanceMaxResolutionTitle2": "Image resolution for enhancements",
"settingsEnhanceMaxResolutionDescription": "Photos larger than the selected resolution will be downscaled.\n\nHigh resolution photos require significantly more memory and time to process. Please lower this setting if the app crashed while enhancing your photos.",
"settingsImageEditSaveResultsToServerTitle": "Save results to server",
"@settingsImageEditSaveResultsToServerTitle": {
"description": "Whether to save the edit/enhance results to server instead of the current device"
},
"settingsImageEditSaveResultsToServerTrueDescription": "Results are saved to server, fallback to device storage if failed",
"settingsImageEditSaveResultsToServerFalseDescription": "Results are saved to this device",
"settingsThemeTitle": "Theme",
"settingsThemeDescription": "Customize the appearance of the app",
"settingsThemePageTitle": "Theme settings",

View file

@ -25,10 +25,13 @@
"settingsAlbumPageTitle",
"settingsShowDateInAlbumTitle",
"settingsShowDateInAlbumDescription",
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
"settingsEnhanceMaxResolutionDescription",
"settingsImageEditSaveResultsToServerTitle",
"settingsImageEditSaveResultsToServerTrueDescription",
"settingsImageEditSaveResultsToServerFalseDescription",
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsDoubleTapExitTitle",
@ -190,10 +193,13 @@
"settingsAlbumPageTitle",
"settingsShowDateInAlbumTitle",
"settingsShowDateInAlbumDescription",
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
"settingsEnhanceMaxResolutionDescription",
"settingsImageEditSaveResultsToServerTitle",
"settingsImageEditSaveResultsToServerTrueDescription",
"settingsImageEditSaveResultsToServerFalseDescription",
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsDoubleTapExitTitle",
@ -351,6 +357,12 @@
"settingsPhotosPageTitle",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
"settingsImageEditSaveResultsToServerTitle",
"settingsImageEditSaveResultsToServerTrueDescription",
"settingsImageEditSaveResultsToServerFalseDescription",
"settingsDoubleTapExitTitle",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
@ -406,6 +418,12 @@
"settingsPhotosPageTitle",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
"settingsImageEditSaveResultsToServerTitle",
"settingsImageEditSaveResultsToServerTrueDescription",
"settingsImageEditSaveResultsToServerFalseDescription",
"rootPickerSkipConfirmationDialogContent2",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
@ -429,6 +447,12 @@
"settingsPhotosPageTitle",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
"settingsImageEditSaveResultsToServerTitle",
"settingsImageEditSaveResultsToServerTrueDescription",
"settingsImageEditSaveResultsToServerFalseDescription",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
@ -456,10 +480,13 @@
"settingsPhotosPageTitle",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
"settingsEnhanceMaxResolutionDescription",
"settingsImageEditSaveResultsToServerTitle",
"settingsImageEditSaveResultsToServerTrueDescription",
"settingsImageEditSaveResultsToServerFalseDescription",
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsDoubleTapExitTitle",
@ -539,10 +566,13 @@
"settingsPhotosPageTitle",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
"settingsEnhanceMaxResolutionDescription",
"settingsImageEditSaveResultsToServerTitle",
"settingsImageEditSaveResultsToServerTrueDescription",
"settingsImageEditSaveResultsToServerFalseDescription",
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsDoubleTapExitTitle",
@ -640,10 +670,13 @@
"settingsPhotosPageTitle",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
"settingsEnhanceMaxResolutionDescription",
"settingsImageEditSaveResultsToServerTitle",
"settingsImageEditSaveResultsToServerTrueDescription",
"settingsImageEditSaveResultsToServerFalseDescription",
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsDoubleTapExitTitle",
@ -720,10 +753,13 @@
"settingsPhotosPageTitle",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
"settingsEnhanceMaxResolutionDescription",
"settingsImageEditSaveResultsToServerTitle",
"settingsImageEditSaveResultsToServerTrueDescription",
"settingsImageEditSaveResultsToServerFalseDescription",
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsDoubleTapExitTitle",
@ -800,10 +836,13 @@
"settingsPhotosPageTitle",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
"settingsEnhanceMaxResolutionDescription",
"settingsImageEditSaveResultsToServerTitle",
"settingsImageEditSaveResultsToServerTrueDescription",
"settingsImageEditSaveResultsToServerFalseDescription",
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsDoubleTapExitTitle",
@ -880,10 +919,13 @@
"settingsPhotosPageTitle",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
"settingsEnhanceMaxResolutionDescription",
"settingsImageEditSaveResultsToServerTitle",
"settingsImageEditSaveResultsToServerTrueDescription",
"settingsImageEditSaveResultsToServerFalseDescription",
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsDoubleTapExitTitle",

View file

@ -249,6 +249,15 @@ class Pref {
Future<bool> setMemoriesRange(int value) => _set<int>(PrefKey.memoriesRange,
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> Function(PrefKey key, T value) setFn) async {
if (await setFn(key, value)) {
@ -561,6 +570,7 @@ enum PrefKey {
shouldProcessExifWifiOnly,
doubleTapExit,
memoriesRange,
saveEditResultToServer,
// account pref
isEnableFaceRecognitionApp,
@ -632,6 +642,8 @@ extension on PrefKey {
return "doubleTapExit";
case PrefKey.memoriesRange:
return "memoriesRange";
case PrefKey.saveEditResultToServer:
return "saveEditResultToServer";
// account pref
case PrefKey.isEnableFaceRecognitionApp:

View file

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

View file

@ -22,6 +22,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/event/native_event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/language_util.dart' as language_util;
@ -78,11 +79,13 @@ class _HomePhotosState extends State<HomePhotos>
_initBloc();
_web?.onInitState();
_prefUpdatedListener.begin();
_imageProcessorUploadSuccessListener?.begin();
}
@override
dispose() {
_prefUpdatedListener.end();
_imageProcessorUploadSuccessListener?.end();
_web?.onDispose();
super.dispose();
}
@ -512,6 +515,13 @@ class _HomePhotosState extends State<HomePhotos>
}
}
void _onImageProcessorUploadSuccessEvent(
ImageProcessorUploadSuccessEvent ev) {
_log.info(
"[_onImageProcessorUploadSuccessEvent] Scheduling metadata task after next refresh");
_hasFiredMetadataTask.value = false;
}
void _tryStartMetadataTask({
bool ignoreFired = false,
}) {
@ -714,6 +724,10 @@ class _HomePhotosState extends State<HomePhotos>
late final _prefUpdatedListener =
AppEventListener<PrefUpdatedEvent>(_onPrefUpdated);
late final _imageProcessorUploadSuccessListener = platform_k.isWeb
? null
: NativeEventListener<ImageProcessorUploadSuccessEvent>(
_onImageProcessorUploadSuccessEvent);
late final _Web? _web = platform_k.isWeb ? _Web(this) : null;

View file

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

View file

@ -25,6 +25,7 @@ import 'package:nc_photos/widget/people_browser.dart';
import 'package:nc_photos/widget/person_browser.dart';
import 'package:nc_photos/widget/place_browser.dart';
import 'package:nc_photos/widget/places_browser.dart';
import 'package:nc_photos/widget/result_viewer.dart';
import 'package:nc_photos/widget/root_picker.dart';
import 'package:nc_photos/widget/settings.dart';
import 'package:nc_photos/widget/setup.dart';
@ -171,6 +172,7 @@ class _MyAppState extends State<MyApp>
route ??= _handlePeopleBrowserRoute(settings);
route ??= _handlePlaceBrowserRoute(settings);
route ??= _handlePlacesBrowserRoute(settings);
route ??= _handleResultViewerRoute(settings);
return route;
}
@ -600,6 +602,25 @@ class _MyAppState extends State<MyApp>
return null;
}
Route<dynamic>? _handleResultViewerRoute(RouteSettings settings) {
try {
if (settings.name == ResultViewer.routeName &&
settings.arguments != null) {
final args = settings.arguments as ResultViewerArguments;
return ResultViewer.buildRoute(args);
} else if (settings.name?.startsWith("${ResultViewer.routeName}?") ==
true) {
final queries = Uri.parse(settings.name!).queryParameters;
final args = ResultViewerArguments(queries["url"]!);
return ResultViewer.buildRoute(args);
}
} catch (e, stackTrace) {
_log.severe("[_handleResultViewerRoute] Failed while handling route", e,
stackTrace);
}
return null;
}
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
final _navigatorKey = GlobalKey<NavigatorState>();

View file

@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/pref.dart';
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/ls_single_file.dart';
import 'package:nc_photos/widget/viewer.dart';
class ResultViewerArguments {
const ResultViewerArguments(this.resultUrl);
final String resultUrl;
}
/// This is an intermediate widget in charge of preparing the file to be
/// eventually shown in [Viewer]
class ResultViewer extends StatefulWidget {
static const routeName = "/result-viewer";
const ResultViewer({
super.key,
required this.resultUrl,
});
ResultViewer.fromArgs(ResultViewerArguments args, {Key? key})
: this(
key: key,
resultUrl: args.resultUrl,
);
static Route buildRoute(ResultViewerArguments args) => MaterialPageRoute(
builder: (_) => ResultViewer.fromArgs(args),
);
@override
createState() => _ResultViewerState();
final String resultUrl;
}
class _ResultViewerState extends State<ResultViewer> {
@override
initState() {
super.initState();
_c = KiwiContainer().resolve<DiContainer>();
_doWork();
}
@override
build(BuildContext context) {
if (_file == null) {
return AppTheme(
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.black,
shadowColor: Colors.black,
foregroundColor: Colors.white.withOpacity(.87),
elevation: 0,
),
body: Container(
color: Colors.black,
alignment: Alignment.topCenter,
child: const LinearProgressIndicator(),
),
),
);
} else {
return Viewer(
account: _account!,
streamFiles: [_file!],
startIndex: 0,
);
}
}
Future<void> _doWork() async {
_log.info("[_doWork] URL: ${widget.resultUrl}");
_account = _c.pref.getCurrentAccount();
if (_account == null) {
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().errorUnauthenticated),
duration: k.snackBarDurationNormal,
));
Navigator.of(context).pop();
return;
}
if (!widget.resultUrl
.startsWith(RegExp(_account!.url, caseSensitive: false))) {
_log.severe("[_doWork] File url and current account does not match");
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().errorUnauthenticated),
duration: k.snackBarDurationNormal,
));
Navigator.of(context).pop();
return;
}
// +1 for the slash
final filePath = widget.resultUrl.substring(_account!.url.length + 1);
// query remote
final File file;
try {
file = await LsSingleFile(_c)(_account!, filePath);
} catch (e, stackTrace) {
_log.severe("[_doWork] Failed while LsSingleFile", e, stackTrace);
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(e)),
duration: k.snackBarDurationNormal,
));
Navigator.of(context).pop();
return;
}
setState(() {
_file = file;
});
}
late final DiContainer _c;
Account? _account;
File? _file;
static final _log = Logger("widget.result_viewer._ResultViewerState");
}

View file

@ -177,7 +177,8 @@ class _SettingsState extends State<Settings> {
Icons.auto_fix_high_outlined,
color: AppTheme.getUnfocusedIconColor(context),
),
label: L10n.global().settingsPhotoEnhancementTitle,
label: L10n.global().settingsImageEditTitle,
description: L10n.global().settingsImageEditDescription,
builder: () => const EnhancementSettings(),
),
_buildSubSettings(
@ -1293,6 +1294,7 @@ class _EnhancementSettingsState extends State<EnhancementSettings> {
super.initState();
_maxWidth = Pref().getEnhanceMaxWidthOr();
_maxHeight = Pref().getEnhanceMaxHeightOr();
_isSaveEditResultToServer = Pref().isSaveEditResultToServerOr();
}
@override
@ -1311,13 +1313,24 @@ class _EnhancementSettingsState extends State<EnhancementSettings> {
slivers: [
SliverAppBar(
pinned: true,
title: Text(L10n.global().settingsPhotoEnhancementPageTitle),
title: Text(L10n.global().settingsImageEditTitle),
),
SliverList(
delegate: SliverChildListDelegate(
[
SwitchListTile(
title: Text(
L10n.global().settingsImageEditSaveResultsToServerTitle),
subtitle: Text(_isSaveEditResultToServer
? L10n.global()
.settingsImageEditSaveResultsToServerTrueDescription
: L10n.global()
.settingsImageEditSaveResultsToServerFalseDescription),
value: _isSaveEditResultToServer,
onChanged: _onSaveEditResultToServerChanged,
),
ListTile(
title: Text(L10n.global().settingsEnhanceMaxResolutionTitle),
title: Text(L10n.global().settingsEnhanceMaxResolutionTitle2),
subtitle: Text("${_maxWidth}x$_maxHeight"),
onTap: () => _onMaxResolutionTap(context),
),
@ -1335,7 +1348,7 @@ class _EnhancementSettingsState extends State<EnhancementSettings> {
context: context,
builder: (_) => AppTheme(
child: AlertDialog(
title: Text(L10n.global().settingsEnhanceMaxResolutionTitle),
title: Text(L10n.global().settingsEnhanceMaxResolutionTitle2),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@ -1394,8 +1407,26 @@ class _EnhancementSettingsState extends State<EnhancementSettings> {
}
}
Future<void> _onSaveEditResultToServerChanged(bool value) async {
final oldValue = _isSaveEditResultToServer;
setState(() {
_isSaveEditResultToServer = value;
});
if (!await Pref().setSaveEditResultToServer(value)) {
_log.severe("[_onSaveEditResultToServerChanged] Failed writing pref");
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().writePreferenceFailureNotification),
duration: k.snackBarDurationNormal,
));
setState(() {
_isSaveEditResultToServer = oldValue;
});
}
}
late int _maxWidth;
late int _maxHeight;
late bool _isSaveEditResultToServer;
static final _log = Logger("widget.settings._EnhancementSettingsState");
}

View file

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

View file

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

View file

@ -37,6 +37,7 @@ class ImageProcessorService : Service() {
const val EXTRA_FILENAME = "filename"
const val EXTRA_MAX_WIDTH = "maxWidth"
const val EXTRA_MAX_HEIGHT = "maxHeight"
const val EXTRA_IS_SAVE_TO_SERVER = "isSaveToServer"
const val EXTRA_RADIUS = "radius"
const val EXTRA_ITERATION = "iteration"
const val EXTRA_STYLE_URI = "styleUri"
@ -134,7 +135,7 @@ class ImageProcessorService : Service() {
// there are commands running in the bg
addCommand(
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 maxWidth = extras.getInt(EXTRA_MAX_WIDTH)
val maxHeight = extras.getInt(EXTRA_MAX_HEIGHT)
val isSaveToServer = extras.getBoolean(EXTRA_IS_SAVE_TO_SERVER)
addCommand(
ImageProcessorFilterCommand(
startId, fileUrl, headers, filename, maxWidth,
maxHeight, filters
maxHeight, isSaveToServer, filters
)
)
}
@ -211,10 +213,11 @@ class ImageProcessorService : Service() {
val filename = extras.getString(EXTRA_FILENAME)!!
val maxWidth = extras.getInt(EXTRA_MAX_WIDTH)
val maxHeight = extras.getInt(EXTRA_MAX_HEIGHT)
val isSaveToServer = extras.getBoolean(EXTRA_IS_SAVE_TO_SERVER)
addCommand(
ImageProcessorEnhanceCommand(
startId, method, fileUrl, headers, filename, maxWidth,
maxHeight, args = args
maxHeight, isSaveToServer, args = args
)
)
}
@ -372,6 +375,7 @@ class ImageProcessorService : Service() {
private fun notifyResult(event: MessageEvent) {
if (event is ImageProcessorCompletedEvent) {
NativeEventChannelHandler.fire(ImageProcessorUploadSuccessEvent())
notificationManager.notify(
RESULT_NOTIFICATION_ID, buildResultNotification(event.result)
)
@ -420,6 +424,7 @@ private abstract class ImageProcessorImageCommand(
val filename: String,
val maxWidth: Int,
val maxHeight: Int,
val isSaveToServer: Boolean,
) : ImageProcessorCommand {
abstract fun apply(context: Context, fileUri: Uri): Bitmap
}
@ -432,9 +437,11 @@ private class ImageProcessorEnhanceCommand(
filename: String,
maxWidth: Int,
maxHeight: Int,
isSaveToServer: Boolean,
val args: Map<String, Any?> = mapOf(),
) : ImageProcessorImageCommand(
startId, method, fileUrl, headers, filename, maxWidth, maxHeight
startId, method, fileUrl, headers, filename, maxWidth, maxHeight,
isSaveToServer
) {
override fun apply(context: Context, fileUri: Uri): Bitmap {
return when (method) {
@ -470,10 +477,11 @@ private class ImageProcessorFilterCommand(
filename: String,
maxWidth: Int,
maxHeight: Int,
isSaveToServer: Boolean,
val filters: List<ImageFilter>,
) : ImageProcessorImageCommand(
startId, ImageProcessorService.METHOD_FILTER, fileUrl, headers, filename,
maxWidth, maxHeight
maxWidth, maxHeight, isSaveToServer
) {
override fun apply(context: Context, fileUri: Uri): Bitmap {
return ImageFilterProcessor(
@ -625,8 +633,7 @@ private open class ImageProcessorCommandTask(context: Context) :
val filter = cmd.filters.first() as Orientation
try {
return loselessRotate(
filter.degree, file, cmd.filename,
"Edited Photos"
filter.degree, file, cmd.filename, cmd
)
} catch (e: Throwable) {
logE(
@ -644,14 +651,7 @@ private open class ImageProcessorCommandTask(context: Context) :
cmd.apply(context, fileUri)
})
handleCancel()
saveBitmap(
output, cmd.filename, file,
if (cmd.method in ImageProcessorService.EDIT_METHODS) {
"Edited Photos"
} else {
"Enhanced Photos"
}
)
saveBitmap(output, cmd.filename, file, cmd)
} finally {
file.delete()
}
@ -680,7 +680,8 @@ private open class ImageProcessorCommandTask(context: Context) :
}
private fun loselessRotate(
degree: Int, srcFile: File, outFilename: String, subDir: String
degree: Int, srcFile: File, outFilename: String,
cmd: ImageProcessorImageCommand
): Uri {
logI(TAG, "[loselessRotate] $outFilename")
val outFile = File.createTempFile("out", null, getTempDir(context))
@ -693,12 +694,8 @@ private open class ImageProcessorCommandTask(context: Context) :
oExif.saveAttributes()
handleCancel()
// move file to user accessible storage
val uri = MediaStoreUtil.copyFileToDownload(
context, Uri.fromFile(outFile), outFilename,
"Photos (for Nextcloud)/$subDir"
)
return uri
val persister = getPersister(cmd.isSaveToServer)
return persister.persist(cmd, outFile)
} finally {
outFile.delete()
}
@ -738,10 +735,12 @@ private open class ImageProcessorCommandTask(context: Context) :
}
private fun saveBitmap(
bitmap: Bitmap, filename: String, srcFile: File, subDir: String
bitmap: Bitmap, filename: String, srcFile: File,
cmd: ImageProcessorImageCommand
): Uri {
logI(TAG, "[saveBitmap] $filename")
val outFile = File.createTempFile("out", null, getTempDir(context))
try {
outFile.outputStream().use {
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, it)
}
@ -756,13 +755,11 @@ private open class ImageProcessorCommandTask(context: Context) :
logE(TAG, "[copyExif] Failed while saving EXIF", e)
}
// move file to user accessible storage
val uri = MediaStoreUtil.copyFileToDownload(
context, Uri.fromFile(outFile), filename,
"Photos (for Nextcloud)/$subDir"
)
val persister = getPersister(cmd.isSaveToServer)
return persister.persist(cmd, outFile)
} finally {
outFile.delete()
return uri
}
}
private fun copyExif(from: ExifInterface, to: ExifInterface) {
@ -783,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")
private val context = context
}
@ -801,3 +806,97 @@ private fun getTempDir(context: Context): File {
}
return f
}
private interface EnhancedFilePersister {
fun persist(cmd: ImageProcessorImageCommand, file: File): Uri
}
private class EnhancedFileDevicePersister(context: Context) :
EnhancedFilePersister {
override fun persist(cmd: ImageProcessorImageCommand, file: File): Uri {
val uri = MediaStoreUtil.copyFileToDownload(
context, Uri.fromFile(file), cmd.filename,
"Photos (for Nextcloud)/${getSubDir(cmd)}"
)
return uri
}
private fun getSubDir(cmd: ImageProcessorImageCommand): String {
return if (cmd.method in ImageProcessorService.EDIT_METHODS) {
"Edited Photos"
} else {
"Enhanced Photos"
}
}
val context = context
}
private class EnhancedFileServerPersister :
EnhancedFilePersister {
companion object {
const val TAG = "EnhancedFileServerPersister"
}
override fun persist(cmd: ImageProcessorImageCommand, file: File): Uri {
val ext = cmd.fileUrl.substringAfterLast('.', "")
val url = if (ext.contains('/')) {
// no ext
"${cmd.fileUrl}_${getSuffix(cmd)}.jpg"
} else {
"${cmd.fileUrl.substringBeforeLast('.', "")}_${getSuffix(cmd)}.jpg"
}
logI(TAG, "[persist] Persist file to server: $url")
(URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "PUT"
instanceFollowRedirects = true
connectTimeout = 8000
for (entry in (cmd.headers ?: mapOf()).entries) {
setRequestProperty(entry.key, entry.value)
}
}.use {
file.inputStream()
.use { iStream -> iStream.copyTo(it.outputStream) }
val responseCode = it.responseCode
if (responseCode / 100 != 2) {
logE(TAG, "[persist] Failed uploading file: HTTP$responseCode")
throw HttpException(
responseCode, "Failed uploading file (HTTP$responseCode)"
)
}
}
return Uri.parse(url)
}
private fun getSuffix(cmd: ImageProcessorImageCommand): String {
val epoch = System.currentTimeMillis() / 1000
return if (cmd.method in ImageProcessorService.EDIT_METHODS) {
"edited_$epoch"
} else {
"enhanced_$epoch"
}
}
}
private class EnhancedFileServerPersisterWithFallback(context: Context) :
EnhancedFilePersister {
companion object {
const val TAG = "EnhancedFileServerPersisterWithFallback"
}
override fun persist(cmd: ImageProcessorImageCommand, file: File): Uri {
try {
return server.persist(cmd, file)
} catch (e: Throwable) {
logW(
TAG,
"[persist] Failed while persisting to server, switch to fallback",
e
)
}
return fallback.persist(cmd, file)
}
private val server = EnhancedFileServerPersister()
private val fallback = EnhancedFileDevicePersister(context)
}

View file

@ -0,0 +1,14 @@
package com.nkming.nc_photos.plugin
interface NativeEvent {
fun getId(): String
fun getData(): String? = null
}
class ImageProcessorUploadSuccessEvent : NativeEvent {
companion object {
const val id = "ImageProcessorUploadSuccessEvent"
}
override fun getId() = id
}

View file

@ -10,6 +10,20 @@ class NativeEventChannelHandler : MethodChannel.MethodCallHandler,
const val EVENT_CHANNEL = "${K.LIB_ID}/native_event"
const val METHOD_CHANNEL = "${K.LIB_ID}/native_event_method"
/**
* Fire native events on the native side
*/
fun fire(eventObj: NativeEvent) {
synchronized(eventSinks) {
for (s in eventSinks.values) {
s.success(buildMap {
put("event", eventObj.getId())
eventObj.getData()?.also { put("data", it) }
})
}
}
}
private val eventSinks = mutableMapOf<Int, EventChannel.EventSink>()
private var nextId = 0
}
@ -29,22 +43,28 @@ class NativeEventChannelHandler : MethodChannel.MethodCallHandler,
}
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
synchronized(eventSinks) {
eventSinks[id] = events
}
}
override fun onCancel(arguments: Any?) {
synchronized(eventSinks) {
eventSinks.remove(id)
}
}
private fun fire(
event: String, data: String?, result: MethodChannel.Result
) {
synchronized(eventSinks) {
for (s in eventSinks.values) {
s.success(buildMap {
put("event", event)
if (data != null) put("data", data)
})
}
}
result.success(null)
}

View file

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