nc-photos/app/lib/widget/image_enhancer.dart
2023-07-18 00:44:30 +08:00

1029 lines
30 KiB
Dart

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/app_localizations.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/help_utils.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/mobile/android/android_info.dart';
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/np_api_util.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/url_launcher_util.dart';
import 'package:nc_photos/widget/handler/permission_handler.dart';
import 'package:nc_photos/widget/image_editor_persist_option_dialog.dart';
import 'package:nc_photos/widget/selectable.dart';
import 'package:nc_photos/widget/settings.dart';
import 'package:nc_photos/widget/stateful_slider.dart';
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
import 'package:np_codegen/np_codegen.dart';
part 'image_enhancer.g.dart';
class ImageEnhancerArguments {
const ImageEnhancerArguments(this.account, this.file, this.isSaveToServer);
final Account account;
final FileDescriptor 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(FileDescriptor file) =>
file_util.isSupportedImageFormat(file) && file.fdMime != "image/gif";
const ImageEnhancer({
super.key,
required this.account,
required this.file,
required this.isSaveToServer,
});
ImageEnhancer.fromArgs(ImageEnhancerArguments args, {Key? key})
: this(
key: key,
account: args.account,
file: args.file,
isSaveToServer: args.isSaveToServer,
);
@override
createState() => _ImageEnhancerState();
final Account account;
final FileDescriptor file;
final bool isSaveToServer;
}
@npLog
class _ImageEnhancerState extends State<ImageEnhancer> {
@override
initState() {
super.initState();
_c = KiwiContainer().resolve<DiContainer>();
WidgetsBinding.instance.addPostFrameCallback((_) {
_showInitialDialogs();
});
}
@override
build(BuildContext context) => Theme(
data: buildDarkTheme(),
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,
elevation: 0,
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<void> _onSavePressed(BuildContext context) async {
final args = await _getArgs(context, _selectedOption.algorithm);
if (args == null) {
// user canceled
return;
}
switch (_selectedOption.algorithm) {
case _Algorithm.zeroDce:
await ImageProcessor.zeroDce(
"${widget.account.url}/${widget.file.fdPath}",
widget.file.filename,
_c.pref.getEnhanceMaxWidthOr(),
_c.pref.getEnhanceMaxHeightOr(),
args["iteration"] ?? 8,
headers: {
"Authorization":
AuthUtil.fromAccount(widget.account).toHeaderValue(),
},
isSaveToServer: widget.isSaveToServer,
);
break;
case _Algorithm.deepLab3Portrait:
await ImageProcessor.deepLab3Portrait(
"${widget.account.url}/${widget.file.fdPath}",
widget.file.filename,
_c.pref.getEnhanceMaxWidthOr(),
_c.pref.getEnhanceMaxHeightOr(),
args["radius"] ?? 16,
headers: {
"Authorization":
AuthUtil.fromAccount(widget.account).toHeaderValue(),
},
isSaveToServer: widget.isSaveToServer,
);
break;
case _Algorithm.esrgan:
await ImageProcessor.esrgan(
"${widget.account.url}/${widget.file.fdPath}",
widget.file.filename,
_c.pref.getEnhanceMaxWidthOr(),
_c.pref.getEnhanceMaxHeightOr(),
headers: {
"Authorization":
AuthUtil.fromAccount(widget.account).toHeaderValue(),
},
isSaveToServer: widget.isSaveToServer,
);
break;
case _Algorithm.arbitraryStyleTransfer:
await ImageProcessor.arbitraryStyleTransfer(
"${widget.account.url}/${widget.file.fdPath}",
widget.file.filename,
math.min(
_c.pref.getEnhanceMaxWidthOr(), _isAtLeast5GbRam() ? 1600 : 1280),
math.min(
_c.pref.getEnhanceMaxHeightOr(), _isAtLeast5GbRam() ? 1200 : 960),
args["styleUri"],
args["weight"],
headers: {
"Authorization":
AuthUtil.fromAccount(widget.account).toHeaderValue(),
},
isSaveToServer: widget.isSaveToServer,
);
break;
case _Algorithm.deepLab3ColorPop:
await ImageProcessor.deepLab3ColorPop(
"${widget.account.url}/${widget.file.fdPath}",
widget.file.filename,
_c.pref.getEnhanceMaxWidthOr(),
_c.pref.getEnhanceMaxHeightOr(),
args["weight"],
headers: {
"Authorization":
AuthUtil.fromAccount(widget.account).toHeaderValue(),
},
isSaveToServer: widget.isSaveToServer,
);
break;
case _Algorithm.neurOp:
await ImageProcessor.neurOp(
"${widget.account.url}/${widget.file.fdPath}",
widget.file.filename,
_c.pref.getEnhanceMaxWidthOr(),
_c.pref.getEnhanceMaxHeightOr(),
headers: {
"Authorization":
AuthUtil.fromAccount(widget.account).toHeaderValue(),
},
isSaveToServer: widget.isSaveToServer,
);
break;
}
Navigator.of(context).pop();
}
Future<void> _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<bool> _ensurePermission() async {
if (!await const PermissionHandler().ensureStorageWritePermission()) {
if (mounted) {
Navigator.of(context).pop();
}
return false;
} else {
return true;
}
}
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),
),
],
),
);
unawaited(_c.pref.setHasShownEnhanceInfo(true));
}
Future<void> _showSaveEditResultDialog(BuildContext context) async {
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) =>
const ImageEditorPersistOptionDialog(isFromEditor: false),
);
}
Future<Map<String, dynamic>?> _getArgs(
BuildContext context, _Algorithm selected) async {
switch (selected) {
case _Algorithm.zeroDce:
return _getZeroDceArgs(context);
case _Algorithm.deepLab3Portrait:
return _getDeepLab3PortraitArgs(context);
case _Algorithm.esrgan:
return {};
case _Algorithm.arbitraryStyleTransfer:
return _getArbitraryStyleTransferArgs(context);
case _Algorithm.deepLab3ColorPop:
return _getDeepLab3ColorPopArgs(context);
case _Algorithm.neurOp:
return {};
}
}
Future<Map<String, dynamic>?> _getZeroDceArgs(BuildContext context) async {
var current = .8;
final iteration = await showDialog<int>(
context: context,
builder: (context) => 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: [
const Icon(Icons.brightness_low),
Expanded(
child: StatefulSlider(
initialValue: current,
onChangeEnd: (value) {
current = value;
},
),
),
const Icon(Icons.brightness_high),
],
),
],
),
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) => 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: [
const Icon(Icons.circle, size: 20),
Expanded(
child: StatefulSlider(
initialValue: current,
onChangeEnd: (value) {
current = value;
},
),
),
const Icon(Icons.blur_on),
],
),
],
),
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});
}
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,
};
}
}
Future<Map<String, dynamic>?> _getDeepLab3ColorPopArgs(
BuildContext context) async {
var current = 1.0;
final weight = await showDialog<double>(
context: context,
builder: (context) => 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: [
const Icon(Icons.water_drop, size: 20),
Expanded(
child: StatefulSlider(
initialValue: current,
onChangeEnd: (value) {
current = value;
},
),
),
const Icon(Icons.water_drop_outlined),
],
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(current);
},
child: Text(L10n.global().enhanceButtonLabel),
),
],
),
);
_log.info("[_getDeepLab3ColorPopArgs] weight: $weight");
return weight?.run((it) => {"weight": it});
}
bool _isAtLeast4GbRam() {
// We can't compare with 4096 directly as some RAM are preserved
return AndroidInfo().totalMemMb > 3584;
}
bool _isAtLeast5GbRam() {
return AndroidInfo().totalMemMb > 4608;
}
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,
),
],
];
late final DiContainer _c;
late var _selectedOption = _options[0];
late final _pageController = PageController(keepPage: false);
}
enum _Algorithm {
zeroDce,
deepLab3Portrait,
esrgan,
arbitraryStyleTransfer,
deepLab3ColorPop,
neurOp,
}
class _Option {
const _Option({
required this.title,
required this.description,
required this.link,
required this.showcaseBuilder,
required this.algorithm,
});
final String title;
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
? Theme.of(context).colorScheme.secondaryContainer
: null,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
title,
style: TextStyle(
color: isSelected
? Theme.of(context).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onSurface,
),
),
),
),
),
);
}
final String title;
final bool isSelected;
final VoidCallback? onTap;
}
mixin _ShowcaseStateMixin<T extends StatefulWidget>
on State<T>, TickerProviderStateMixin<T> {
@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<double> 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);
final String styleUri;
final double weight;
}
class _StylePicker extends StatefulWidget {
const _StylePicker({
Key? key,
}) : super(key: key);
@override
createState() => _StylePickerState();
}
@npLog
class _StylePickerState extends State<_StylePicker> {
@override
build(BuildContext context) {
return AlertDialog(
title: Text(L10n.global().enhanceStyleTransferStyleDialogTitle),
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: [
..._bundledStyles.mapIndexed((i, e) => _buildItem(
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),
),
),
],
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.max,
children: [
const Icon(Icons.auto_fix_normal),
Expanded(
child: StatefulSlider(
initialValue: _weight,
min: .01,
onChangeEnd: (value) {
_weight = value;
},
),
),
const Icon(Icons.auto_fix_high),
],
),
],
),
actions: [
TextButton(
onPressed: () {
if (_selected == null) {
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global()
.enhanceStyleTransferNoStyleSelectedNotification),
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,
indicatorAlignment: Alignment.center,
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",
];
}