diff --git a/app/lib/widget/image_editor.dart b/app/lib/widget/image_editor.dart index ba7c15dc..3668f159 100644 --- a/app/lib/widget/image_editor.dart +++ b/app/lib/widget/image_editor.dart @@ -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 { ) : Container(), ), - _buildFilterOption(context), - _buildFilterBar(context), + ColorToolbar( + initialState: _colorFilters, + onActiveFiltersChanged: (colorFilters) { + _colorFilters = colorFilters.toList(); + _applyFilters(); + }, + ), ], ), ), @@ -137,7 +140,7 @@ class _ImageEditorState extends State { 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 { ], ); - 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? 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 _onBackButton(BuildContext context) async { - if (_filters.isEmpty) { + if (!_isModified) { Navigator.of(context).pop(); return; } @@ -459,69 +202,8 @@ class _ImageEditorState extends State { 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 _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 _applyFilters() async { @@ -531,121 +213,11 @@ class _ImageEditorState extends State { }); } + 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 = []; } diff --git a/app/lib/widget/image_editor/color_toolbar.dart b/app/lib/widget/image_editor/color_toolbar.dart new file mode 100644 index 00000000..76104cd7 --- /dev/null +++ b/app/lib/widget/image_editor/color_toolbar.dart @@ -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 initialState; + final ValueChanged> onActiveFiltersChanged; +} + +class _ColorToolbarState extends State { + @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? 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? _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; +} diff --git a/app/lib/widget/image_editor/toolbar_button.dart b/app/lib/widget/image_editor/toolbar_button.dart new file mode 100644 index 00000000..41e8f252 --- /dev/null +++ b/app/lib/widget/image_editor/toolbar_button.dart @@ -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; +}