nc-photos/app/lib/widget/handler/enhance_handler.dart

682 lines
21 KiB
Dart
Raw Normal View History

2022-07-28 18:59:26 +02:00
import 'dart:async';
2022-05-25 15:40:47 +02:00
import 'dart:math' as math;
import 'package:android_intent_plus/android_intent.dart';
2022-07-25 07:51:52 +02:00
import 'package:collection/collection.dart';
2022-05-04 10:42:46 +02:00
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/help_utils.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/mobile/android/android_info.dart';
2022-05-25 15:40:47 +02:00
import 'package:nc_photos/mobile/android/content_uri_image_provider.dart';
import 'package:nc_photos/mobile/android/k.dart' as android;
2022-05-04 10:42:46 +02:00
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:nc_photos/pref.dart';
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart';
2022-07-08 11:38:24 +02:00
import 'package:nc_photos/url_launcher_util.dart';
2022-07-12 21:17:17 +02:00
import 'package:nc_photos/widget/handler/permission_handler.dart';
import 'package:nc_photos/widget/image_editor_persist_option_dialog.dart';
2022-05-25 15:40:47 +02:00
import 'package:nc_photos/widget/selectable.dart';
import 'package:nc_photos/widget/settings.dart';
import 'package:nc_photos/widget/stateful_slider.dart';
2022-05-04 10:42:46 +02:00
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
class EnhanceHandler {
const EnhanceHandler({
required this.account,
required this.file,
required this.isSaveToServer,
2022-05-04 10:42:46 +02:00
});
static bool isSupportedFormat(File file) =>
file_util.isSupportedImageFormat(file) && file.contentType != "image/gif";
Future<void> call(BuildContext context) async {
if (!Pref().hasShownEnhanceInfoOr()) {
await _showInfo(context);
}
if (!Pref().hasShownSaveEditResultDialogOr()) {
await _showSaveEditResultDialog(context);
}
2022-07-12 21:17:17 +02:00
if (!await const PermissionHandler().ensureStorageWritePermission()) {
return;
}
final selected = await _pickAlgorithm(context);
2022-05-04 10:42:46 +02:00
if (selected == null) {
// user canceled
return;
}
_log.info("[call] Selected: ${selected.name}");
final args = await _getArgs(context, selected);
if (args == null) {
// user canceled
return;
}
2022-05-04 10:42:46 +02:00
switch (selected) {
case _Algorithm.zeroDce:
await ImageProcessor.zeroDce(
"${account.url}/${file.path}",
file.filename,
Pref().getEnhanceMaxWidthOr(),
Pref().getEnhanceMaxHeightOr(),
2022-05-15 22:25:46 +02:00
args["iteration"] ?? 8,
headers: {
"Authorization": Api.getAuthorizationHeaderValue(account),
},
isSaveToServer: isSaveToServer,
);
2022-05-04 10:42:46 +02:00
break;
case _Algorithm.deepLab3Portrait:
await ImageProcessor.deepLab3Portrait(
"${account.url}/${file.path}",
file.filename,
Pref().getEnhanceMaxWidthOr(),
Pref().getEnhanceMaxHeightOr(),
args["radius"] ?? 16,
headers: {
"Authorization": Api.getAuthorizationHeaderValue(account),
},
isSaveToServer: isSaveToServer,
);
break;
case _Algorithm.esrgan:
await ImageProcessor.esrgan(
"${account.url}/${file.path}",
file.filename,
Pref().getEnhanceMaxWidthOr(),
Pref().getEnhanceMaxHeightOr(),
headers: {
"Authorization": Api.getAuthorizationHeaderValue(account),
},
isSaveToServer: isSaveToServer,
);
break;
2022-05-25 15:40:47 +02:00
case _Algorithm.arbitraryStyleTransfer:
await ImageProcessor.arbitraryStyleTransfer(
"${account.url}/${file.path}",
file.filename,
math.min(
Pref().getEnhanceMaxWidthOr(), isAtLeast5GbRam() ? 1600 : 1280),
math.min(
Pref().getEnhanceMaxHeightOr(), isAtLeast5GbRam() ? 1200 : 960),
args["styleUri"],
args["weight"],
headers: {
"Authorization": Api.getAuthorizationHeaderValue(account),
},
isSaveToServer: isSaveToServer,
2022-05-25 15:40:47 +02:00
);
break;
2022-09-10 13:24:14 +02:00
case _Algorithm.deepLab3ColorPop:
await ImageProcessor.deepLab3ColorPop(
"${account.url}/${file.path}",
file.filename,
Pref().getEnhanceMaxWidthOr(),
Pref().getEnhanceMaxHeightOr(),
args["weight"],
headers: {
"Authorization": Api.getAuthorizationHeaderValue(account),
},
isSaveToServer: isSaveToServer,
);
break;
2022-09-15 06:59:52 +02:00
case _Algorithm.neurOp:
await ImageProcessor.neurOp(
"${account.url}/${file.path}",
file.filename,
Pref().getEnhanceMaxWidthOr(),
Pref().getEnhanceMaxHeightOr(),
headers: {
"Authorization": Api.getAuthorizationHeaderValue(account),
},
isSaveToServer: isSaveToServer,
);
break;
2022-05-04 10:42:46 +02:00
}
}
Future<void> _showInfo(BuildContext context) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(L10n.global().enhanceIntroDialogTitle),
content: Text(L10n.global().enhanceIntroDialogDescription),
actions: [
TextButton(
onPressed: () {
launch(enhanceUrl);
},
child: Text(L10n.global().learnMoreButtonLabel),
),
TextButton(
onPressed: () {
Navigator.of(context).pushNamed(EnhancementSettings.routeName);
},
child: Text(L10n.global().configButtonLabel),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
),
],
),
);
2022-07-28 18:59:26 +02:00
unawaited(Pref().setHasShownEnhanceInfo(true));
}
Future<void> _showSaveEditResultDialog(BuildContext context) async {
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) =>
const ImageEditorPersistOptionDialog(isFromEditor: false),
);
}
Future<_Algorithm?> _pickAlgorithm(BuildContext context) =>
showDialog<_Algorithm>(
context: context,
builder: (context) => SimpleDialog(
children: _getOptions()
.map((o) => SimpleDialogOption(
padding: const EdgeInsets.all(0),
child: ListTile(
title: Text(o.title),
subtitle: o.subtitle?.run((t) => Text(t)),
trailing: o.link != null
? SizedBox(
height: double.maxFinite,
child: TextButton(
child: Text(L10n.global().detailsTooltip),
onPressed: () {
launch(o.link!);
},
),
)
: null,
onTap: () {
Navigator.of(context).pop(o.algorithm);
},
),
))
.toList(),
),
);
2022-05-04 10:42:46 +02:00
List<_Option> _getOptions() => [
2022-09-15 06:59:52 +02:00
if (platform_k.isAndroid)
_Option(
title: L10n.global().enhanceRetouchTitle,
link: enhanceRetouchUrl,
algorithm: _Algorithm.neurOp,
),
2022-09-10 13:24:14 +02:00
if (platform_k.isAndroid)
_Option(
title: L10n.global().enhanceColorPopTitle,
subtitle: "DeepLap v3",
link: enhanceDeepLabColorPopUrl,
algorithm: _Algorithm.deepLab3ColorPop,
),
2022-05-04 10:42:46 +02:00
if (platform_k.isAndroid)
_Option(
title: L10n.global().enhanceLowLightTitle,
subtitle: "Zero-DCE",
link: enhanceZeroDceUrl,
algorithm: _Algorithm.zeroDce,
),
if (platform_k.isAndroid)
_Option(
title: L10n.global().enhancePortraitBlurTitle,
subtitle: "DeepLap v3",
link: enhanceDeepLabPortraitBlurUrl,
algorithm: _Algorithm.deepLab3Portrait,
),
if (platform_k.isAndroid)
_Option(
title: L10n.global().enhanceSuperResolution4xTitle,
subtitle: "ESRGAN",
link: enhanceEsrganUrl,
algorithm: _Algorithm.esrgan,
),
2022-05-25 15:40:47 +02:00
if (platform_k.isAndroid && isAtLeast4GbRam())
_Option(
title: L10n.global().enhanceStyleTransferTitle,
link: enhanceStyleTransferUrl,
algorithm: _Algorithm.arbitraryStyleTransfer,
),
2022-05-04 10:42:46 +02:00
];
Future<Map<String, dynamic>?> _getArgs(
BuildContext context, _Algorithm selected) async {
switch (selected) {
case _Algorithm.zeroDce:
2022-05-15 22:25:46 +02:00
return _getZeroDceArgs(context);
case _Algorithm.deepLab3Portrait:
return _getDeepLab3PortraitArgs(context);
case _Algorithm.esrgan:
return {};
2022-05-25 15:40:47 +02:00
case _Algorithm.arbitraryStyleTransfer:
return _getArbitraryStyleTransferArgs(context);
2022-09-10 13:24:14 +02:00
case _Algorithm.deepLab3ColorPop:
return _getDeepLab3ColorPopArgs(context);
2022-09-15 06:59:52 +02:00
case _Algorithm.neurOp:
return {};
}
}
2022-05-15 22:25:46 +02:00
Future<Map<String, dynamic>?> _getZeroDceArgs(BuildContext context) async {
var current = .8;
final iteration = await showDialog<int>(
context: context,
builder: (context) => AppTheme(
child: AlertDialog(
title: Text(L10n.global().enhanceLowLightParamBrightnessLabel),
contentPadding: const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 0),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: [
Icon(
Icons.brightness_low,
color: AppTheme.getSecondaryTextColor(context),
),
Expanded(
child: StatefulSlider(
initialValue: current,
onChangeEnd: (value) {
current = value;
},
),
),
Icon(
Icons.brightness_high,
color: AppTheme.getSecondaryTextColor(context),
),
],
),
],
),
actions: [
TextButton(
onPressed: () {
final iteration = (current * 10).round().clamp(1, 10);
Navigator.of(context).pop(iteration);
},
child: Text(L10n.global().enhanceButtonLabel),
),
],
),
),
);
_log.info("[_getZeroDceArgs] iteration: $iteration");
return iteration?.run((it) => {"iteration": it});
}
Future<Map<String, dynamic>?> _getDeepLab3PortraitArgs(
BuildContext context) async {
var current = .5;
final radius = await showDialog<int>(
context: context,
builder: (context) => AppTheme(
child: AlertDialog(
title: Text(L10n.global().enhancePortraitBlurParamBlurLabel),
contentPadding: const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 0),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: [
Icon(
Icons.circle,
size: 20,
color: AppTheme.getSecondaryTextColor(context),
),
Expanded(
child: StatefulSlider(
initialValue: current,
onChangeEnd: (value) {
current = value;
},
),
),
Icon(
Icons.blur_on,
color: AppTheme.getSecondaryTextColor(context),
),
],
),
],
),
actions: [
TextButton(
onPressed: () {
final radius = (current * 25).round().clamp(1, 25);
Navigator.of(context).pop(radius);
},
child: Text(L10n.global().enhanceButtonLabel),
),
],
),
),
);
_log.info("[_getDeepLab3PortraitArgs] radius: $radius");
return radius?.run((it) => {"radius": it});
}
2022-05-25 15:40:47 +02:00
Future<Map<String, dynamic>?> _getArbitraryStyleTransferArgs(
BuildContext context) async {
final result = await showDialog<_StylePickerResult>(
context: context,
builder: (_) => const _StylePicker(),
);
if (result == null) {
// user canceled
return null;
} else {
return {
"styleUri": result.styleUri,
"weight": result.weight,
};
}
}
2022-09-10 13:24:14 +02:00
Future<Map<String, dynamic>?> _getDeepLab3ColorPopArgs(
BuildContext context) async {
var current = 1.0;
final weight = await showDialog<double>(
context: context,
builder: (context) => AppTheme(
child: AlertDialog(
title: Text(L10n.global().enhanceGenericParamWeightLabel),
contentPadding: const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 0),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: [
Icon(
Icons.water_drop,
size: 20,
color: AppTheme.getSecondaryTextColor(context),
),
Expanded(
child: StatefulSlider(
initialValue: current,
onChangeEnd: (value) {
current = value;
},
),
),
Icon(
Icons.water_drop_outlined,
color: AppTheme.getSecondaryTextColor(context),
),
],
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(current);
},
child: Text(L10n.global().enhanceButtonLabel),
),
],
),
),
);
_log.info("[_getDeepLab3ColorPopArgs] weight: $weight");
return weight?.run((it) => {"weight": it});
}
2022-05-25 15:40:47 +02:00
bool isAtLeast4GbRam() {
// We can't compare with 4096 directly as some RAM are preserved
return AndroidInfo().totalMemMb > 3584;
}
bool isAtLeast5GbRam() {
return AndroidInfo().totalMemMb > 4608;
}
2022-05-04 10:42:46 +02:00
final Account account;
final File file;
final bool isSaveToServer;
2022-05-04 10:42:46 +02:00
static final _log = Logger("widget.handler.enhance_handler.EnhanceHandler");
}
enum _Algorithm {
zeroDce,
deepLab3Portrait,
esrgan,
2022-05-25 15:40:47 +02:00
arbitraryStyleTransfer,
2022-09-10 13:24:14 +02:00
deepLab3ColorPop,
2022-09-15 06:59:52 +02:00
neurOp,
2022-05-04 10:42:46 +02:00
}
class _Option {
const _Option({
required this.title,
this.subtitle,
this.link,
required this.algorithm,
});
final String title;
final String? subtitle;
final String? link;
final _Algorithm algorithm;
}
2022-05-25 15:40:47 +02:00
class _StylePickerResult {
const _StylePickerResult(this.styleUri, this.weight);
final String styleUri;
final double weight;
}
class _StylePicker extends StatefulWidget {
const _StylePicker({
Key? key,
}) : super(key: key);
@override
createState() => _StylePickerState();
}
class _StylePickerState extends State<_StylePicker> {
@override
build(BuildContext context) {
return AppTheme(
child: AlertDialog(
2022-06-06 07:16:36 +02:00
title: Text(L10n.global().enhanceStyleTransferStyleDialogTitle),
2022-05-25 15:40:47 +02:00
contentPadding: const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 0),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (_selected != null) ...[
Align(
alignment: Alignment.center,
child: SizedBox(
width: 128,
height: 128,
child: Image(
image: ResizeImage.resizeIfNeeded(
128, null, ContentUriImage(_getSelectedUri())),
fit: BoxFit.cover,
),
),
),
const SizedBox(height: 16),
],
Wrap(
runSpacing: 8,
spacing: 8,
children: [
2022-07-25 07:51:52 +02:00
..._bundledStyles.mapIndexed((i, e) => _buildItem(
2022-05-25 15:40:47 +02:00
i,
Image(
image: ResizeImage.resizeIfNeeded(
_thumbSize, null, ContentUriImage(e)),
fit: BoxFit.cover,
),
)),
if (_customUri != null)
_buildItem(
_bundledStyles.length,
Image(
image: ResizeImage.resizeIfNeeded(
_thumbSize, null, ContentUriImage(_customUri!)),
fit: BoxFit.cover,
),
),
InkWell(
onTap: _onCustomTap,
child: SizedBox(
width: _thumbSize.toDouble(),
height: _thumbSize.toDouble(),
child: const Icon(
Icons.file_open_outlined,
size: 24,
),
),
),
],
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Icon(
Icons.auto_fix_normal,
color: AppTheme.getSecondaryTextColor(context),
),
Expanded(
child: StatefulSlider(
initialValue: _weight,
min: .01,
onChangeEnd: (value) {
_weight = value;
},
),
),
Icon(
Icons.auto_fix_high,
color: AppTheme.getSecondaryTextColor(context),
),
],
),
],
),
actions: [
TextButton(
onPressed: () {
if (_selected == null) {
SnackBarManager().showSnackBar(const SnackBar(
content: Text("Please pick a style"),
duration: k.snackBarDurationNormal,
));
} else {
final result = _StylePickerResult(_getSelectedUri(), _weight);
Navigator.of(context).pop(result);
}
},
child: Text(L10n.global().enhanceButtonLabel),
),
],
),
);
}
Widget _buildItem(int index, Widget child) {
return SizedBox(
width: _thumbSize.toDouble(),
height: _thumbSize.toDouble(),
child: Selectable(
isSelected: _selected == index,
iconSize: 24,
child: child,
onTap: () {
setState(() {
_selected = index;
});
},
),
);
}
Future<void> _onCustomTap() async {
const intent = AndroidIntent(
action: android.ACTION_GET_CONTENT,
type: "image/*",
category: android.CATEGORY_OPENABLE,
arguments: {
android.EXTRA_LOCAL_ONLY: true,
},
);
final result = await intent.launchForResult();
_log.info("[onCustomTap] Intent result: $result");
if (result?.resultCode == android.resultOk) {
if (mounted) {
setState(() {
_customUri = result!.data;
_selected = _bundledStyles.length;
});
}
}
}
String _getSelectedUri() {
return _selected! < _bundledStyles.length
? _bundledStyles[_selected!]
: _customUri!;
}
int? _selected;
String? _customUri;
double _weight = .85;
static const _thumbSize = 56;
static const _bundledStyles = [
"file:///android_asset/tf/arbitrary-style-transfer/1.jpg",
"file:///android_asset/tf/arbitrary-style-transfer/2.jpg",
"file:///android_asset/tf/arbitrary-style-transfer/3.jpg",
"file:///android_asset/tf/arbitrary-style-transfer/4.jpg",
"file:///android_asset/tf/arbitrary-style-transfer/5.jpg",
"file:///android_asset/tf/arbitrary-style-transfer/6.jpg",
];
static final _log =
Logger("widget.handler.enhance_handler._StylePickerState");
}