mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +01:00
Refactor: extract color tools from main editor
This commit is contained in:
parent
50849817a6
commit
839ef8bb03
3 changed files with 537 additions and 443 deletions
|
@ -6,17 +6,15 @@ import 'package:nc_photos/api/api.dart';
|
||||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||||
import 'package:nc_photos/app_localizations.dart';
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
import 'package:nc_photos/cache_manager_util.dart';
|
import 'package:nc_photos/cache_manager_util.dart';
|
||||||
import 'package:nc_photos/double_extension.dart';
|
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/help_utils.dart' as help_util;
|
import 'package:nc_photos/help_utils.dart' as help_util;
|
||||||
import 'package:nc_photos/iterable_extension.dart';
|
|
||||||
import 'package:nc_photos/k.dart' as k;
|
import 'package:nc_photos/k.dart' as k;
|
||||||
import 'package:nc_photos/object_extension.dart';
|
import 'package:nc_photos/object_extension.dart';
|
||||||
import 'package:nc_photos/pixel_image_provider.dart';
|
import 'package:nc_photos/pixel_image_provider.dart';
|
||||||
import 'package:nc_photos/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/url_launcher_util.dart';
|
import 'package:nc_photos/url_launcher_util.dart';
|
||||||
import 'package:nc_photos/widget/handler/permission_handler.dart';
|
import 'package:nc_photos/widget/handler/permission_handler.dart';
|
||||||
import 'package:nc_photos/widget/stateful_slider.dart';
|
import 'package:nc_photos/widget/image_editor/color_toolbar.dart';
|
||||||
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
||||||
|
|
||||||
class ImageEditorArguments {
|
class ImageEditorArguments {
|
||||||
|
@ -122,8 +120,13 @@ class _ImageEditorState extends State<ImageEditor> {
|
||||||
)
|
)
|
||||||
: Container(),
|
: Container(),
|
||||||
),
|
),
|
||||||
_buildFilterOption(context),
|
ColorToolbar(
|
||||||
_buildFilterBar(context),
|
initialState: _colorFilters,
|
||||||
|
onActiveFiltersChanged: (colorFilters) {
|
||||||
|
_colorFilters = colorFilters.toList();
|
||||||
|
_applyFilters();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -137,7 +140,7 @@ class _ImageEditorState extends State<ImageEditor> {
|
||||||
leading: BackButton(onPressed: () => _onBackButton(context)),
|
leading: BackButton(onPressed: () => _onBackButton(context)),
|
||||||
title: Text(L10n.global().imageEditTitle),
|
title: Text(L10n.global().imageEditTitle),
|
||||||
actions: [
|
actions: [
|
||||||
if (_filters.isNotEmpty)
|
if (_isModified)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.save_outlined),
|
icon: const Icon(Icons.save_outlined),
|
||||||
tooltip: L10n.global().saveTooltip,
|
tooltip: L10n.global().saveTooltip,
|
||||||
|
@ -153,268 +156,8 @@ class _ImageEditorState extends State<ImageEditor> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildFilterBar(BuildContext context) {
|
|
||||||
return Align(
|
|
||||||
alignment: AlignmentDirectional.centerStart,
|
|
||||||
child: Material(
|
|
||||||
type: MaterialType.transparency,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
_FilterButton(
|
|
||||||
icon: Icons.brightness_medium,
|
|
||||||
label: L10n.global().imageEditColorBrightness,
|
|
||||||
onPressed: _onBrightnessPressed,
|
|
||||||
isSelected: _selectedFilter == _ColorFilterType.brightness,
|
|
||||||
activationOrder:
|
|
||||||
_filters.keys.indexOf(_ColorFilterType.brightness),
|
|
||||||
),
|
|
||||||
_FilterButton(
|
|
||||||
icon: Icons.contrast,
|
|
||||||
label: L10n.global().imageEditColorContrast,
|
|
||||||
onPressed: _onContrastPressed,
|
|
||||||
isSelected: _selectedFilter == _ColorFilterType.contrast,
|
|
||||||
activationOrder:
|
|
||||||
_filters.keys.indexOf(_ColorFilterType.contrast),
|
|
||||||
),
|
|
||||||
_FilterButton(
|
|
||||||
icon: Icons.circle,
|
|
||||||
label: L10n.global().imageEditColorWhitePoint,
|
|
||||||
onPressed: _onWhitePointPressed,
|
|
||||||
isSelected: _selectedFilter == _ColorFilterType.whitePoint,
|
|
||||||
activationOrder:
|
|
||||||
_filters.keys.indexOf(_ColorFilterType.whitePoint),
|
|
||||||
),
|
|
||||||
_FilterButton(
|
|
||||||
icon: Icons.circle_outlined,
|
|
||||||
label: L10n.global().imageEditColorBlackPoint,
|
|
||||||
onPressed: _onBlackPointPressed,
|
|
||||||
isSelected: _selectedFilter == _ColorFilterType.blackPoint,
|
|
||||||
activationOrder:
|
|
||||||
_filters.keys.indexOf(_ColorFilterType.blackPoint),
|
|
||||||
),
|
|
||||||
_FilterButton(
|
|
||||||
icon: Icons.invert_colors,
|
|
||||||
label: L10n.global().imageEditColorSaturation,
|
|
||||||
onPressed: _onSaturationPressed,
|
|
||||||
isSelected: _selectedFilter == _ColorFilterType.saturation,
|
|
||||||
activationOrder:
|
|
||||||
_filters.keys.indexOf(_ColorFilterType.saturation),
|
|
||||||
),
|
|
||||||
_FilterButton(
|
|
||||||
icon: Icons.thermostat,
|
|
||||||
label: L10n.global().imageEditColorWarmth,
|
|
||||||
onPressed: _onWarmthPressed,
|
|
||||||
isSelected: _selectedFilter == _ColorFilterType.warmth,
|
|
||||||
activationOrder: _filters.keys.indexOf(_ColorFilterType.warmth),
|
|
||||||
),
|
|
||||||
_FilterButton(
|
|
||||||
icon: Icons.colorize,
|
|
||||||
label: L10n.global().imageEditColorTint,
|
|
||||||
onPressed: _onTintPressed,
|
|
||||||
isSelected: _selectedFilter == _ColorFilterType.tint,
|
|
||||||
activationOrder: _filters.keys.indexOf(_ColorFilterType.tint),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFilterOption(BuildContext context) {
|
|
||||||
Widget? child;
|
|
||||||
switch (_selectedFilter) {
|
|
||||||
case _ColorFilterType.brightness:
|
|
||||||
child = _buildBrightnessOption(context);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case _ColorFilterType.contrast:
|
|
||||||
child = _buildContrastOption(context);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case _ColorFilterType.whitePoint:
|
|
||||||
child = _buildWhitePointOption(context);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case _ColorFilterType.blackPoint:
|
|
||||||
child = _buildBlackPointOption(context);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case _ColorFilterType.saturation:
|
|
||||||
child = _buildSaturationOption(context);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case _ColorFilterType.warmth:
|
|
||||||
child = _buildWarmthOption(context);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case _ColorFilterType.tint:
|
|
||||||
child = _buildTintOption(context);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case null:
|
|
||||||
child = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return Container(
|
|
||||||
height: 96,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSliderOption(
|
|
||||||
BuildContext context, {
|
|
||||||
required Key key,
|
|
||||||
required double min,
|
|
||||||
required double max,
|
|
||||||
required double initialValue,
|
|
||||||
ValueChanged<double>? onChangeEnd,
|
|
||||||
}) {
|
|
||||||
return AppTheme.dark(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: AlignmentDirectional.centerStart,
|
|
||||||
child: Text(min.toStringAsFixedTruncated(1)),
|
|
||||||
),
|
|
||||||
if (min < 0 && max > 0)
|
|
||||||
const Align(
|
|
||||||
alignment: AlignmentDirectional.center,
|
|
||||||
child: Text("0"),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: AlignmentDirectional.centerEnd,
|
|
||||||
child: Text(max.toStringAsFixedTruncated(1)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
StatefulSlider(
|
|
||||||
key: key,
|
|
||||||
initialValue: initialValue.toDouble(),
|
|
||||||
min: min.toDouble(),
|
|
||||||
max: max.toDouble(),
|
|
||||||
onChangeEnd: onChangeEnd,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBrightnessOption(BuildContext context) => _buildSliderOption(
|
|
||||||
context,
|
|
||||||
key: Key(_ColorFilterType.brightness.name),
|
|
||||||
min: -100,
|
|
||||||
max: 100,
|
|
||||||
initialValue:
|
|
||||||
(_filters[_ColorFilterType.brightness] as _BrightnessArguments)
|
|
||||||
.value,
|
|
||||||
onChangeEnd: (value) {
|
|
||||||
(_filters[_ColorFilterType.brightness] as _BrightnessArguments)
|
|
||||||
.value = value;
|
|
||||||
_applyFilters();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildContrastOption(BuildContext context) => _buildSliderOption(
|
|
||||||
context,
|
|
||||||
key: Key(_ColorFilterType.contrast.name),
|
|
||||||
min: -100,
|
|
||||||
max: 100,
|
|
||||||
initialValue:
|
|
||||||
(_filters[_ColorFilterType.contrast] as _ContrastArguments).value,
|
|
||||||
onChangeEnd: (value) {
|
|
||||||
(_filters[_ColorFilterType.contrast] as _ContrastArguments).value =
|
|
||||||
value;
|
|
||||||
_applyFilters();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildWhitePointOption(BuildContext context) => _buildSliderOption(
|
|
||||||
context,
|
|
||||||
key: Key(_ColorFilterType.whitePoint.name),
|
|
||||||
min: -100,
|
|
||||||
max: 100,
|
|
||||||
initialValue:
|
|
||||||
(_filters[_ColorFilterType.whitePoint] as _WhitePointArguments)
|
|
||||||
.value,
|
|
||||||
onChangeEnd: (value) {
|
|
||||||
(_filters[_ColorFilterType.whitePoint] as _WhitePointArguments)
|
|
||||||
.value = value;
|
|
||||||
_applyFilters();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildBlackPointOption(BuildContext context) => _buildSliderOption(
|
|
||||||
context,
|
|
||||||
key: Key(_ColorFilterType.blackPoint.name),
|
|
||||||
min: -100,
|
|
||||||
max: 100,
|
|
||||||
initialValue:
|
|
||||||
(_filters[_ColorFilterType.blackPoint] as _BlackPointArguments)
|
|
||||||
.value,
|
|
||||||
onChangeEnd: (value) {
|
|
||||||
(_filters[_ColorFilterType.blackPoint] as _BlackPointArguments)
|
|
||||||
.value = value;
|
|
||||||
_applyFilters();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildSaturationOption(BuildContext context) => _buildSliderOption(
|
|
||||||
context,
|
|
||||||
key: Key(_ColorFilterType.saturation.name),
|
|
||||||
min: -100,
|
|
||||||
max: 100,
|
|
||||||
initialValue:
|
|
||||||
(_filters[_ColorFilterType.saturation] as _SaturationArguments)
|
|
||||||
.value,
|
|
||||||
onChangeEnd: (value) {
|
|
||||||
(_filters[_ColorFilterType.saturation] as _SaturationArguments)
|
|
||||||
.value = value;
|
|
||||||
_applyFilters();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildWarmthOption(BuildContext context) => _buildSliderOption(
|
|
||||||
context,
|
|
||||||
key: Key(_ColorFilterType.warmth.name),
|
|
||||||
min: -100,
|
|
||||||
max: 100,
|
|
||||||
initialValue:
|
|
||||||
(_filters[_ColorFilterType.warmth] as _WarmthArguments).value,
|
|
||||||
onChangeEnd: (value) {
|
|
||||||
(_filters[_ColorFilterType.warmth] as _WarmthArguments).value = value;
|
|
||||||
_applyFilters();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildTintOption(BuildContext context) => _buildSliderOption(
|
|
||||||
context,
|
|
||||||
key: Key(_ColorFilterType.tint.name),
|
|
||||||
min: -100,
|
|
||||||
max: 100,
|
|
||||||
initialValue: (_filters[_ColorFilterType.tint] as _TintArguments).value,
|
|
||||||
onChangeEnd: (value) {
|
|
||||||
(_filters[_ColorFilterType.tint] as _TintArguments).value = value;
|
|
||||||
_applyFilters();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<void> _onBackButton(BuildContext context) async {
|
Future<void> _onBackButton(BuildContext context) async {
|
||||||
if (_filters.isEmpty) {
|
if (!_isModified) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -459,69 +202,8 @@ class _ImageEditorState extends State<ImageEditor> {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onFilterPressed(_ColorFilterType type, _FilterArguments defArgs) {
|
|
||||||
if (_selectedFilter == type) {
|
|
||||||
// deactivate filter
|
|
||||||
setState(() {
|
|
||||||
_selectedFilter = null;
|
|
||||||
_filters.remove(type);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_selectedFilter = type;
|
|
||||||
_filters[type] ??= defArgs;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_applyFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onBrightnessPressed() =>
|
|
||||||
_onFilterPressed(_ColorFilterType.brightness, _BrightnessArguments(0));
|
|
||||||
void _onContrastPressed() =>
|
|
||||||
_onFilterPressed(_ColorFilterType.contrast, _ContrastArguments(0));
|
|
||||||
void _onWhitePointPressed() =>
|
|
||||||
_onFilterPressed(_ColorFilterType.whitePoint, _WhitePointArguments(0));
|
|
||||||
void _onBlackPointPressed() =>
|
|
||||||
_onFilterPressed(_ColorFilterType.blackPoint, _BlackPointArguments(0));
|
|
||||||
void _onSaturationPressed() =>
|
|
||||||
_onFilterPressed(_ColorFilterType.saturation, _SaturationArguments(0));
|
|
||||||
void _onWarmthPressed() =>
|
|
||||||
_onFilterPressed(_ColorFilterType.warmth, _WarmthArguments(0));
|
|
||||||
void _onTintPressed() =>
|
|
||||||
_onFilterPressed(_ColorFilterType.tint, _TintArguments(0));
|
|
||||||
|
|
||||||
List<ImageFilter> _buildFilterList() {
|
List<ImageFilter> _buildFilterList() {
|
||||||
return _filters.entries.map((e) {
|
return _colorFilters.map((f) => f.toImageFilter()).toList();
|
||||||
switch (e.key) {
|
|
||||||
case _ColorFilterType.brightness:
|
|
||||||
return (e.value as _BrightnessArguments)
|
|
||||||
.run((arg) => ColorBrightnessFilter(arg.value / 100));
|
|
||||||
|
|
||||||
case _ColorFilterType.contrast:
|
|
||||||
return (e.value as _ContrastArguments)
|
|
||||||
.run((arg) => ColorContrastFilter(arg.value / 100));
|
|
||||||
|
|
||||||
case _ColorFilterType.whitePoint:
|
|
||||||
return (e.value as _WhitePointArguments)
|
|
||||||
.run((arg) => ColorWhitePointFilter(arg.value / 100));
|
|
||||||
|
|
||||||
case _ColorFilterType.blackPoint:
|
|
||||||
return (e.value as _BlackPointArguments)
|
|
||||||
.run((arg) => ColorBlackPointFilter(arg.value / 100));
|
|
||||||
|
|
||||||
case _ColorFilterType.saturation:
|
|
||||||
return (e.value as _SaturationArguments)
|
|
||||||
.run((arg) => ColorSaturationFilter(arg.value / 100));
|
|
||||||
|
|
||||||
case _ColorFilterType.warmth:
|
|
||||||
return (e.value as _WarmthArguments)
|
|
||||||
.run((arg) => ColorWarmthFilter(arg.value / 100));
|
|
||||||
|
|
||||||
case _ColorFilterType.tint:
|
|
||||||
return (e.value as _TintArguments)
|
|
||||||
.run((arg) => ColorTintFilter(arg.value / 100));
|
|
||||||
}
|
|
||||||
}).toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _applyFilters() async {
|
Future<void> _applyFilters() async {
|
||||||
|
@ -531,121 +213,11 @@ class _ImageEditorState extends State<ImageEditor> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _isModified => _colorFilters.isNotEmpty;
|
||||||
|
|
||||||
bool _isDoneInit = false;
|
bool _isDoneInit = false;
|
||||||
late final Rgba8Image _src;
|
late final Rgba8Image _src;
|
||||||
Rgba8Image? _dst;
|
Rgba8Image? _dst;
|
||||||
final _filters = <_ColorFilterType, _FilterArguments>{};
|
|
||||||
_ColorFilterType? _selectedFilter;
|
var _colorFilters = <ColorArguments>[];
|
||||||
}
|
|
||||||
|
|
||||||
enum _ColorFilterType {
|
|
||||||
brightness,
|
|
||||||
contrast,
|
|
||||||
whitePoint,
|
|
||||||
blackPoint,
|
|
||||||
saturation,
|
|
||||||
warmth,
|
|
||||||
tint,
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class _FilterArguments {}
|
|
||||||
|
|
||||||
class _FilterDoubleArguments implements _FilterArguments {
|
|
||||||
_FilterDoubleArguments(this.value);
|
|
||||||
|
|
||||||
double value;
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef _BrightnessArguments = _FilterDoubleArguments;
|
|
||||||
typedef _ContrastArguments = _FilterDoubleArguments;
|
|
||||||
typedef _WhitePointArguments = _FilterDoubleArguments;
|
|
||||||
typedef _BlackPointArguments = _FilterDoubleArguments;
|
|
||||||
typedef _SaturationArguments = _FilterDoubleArguments;
|
|
||||||
typedef _WarmthArguments = _FilterDoubleArguments;
|
|
||||||
typedef _TintArguments = _FilterDoubleArguments;
|
|
||||||
|
|
||||||
class _FilterButton extends StatelessWidget {
|
|
||||||
const _FilterButton({
|
|
||||||
Key? key,
|
|
||||||
required this.icon,
|
|
||||||
required this.label,
|
|
||||||
required this.onPressed,
|
|
||||||
this.isSelected = false,
|
|
||||||
this.activationOrder = -1,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
build(BuildContext context) {
|
|
||||||
final color = !isSelected && isActivated
|
|
||||||
? AppTheme.primarySwatchDark[900]!.withOpacity(0.4)
|
|
||||||
: AppTheme.primarySwatchDark[500]!.withOpacity(0.7);
|
|
||||||
return InkWell(
|
|
||||||
onTap: onPressed,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
AnimatedOpacity(
|
|
||||||
opacity: isSelected || isActivated ? 1 : 0,
|
|
||||||
duration: k.animationDurationNormal,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(32),
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: Icon(
|
|
||||||
icon,
|
|
||||||
size: 32,
|
|
||||||
color: AppTheme.unfocusedIconColorDark,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (isActivated)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 2),
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
child: Text(
|
|
||||||
(activationOrder + 1).toString(),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: AppTheme.unfocusedIconColorDark,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: AppTheme.unfocusedIconColorDark,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isActivated => activationOrder >= 0;
|
|
||||||
|
|
||||||
final IconData icon;
|
|
||||||
final String label;
|
|
||||||
final VoidCallback? onPressed;
|
|
||||||
final bool isSelected;
|
|
||||||
final int activationOrder;
|
|
||||||
}
|
}
|
||||||
|
|
428
app/lib/widget/image_editor/color_toolbar.dart
Normal file
428
app/lib/widget/image_editor/color_toolbar.dart
Normal file
|
@ -0,0 +1,428 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
|
import 'package:nc_photos/double_extension.dart';
|
||||||
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
|
import 'package:nc_photos/theme.dart';
|
||||||
|
import 'package:nc_photos/widget/image_editor/toolbar_button.dart';
|
||||||
|
import 'package:nc_photos/widget/stateful_slider.dart';
|
||||||
|
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
||||||
|
|
||||||
|
enum ColorToolType {
|
||||||
|
brightness,
|
||||||
|
contrast,
|
||||||
|
whitePoint,
|
||||||
|
blackPoint,
|
||||||
|
saturation,
|
||||||
|
warmth,
|
||||||
|
tint,
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ColorArguments {
|
||||||
|
ImageFilter toImageFilter();
|
||||||
|
|
||||||
|
ColorToolType _getToolType();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ColorToolbar extends StatefulWidget {
|
||||||
|
const ColorToolbar({
|
||||||
|
Key? key,
|
||||||
|
required this.initialState,
|
||||||
|
required this.onActiveFiltersChanged,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
createState() => _ColorToolbarState();
|
||||||
|
|
||||||
|
final List<ColorArguments> initialState;
|
||||||
|
final ValueChanged<Iterable<ColorArguments>> onActiveFiltersChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ColorToolbarState extends State<ColorToolbar> {
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
for (final s in widget.initialState) {
|
||||||
|
_filters[s._getToolType()] = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
build(BuildContext context) => Column(
|
||||||
|
children: [
|
||||||
|
_buildFilterOption(context),
|
||||||
|
_buildFilterBar(context),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildFilterOption(BuildContext context) {
|
||||||
|
Widget? child;
|
||||||
|
switch (_selectedFilter) {
|
||||||
|
case ColorToolType.brightness:
|
||||||
|
child = _buildBrightnessOption(context);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ColorToolType.contrast:
|
||||||
|
child = _buildContrastOption(context);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ColorToolType.whitePoint:
|
||||||
|
child = _buildWhitePointOption(context);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ColorToolType.blackPoint:
|
||||||
|
child = _buildBlackPointOption(context);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ColorToolType.saturation:
|
||||||
|
child = _buildSaturationOption(context);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ColorToolType.warmth:
|
||||||
|
child = _buildWarmthOption(context);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ColorToolType.tint:
|
||||||
|
child = _buildTintOption(context);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case null:
|
||||||
|
child = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
height: 80,
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilterBar(BuildContext context) {
|
||||||
|
return Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
ToolbarButton(
|
||||||
|
icon: Icons.brightness_medium,
|
||||||
|
label: L10n.global().imageEditColorBrightness,
|
||||||
|
onPressed: _onBrightnessPressed,
|
||||||
|
isSelected: _selectedFilter == ColorToolType.brightness,
|
||||||
|
activationOrder:
|
||||||
|
_filters.keys.indexOf(ColorToolType.brightness),
|
||||||
|
),
|
||||||
|
ToolbarButton(
|
||||||
|
icon: Icons.contrast,
|
||||||
|
label: L10n.global().imageEditColorContrast,
|
||||||
|
onPressed: _onContrastPressed,
|
||||||
|
isSelected: _selectedFilter == ColorToolType.contrast,
|
||||||
|
activationOrder: _filters.keys.indexOf(ColorToolType.contrast),
|
||||||
|
),
|
||||||
|
ToolbarButton(
|
||||||
|
icon: Icons.circle,
|
||||||
|
label: L10n.global().imageEditColorWhitePoint,
|
||||||
|
onPressed: _onWhitePointPressed,
|
||||||
|
isSelected: _selectedFilter == ColorToolType.whitePoint,
|
||||||
|
activationOrder:
|
||||||
|
_filters.keys.indexOf(ColorToolType.whitePoint),
|
||||||
|
),
|
||||||
|
ToolbarButton(
|
||||||
|
icon: Icons.circle_outlined,
|
||||||
|
label: L10n.global().imageEditColorBlackPoint,
|
||||||
|
onPressed: _onBlackPointPressed,
|
||||||
|
isSelected: _selectedFilter == ColorToolType.blackPoint,
|
||||||
|
activationOrder:
|
||||||
|
_filters.keys.indexOf(ColorToolType.blackPoint),
|
||||||
|
),
|
||||||
|
ToolbarButton(
|
||||||
|
icon: Icons.invert_colors,
|
||||||
|
label: L10n.global().imageEditColorSaturation,
|
||||||
|
onPressed: _onSaturationPressed,
|
||||||
|
isSelected: _selectedFilter == ColorToolType.saturation,
|
||||||
|
activationOrder:
|
||||||
|
_filters.keys.indexOf(ColorToolType.saturation),
|
||||||
|
),
|
||||||
|
ToolbarButton(
|
||||||
|
icon: Icons.thermostat,
|
||||||
|
label: L10n.global().imageEditColorWarmth,
|
||||||
|
onPressed: _onWarmthPressed,
|
||||||
|
isSelected: _selectedFilter == ColorToolType.warmth,
|
||||||
|
activationOrder: _filters.keys.indexOf(ColorToolType.warmth),
|
||||||
|
),
|
||||||
|
ToolbarButton(
|
||||||
|
icon: Icons.colorize,
|
||||||
|
label: L10n.global().imageEditColorTint,
|
||||||
|
onPressed: _onTintPressed,
|
||||||
|
isSelected: _selectedFilter == ColorToolType.tint,
|
||||||
|
activationOrder: _filters.keys.indexOf(ColorToolType.tint),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSliderOption(
|
||||||
|
BuildContext context, {
|
||||||
|
required Key key,
|
||||||
|
required double min,
|
||||||
|
required double max,
|
||||||
|
required double initialValue,
|
||||||
|
ValueChanged<double>? onChangeEnd,
|
||||||
|
}) {
|
||||||
|
return AppTheme.dark(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: Text(min.toStringAsFixedTruncated(1)),
|
||||||
|
),
|
||||||
|
if (min < 0 && max > 0)
|
||||||
|
const Align(
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
child: Text("0"),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
|
child: Text(max.toStringAsFixedTruncated(1)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
StatefulSlider(
|
||||||
|
key: key,
|
||||||
|
initialValue: initialValue.toDouble(),
|
||||||
|
min: min.toDouble(),
|
||||||
|
max: max.toDouble(),
|
||||||
|
onChangeEnd: onChangeEnd,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBrightnessOption(BuildContext context) => _buildSliderOption(
|
||||||
|
context,
|
||||||
|
key: Key(ColorToolType.brightness.name),
|
||||||
|
min: -100,
|
||||||
|
max: 100,
|
||||||
|
initialValue:
|
||||||
|
(_filters[ColorToolType.brightness] as _BrightnessArguments).value,
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
_filters[ColorToolType.brightness] = _BrightnessArguments(value);
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildContrastOption(BuildContext context) => _buildSliderOption(
|
||||||
|
context,
|
||||||
|
key: Key(ColorToolType.contrast.name),
|
||||||
|
min: -100,
|
||||||
|
max: 100,
|
||||||
|
initialValue:
|
||||||
|
(_filters[ColorToolType.contrast] as _ContrastArguments).value,
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
_filters[ColorToolType.contrast] = _ContrastArguments(value);
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildWhitePointOption(BuildContext context) => _buildSliderOption(
|
||||||
|
context,
|
||||||
|
key: Key(ColorToolType.whitePoint.name),
|
||||||
|
min: -100,
|
||||||
|
max: 100,
|
||||||
|
initialValue:
|
||||||
|
(_filters[ColorToolType.whitePoint] as _WhitePointArguments).value,
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
_filters[ColorToolType.whitePoint] = _WhitePointArguments(value);
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildBlackPointOption(BuildContext context) => _buildSliderOption(
|
||||||
|
context,
|
||||||
|
key: Key(ColorToolType.blackPoint.name),
|
||||||
|
min: -100,
|
||||||
|
max: 100,
|
||||||
|
initialValue:
|
||||||
|
(_filters[ColorToolType.blackPoint] as _BlackPointArguments).value,
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
_filters[ColorToolType.blackPoint] = _BlackPointArguments(value);
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildSaturationOption(BuildContext context) => _buildSliderOption(
|
||||||
|
context,
|
||||||
|
key: Key(ColorToolType.saturation.name),
|
||||||
|
min: -100,
|
||||||
|
max: 100,
|
||||||
|
initialValue:
|
||||||
|
(_filters[ColorToolType.saturation] as _SaturationArguments).value,
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
_filters[ColorToolType.saturation] = _SaturationArguments(value);
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildWarmthOption(BuildContext context) => _buildSliderOption(
|
||||||
|
context,
|
||||||
|
key: Key(ColorToolType.warmth.name),
|
||||||
|
min: -100,
|
||||||
|
max: 100,
|
||||||
|
initialValue:
|
||||||
|
(_filters[ColorToolType.warmth] as _WarmthArguments).value,
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
_filters[ColorToolType.warmth] = _WarmthArguments(value);
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildTintOption(BuildContext context) => _buildSliderOption(
|
||||||
|
context,
|
||||||
|
key: Key(ColorToolType.tint.name),
|
||||||
|
min: -100,
|
||||||
|
max: 100,
|
||||||
|
initialValue: (_filters[ColorToolType.tint] as _TintArguments).value,
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
_filters[ColorToolType.tint] = _TintArguments(value);
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
void _onFilterPressed(ColorToolType type, ColorArguments defArgs) {
|
||||||
|
if (_selectedFilter == type) {
|
||||||
|
// deactivate filter
|
||||||
|
setState(() {
|
||||||
|
_selectedFilter = null;
|
||||||
|
_filters.remove(type);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_selectedFilter = type;
|
||||||
|
_filters[type] ??= defArgs;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onBrightnessPressed() =>
|
||||||
|
_onFilterPressed(ColorToolType.brightness, const _BrightnessArguments(0));
|
||||||
|
void _onContrastPressed() =>
|
||||||
|
_onFilterPressed(ColorToolType.contrast, const _ContrastArguments(0));
|
||||||
|
void _onWhitePointPressed() =>
|
||||||
|
_onFilterPressed(ColorToolType.whitePoint, const _WhitePointArguments(0));
|
||||||
|
void _onBlackPointPressed() =>
|
||||||
|
_onFilterPressed(ColorToolType.blackPoint, const _BlackPointArguments(0));
|
||||||
|
void _onSaturationPressed() =>
|
||||||
|
_onFilterPressed(ColorToolType.saturation, const _SaturationArguments(0));
|
||||||
|
void _onWarmthPressed() =>
|
||||||
|
_onFilterPressed(ColorToolType.warmth, const _WarmthArguments(0));
|
||||||
|
void _onTintPressed() =>
|
||||||
|
_onFilterPressed(ColorToolType.tint, const _TintArguments(0));
|
||||||
|
|
||||||
|
void _notifyFiltersChanged() {
|
||||||
|
widget.onActiveFiltersChanged.call(_filters.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
final _filters = <ColorToolType, ColorArguments>{};
|
||||||
|
ColorToolType? _selectedFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BrightnessArguments implements ColorArguments {
|
||||||
|
const _BrightnessArguments(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toImageFilter() => ColorBrightnessFilter(value / 100);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_getToolType() => ColorToolType.brightness;
|
||||||
|
|
||||||
|
final double value;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContrastArguments implements ColorArguments {
|
||||||
|
const _ContrastArguments(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toImageFilter() => ColorContrastFilter(value / 100);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_getToolType() => ColorToolType.contrast;
|
||||||
|
|
||||||
|
final double value;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WhitePointArguments implements ColorArguments {
|
||||||
|
const _WhitePointArguments(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toImageFilter() => ColorWhitePointFilter(value / 100);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_getToolType() => ColorToolType.whitePoint;
|
||||||
|
|
||||||
|
final double value;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BlackPointArguments implements ColorArguments {
|
||||||
|
const _BlackPointArguments(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toImageFilter() => ColorBlackPointFilter(value / 100);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_getToolType() => ColorToolType.blackPoint;
|
||||||
|
|
||||||
|
final double value;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SaturationArguments implements ColorArguments {
|
||||||
|
const _SaturationArguments(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toImageFilter() => ColorSaturationFilter(value / 100);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_getToolType() => ColorToolType.saturation;
|
||||||
|
|
||||||
|
final double value;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WarmthArguments implements ColorArguments {
|
||||||
|
const _WarmthArguments(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toImageFilter() => ColorWarmthFilter(value / 100);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_getToolType() => ColorToolType.warmth;
|
||||||
|
|
||||||
|
final double value;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TintArguments implements ColorArguments {
|
||||||
|
const _TintArguments(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toImageFilter() => ColorTintFilter(value / 100);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_getToolType() => ColorToolType.tint;
|
||||||
|
|
||||||
|
final double value;
|
||||||
|
}
|
94
app/lib/widget/image_editor/toolbar_button.dart
Normal file
94
app/lib/widget/image_editor/toolbar_button.dart
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:nc_photos/k.dart' as k;
|
||||||
|
import 'package:nc_photos/theme.dart';
|
||||||
|
|
||||||
|
class ToolbarButton extends StatelessWidget {
|
||||||
|
const ToolbarButton({
|
||||||
|
Key? key,
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.onPressed,
|
||||||
|
this.isSelected = false,
|
||||||
|
this.activationOrder = -1,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
build(BuildContext context) {
|
||||||
|
final color = !isSelected && isActivated
|
||||||
|
? AppTheme.primarySwatchDark[900]!.withOpacity(0.4)
|
||||||
|
: AppTheme.primarySwatchDark[500]!.withOpacity(0.7);
|
||||||
|
return InkWell(
|
||||||
|
onTap: onPressed,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
AnimatedOpacity(
|
||||||
|
opacity: isSelected || isActivated ? 1 : 0,
|
||||||
|
duration: k.animationDurationNormal,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
size: 32,
|
||||||
|
color: isSelected
|
||||||
|
? Colors.white
|
||||||
|
: AppTheme.unfocusedIconColorDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isActivated)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 2),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: Text(
|
||||||
|
(activationOrder + 1).toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: isSelected
|
||||||
|
? Colors.white
|
||||||
|
: AppTheme.unfocusedIconColorDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color:
|
||||||
|
isSelected ? Colors.white : AppTheme.unfocusedIconColorDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isActivated => activationOrder >= 0;
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final bool isSelected;
|
||||||
|
final int activationOrder;
|
||||||
|
}
|
Loading…
Reference in a new issue