diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/document_color_setting_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/document_color_setting_button.dart index d770e2e3bb..04e959470a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/document_color_setting_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/document_color_setting_button.dart @@ -1,12 +1,14 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/util/color_to_hex_string.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flex_color_picker/flex_color_picker.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; +import 'package:flutter/material.dart'; class DocumentColorSettingButton extends StatefulWidget { const DocumentColorSettingButton({ @@ -90,24 +92,11 @@ class DocumentColorSettingDialogState late TextEditingController hexController; late TextEditingController opacityController; - void updateSelectedColor() { - if (widget.formKey.currentState!.validate()) { - setState(() { - final colorValue = int.tryParse( - hexController.text.combineHexWithOpacity(opacityController.text), - ); - // colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point - selectedColorOnDialog = Color(colorValue!); - widget.onChanged(selectedColorOnDialog!); - }); - } - } - @override void initState() { super.initState(); selectedColorOnDialog = widget.currentColor; - currentColorHexString = widget.currentColor.toHexString(); + currentColorHexString = ColorExtension(widget.currentColor).toHexString(); hexController = TextEditingController( text: currentColorHexString.extractHex(), ); @@ -145,17 +134,25 @@ class DocumentColorSettingDialogState controller: hexController, labelText: LocaleKeys.editor_hexValue.tr(), hintText: '6fc9e7', - onChanged: (_) => updateSelectedColor(), - onFieldSubmitted: (_) => updateSelectedColor(), + onChanged: (_) => _updateSelectedColor(), + onFieldSubmitted: (_) => _updateSelectedColor(), validator: (v) => validateHexValue(v, opacityController.text), + suffixIcon: GestureDetector( + onTap: () => _showColorPickerDialog( + context: context, + currentColor: widget.currentColor, + updateColor: _updateColor, + ), + child: const Icon(Icons.color_lens_rounded), + ), ), const VSpace(8), _ColorSettingTextField( controller: opacityController, labelText: LocaleKeys.editor_opacity.tr(), hintText: '50', - onChanged: (_) => updateSelectedColor(), - onFieldSubmitted: (_) => updateSelectedColor(), + onChanged: (_) => _updateSelectedColor(), + onFieldSubmitted: (_) => _updateSelectedColor(), validator: (value) => validateOpacityValue(value), ), ], @@ -164,6 +161,28 @@ class DocumentColorSettingDialogState ], ); } + + void _updateSelectedColor() { + if (widget.formKey.currentState!.validate()) { + setState(() { + final colorValue = int.tryParse( + hexController.text.combineHexWithOpacity(opacityController.text), + ); + // colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point + selectedColorOnDialog = Color(colorValue!); + widget.onChanged(selectedColorOnDialog!); + }); + } + } + + void _updateColor(Color color) { + setState(() { + hexController.text = ColorExtension(color).toHexString().extractHex(); + opacityController.text = + ColorExtension(color).toHexString().extractOpacity(); + }); + _updateSelectedColor(); + } } class _ColorSettingTextField extends StatelessWidget { @@ -172,6 +191,7 @@ class _ColorSettingTextField extends StatelessWidget { required this.labelText, required this.hintText, required this.onFieldSubmitted, + this.suffixIcon, this.onChanged, this.validator, }); @@ -180,6 +200,7 @@ class _ColorSettingTextField extends StatelessWidget { final String labelText; final String hintText; final void Function(String) onFieldSubmitted; + final Widget? suffixIcon; final void Function(String)? onChanged; final String? Function(String?)? validator; @@ -191,6 +212,7 @@ class _ColorSettingTextField extends StatelessWidget { decoration: InputDecoration( labelText: labelText, hintText: hintText, + suffixIcon: suffixIcon, border: OutlineInputBorder( borderSide: BorderSide(color: style.colorScheme.outline), ), @@ -241,3 +263,145 @@ String? validateOpacityValue(String? value) { } return null; } + +const _kColorCircleWidth = 46.0; +const _kColorCircleHeight = 46.0; +const _kColorCircleRadius = 23.0; +const _kColorOpacityThumbRadius = 23.0; +const _kDialogButtonPaddingHorizontal = 24.0; +const _kDialogButtonPaddingVertical = 12.0; +const _kColorsColumnSpacing = 3.0; + +class _ColorPicker extends StatelessWidget { + const _ColorPicker({ + required this.selectedColor, + required this.onColorChanged, + }); + + final Color selectedColor; + final void Function(Color) onColorChanged; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return SingleChildScrollView( + child: ColorPicker( + width: _kColorCircleWidth, + height: _kColorCircleHeight, + borderRadius: _kColorCircleRadius, + enableOpacity: true, + opacityThumbRadius: _kColorOpacityThumbRadius, + columnSpacing: _kColorsColumnSpacing, + enableTooltips: false, + pickersEnabled: const { + ColorPickerType.both: false, + ColorPickerType.primary: true, + ColorPickerType.accent: true, + ColorPickerType.wheel: true, + }, + subheading: Text( + LocaleKeys.settings_appearance_documentSettings_colorShade.tr(), + style: theme.textTheme.labelLarge, + ), + opacitySubheading: Text( + LocaleKeys.settings_appearance_documentSettings_opacity.tr(), + style: theme.textTheme.labelLarge, + ), + onColorChanged: onColorChanged, + ), + ); + } +} + +class _ColorPickerActions extends StatelessWidget { + const _ColorPickerActions({ + required this.onReset, + required this.onUpdate, + }); + + final VoidCallback onReset; + final VoidCallback onUpdate; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 24, + child: FlowyTextButton( + LocaleKeys.button_cancel.tr(), + padding: const EdgeInsets.symmetric( + horizontal: _kDialogButtonPaddingHorizontal, + vertical: _kDialogButtonPaddingVertical, + ), + fontColor: AFThemeExtension.of(context).textColor, + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + radius: Corners.s12Border, + onPressed: onReset, + ), + ), + const HSpace(8), + SizedBox( + height: 48, + child: FlowyTextButton( + LocaleKeys.button_done.tr(), + padding: const EdgeInsets.symmetric( + horizontal: _kDialogButtonPaddingHorizontal, + vertical: _kDialogButtonPaddingVertical, + ), + radius: Corners.s12Border, + fontHoverColor: Colors.white, + fillColor: Theme.of(context).colorScheme.primary, + hoverColor: const Color(0xFF005483), + onPressed: onUpdate, + ), + ), + ], + ); + } +} + +void _showColorPickerDialog({ + required BuildContext context, + String? title, + required Color currentColor, + required void Function(Color) updateColor, +}) { + final style = Theme.of(context); + Color selectedColor = currentColor; + + showDialog( + context: context, + barrierColor: const Color.fromARGB(128, 0, 0, 0), + builder: (context) { + return AlertDialog( + icon: const Icon(Icons.palette), + title: Text( + title ?? + LocaleKeys.settings_appearance_documentSettings_pickColor.tr(), + style: style.textTheme.titleLarge, + ), + content: _ColorPicker( + selectedColor: selectedColor, + onColorChanged: (color) => selectedColor = color, + ), + actionsPadding: const EdgeInsets.all(8), + actions: [ + _ColorPickerActions( + onReset: () { + updateColor(currentColor); + Navigator.of(context).pop(); + }, + onUpdate: () { + updateColor(selectedColor); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); +} diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 5489eaf67b..bd056d1491 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -602,6 +602,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + flex_color_picker: + dependency: "direct main" + description: + name: flex_color_picker + sha256: "809af4ec82ede3b140ed0219b97d548de99e47aa4b99b14a10f705a2dbbcba5e" + url: "https://pub.dev" + source: hosted + version: "3.5.1" + flex_seed_scheme: + dependency: transitive + description: + name: flex_seed_scheme + sha256: "6c595e545b0678e1fe17e8eec3d1fbca7237482da194fadc20ad8607dc7a7f3d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" flowy_infra: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 01921f368b..f054aebb60 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: styled_widget: ^0.4.1 expandable: ^5.0.1 flutter_colorpicker: ^1.0.3 + flex_color_picker: ^3.5.1 highlight: ^0.7.0 package_info_plus: ^6.0.0 url_launcher: ^6.1.11 diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 17c7b750b3..d52b7dbb0c 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -857,6 +857,9 @@ "documentSettings": { "cursorColor": "Document cursor color", "selectionColor": "Document selection color", + "pickColor": "Select a color", + "colorShade": "Color shade", + "opacity": "Opacity", "hexEmptyError": "Hex color cannot be empty", "hexLengthError": "Hex value must be 6 digits long", "hexInvalidError": "Invalid hex value",