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/app_localizations.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/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/object_extension.dart';
|
||||
import 'package:nc_photos/pixel_image_provider.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/stateful_slider.dart';
|
||||
import 'package:nc_photos/widget/image_editor/color_toolbar.dart';
|
||||
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
||||
|
||||
class ImageEditorArguments {
|
||||
|
@ -122,8 +120,13 @@ class _ImageEditorState extends State<ImageEditor> {
|
|||
)
|
||||
: Container(),
|
||||
),
|
||||
_buildFilterOption(context),
|
||||
_buildFilterBar(context),
|
||||
ColorToolbar(
|
||||
initialState: _colorFilters,
|
||||
onActiveFiltersChanged: (colorFilters) {
|
||||
_colorFilters = colorFilters.toList();
|
||||
_applyFilters();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -137,7 +140,7 @@ class _ImageEditorState extends State<ImageEditor> {
|
|||
leading: BackButton(onPressed: () => _onBackButton(context)),
|
||||
title: Text(L10n.global().imageEditTitle),
|
||||
actions: [
|
||||
if (_filters.isNotEmpty)
|
||||
if (_isModified)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.save_outlined),
|
||||
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 {
|
||||
if (_filters.isEmpty) {
|
||||
if (!_isModified) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
@ -459,69 +202,8 @@ class _ImageEditorState extends State<ImageEditor> {
|
|||
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() {
|
||||
return _filters.entries.map((e) {
|
||||
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();
|
||||
return _colorFilters.map((f) => f.toImageFilter()).toList();
|
||||
}
|
||||
|
||||
Future<void> _applyFilters() async {
|
||||
|
@ -531,121 +213,11 @@ class _ImageEditorState extends State<ImageEditor> {
|
|||
});
|
||||
}
|
||||
|
||||
bool get _isModified => _colorFilters.isNotEmpty;
|
||||
|
||||
bool _isDoneInit = false;
|
||||
late final Rgba8Image _src;
|
||||
Rgba8Image? _dst;
|
||||
final _filters = <_ColorFilterType, _FilterArguments>{};
|
||||
_ColorFilterType? _selectedFilter;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
var _colorFilters = <ColorArguments>[];
|
||||
}
|
||||
|
|
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