diff --git a/app/assets/color-pop0.jpg b/app/assets/color-pop0.jpg new file mode 100644 index 00000000..a096ed3d Binary files /dev/null and b/app/assets/color-pop0.jpg differ diff --git a/app/assets/color-pop1.jpg b/app/assets/color-pop1.jpg new file mode 100644 index 00000000..222cd39f Binary files /dev/null and b/app/assets/color-pop1.jpg differ diff --git a/app/assets/low-light0.jpg b/app/assets/low-light0.jpg new file mode 100644 index 00000000..82f35933 Binary files /dev/null and b/app/assets/low-light0.jpg differ diff --git a/app/assets/low-light1.jpg b/app/assets/low-light1.jpg new file mode 100644 index 00000000..c3f24f63 Binary files /dev/null and b/app/assets/low-light1.jpg differ diff --git a/app/assets/portrait-blur0.jpg b/app/assets/portrait-blur0.jpg new file mode 100644 index 00000000..2559ca62 Binary files /dev/null and b/app/assets/portrait-blur0.jpg differ diff --git a/app/assets/portrait-blur1.jpg b/app/assets/portrait-blur1.jpg new file mode 100644 index 00000000..07c4f736 Binary files /dev/null and b/app/assets/portrait-blur1.jpg differ diff --git a/app/assets/retouch0.jpg b/app/assets/retouch0.jpg new file mode 100644 index 00000000..258b83e1 Binary files /dev/null and b/app/assets/retouch0.jpg differ diff --git a/app/assets/retouch1.jpg b/app/assets/retouch1.jpg new file mode 100644 index 00000000..289b8b90 Binary files /dev/null and b/app/assets/retouch1.jpg differ diff --git a/app/assets/style-transfer0.jpg b/app/assets/style-transfer0.jpg new file mode 100644 index 00000000..920900c2 Binary files /dev/null and b/app/assets/style-transfer0.jpg differ diff --git a/app/assets/style-transfer1.jpg b/app/assets/style-transfer1.jpg new file mode 100644 index 00000000..dc973069 Binary files /dev/null and b/app/assets/style-transfer1.jpg differ diff --git a/app/assets/super-resolution0.jpg b/app/assets/super-resolution0.jpg new file mode 100644 index 00000000..f3ad24f1 Binary files /dev/null and b/app/assets/super-resolution0.jpg differ diff --git a/app/assets/super-resolution1.jpg b/app/assets/super-resolution1.jpg new file mode 100644 index 00000000..315fe57f Binary files /dev/null and b/app/assets/super-resolution1.jpg differ diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 1d7658cd..c3a351a0 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1234,6 +1234,7 @@ "@enhanceLowLightTitle": { "description": "Enhance a photo taken in low-light environment" }, + "enhanceLowLightDescription": "Brighten your photos taken in low-light environments", "enhanceLowLightParamBrightnessLabel": "Brightness", "@enhanceLowLightParamBrightnessLabel": { "description": "This parameter sets how much brighter the output will be" @@ -1250,6 +1251,7 @@ "@enhancePortraitBlurTitle": { "description": "Blur the background of a photo" }, + "enhancePortraitBlurDescription": "Blur the background of your photos, work best with portraits", "enhancePortraitBlurParamBlurLabel": "Blurriness", "@enhancePortraitBlurParamBlurLabel": { "description": "This parameter sets the radius of the blur filter" @@ -1258,6 +1260,7 @@ "@enhanceSuperResolution4xTitle": { "description": "Upscale an image. The algorithm implemented in the app will upscale to 4x the original resolution (eg, 100x100 to 400x400)" }, + "enhanceSuperResolution4xDescription": "Enlarge your photos to 4x of its original resolution (see Help for details on how max resolution applies here)", "enhanceStyleTransferTitle": "Style transfer", "@enhanceStyleTransferTitle": { "description": "Transfer the image style from a reference image to a photo" @@ -1266,10 +1269,16 @@ "@enhanceStyleTransferStyleDialogTitle": { "description": "Pick a reference image for the style transfer algorithm" }, + "enhanceStyleTransferStyleDialogDescription": "Transfer image style from a reference image to your photos", + "enhanceStyleTransferNoStyleSelectedNotification": "Please pick a style", + "@enhanceStyleTransferNoStyleSelectedNotification": { + "description": "Show this error if users did not pick a reference image" + }, "enhanceColorPopTitle": "Color pop", "@enhanceColorPopTitle": { "description": "Desaturate the background of a photo" }, + "enhanceColorPopDescription": "Desaturate the background of your photos, work best with portraits", "enhanceGenericParamWeightLabel": "Weight", "@enhanceGenericParamWeightLabel": { "description": "This generic parameter sets the weight of the applied effect. The effect will be more obvious when the weight is high." @@ -1278,6 +1287,7 @@ "@enhanceRetouchTitle": { "description": "Automatically improve your photo" }, + "enhanceRetouchDescription": "Automatically retouch your photos, improve overall color and vibrance", "doubleTapExitNotification": "Tap again to exit", "@doubleTapExitNotification": { "description": "If double tap to exit is enabled in settings, shown when users tap the back button" diff --git a/app/lib/l10n/untranslated-messages.txt b/app/lib/l10n/untranslated-messages.txt index d250685a..f2dcd06d 100644 --- a/app/lib/l10n/untranslated-messages.txt +++ b/app/lib/l10n/untranslated-messages.txt @@ -110,17 +110,24 @@ "enhanceIntroDialogTitle", "enhanceIntroDialogDescription", "enhanceLowLightTitle", + "enhanceLowLightDescription", "enhanceLowLightParamBrightnessLabel", "collectionEditedPhotosLabel", "deletePermanentlyLocalConfirmationDialogContent", "enhancePortraitBlurTitle", + "enhancePortraitBlurDescription", "enhancePortraitBlurParamBlurLabel", "enhanceSuperResolution4xTitle", + "enhanceSuperResolution4xDescription", "enhanceStyleTransferTitle", "enhanceStyleTransferStyleDialogTitle", + "enhanceStyleTransferStyleDialogDescription", + "enhanceStyleTransferNoStyleSelectedNotification", "enhanceColorPopTitle", + "enhanceColorPopDescription", "enhanceGenericParamWeightLabel", "enhanceRetouchTitle", + "enhanceRetouchDescription", "doubleTapExitNotification", "imageEditDiscardDialogTitle", "imageEditDiscardDialogContent", @@ -292,17 +299,24 @@ "enhanceIntroDialogTitle", "enhanceIntroDialogDescription", "enhanceLowLightTitle", + "enhanceLowLightDescription", "enhanceLowLightParamBrightnessLabel", "collectionEditedPhotosLabel", "deletePermanentlyLocalConfirmationDialogContent", "enhancePortraitBlurTitle", + "enhancePortraitBlurDescription", "enhancePortraitBlurParamBlurLabel", "enhanceSuperResolution4xTitle", + "enhanceSuperResolution4xDescription", "enhanceStyleTransferTitle", "enhanceStyleTransferStyleDialogTitle", + "enhanceStyleTransferStyleDialogDescription", + "enhanceStyleTransferNoStyleSelectedNotification", "enhanceColorPopTitle", + "enhanceColorPopDescription", "enhanceGenericParamWeightLabel", "enhanceRetouchTitle", + "enhanceRetouchDescription", "doubleTapExitNotification", "imageEditDiscardDialogTitle", "imageEditDiscardDialogContent", @@ -371,11 +385,18 @@ "shareMethodPreviewDescription", "shareMethodOriginalFileTitle", "shareMethodOriginalFileDescription", + "enhanceLowLightDescription", "collectionEditedPhotosLabel", + "enhancePortraitBlurDescription", + "enhanceSuperResolution4xDescription", "enhanceStyleTransferStyleDialogTitle", + "enhanceStyleTransferStyleDialogDescription", + "enhanceStyleTransferNoStyleSelectedNotification", "enhanceColorPopTitle", + "enhanceColorPopDescription", "enhanceGenericParamWeightLabel", "enhanceRetouchTitle", + "enhanceRetouchDescription", "doubleTapExitNotification", "imageEditDiscardDialogTitle", "imageEditDiscardDialogContent", @@ -439,10 +460,17 @@ "shareMethodPreviewDescription", "shareMethodOriginalFileTitle", "shareMethodOriginalFileDescription", + "enhanceLowLightDescription", "collectionEditedPhotosLabel", + "enhancePortraitBlurDescription", + "enhanceSuperResolution4xDescription", + "enhanceStyleTransferStyleDialogDescription", + "enhanceStyleTransferNoStyleSelectedNotification", "enhanceColorPopTitle", + "enhanceColorPopDescription", "enhanceGenericParamWeightLabel", "enhanceRetouchTitle", + "enhanceRetouchDescription", "imageEditToolbarColorLabel", "imageEditToolbarTransformLabel", "imageEditTransformOrientation", @@ -474,10 +502,17 @@ "shareMethodPreviewDescription", "shareMethodOriginalFileTitle", "shareMethodOriginalFileDescription", + "enhanceLowLightDescription", "collectionEditedPhotosLabel", + "enhancePortraitBlurDescription", + "enhanceSuperResolution4xDescription", + "enhanceStyleTransferStyleDialogDescription", + "enhanceStyleTransferNoStyleSelectedNotification", "enhanceColorPopTitle", + "enhanceColorPopDescription", "enhanceGenericParamWeightLabel", "enhanceRetouchTitle", + "enhanceRetouchDescription", "imageEditToolbarColorLabel", "imageEditToolbarTransformLabel", "imageEditTransformOrientation", @@ -528,17 +563,24 @@ "enhanceIntroDialogTitle", "enhanceIntroDialogDescription", "enhanceLowLightTitle", + "enhanceLowLightDescription", "enhanceLowLightParamBrightnessLabel", "collectionEditedPhotosLabel", "deletePermanentlyLocalConfirmationDialogContent", "enhancePortraitBlurTitle", + "enhancePortraitBlurDescription", "enhancePortraitBlurParamBlurLabel", "enhanceSuperResolution4xTitle", + "enhanceSuperResolution4xDescription", "enhanceStyleTransferTitle", "enhanceStyleTransferStyleDialogTitle", + "enhanceStyleTransferStyleDialogDescription", + "enhanceStyleTransferNoStyleSelectedNotification", "enhanceColorPopTitle", + "enhanceColorPopDescription", "enhanceGenericParamWeightLabel", "enhanceRetouchTitle", + "enhanceRetouchDescription", "doubleTapExitNotification", "imageEditDiscardDialogTitle", "imageEditDiscardDialogContent", @@ -637,17 +679,24 @@ "enhanceIntroDialogTitle", "enhanceIntroDialogDescription", "enhanceLowLightTitle", + "enhanceLowLightDescription", "enhanceLowLightParamBrightnessLabel", "collectionEditedPhotosLabel", "deletePermanentlyLocalConfirmationDialogContent", "enhancePortraitBlurTitle", + "enhancePortraitBlurDescription", "enhancePortraitBlurParamBlurLabel", "enhanceSuperResolution4xTitle", + "enhanceSuperResolution4xDescription", "enhanceStyleTransferTitle", "enhanceStyleTransferStyleDialogTitle", + "enhanceStyleTransferStyleDialogDescription", + "enhanceStyleTransferNoStyleSelectedNotification", "enhanceColorPopTitle", + "enhanceColorPopDescription", "enhanceGenericParamWeightLabel", "enhanceRetouchTitle", + "enhanceRetouchDescription", "doubleTapExitNotification", "imageEditDiscardDialogTitle", "imageEditDiscardDialogContent", @@ -725,17 +774,24 @@ "enhanceIntroDialogTitle", "enhanceIntroDialogDescription", "enhanceLowLightTitle", + "enhanceLowLightDescription", "enhanceLowLightParamBrightnessLabel", "collectionEditedPhotosLabel", "deletePermanentlyLocalConfirmationDialogContent", "enhancePortraitBlurTitle", + "enhancePortraitBlurDescription", "enhancePortraitBlurParamBlurLabel", "enhanceSuperResolution4xTitle", + "enhanceSuperResolution4xDescription", "enhanceStyleTransferTitle", "enhanceStyleTransferStyleDialogTitle", + "enhanceStyleTransferStyleDialogDescription", + "enhanceStyleTransferNoStyleSelectedNotification", "enhanceColorPopTitle", + "enhanceColorPopDescription", "enhanceGenericParamWeightLabel", "enhanceRetouchTitle", + "enhanceRetouchDescription", "doubleTapExitNotification", "imageEditDiscardDialogTitle", "imageEditDiscardDialogContent", @@ -813,17 +869,24 @@ "enhanceIntroDialogTitle", "enhanceIntroDialogDescription", "enhanceLowLightTitle", + "enhanceLowLightDescription", "enhanceLowLightParamBrightnessLabel", "collectionEditedPhotosLabel", "deletePermanentlyLocalConfirmationDialogContent", "enhancePortraitBlurTitle", + "enhancePortraitBlurDescription", "enhancePortraitBlurParamBlurLabel", "enhanceSuperResolution4xTitle", + "enhanceSuperResolution4xDescription", "enhanceStyleTransferTitle", "enhanceStyleTransferStyleDialogTitle", + "enhanceStyleTransferStyleDialogDescription", + "enhanceStyleTransferNoStyleSelectedNotification", "enhanceColorPopTitle", + "enhanceColorPopDescription", "enhanceGenericParamWeightLabel", "enhanceRetouchTitle", + "enhanceRetouchDescription", "doubleTapExitNotification", "imageEditDiscardDialogTitle", "imageEditDiscardDialogContent", @@ -901,17 +964,24 @@ "enhanceIntroDialogTitle", "enhanceIntroDialogDescription", "enhanceLowLightTitle", + "enhanceLowLightDescription", "enhanceLowLightParamBrightnessLabel", "collectionEditedPhotosLabel", "deletePermanentlyLocalConfirmationDialogContent", "enhancePortraitBlurTitle", + "enhancePortraitBlurDescription", "enhancePortraitBlurParamBlurLabel", "enhanceSuperResolution4xTitle", + "enhanceSuperResolution4xDescription", "enhanceStyleTransferTitle", "enhanceStyleTransferStyleDialogTitle", + "enhanceStyleTransferStyleDialogDescription", + "enhanceStyleTransferNoStyleSelectedNotification", "enhanceColorPopTitle", + "enhanceColorPopDescription", "enhanceGenericParamWeightLabel", "enhanceRetouchTitle", + "enhanceRetouchDescription", "doubleTapExitNotification", "imageEditDiscardDialogTitle", "imageEditDiscardDialogContent", @@ -989,17 +1059,24 @@ "enhanceIntroDialogTitle", "enhanceIntroDialogDescription", "enhanceLowLightTitle", + "enhanceLowLightDescription", "enhanceLowLightParamBrightnessLabel", "collectionEditedPhotosLabel", "deletePermanentlyLocalConfirmationDialogContent", "enhancePortraitBlurTitle", + "enhancePortraitBlurDescription", "enhancePortraitBlurParamBlurLabel", "enhanceSuperResolution4xTitle", + "enhanceSuperResolution4xDescription", "enhanceStyleTransferTitle", "enhanceStyleTransferStyleDialogTitle", + "enhanceStyleTransferStyleDialogDescription", + "enhanceStyleTransferNoStyleSelectedNotification", "enhanceColorPopTitle", + "enhanceColorPopDescription", "enhanceGenericParamWeightLabel", "enhanceRetouchTitle", + "enhanceRetouchDescription", "doubleTapExitNotification", "imageEditDiscardDialogTitle", "imageEditDiscardDialogContent", diff --git a/app/lib/widget/handler/enhance_handler.dart b/app/lib/widget/image_enhancer.dart similarity index 51% rename from app/lib/widget/handler/enhance_handler.dart rename to app/lib/widget/image_enhancer.dart index 6f40e17c..197435fb 100644 --- a/app/lib/widget/handler/enhance_handler.dart +++ b/app/lib/widget/image_enhancer.dart @@ -2,12 +2,15 @@ import 'dart:async'; import 'dart:math' as math; import 'package:android_intent_plus/android_intent.dart'; +import 'package:circular_reveal_animation/circular_reveal_animation.dart'; import 'package:collection/collection.dart'; 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/api/api.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/entity/file_util.dart' as file_util; import 'package:nc_photos/help_utils.dart'; @@ -17,7 +20,6 @@ import 'package:nc_photos/mobile/android/content_uri_image_provider.dart'; import 'package:nc_photos/mobile/android/k.dart' as android; 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'; import 'package:nc_photos/url_launcher_util.dart'; @@ -28,125 +30,268 @@ import 'package:nc_photos/widget/settings.dart'; import 'package:nc_photos/widget/stateful_slider.dart'; import 'package:nc_photos_plugin/nc_photos_plugin.dart'; -class EnhanceHandler { - const EnhanceHandler({ +class ImageEnhancerArguments { + const ImageEnhancerArguments(this.account, this.file, this.isSaveToServer); + + final Account account; + final File file; + final bool isSaveToServer; +} + +class ImageEnhancer extends StatefulWidget { + static const routeName = "/image-enhancer"; + + static Route buildRoute(ImageEnhancerArguments args) => MaterialPageRoute( + builder: (context) => ImageEnhancer.fromArgs(args), + ); + + static bool isSupportedFormat(File file) => + file_util.isSupportedImageFormat(file) && file.contentType != "image/gif"; + + const ImageEnhancer({ + super.key, required this.account, required this.file, required this.isSaveToServer, }); - static bool isSupportedFormat(File file) => - file_util.isSupportedImageFormat(file) && file.contentType != "image/gif"; + ImageEnhancer.fromArgs(ImageEnhancerArguments args, {Key? key}) + : this( + key: key, + account: args.account, + file: args.file, + isSaveToServer: args.isSaveToServer, + ); - Future call(BuildContext context) async { - if (!Pref().hasShownEnhanceInfoOr()) { - await _showInfo(context); - } - if (!Pref().hasShownSaveEditResultDialogOr()) { - await _showSaveEditResultDialog(context); - } + @override + createState() => _ImageEnhancerState(); - if (!await const PermissionHandler().ensureStorageWritePermission()) { - return; - } + final Account account; + final File file; + final bool isSaveToServer; +} - final selected = await _pickAlgorithm(context); - if (selected == null) { - // user canceled - return; - } - _log.info("[call] Selected: ${selected.name}"); - final args = await _getArgs(context, selected); +class _ImageEnhancerState extends State { + @override + initState() { + super.initState(); + _c = KiwiContainer().resolve(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _showInitialDialogs(); + }); + } + + @override + build(BuildContext context) => AppTheme( + child: Scaffold( + body: Builder( + builder: _buildContent, + ), + ), + ); + + Widget _buildContent(BuildContext context) { + return ColoredBox( + color: Colors.black, + child: Column( + children: [ + _buildAppBar(context), + Expanded( + child: PageView.builder( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + itemCount: _options.length, + itemBuilder: (context, i) => Padding( + padding: const EdgeInsets.all(48), + child: _options[i].showcaseBuilder(context), + ), + ), + ), + SizedBox( + height: 36, + child: ListView.builder( + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width / 2 - 80), + scrollDirection: Axis.horizontal, + itemCount: _options.length, + itemBuilder: _buildItem, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 24), + height: 72, + alignment: AlignmentDirectional.centerStart, + child: Text(_selectedOption.description), + ), + ], + ), + ); + } + + Widget _buildAppBar(BuildContext context) => AppBar( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + foregroundColor: Colors.white.withOpacity(.87), + title: Text(L10n.global().enhanceTooltip), + actions: [ + TextButton( + child: Text( + L10n.global().applyButtonLabel, + style: const TextStyle( + color: Colors.white, + ), + ), + onPressed: () => _onSavePressed(context), + ), + IconButton( + icon: const Icon(Icons.help_outline), + tooltip: L10n.global().helpTooltip, + onPressed: () { + launch(_selectedOption.link); + }, + ), + ], + ); + + Widget _buildItem(BuildContext context, int index) { + final opt = _options[index]; + return _ListChild( + title: opt.title, + isSelected: identical(_selectedOption, opt), + onTap: () { + setState(() { + _selectedOption = opt; + _pageController.animateToPage( + index, + duration: k.animationDurationNormal, + curve: Curves.easeInOut, + ); + }); + }, + ); + } + + Future _onSavePressed(BuildContext context) async { + final args = await _getArgs(context, _selectedOption.algorithm); if (args == null) { // user canceled return; } - switch (selected) { + switch (_selectedOption.algorithm) { case _Algorithm.zeroDce: await ImageProcessor.zeroDce( - "${account.url}/${file.path}", - file.filename, - Pref().getEnhanceMaxWidthOr(), - Pref().getEnhanceMaxHeightOr(), + "${widget.account.url}/${widget.file.path}", + widget.file.filename, + _c.pref.getEnhanceMaxWidthOr(), + _c.pref.getEnhanceMaxHeightOr(), args["iteration"] ?? 8, headers: { - "Authorization": Api.getAuthorizationHeaderValue(account), + "Authorization": Api.getAuthorizationHeaderValue(widget.account), }, - isSaveToServer: isSaveToServer, + isSaveToServer: widget.isSaveToServer, ); break; case _Algorithm.deepLab3Portrait: await ImageProcessor.deepLab3Portrait( - "${account.url}/${file.path}", - file.filename, - Pref().getEnhanceMaxWidthOr(), - Pref().getEnhanceMaxHeightOr(), + "${widget.account.url}/${widget.file.path}", + widget.file.filename, + _c.pref.getEnhanceMaxWidthOr(), + _c.pref.getEnhanceMaxHeightOr(), args["radius"] ?? 16, headers: { - "Authorization": Api.getAuthorizationHeaderValue(account), + "Authorization": Api.getAuthorizationHeaderValue(widget.account), }, - isSaveToServer: isSaveToServer, + isSaveToServer: widget.isSaveToServer, ); break; case _Algorithm.esrgan: await ImageProcessor.esrgan( - "${account.url}/${file.path}", - file.filename, - Pref().getEnhanceMaxWidthOr(), - Pref().getEnhanceMaxHeightOr(), + "${widget.account.url}/${widget.file.path}", + widget.file.filename, + _c.pref.getEnhanceMaxWidthOr(), + _c.pref.getEnhanceMaxHeightOr(), headers: { - "Authorization": Api.getAuthorizationHeaderValue(account), + "Authorization": Api.getAuthorizationHeaderValue(widget.account), }, - isSaveToServer: isSaveToServer, + isSaveToServer: widget.isSaveToServer, ); break; case _Algorithm.arbitraryStyleTransfer: await ImageProcessor.arbitraryStyleTransfer( - "${account.url}/${file.path}", - file.filename, + "${widget.account.url}/${widget.file.path}", + widget.file.filename, math.min( - Pref().getEnhanceMaxWidthOr(), isAtLeast5GbRam() ? 1600 : 1280), + _c.pref.getEnhanceMaxWidthOr(), _isAtLeast5GbRam() ? 1600 : 1280), math.min( - Pref().getEnhanceMaxHeightOr(), isAtLeast5GbRam() ? 1200 : 960), + _c.pref.getEnhanceMaxHeightOr(), _isAtLeast5GbRam() ? 1200 : 960), args["styleUri"], args["weight"], headers: { - "Authorization": Api.getAuthorizationHeaderValue(account), + "Authorization": Api.getAuthorizationHeaderValue(widget.account), }, - isSaveToServer: isSaveToServer, + isSaveToServer: widget.isSaveToServer, ); break; case _Algorithm.deepLab3ColorPop: await ImageProcessor.deepLab3ColorPop( - "${account.url}/${file.path}", - file.filename, - Pref().getEnhanceMaxWidthOr(), - Pref().getEnhanceMaxHeightOr(), + "${widget.account.url}/${widget.file.path}", + widget.file.filename, + _c.pref.getEnhanceMaxWidthOr(), + _c.pref.getEnhanceMaxHeightOr(), args["weight"], headers: { - "Authorization": Api.getAuthorizationHeaderValue(account), + "Authorization": Api.getAuthorizationHeaderValue(widget.account), }, - isSaveToServer: isSaveToServer, + isSaveToServer: widget.isSaveToServer, ); break; case _Algorithm.neurOp: await ImageProcessor.neurOp( - "${account.url}/${file.path}", - file.filename, - Pref().getEnhanceMaxWidthOr(), - Pref().getEnhanceMaxHeightOr(), + "${widget.account.url}/${widget.file.path}", + widget.file.filename, + _c.pref.getEnhanceMaxWidthOr(), + _c.pref.getEnhanceMaxHeightOr(), headers: { - "Authorization": Api.getAuthorizationHeaderValue(account), + "Authorization": Api.getAuthorizationHeaderValue(widget.account), }, - isSaveToServer: isSaveToServer, + isSaveToServer: widget.isSaveToServer, ); break; } + Navigator.of(context).pop(); + } + + Future _showInitialDialogs() async { + if (!_c.pref.hasShownEnhanceInfoOr()) { + await _showInfo(context); + } + if (!mounted) { + return; + } + final value = await _ensurePermission(); + if (!mounted || !value) { + return; + } + if (!_c.pref.hasShownSaveEditResultDialogOr()) { + await _showSaveEditResultDialog(context); + } + } + + Future _ensurePermission() async { + if (!await const PermissionHandler().ensureStorageWritePermission()) { + if (mounted) { + Navigator.of(context).pop(); + } + return false; + } else { + return true; + } } Future _showInfo(BuildContext context) async { @@ -177,7 +322,7 @@ class EnhanceHandler { ], ), ); - unawaited(Pref().setHasShownEnhanceInfo(true)); + unawaited(_c.pref.setHasShownEnhanceInfo(true)); } Future _showSaveEditResultDialog(BuildContext context) async { @@ -189,79 +334,6 @@ class EnhanceHandler { ); } - 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(), - ), - ); - - List<_Option> _getOptions() => [ - if (platform_k.isAndroid) - _Option( - title: L10n.global().enhanceRetouchTitle, - link: enhanceRetouchUrl, - algorithm: _Algorithm.neurOp, - ), - if (platform_k.isAndroid) - _Option( - title: L10n.global().enhanceColorPopTitle, - subtitle: "DeepLap v3", - link: enhanceDeepLabColorPopUrl, - algorithm: _Algorithm.deepLab3ColorPop, - ), - 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, - ), - if (platform_k.isAndroid && isAtLeast4GbRam()) - _Option( - title: L10n.global().enhanceStyleTransferTitle, - link: enhanceStyleTransferUrl, - algorithm: _Algorithm.arbitraryStyleTransfer, - ), - ]; - Future?> _getArgs( BuildContext context, _Algorithm selected) async { switch (selected) { @@ -458,20 +530,68 @@ class EnhanceHandler { return weight?.run((it) => {"weight": it}); } - bool isAtLeast4GbRam() { + bool _isAtLeast4GbRam() { // We can't compare with 4096 directly as some RAM are preserved return AndroidInfo().totalMemMb > 3584; } - bool isAtLeast5GbRam() { + bool _isAtLeast5GbRam() { return AndroidInfo().totalMemMb > 4608; } - final Account account; - final File file; - final bool isSaveToServer; + late final _options = [ + if (platform_k.isAndroid) ...[ + _Option( + title: L10n.global().enhanceRetouchTitle, + description: L10n.global().enhanceRetouchDescription, + link: enhanceRetouchUrl, + showcaseBuilder: (_) => const _RetouchShowcase(), + algorithm: _Algorithm.neurOp, + ), + _Option( + title: L10n.global().enhanceColorPopTitle, + description: L10n.global().enhanceColorPopDescription, + link: enhanceDeepLabColorPopUrl, + showcaseBuilder: (_) => const _ColorPopShowcase(), + algorithm: _Algorithm.deepLab3ColorPop, + ), + _Option( + title: L10n.global().enhanceLowLightTitle, + description: L10n.global().enhanceLowLightDescription, + link: enhanceZeroDceUrl, + showcaseBuilder: (_) => const _LowLightShowcase(), + algorithm: _Algorithm.zeroDce, + ), + _Option( + title: L10n.global().enhancePortraitBlurTitle, + description: L10n.global().enhancePortraitBlurDescription, + link: enhanceDeepLabPortraitBlurUrl, + showcaseBuilder: (_) => const _PortraitBlurShowcase(), + algorithm: _Algorithm.deepLab3Portrait, + ), + _Option( + title: L10n.global().enhanceSuperResolution4xTitle, + description: L10n.global().enhanceSuperResolution4xDescription, + link: enhanceEsrganUrl, + showcaseBuilder: (_) => const _SuperResolutionShowcase(), + algorithm: _Algorithm.esrgan, + ), + if (_isAtLeast4GbRam()) + _Option( + title: L10n.global().enhanceStyleTransferTitle, + description: L10n.global().enhanceStyleTransferStyleDialogDescription, + link: enhanceStyleTransferUrl, + showcaseBuilder: (_) => const _StyleTransferShowcase(), + algorithm: _Algorithm.arbitraryStyleTransfer, + ), + ], + ]; - static final _log = Logger("widget.handler.enhance_handler.EnhanceHandler"); + late final DiContainer _c; + late var _selectedOption = _options[0]; + late final _pageController = PageController(keepPage: false); + + static final _log = Logger("widget.image_enhancer._ImageEnhancerState"); } enum _Algorithm { @@ -486,17 +606,270 @@ enum _Algorithm { class _Option { const _Option({ required this.title, - this.subtitle, - this.link, + required this.description, + required this.link, + required this.showcaseBuilder, required this.algorithm, }); final String title; - final String? subtitle; - final String? link; + final String description; + final String link; + final Widget Function(BuildContext context) showcaseBuilder; final _Algorithm algorithm; } +class _ListChild extends StatelessWidget { + const _ListChild({ + required this.title, + required this.isSelected, + required this.onTap, + }); + + @override + build(BuildContext context) { + return ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: Material( + type: MaterialType.transparency, + child: InkWell( + onTap: onTap, + child: Container( + color: isSelected ? Colors.white24 : null, + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + title, + style: TextStyle( + color: + isSelected ? Colors.white : AppTheme.unfocusedIconColorDark, + ), + ), + ), + ), + ), + ); + } + + final String title; + final bool isSelected; + final VoidCallback? onTap; +} + +mixin _ShowcaseStateMixin + on State, TickerProviderStateMixin { + @override + initState() { + super.initState(); + Future.delayed(const Duration(milliseconds: 250)).then((_) { + if (mounted) { + animController.forward(); + } + }); + } + + @override + dispose() { + animController.dispose(); + super.dispose(); + } + + late final animController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + late final Animation anim = CurvedAnimation( + parent: animController, + curve: Curves.easeIn, + ); +} + +class _RetouchShowcase extends StatefulWidget { + const _RetouchShowcase(); + + @override + createState() => _RetouchShowcaseState(); +} + +class _RetouchShowcaseState extends State<_RetouchShowcase> + with TickerProviderStateMixin, _ShowcaseStateMixin { + @override + build(BuildContext context) => Stack( + fit: StackFit.expand, + children: [ + Image.asset( + "assets/retouch0.jpg", + fit: BoxFit.contain, + gaplessPlayback: true, + ), + CircularRevealAnimation( + animation: anim, + centerAlignment: Alignment.bottomCenter, + child: Image.asset( + "assets/retouch1.jpg", + fit: BoxFit.contain, + gaplessPlayback: true, + ), + ), + ], + ); +} + +class _ColorPopShowcase extends StatefulWidget { + const _ColorPopShowcase(); + + @override + createState() => _ColorPopShowcaseState(); +} + +class _ColorPopShowcaseState extends State<_ColorPopShowcase> + with TickerProviderStateMixin, _ShowcaseStateMixin { + @override + build(BuildContext context) => Stack( + fit: StackFit.expand, + children: [ + Image.asset( + "assets/color-pop0.jpg", + fit: BoxFit.contain, + gaplessPlayback: true, + ), + CircularRevealAnimation( + animation: anim, + centerAlignment: Alignment.bottomCenter, + child: Image.asset( + "assets/color-pop1.jpg", + fit: BoxFit.contain, + gaplessPlayback: true, + ), + ), + ], + ); +} + +class _LowLightShowcase extends StatefulWidget { + const _LowLightShowcase(); + + @override + createState() => _LowLightShowcaseState(); +} + +class _LowLightShowcaseState extends State<_LowLightShowcase> + with TickerProviderStateMixin, _ShowcaseStateMixin { + @override + build(BuildContext context) => Stack( + fit: StackFit.expand, + children: [ + Image.asset( + "assets/low-light0.jpg", + fit: BoxFit.contain, + gaplessPlayback: true, + ), + CircularRevealAnimation( + animation: anim, + centerAlignment: Alignment.bottomCenter, + child: Image.asset( + "assets/low-light1.jpg", + fit: BoxFit.contain, + gaplessPlayback: true, + ), + ), + ], + ); +} + +class _PortraitBlurShowcase extends StatefulWidget { + const _PortraitBlurShowcase(); + + @override + createState() => _PortraitBlurShowcaseState(); +} + +class _PortraitBlurShowcaseState extends State<_PortraitBlurShowcase> + with TickerProviderStateMixin, _ShowcaseStateMixin { + @override + build(BuildContext context) => Stack( + fit: StackFit.expand, + children: [ + Image.asset( + "assets/portrait-blur0.jpg", + fit: BoxFit.contain, + gaplessPlayback: true, + ), + CircularRevealAnimation( + animation: anim, + centerAlignment: Alignment.bottomCenter, + child: Image.asset( + "assets/portrait-blur1.jpg", + fit: BoxFit.contain, + gaplessPlayback: true, + ), + ), + ], + ); +} + +class _SuperResolutionShowcase extends StatefulWidget { + const _SuperResolutionShowcase(); + + @override + createState() => _SuperResolutionShowcaseState(); +} + +class _SuperResolutionShowcaseState extends State<_SuperResolutionShowcase> + with TickerProviderStateMixin, _ShowcaseStateMixin { + @override + build(BuildContext context) => Stack( + fit: StackFit.expand, + children: [ + Image.asset( + "assets/super-resolution0.jpg", + fit: BoxFit.contain, + gaplessPlayback: true, + ), + CircularRevealAnimation( + animation: anim, + centerAlignment: Alignment.bottomCenter, + child: Image.asset( + "assets/super-resolution1.jpg", + fit: BoxFit.contain, + gaplessPlayback: true, + ), + ), + ], + ); +} + +class _StyleTransferShowcase extends StatefulWidget { + const _StyleTransferShowcase(); + + @override + createState() => _StyleTransferShowcaseState(); +} + +class _StyleTransferShowcaseState extends State<_StyleTransferShowcase> + with TickerProviderStateMixin, _ShowcaseStateMixin { + @override + build(BuildContext context) => Stack( + fit: StackFit.expand, + children: [ + Image.asset( + "assets/style-transfer0.jpg", + fit: BoxFit.contain, + gaplessPlayback: true, + ), + CircularRevealAnimation( + animation: anim, + centerAlignment: Alignment.bottomCenter, + child: Image.asset( + "assets/style-transfer1.jpg", + fit: BoxFit.contain, + gaplessPlayback: true, + ), + ), + ], + ); +} + class _StylePickerResult { const _StylePickerResult(this.styleUri, this.weight); @@ -602,8 +975,9 @@ class _StylePickerState extends State<_StylePicker> { TextButton( onPressed: () { if (_selected == null) { - SnackBarManager().showSnackBar(const SnackBar( - content: Text("Please pick a style"), + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global() + .enhanceStyleTransferNoStyleSelectedNotification), duration: k.snackBarDurationNormal, )); } else { @@ -676,6 +1050,5 @@ class _StylePickerState extends State<_StylePicker> { "file:///android_asset/tf/arbitrary-style-transfer/6.jpg", ]; - static final _log = - Logger("widget.handler.enhance_handler._StylePickerState"); + static final _log = Logger("widget.image_enhancer._StylePickerState"); } diff --git a/app/lib/widget/my_app.dart b/app/lib/widget/my_app.dart index f067d6ec..95fa07c1 100644 --- a/app/lib/widget/my_app.dart +++ b/app/lib/widget/my_app.dart @@ -20,6 +20,7 @@ import 'package:nc_photos/widget/dynamic_album_browser.dart'; import 'package:nc_photos/widget/enhanced_photo_browser.dart'; import 'package:nc_photos/widget/home.dart'; import 'package:nc_photos/widget/image_editor.dart'; +import 'package:nc_photos/widget/image_enhancer.dart'; import 'package:nc_photos/widget/local_file_viewer.dart'; import 'package:nc_photos/widget/people_browser.dart'; import 'package:nc_photos/widget/person_browser.dart'; @@ -173,6 +174,7 @@ class _MyAppState extends State route ??= _handlePlaceBrowserRoute(settings); route ??= _handlePlacesBrowserRoute(settings); route ??= _handleResultViewerRoute(settings); + route ??= _handleImageEnhancerRoute(settings); return route; } @@ -621,6 +623,19 @@ class _MyAppState extends State return null; } + Route? _handleImageEnhancerRoute(RouteSettings settings) { + try { + if (settings.name == ImageEnhancer.routeName && + settings.arguments != null) { + final args = settings.arguments as ImageEnhancerArguments; + return ImageEnhancer.buildRoute(args); + } + } catch (e) { + _log.severe("[_handleImageEnhancerRoute] Failed while handling route", e); + } + return null; + } + final _scaffoldMessengerKey = GlobalKey(); final _navigatorKey = GlobalKey(); diff --git a/app/lib/widget/viewer.dart b/app/lib/widget/viewer.dart index 3efec664..2e452c5e 100644 --- a/app/lib/widget/viewer.dart +++ b/app/lib/widget/viewer.dart @@ -24,10 +24,10 @@ import 'package:nc_photos/theme.dart'; import 'package:nc_photos/use_case/update_property.dart'; import 'package:nc_photos/widget/animated_visibility.dart'; import 'package:nc_photos/widget/disposable.dart'; -import 'package:nc_photos/widget/handler/enhance_handler.dart'; import 'package:nc_photos/widget/handler/remove_selection_handler.dart'; import 'package:nc_photos/widget/horizontal_page_viewer.dart'; import 'package:nc_photos/widget/image_editor.dart'; +import 'package:nc_photos/widget/image_enhancer.dart'; import 'package:nc_photos/widget/image_viewer.dart'; import 'package:nc_photos/widget/slideshow_dialog.dart'; import 'package:nc_photos/widget/slideshow_viewer.dart'; @@ -217,7 +217,7 @@ class _ViewerState extends State onPressed: () => _onSharePressed(context), ), if (features.isSupportEnhancement && - EnhanceHandler.isSupportedFormat(file)) ...[ + ImageEnhancer.isSupportedFormat(file)) ...[ IconButton( icon: Icon( Icons.tune_outlined, @@ -605,11 +605,9 @@ class _ViewerState extends State final c = KiwiContainer().resolve(); _log.info("[_onEnhancePressed] Enhance file: ${file.path}"); - EnhanceHandler( - account: widget.account, - file: file, - isSaveToServer: c.pref.isSaveEditResultToServerOr(), - )(context); + Navigator.of(context).pushNamed(ImageEnhancer.routeName, + arguments: ImageEnhancerArguments( + widget.account, file, c.pref.isSaveEditResultToServerOr())); } void _onDownloadPressed() { diff --git a/app/pubspec.lock b/app/pubspec.lock index 0428992d..5b5061e3 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -213,6 +213,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + circular_reveal_animation: + dependency: "direct main" + description: + name: circular_reveal_animation + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" cli_util: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index c04ad154..6f86ad15 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: bloc: ^8.0.0 bloc_concurrency: ^0.2.0 cached_network_image: ^3.2.1 + circular_reveal_animation: ^2.0.1 collection: ^1.15.0 connectivity_plus: ^2.0.2 devicelocale: ^0.5.0