diff --git a/lib/app/accounts/account_form.dart b/lib/app/accounts/account_form.dart index e6229210..f12f1901 100644 --- a/lib/app/accounts/account_form.dart +++ b/lib/app/accounts/account_form.dart @@ -15,6 +15,7 @@ import 'package:monekin/core/models/account/account.dart'; import 'package:monekin/core/models/currency/currency.dart'; import 'package:monekin/core/models/supported-icon/icon_displayer.dart'; import 'package:monekin/core/models/supported-icon/supported_icon.dart'; +import 'package:monekin/core/presentation/theme.dart'; import 'package:monekin/core/presentation/widgets/color_picker/color_picker.dart'; import 'package:monekin/core/presentation/widgets/currency_selector_modal.dart'; import 'package:monekin/core/presentation/widgets/expansion_panel/single_expansion_panel.dart'; @@ -98,7 +99,7 @@ class _AccountFormPageState extends State { closingDate: _closeDate, type: _type, iconId: _icon.id, - color: _color.toHex(leadingHashSign: false), + color: _color.toHex(), currency: _currency!, iban: _ibanController.text.isEmpty ? null : _ibanController.text, description: _textController.text.isEmpty ? null : _textController.text, @@ -233,6 +234,8 @@ class _AccountFormPageState extends State { return const LinearProgressIndicator(); } + final isDark = isAppInDarkBrightness(context); + return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Form( @@ -248,8 +251,10 @@ class _AccountFormPageState extends State { size: 36, isOutline: true, outlineWidth: 1.5, - mainColor: _color.lighten(IconDisplayer.darkLightenFactor), - secondaryColor: _color, + mainColor: _color + .lighten(isDark ? IconDisplayer.darkLightenFactor : 0), + secondaryColor: _color + .lighten(isDark ? 0 : IconDisplayer.darkLightenFactor), displayMode: IconDisplayMode.polygon, ), onDataChange: ((data) { diff --git a/lib/app/categories/form/icon_and_color_selector.dart b/lib/app/categories/form/icon_and_color_selector.dart index ec046255..387e0152 100644 --- a/lib/app/categories/form/icon_and_color_selector.dart +++ b/lib/app/categories/form/icon_and_color_selector.dart @@ -3,6 +3,7 @@ import 'package:monekin/core/extensions/color.extensions.dart'; import 'package:monekin/core/models/supported-icon/icon_displayer.dart'; import 'package:monekin/core/models/supported-icon/supported_icon.dart'; import 'package:monekin/core/presentation/app_colors.dart'; +import 'package:monekin/core/presentation/theme.dart'; import 'package:monekin/core/presentation/widgets/color_picker/color_picker.dart'; import 'package:monekin/core/presentation/widgets/color_picker/color_picker_modal.dart'; import 'package:monekin/core/presentation/widgets/icon_selector_modal.dart'; @@ -75,12 +76,21 @@ class IconAndColorSelector extends StatelessWidget { ColorPickerModal( colorOptions: defaultColorPickerOptions, selectedColor: data.color.toHex(), + customColorPreviewBuilder: (color) => + iconDisplayer.copyWith( + secondaryColor: + isAppInDarkBrightness(context) ? color : null, + mainColor: + isAppInLightBrightness(context) ? color : null, + size: 32, + outlineWidth: 2, + ), + onColorSelected: (selColor) { + Navigator.pop(context); + onDataChange((color: selColor, icon: data.icon)); + }, ), - ).then((selColor) { - if (selColor == null) return; - - onDataChange((color: selColor, icon: data.icon)); - }), + ), bgColor: Theme.of(context).colorSchemeExtended.inputFill, child: ListTile( mouseCursor: SystemMouseCursors.click, diff --git a/lib/app/settings/appearance_settings_page.dart b/lib/app/settings/appearance_settings_page.dart index 32446bb7..9201b9d4 100644 --- a/lib/app/settings/appearance_settings_page.dart +++ b/lib/app/settings/appearance_settings_page.dart @@ -199,7 +199,7 @@ class _AdvancedSettingsPageState extends State { onSwitch: (bool value) async { await UserSettingService.instance.setItem( SettingKey.accentColor, - value ? 'auto' : brandBlue.toHex(leadingHashSign: false), + value ? 'auto' : brandBlue.toHex(), updateGlobalState: true, ); }, @@ -224,22 +224,23 @@ class _AdvancedSettingsPageState extends State { context, ColorPickerModal( colorOptions: [ - brandBlue.toHex(leadingHashSign: false), + brandBlue.toHex(), ...defaultColorPickerOptions ], selectedColor: color.toHex(), - ), - ).then((value) { - if (value == null) return; + onColorSelected: (value) { + Navigator.pop(context); - setState(() { - UserSettingService.instance.setItem( - SettingKey.accentColor, - value.toHex(), - updateGlobalState: true, - ); - }); - }), + setState(() { + UserSettingService.instance.setItem( + SettingKey.accentColor, + value.toHex(), + updateGlobalState: true, + ); + }); + }, + ), + ), title: Text(t.settings.accent_color), subtitle: Text(t.settings.accent_color_descr), enabled: snapshot.data! != 'auto', diff --git a/lib/app/tags/tag_form_page.dart b/lib/app/tags/tag_form_page.dart index cc20c661..1dd97a8f 100644 --- a/lib/app/tags/tag_form_page.dart +++ b/lib/app/tags/tag_form_page.dart @@ -172,26 +172,26 @@ class _TagFormPageState extends State { ), const SizedBox(height: 16), ReadOnlyTextFormField( - displayValue: null, - decoration: InputDecoration( - hintText: t.icon_selector.color, - suffixIcon: const Icon(Icons.circle), - suffixIconColor: ColorHex.get(_color), - ), - onTap: () => showColorPickerModal( - context, - ColorPickerModal( - colorOptions: defaultColorPickerOptions, - selectedColor: _color, + displayValue: null, + decoration: InputDecoration( + hintText: t.icon_selector.color, + suffixIcon: const Icon(Icons.circle), + suffixIconColor: ColorHex.get(_color), ), - ).then((value) { - if (value == null) return; - - setState(() { - _color = value.toHex(); - }); - }), - ), + onTap: () => showColorPickerModal( + context, + ColorPickerModal( + colorOptions: defaultColorPickerOptions, + selectedColor: _color, + onColorSelected: (value) { + Navigator.pop(context); + + setState(() { + _color = value.toHex(); + }); + }, + ), + )), const SizedBox(height: 12), TextFormField( controller: _descrController, diff --git a/lib/core/extensions/color.extensions.dart b/lib/core/extensions/color.extensions.dart index 3c3194e0..601aa6b0 100644 --- a/lib/core/extensions/color.extensions.dart +++ b/lib/core/extensions/color.extensions.dart @@ -2,16 +2,32 @@ import 'package:flutter/material.dart'; extension ColorHex on Color { /// Return a color instance from an hex string - static Color get(String hex) => - Color(int.parse("0xff${hex.replaceAll('#', '')}")); - - String toHex({bool leadingHashSign = true}) { - return '${leadingHashSign ? '#' : ''}' - '${alpha.toRadixString(16).padLeft(2, '0')}' - '${red.toRadixString(16).padLeft(2, '0')}' - '${green.toRadixString(16).padLeft(2, '0')}' - '${blue.toRadixString(16).padLeft(2, '0')}'; + static Color get(String hex) { + hex = hex.toUpperCase().replaceAll('#', ''); + + if (hex.length == 6) { + hex = 'FF$hex'; + } + + // Parser will return errors on invalid strings, so we don't need error catching here + return Color(int.parse(hex, radix: 16)); } + + String toHex({ + bool leadingHashSign = false, + bool enableAlpha = false, + bool toUpperCase = true, + }) { + final String hex = (leadingHashSign ? '#' : '') + + (enableAlpha ? _padRadix(alpha) : '') + + _padRadix(red) + + _padRadix(green) + + _padRadix(blue); + return toUpperCase ? hex.toUpperCase() : hex; + } + +// Shorthand for padLeft of RadixString, DRY. + String _padRadix(int value) => value.toRadixString(16).padLeft(2, '0'); } extension ColorBrightness on Color { diff --git a/lib/core/models/supported-icon/supported_icon.dart b/lib/core/models/supported-icon/supported_icon.dart index 42bd1743..91404d9f 100644 --- a/lib/core/models/supported-icon/supported_icon.dart +++ b/lib/core/models/supported-icon/supported_icon.dart @@ -1,6 +1,9 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:monekin/core/presentation/widgets/simple_shadow.dart'; part 'supported_icon.g.dart'; @@ -28,12 +31,18 @@ class SupportedIcon { return SizedBox( height: size, width: size, - child: SvgPicture.asset( - urlToAssets, - height: size, - width: size, - colorFilter: - color != null ? ColorFilter.mode(color, BlendMode.srcIn) : null, + child: SimpleShadow( + opacity: 0.55, + sigma: min(size / 50, 0.6), + offset: const Offset(0, 0), + color: Colors.black, + child: SvgPicture.asset( + urlToAssets, + height: size, + width: size, + colorFilter: + color != null ? ColorFilter.mode(color, BlendMode.srcIn) : null, + ), ), ); } diff --git a/lib/core/presentation/widgets/color_picker/color_picker_modal.dart b/lib/core/presentation/widgets/color_picker/color_picker_modal.dart index 27292a98..e0c1c99a 100644 --- a/lib/core/presentation/widgets/color_picker/color_picker_modal.dart +++ b/lib/core/presentation/widgets/color_picker/color_picker_modal.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import 'package:monekin/core/extensions/color.extensions.dart'; +import 'package:monekin/core/extensions/lists.extensions.dart'; +import 'package:monekin/core/presentation/widgets/color_picker/custom_color_picker_modal.dart'; +import 'package:monekin/core/presentation/widgets/gradient-box.borders.dart'; import 'package:monekin/core/presentation/widgets/modal_container.dart'; +import 'package:monekin/core/presentation/widgets/tappable.dart'; import 'package:monekin/i18n/translations.g.dart'; -Future showColorPickerModal( +Future showColorPickerModal( BuildContext context, ColorPickerModal component) { return showModalBottomSheet( context: context, @@ -16,16 +20,29 @@ Future showColorPickerModal( } class ColorPickerModal extends StatelessWidget { - const ColorPickerModal( - {super.key, required this.colorOptions, this.selectedColor}); + const ColorPickerModal({ + super.key, + required this.colorOptions, + this.selectedColor, + this.showCustomColorCircleOption = true, + required this.onColorSelected, + this.customColorPreviewBuilder, + }); final List colorOptions; + final bool showCustomColorCircleOption; + final String? selectedColor; + final Widget Function(Color color)? customColorPreviewBuilder; + + final void Function(Color) onColorSelected; + @override Widget build(BuildContext context) { final t = Translations.of(context); + double circleSize = 54; return DraggableScrollableSheet( expand: false, @@ -42,56 +59,103 @@ class ColorPickerModal extends StatelessWidget { alignment: Alignment.center, heightFactor: 1, child: Wrap( - runAlignment: WrapAlignment.center, - spacing: 6, - runSpacing: 12, - children: List.generate(colorOptions.length, (index) { - final colorItem = colorOptions[index]; + runAlignment: WrapAlignment.center, + spacing: 6, + runSpacing: 12, + children: [ + if (showCustomColorCircleOption) + buildCustomColorCircleSelector(circleSize, context), + ...List.generate(colorOptions.length, (index) { + final colorItem = colorOptions[index]; - return Container( - clipBehavior: Clip.hardEdge, - width: 52, - height: 52, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(999)), - child: Stack( - children: [ - DecoratedBox( - decoration: BoxDecoration( - color: ColorHex.get(colorItem), - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => Navigator.pop( - context, - ColorHex.get('#$colorItem'), - ), - ), - ), - ), - if (selectedColor != null && - ColorHex.get('#$colorItem') == - ColorHex.get(selectedColor!)) - const DecoratedBox( - decoration: BoxDecoration( - color: Color.fromARGB(47, 255, 255, 255), - ), - child: Center( - child: Icon( - Icons.check, - color: Colors.white, - ), - ), - ) - ], - ), - ); - }), - ), + return buildSelectableColorCircle(colorItem, context, + size: circleSize); + }), + ]), ), ), ); }); } + + Tooltip buildCustomColorCircleSelector( + double circleSize, BuildContext context) { + return Tooltip( + message: t.icon_selector.custom_color, + child: Container( + width: circleSize, + height: circleSize, + decoration: BoxDecoration( + border: GradientBoxBorder( + gradient: LinearGradient(colors: [ + Colors.red.withOpacity(0.8), + Colors.yellow.withOpacity(0.8), + Colors.green.withOpacity(0.8), + Colors.blue.withOpacity(0.8), + Colors.purple.withOpacity(0.8), + ]), + width: 3, + ), + borderRadius: BorderRadius.circular(999), + ), + child: Tappable( + bgColor: Colors.transparent, + onTap: () { + Navigator.pop(context); + + showCustomColorPickerModal( + context, + CustomColorPickerModal( + initialColor: selectedColor == null + ? ColorHex.get(colorOptions.randomItem()) + : ColorHex.get(selectedColor!), + onColorSelected: onColorSelected, + previewBuilder: customColorPreviewBuilder, + ), + ); + }, + borderRadius: BorderRadius.circular(999), + child: Icon( + Icons.colorize_rounded, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + ); + } + + Container buildSelectableColorCircle( + String colorItem, + BuildContext context, { + required double size, + }) { + return Container( + clipBehavior: Clip.hardEdge, + width: size, + height: size, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(999)), + child: Stack( + children: [ + Tappable( + bgColor: ColorHex.get(colorItem), + onTap: () => onColorSelected(ColorHex.get('#$colorItem')), + child: SizedBox(height: size, width: size), + ), + if (selectedColor != null && + ColorHex.get('#$colorItem') == ColorHex.get(selectedColor!)) + const DecoratedBox( + decoration: BoxDecoration( + color: Color.fromARGB(47, 255, 255, 255), + ), + child: Center( + child: Icon( + Icons.check, + color: Colors.white, + ), + ), + ) + ], + ), + ); + } } diff --git a/lib/core/presentation/widgets/color_picker/custom_color_picker.dart b/lib/core/presentation/widgets/color_picker/custom_color_picker.dart new file mode 100644 index 00000000..22eb6580 --- /dev/null +++ b/lib/core/presentation/widgets/color_picker/custom_color_picker.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; +import 'package:monekin/core/extensions/color.extensions.dart'; +import 'package:monekin/core/presentation/responsive/breakpoints.dart'; +import 'package:monekin/core/presentation/responsive/responsive_row_column.dart'; +import 'package:monekin/core/utils/logger.dart'; +import 'package:monekin/i18n/translations.g.dart'; + +class CustomColorPicker extends StatefulWidget { + const CustomColorPicker({ + super.key, + required this.pickerColor, + required this.onColorChanged, + this.colorPickerHeight = 250.0, + this.pickerAreaBorderRadius = const BorderRadius.all(Radius.zero), + }); + + final Color pickerColor; + final ValueChanged onColorChanged; + final double colorPickerHeight; + final BorderRadius pickerAreaBorderRadius; + + @override + State createState() => _CustomColorPickerState(); +} + +class _CustomColorPickerState extends State { + HSVColor currentHsvColor = const HSVColor.fromAHSV(0.0, 0.0, 0.0, 0.0); + Widget? previewWidget; + + Color get currentColor => currentHsvColor.toColor().withOpacity(1); + + final TextEditingController hexColorText = TextEditingController(text: ''); + + @override + void initState() { + currentHsvColor = HSVColor.fromColor(widget.pickerColor); + hexColorText.text = currentColor.toHex(); + + super.initState(); + } + + @override + void didUpdateWidget(CustomColorPicker oldWidget) { + super.didUpdateWidget(oldWidget); + currentHsvColor = HSVColor.fromColor(widget.pickerColor); + } + + void onColorChanging(HSVColor color) { + setState(() => currentHsvColor = color); + widget.onColorChanged(currentColor); + hexColorText.text = currentColor.toHex(); + } + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + final isComponenHorizontal = + BreakPoint.of(context).isLargerThan(BreakpointID.sm); + + return SizedBox( + height: widget.colorPickerHeight + 50, + child: ResponsiveRowColumn( + direction: isComponenHorizontal ? Axis.horizontal : Axis.vertical, + rowCrossAxisAlignment: CrossAxisAlignment.start, + rowSpacing: 10, + columnSpacing: 0, + children: [ + ResponsiveRowColumnItem( + columnOrder: 1, + child: ResponsiveRowColumn( + columnCrossAxisAlignment: CrossAxisAlignment.start, + rowCrossAxisAlignment: CrossAxisAlignment.end, + rowMainAxisAlignment: MainAxisAlignment.spaceEvenly, + direction: + isComponenHorizontal ? Axis.vertical : Axis.horizontal, + columnSpacing: 16, + rowSpacing: 12, + children: [ + ResponsiveRowColumnItem( + rowFit: FlexFit.loose, + rowFlex: 1, + child: SizedBox( + width: 180, + child: TextFormField( + controller: hexColorText, + decoration: const InputDecoration( + prefixIcon: Icon(Icons.numbers_rounded), + counterText: '', + ), + maxLength: 6, + onChanged: (newValue) { + try { + currentHsvColor = + HSVColor.fromColor(ColorHex.get(newValue)); + widget.onColorChanged(currentColor); + + setState(() {}); + } catch (e) { + Logger.printDebug('Invalid color'); + } + }, + ), + ), + ), + ResponsiveRowColumnItem( + rowFit: FlexFit.loose, + rowFlex: 1, + child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + t.icon_selector.current_color_selection, + style: Theme.of(context).textTheme.labelMedium, + ), + const SizedBox(height: 3), + Container( + width: 180, + height: 38, + decoration: BoxDecoration( + color: currentColor, + borderRadius: BorderRadius.circular(8), + ), + ) + ], + )), + ], + ), + ), + ResponsiveRowColumnItem( + columnOrder: 0, + child: Expanded( + child: Column( + children: [ + Expanded( + child: LayoutBuilder(builder: (context, constraints) { + return SizedBox( + height: widget.colorPickerHeight, + width: constraints.maxWidth / 1.05, + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: ColorPickerArea(currentHsvColor, + onColorChanging, PaletteType.hsv), + ), + ); + }), + ), + LayoutBuilder(builder: (context, constraints) { + return SizedBox( + height: 40, + width: constraints.maxWidth, + child: ColorPickerSlider( + TrackType.hue, + currentHsvColor, + onColorChanging, + displayThumbColor: true, + ), + ); + }), + ], + ), + ), + ) + ]), + ); + } +} diff --git a/lib/core/presentation/widgets/color_picker/custom_color_picker_modal.dart b/lib/core/presentation/widgets/color_picker/custom_color_picker_modal.dart new file mode 100644 index 00000000..4ae7b0b6 --- /dev/null +++ b/lib/core/presentation/widgets/color_picker/custom_color_picker_modal.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:monekin/core/presentation/widgets/bottomSheetFooter.dart'; +import 'package:monekin/core/presentation/widgets/color_picker/custom_color_picker.dart'; +import 'package:monekin/core/presentation/widgets/modal_container.dart'; +import 'package:monekin/i18n/translations.g.dart'; + +Future showCustomColorPickerModal( + BuildContext context, CustomColorPickerModal component) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + builder: (context) { + return component; + }, + ); +} + +class CustomColorPickerModal extends StatefulWidget { + const CustomColorPickerModal({ + super.key, + required this.initialColor, + this.previewBuilder, + required this.onColorSelected, + }); + + final Color initialColor; + final Widget Function(Color color)? previewBuilder; + final void Function(Color) onColorSelected; + + @override + State createState() => _CustomColorPickerModalState(); +} + +class _CustomColorPickerModalState extends State { + late Color color; + + @override + void initState() { + super.initState(); + + color = widget.initialColor; + } + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + + return ModalContainer( + title: t.icon_selector.custom_color, + bodyPadding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + body: CustomColorPicker( + pickerColor: color, + onColorChanged: (newColor) { + setState(() { + color = newColor; + }); + }), + endWidget: + widget.previewBuilder == null ? null : widget.previewBuilder!(color), + footer: BottomSheetFooter(onSaved: () { + widget.onColorSelected(color); + }), + ); + } +} diff --git a/lib/core/presentation/widgets/gradient-box.borders.dart b/lib/core/presentation/widgets/gradient-box.borders.dart new file mode 100644 index 00000000..0e7593a2 --- /dev/null +++ b/lib/core/presentation/widgets/gradient-box.borders.dart @@ -0,0 +1,76 @@ +import 'package:flutter/widgets.dart'; + +// All credits to https://github.com/obiwanzenobi/gradient-borders/tree/master + +class GradientBoxBorder extends BoxBorder { + const GradientBoxBorder({required this.gradient, this.width = 1.0}); + + final Gradient gradient; + + final double width; + + @override + BorderSide get bottom => BorderSide.none; + + @override + BorderSide get top => BorderSide.none; + + @override + EdgeInsetsGeometry get dimensions => EdgeInsets.all(width); + + @override + bool get isUniform => true; + + @override + void paint( + Canvas canvas, + Rect rect, { + TextDirection? textDirection, + BoxShape shape = BoxShape.rectangle, + BorderRadius? borderRadius, + }) { + switch (shape) { + case BoxShape.circle: + assert( + borderRadius == null, + 'A borderRadius can only be given for rectangular boxes.', + ); + _paintCircle(canvas, rect); + break; + case BoxShape.rectangle: + if (borderRadius != null) { + _paintRRect(canvas, rect, borderRadius); + return; + } + _paintRect(canvas, rect); + break; + } + } + + void _paintRect(Canvas canvas, Rect rect) { + canvas.drawRect(rect.deflate(width / 2), _getPaint(rect)); + } + + void _paintRRect(Canvas canvas, Rect rect, BorderRadius borderRadius) { + final rrect = borderRadius.toRRect(rect).deflate(width / 2); + canvas.drawRRect(rrect, _getPaint(rect)); + } + + void _paintCircle(Canvas canvas, Rect rect) { + final paint = _getPaint(rect); + final radius = (rect.shortestSide - width) / 2.0; + canvas.drawCircle(rect.center, radius, paint); + } + + @override + ShapeBorder scale(double t) { + return this; + } + + Paint _getPaint(Rect rect) { + return Paint() + ..strokeWidth = width + ..shader = gradient.createShader(rect) + ..style = PaintingStyle.stroke; + } +} diff --git a/lib/core/presentation/widgets/modal_container.dart b/lib/core/presentation/widgets/modal_container.dart index b4827019..52afaad2 100644 --- a/lib/core/presentation/widgets/modal_container.dart +++ b/lib/core/presentation/widgets/modal_container.dart @@ -60,24 +60,26 @@ class ModalContainer extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DefaultTextStyle( - style: Theme.of(context) - .textTheme - .headlineSmall! - .copyWith(fontWeight: FontWeight.w800), - child: titleBuilder != null - ? titleBuilder!(title) - : Text(title), - ), - if (subtitle != null) ...[ - const SizedBox(height: 2), - Text(subtitle!) + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context) + .textTheme + .headlineSmall! + .copyWith(fontWeight: FontWeight.w800), + child: titleBuilder != null + ? titleBuilder!(title) + : Text(title), + ), + if (subtitle != null) ...[ + const SizedBox(height: 2), + Text(subtitle!) + ], ], - ], + ), ), if (endWidget != null) endWidget! ], diff --git a/lib/core/presentation/widgets/simple_shadow.dart b/lib/core/presentation/widgets/simple_shadow.dart new file mode 100644 index 00000000..ea7562c2 --- /dev/null +++ b/lib/core/presentation/widgets/simple_shadow.dart @@ -0,0 +1,53 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +// Credits to https://github.com/marcelopmont/simple_shadow/tree/main for this amazing component + +class SimpleShadow extends StatelessWidget { + final Widget child; + final double opacity; + final double sigma; + final Color color; + final Offset offset; + + const SimpleShadow({ + required this.child, + this.opacity = 0.5, + this.sigma = 2, + this.color = Colors.black, + this.offset = const Offset(2, 2), + }); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + if (color.alpha != 0) + Transform.translate( + offset: offset, + child: ImageFiltered( + imageFilter: ImageFilter.blur( + sigmaY: sigma, sigmaX: sigma, tileMode: TileMode.decal), + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: Colors.transparent, + width: 0, + ), + ), + child: ColorFiltered( + colorFilter: ColorFilter.mode( + color.withOpacity(opacity), + BlendMode.srcATop, + ), + child: child, + ), + ), + ), + ), + child, + ], + ); + } +} diff --git a/lib/i18n/strings_de.json b/lib/i18n/strings_de.json index dbee99fd..11309418 100644 --- a/lib/i18n/strings_de.json +++ b/lib/i18n/strings_de.json @@ -245,6 +245,8 @@ "color": "Farbe", "select-icon": "Wähle ein Symbol aus", "select-color": "Wähle eine Farbe", + "custom-color": "Benutzerdefinierte Farbe", + "current-color-selection": "Aktuelle Auswahl", "select-account-icon": "Identifiziere Dein Konto", "select-category-icon": "Identifiziere Deine Kategorie", "SCOPES": { diff --git a/lib/i18n/strings_en.json b/lib/i18n/strings_en.json index 2b5f4b02..05c6b95b 100644 --- a/lib/i18n/strings_en.json +++ b/lib/i18n/strings_en.json @@ -236,6 +236,8 @@ "color": "Color", "select-icon": "Select an icon", "select-color": "Select a color", + "custom-color": "Custom color", + "current-color-selection": "Current selection", "select-account-icon": "Identify your account", "select-category-icon": "Identify your category", "SCOPES": { diff --git a/lib/i18n/strings_es.json b/lib/i18n/strings_es.json index 7b09d452..8672d877 100644 --- a/lib/i18n/strings_es.json +++ b/lib/i18n/strings_es.json @@ -240,6 +240,8 @@ "color": "Color", "select-icon": "Selecciona un icono", "select-color": "Selecciona un color", + "custom-color": "Color personalizado", + "current-color-selection": "Selección actual", "select-account-icon": "Identifica tu cuenta", "select-category-icon": "Identifica tu categoría", "SCOPES": { diff --git a/lib/i18n/strings_hu.json b/lib/i18n/strings_hu.json index cfa3be40..d6c8b051 100644 --- a/lib/i18n/strings_hu.json +++ b/lib/i18n/strings_hu.json @@ -236,6 +236,8 @@ "color": "Szín", "select-icon": "Ikon kiválasztása", "select-color": "Szín kiválasztása", + "custom-color": "Egyedi szín", + "current-color-selection": "Jelenlegi kiválasztás", "select-account-icon": "Számla azonosítása", "select-category-icon": "Kategória azonosítása", "SCOPES": { diff --git a/lib/i18n/strings_uk.json b/lib/i18n/strings_uk.json index fd010b5d..b06cfe9e 100644 --- a/lib/i18n/strings_uk.json +++ b/lib/i18n/strings_uk.json @@ -234,6 +234,8 @@ "icon": "Іконка", "color": "Колір", "select-color": "Виберіть колір", + "custom-color": "Користувацький колір", + "current-color-selection": "Поточний вибір", "select-icon": "Виберіть іконку", "select-account-icon": "Ідентифікуйте ваш рахунок", "select-category-icon": "Ідентифікуйте вашу категорію", diff --git a/lib/i18n/strings_zh-TW.json b/lib/i18n/strings_zh-TW.json index b62f6586..bd53a98c 100644 --- a/lib/i18n/strings_zh-TW.json +++ b/lib/i18n/strings_zh-TW.json @@ -236,6 +236,8 @@ "color": "顏色", "select-icon": "選擇一個圖示", "select-color": "選擇一種顏色", + "current-color-selection": "目前選擇", + "custom-color": "自訂顏色", "select-account-icon": "識別您的帳戶", "select-category-icon": "確定您的類別", "SCOPES": { diff --git a/lib/i18n/translations.g.dart b/lib/i18n/translations.g.dart index a4c08b73..e9ae175c 100644 --- a/lib/i18n/translations.g.dart +++ b/lib/i18n/translations.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 6 -/// Strings: 3301 (550 per locale) +/// Strings: 3313 (552 per locale) /// -/// Built on 2025-01-01 at 21:33 UTC +/// Built on 2025-01-09 at 14:29 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -320,6 +320,8 @@ class _TranslationsIconSelectorEn { String get color => 'Color'; String get select_icon => 'Select an icon'; String get select_color => 'Select a color'; + String get custom_color => 'Custom color'; + String get current_color_selection => 'Current selection'; String get select_account_icon => 'Identify your account'; String get select_category_icon => 'Identify your category'; late final _TranslationsIconSelectorScopesEn scopes = _TranslationsIconSelectorScopesEn._(_root); @@ -1632,6 +1634,8 @@ class _TranslationsIconSelectorDe implements _TranslationsIconSelectorEn { @override String get color => 'Farbe'; @override String get select_icon => 'Wähle ein Symbol aus'; @override String get select_color => 'Wähle eine Farbe'; + @override String get custom_color => 'Benutzerdefinierte Farbe'; + @override String get current_color_selection => 'Aktuelle Auswahl'; @override String get select_account_icon => 'Identifiziere Dein Konto'; @override String get select_category_icon => 'Identifiziere Deine Kategorie'; @override late final _TranslationsIconSelectorScopesDe scopes = _TranslationsIconSelectorScopesDe._(_root); @@ -2944,6 +2948,8 @@ class _TranslationsIconSelectorEs implements _TranslationsIconSelectorEn { @override String get color => 'Color'; @override String get select_icon => 'Selecciona un icono'; @override String get select_color => 'Selecciona un color'; + @override String get custom_color => 'Color personalizado'; + @override String get current_color_selection => 'Selección actual'; @override String get select_account_icon => 'Identifica tu cuenta'; @override String get select_category_icon => 'Identifica tu categoría'; @override late final _TranslationsIconSelectorScopesEs scopes = _TranslationsIconSelectorScopesEs._(_root); @@ -4257,6 +4263,8 @@ class _TranslationsIconSelectorHu implements _TranslationsIconSelectorEn { @override String get color => 'Szín'; @override String get select_icon => 'Ikon kiválasztása'; @override String get select_color => 'Szín kiválasztása'; + @override String get custom_color => 'Egyedi szín'; + @override String get current_color_selection => 'Jelenlegi kiválasztás'; @override String get select_account_icon => 'Számla azonosítása'; @override String get select_category_icon => 'Kategória azonosítása'; @override late final _TranslationsIconSelectorScopesHu scopes = _TranslationsIconSelectorScopesHu._(_root); @@ -5568,6 +5576,8 @@ class _TranslationsIconSelectorUk implements _TranslationsIconSelectorEn { @override String get icon => 'Іконка'; @override String get color => 'Колір'; @override String get select_color => 'Виберіть колір'; + @override String get custom_color => 'Користувацький колір'; + @override String get current_color_selection => 'Поточний вибір'; @override String get select_icon => 'Виберіть іконку'; @override String get select_account_icon => 'Ідентифікуйте ваш рахунок'; @override String get select_category_icon => 'Ідентифікуйте вашу категорію'; @@ -6881,6 +6891,8 @@ class _TranslationsIconSelectorZhTw implements _TranslationsIconSelectorEn { @override String get color => '顏色'; @override String get select_icon => '選擇一個圖示'; @override String get select_color => '選擇一種顏色'; + @override String get current_color_selection => '目前選擇'; + @override String get custom_color => '自訂顏色'; @override String get select_account_icon => '識別您的帳戶'; @override String get select_category_icon => '確定您的類別'; @override late final _TranslationsIconSelectorScopesZhTw scopes = _TranslationsIconSelectorScopesZhTw._(_root); @@ -8251,6 +8263,8 @@ extension on Translations { case 'icon_selector.color': return 'Color'; case 'icon_selector.select_icon': return 'Select an icon'; case 'icon_selector.select_color': return 'Select a color'; + case 'icon_selector.custom_color': return 'Custom color'; + case 'icon_selector.current_color_selection': return 'Current selection'; case 'icon_selector.select_account_icon': return 'Identify your account'; case 'icon_selector.select_category_icon': return 'Identify your category'; case 'icon_selector.scopes.transport': return 'Transport'; @@ -8881,6 +8895,8 @@ extension on _TranslationsDe { case 'icon_selector.color': return 'Farbe'; case 'icon_selector.select_icon': return 'Wähle ein Symbol aus'; case 'icon_selector.select_color': return 'Wähle eine Farbe'; + case 'icon_selector.custom_color': return 'Benutzerdefinierte Farbe'; + case 'icon_selector.current_color_selection': return 'Aktuelle Auswahl'; case 'icon_selector.select_account_icon': return 'Identifiziere Dein Konto'; case 'icon_selector.select_category_icon': return 'Identifiziere Deine Kategorie'; case 'icon_selector.scopes.transport': return 'Transport'; @@ -9512,6 +9528,8 @@ extension on _TranslationsEs { case 'icon_selector.color': return 'Color'; case 'icon_selector.select_icon': return 'Selecciona un icono'; case 'icon_selector.select_color': return 'Selecciona un color'; + case 'icon_selector.custom_color': return 'Color personalizado'; + case 'icon_selector.current_color_selection': return 'Selección actual'; case 'icon_selector.select_account_icon': return 'Identifica tu cuenta'; case 'icon_selector.select_category_icon': return 'Identifica tu categoría'; case 'icon_selector.scopes.transport': return 'Transporte'; @@ -10142,6 +10160,8 @@ extension on _TranslationsHu { case 'icon_selector.color': return 'Szín'; case 'icon_selector.select_icon': return 'Ikon kiválasztása'; case 'icon_selector.select_color': return 'Szín kiválasztása'; + case 'icon_selector.custom_color': return 'Egyedi szín'; + case 'icon_selector.current_color_selection': return 'Jelenlegi kiválasztás'; case 'icon_selector.select_account_icon': return 'Számla azonosítása'; case 'icon_selector.select_category_icon': return 'Kategória azonosítása'; case 'icon_selector.scopes.transport': return 'Közlekedés'; @@ -10771,6 +10791,8 @@ extension on _TranslationsUk { case 'icon_selector.icon': return 'Іконка'; case 'icon_selector.color': return 'Колір'; case 'icon_selector.select_color': return 'Виберіть колір'; + case 'icon_selector.custom_color': return 'Користувацький колір'; + case 'icon_selector.current_color_selection': return 'Поточний вибір'; case 'icon_selector.select_icon': return 'Виберіть іконку'; case 'icon_selector.select_account_icon': return 'Ідентифікуйте ваш рахунок'; case 'icon_selector.select_category_icon': return 'Ідентифікуйте вашу категорію'; @@ -11402,6 +11424,8 @@ extension on _TranslationsZhTw { case 'icon_selector.color': return '顏色'; case 'icon_selector.select_icon': return '選擇一個圖示'; case 'icon_selector.select_color': return '選擇一種顏色'; + case 'icon_selector.current_color_selection': return '目前選擇'; + case 'icon_selector.custom_color': return '自訂顏色'; case 'icon_selector.select_account_icon': return '識別您的帳戶'; case 'icon_selector.select_category_icon': return '確定您的類別'; case 'icon_selector.scopes.transport': return '運輸'; diff --git a/pubspec.lock b/pubspec.lock index b9034719..e57f5d9a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -342,6 +342,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_colorpicker: + dependency: "direct main" + description: + name: flutter_colorpicker + sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter_keyboard_visibility: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6efac3f5..6a8a7c44 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,7 @@ dependencies: copy_with_extension: ^5.0.4 freezed_annotation: ^2.4.1 in_app_purchase: ^3.1.11 + flutter_colorpicker: ^1.1.0 dev_dependencies: flutter_test: