TF-4178 Add color picker modal
This commit is contained in:
@@ -78,6 +78,7 @@ export 'presentation/views/responsive/responsive_widget.dart';
|
||||
export 'presentation/views/list/tree_view.dart';
|
||||
export 'presentation/views/button/icon_button_web.dart';
|
||||
export 'presentation/views/button/tmail_button_widget.dart';
|
||||
export 'presentation/views/button/default_close_button_widget.dart';
|
||||
export 'presentation/views/image/avatar_builder.dart';
|
||||
export 'presentation/views/list/sliver_grid_delegate_fixed_height.dart';
|
||||
export 'presentation/views/list/no_stretch_scroll_behavior.dart';
|
||||
@@ -90,6 +91,7 @@ export 'presentation/views/dialog/downloading_file_dialog_builder.dart';
|
||||
export 'presentation/views/dialog/confirmation_dialog_builder.dart';
|
||||
export 'presentation/views/dialog/edit_text_dialog_builder.dart';
|
||||
export 'presentation/views/dialog/color_picker_dialog_builder.dart';
|
||||
export 'presentation/views/dialog/modal_list_action_button_widget.dart';
|
||||
export 'presentation/views/html_viewer/html_content_viewer_widget.dart';
|
||||
export 'presentation/views/html_viewer/html_content_viewer_on_web_widget.dart';
|
||||
export 'presentation/views/floating_button/scrolling_floating_button_animated.dart';
|
||||
@@ -119,6 +121,7 @@ export 'presentation/views/shortcut/key_shortcut.dart';
|
||||
export 'presentation/views/tooltip/iframe_tooltip_overlay.dart';
|
||||
export 'presentation/views/color/color_circle_widget.dart';
|
||||
export 'presentation/views/color/colors_map_widget.dart';
|
||||
export 'presentation/views/color/color_picker_modal.dart';
|
||||
|
||||
// Resources
|
||||
export 'presentation/resources/assets_paths.dart';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
import 'package:core/presentation/extensions/color_extension.dart';
|
||||
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DefaultCloseButtonWidget extends StatelessWidget {
|
||||
final String iconClose;
|
||||
final VoidCallback onTapActionCallback;
|
||||
|
||||
const DefaultCloseButtonWidget({
|
||||
super.key,
|
||||
required this.iconClose,
|
||||
required this.onTapActionCallback,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PositionedDirectional(
|
||||
top: 4,
|
||||
end: 4,
|
||||
child: TMailButtonWidget.fromIcon(
|
||||
icon: iconClose,
|
||||
iconSize: 24,
|
||||
iconColor: AppColor.m3Tertiary,
|
||||
padding: const EdgeInsets.all(10),
|
||||
borderRadius: 24,
|
||||
backgroundColor: Colors.transparent,
|
||||
onTapActionCallback: onTapActionCallback,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:core/presentation/extensions/color_extension.dart';
|
||||
import 'package:core/presentation/extensions/string_extension.dart';
|
||||
import 'package:core/presentation/resources/image_paths.dart';
|
||||
import 'package:core/presentation/utils/responsive_utils.dart';
|
||||
import 'package:core/presentation/utils/theme_utils.dart';
|
||||
import 'package:core/presentation/views/button/default_close_button_widget.dart';
|
||||
import 'package:core/presentation/views/dialog/modal_list_action_button_widget.dart';
|
||||
import 'package:core/utils/platform_info.dart';
|
||||
import 'package:flex_color_picker/flex_color_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hsvcolor_picker/flutter_hsvcolor_picker.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
typedef OnSelectColorCallback = void Function(Color? color);
|
||||
|
||||
class ColorPickerModal extends StatefulWidget {
|
||||
final ImagePaths imagePaths;
|
||||
final String modalTitle;
|
||||
final String modalSubtitle;
|
||||
final String negativeButtonText;
|
||||
final String positiveButtonText;
|
||||
final String hexColorText;
|
||||
final Color? initialColor;
|
||||
final OnSelectColorCallback onSelectColorCallback;
|
||||
final VoidCallback? onNegativeAction;
|
||||
|
||||
const ColorPickerModal({
|
||||
super.key,
|
||||
required this.imagePaths,
|
||||
required this.modalTitle,
|
||||
required this.modalSubtitle,
|
||||
required this.onSelectColorCallback,
|
||||
this.negativeButtonText = 'Cancel',
|
||||
this.positiveButtonText = 'Save',
|
||||
this.hexColorText = 'Hex',
|
||||
this.initialColor,
|
||||
this.onNegativeAction,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ColorPickerModal> createState() => _ColorPickerModalState();
|
||||
}
|
||||
|
||||
class _ColorPickerModalState extends State<ColorPickerModal> {
|
||||
static const Color _defaultColor = Colors.blue;
|
||||
|
||||
final TextEditingController _hexColorInputController =
|
||||
TextEditingController();
|
||||
final FocusNode _hexColorFocusNode = FocusNode();
|
||||
final ValueNotifier<HSVColor> _hsvColorNotifier =
|
||||
ValueNotifier(HSVColor.fromColor(_defaultColor));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final currentColor = widget.initialColor ?? _defaultColor;
|
||||
_hsvColorNotifier.value = HSVColor.fromColor(currentColor);
|
||||
_hexColorInputController.text = currentColor.toHexTriplet();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return LayoutBuilder(builder: (_, constraints) {
|
||||
final currentScreenWidth = constraints.maxWidth;
|
||||
final currentScreenHeight = constraints.maxHeight;
|
||||
final isMobile = currentScreenWidth < ResponsiveUtils.minTabletWidth;
|
||||
|
||||
Widget bodyWidget = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.08),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.08),
|
||||
blurRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
width: math.min(
|
||||
currentScreenWidth - 32,
|
||||
554,
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: currentScreenHeight - 100),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 64,
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
start: 32,
|
||||
end: 32,
|
||||
top: 16,
|
||||
bottom: isMobile ? 0 : 16,
|
||||
),
|
||||
child: Text(
|
||||
widget.modalTitle,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: AppColor.m3SurfaceBackground,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
start: 32,
|
||||
end: 32,
|
||||
bottom: 24,
|
||||
),
|
||||
child: Text(
|
||||
widget.modalSubtitle,
|
||||
style: ThemeUtils.textStyleInter400.copyWith(
|
||||
color: AppColor.steelGrayA540,
|
||||
fontSize: 13,
|
||||
height: 20 / 13,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
start: 32,
|
||||
end: 32,
|
||||
bottom: isMobile ? 0 : 16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
bottom: 24,
|
||||
),
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: 12,
|
||||
),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _hsvColorNotifier,
|
||||
builder: (_, value, __) {
|
||||
return PaletteHuePicker(
|
||||
color: value,
|
||||
onChanged: _onHsvColorChanged,
|
||||
hueHeight: 32,
|
||||
paletteHeight: 200,
|
||||
palettePadding: const EdgeInsets.only(
|
||||
bottom: 16,
|
||||
),
|
||||
paletteBorderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
hueBorderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.hexColorText,
|
||||
style: ThemeUtils.textStyleInter400.copyWith(
|
||||
color: AppColor.steelGrayA540,
|
||||
fontSize: 13,
|
||||
height: 20 / 13,
|
||||
letterSpacing: 0.0,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 107,
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
start: 12,
|
||||
top: 6,
|
||||
),
|
||||
child: TextField(
|
||||
controller: _hexColorInputController,
|
||||
focusNode: _hexColorFocusNode,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.black,
|
||||
),
|
||||
cursorColor: AppColor.primaryColor,
|
||||
decoration: InputDecoration(
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.black.withValues(
|
||||
alpha: 0.12,
|
||||
),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.black.withValues(
|
||||
alpha: 0.12,
|
||||
),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.black.withValues(
|
||||
alpha: 0.12,
|
||||
),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.only(
|
||||
bottom: 2,
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.text,
|
||||
textInputAction: TextInputAction.done,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'[0-9A-Fa-f#]'),
|
||||
),
|
||||
],
|
||||
onChanged: _onHexColorChanged,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
ModalListActionButtonWidget(
|
||||
positiveLabel: widget.positiveButtonText,
|
||||
negativeLabel: widget.negativeButtonText,
|
||||
padding: const EdgeInsets.symmetric(vertical: 25),
|
||||
onPositiveAction: _onPositiveAction,
|
||||
onNegativeAction: _onNegativeAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
DefaultCloseButtonWidget(
|
||||
iconClose: widget.imagePaths.icCloseDialog,
|
||||
onTapActionCallback: _onCloseModal,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
bodyWidget = Center(child: bodyWidget);
|
||||
|
||||
if (PlatformInfo.isMobile) {
|
||||
bodyWidget = GestureDetector(
|
||||
onTap: _onCloseModal,
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColor.blackAlpha20,
|
||||
body: GestureDetector(
|
||||
onTap: _clearInputFocus,
|
||||
child: bodyWidget,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return bodyWidget;
|
||||
});
|
||||
}
|
||||
|
||||
void _onCloseModal() {
|
||||
_clearInputFocus();
|
||||
Get.back();
|
||||
}
|
||||
|
||||
void _onNegativeAction() {
|
||||
_clearInputFocus();
|
||||
widget.onNegativeAction?.call();
|
||||
Get.back();
|
||||
}
|
||||
|
||||
void _onPositiveAction() {
|
||||
_clearInputFocus();
|
||||
final hexColorText = _hexColorInputController.text.trimmed;
|
||||
if (hexColorText.isNotEmpty) {
|
||||
widget.onSelectColorCallback(hexColorText.toColor);
|
||||
}
|
||||
Get.back();
|
||||
}
|
||||
|
||||
void _clearInputFocus() {
|
||||
_hexColorFocusNode.unfocus();
|
||||
}
|
||||
|
||||
void _onHsvColorChanged(HSVColor hsvColor) {
|
||||
_hsvColorNotifier.value = hsvColor;
|
||||
_hexColorInputController.text = hsvColor.toColor().toHexTriplet();
|
||||
}
|
||||
|
||||
void _onHexColorChanged(String value) {
|
||||
_hsvColorNotifier.value = HSVColor.fromColor(value.toColor);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hexColorFocusNode.dispose();
|
||||
_hexColorInputController.dispose();
|
||||
_hsvColorNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,21 @@ import 'package:core/presentation/extensions/color_extension.dart';
|
||||
import 'package:core/presentation/resources/image_paths.dart';
|
||||
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
||||
import 'package:core/presentation/views/color/color_circle_widget.dart';
|
||||
import 'package:core/presentation/views/color/color_picker_modal.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ColorsMapWidget extends StatefulWidget {
|
||||
final Color? initialColor;
|
||||
final ImagePaths imagePaths;
|
||||
final Color? customColor;
|
||||
final VoidCallback? onOpenColorPicker;
|
||||
final OnSelectColorCallback? onSelectColorCallback;
|
||||
|
||||
const ColorsMapWidget({
|
||||
super.key,
|
||||
this.initialColor,
|
||||
required this.imagePaths,
|
||||
this.customColor,
|
||||
this.onOpenColorPicker,
|
||||
this.onSelectColorCallback,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -41,14 +47,21 @@ class _ColorsMapWidgetState extends State<ColorsMapWidget> {
|
||||
Color(0xFF646580),
|
||||
];
|
||||
|
||||
final _imagePaths = Get.find<ImagePaths>();
|
||||
|
||||
final ValueNotifier<Color?> _selectedColor = ValueNotifier(null);
|
||||
List<Color> _colorList = <Color>[];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedColor.value = widget.initialColor;
|
||||
_setUpColorList();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ColorsMapWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.customColor != widget.customColor) {
|
||||
_setUpColorList();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -58,7 +71,7 @@ class _ColorsMapWidgetState extends State<ColorsMapWidget> {
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
TMailButtonWidget.fromIcon(
|
||||
icon: _imagePaths.icCloseDialog,
|
||||
icon: widget.imagePaths.icCloseDialog,
|
||||
iconColor: AppColor.m3SurfaceBackground.withValues(alpha: 0.48),
|
||||
iconSize: 18.46,
|
||||
width: 40,
|
||||
@@ -68,7 +81,7 @@ class _ColorsMapWidgetState extends State<ColorsMapWidget> {
|
||||
border: Border.all(width: 2, color: AppColor.grayCDCDCD),
|
||||
onTapActionCallback: _clearColor,
|
||||
),
|
||||
..._defaultColors
|
||||
..._colorList
|
||||
.map(
|
||||
(color) => ValueListenableBuilder(
|
||||
valueListenable: _selectedColor,
|
||||
@@ -77,7 +90,7 @@ class _ColorsMapWidgetState extends State<ColorsMapWidget> {
|
||||
color: color,
|
||||
isSelected: color == value,
|
||||
onTap: () => _selectColor(color),
|
||||
imagePaths: _imagePaths,
|
||||
imagePaths: widget.imagePaths,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -99,30 +112,36 @@ class _ColorsMapWidgetState extends State<ColorsMapWidget> {
|
||||
),
|
||||
),
|
||||
child: TMailButtonWidget.fromIcon(
|
||||
icon: _imagePaths.icColorPicker,
|
||||
icon: widget.imagePaths.icColorPicker,
|
||||
iconColor: AppColor.textSecondary,
|
||||
iconSize: 18.46,
|
||||
width: 40,
|
||||
height: 40,
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 100,
|
||||
backgroundColor: Colors.white,
|
||||
onTapActionCallback: _openColorPicker,
|
||||
onTapActionCallback: widget.onOpenColorPicker,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _setUpColorList() {
|
||||
final custom = widget.customColor;
|
||||
_colorList = custom != null ? [custom, ..._defaultColors] : _defaultColors;
|
||||
_selectedColor.value = custom;
|
||||
}
|
||||
|
||||
void _selectColor(Color color) {
|
||||
_selectedColor.value = color;
|
||||
widget.onSelectColorCallback?.call(color);
|
||||
}
|
||||
|
||||
void _clearColor() {
|
||||
_selectedColor.value = null;
|
||||
widget.onSelectColorCallback?.call(null);
|
||||
}
|
||||
|
||||
void _openColorPicker() {}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_selectedColor.dispose();
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:core/presentation/extensions/color_extension.dart';
|
||||
import 'package:core/presentation/views/dialog/confirm_dialog_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ModalListActionButtonWidget extends StatelessWidget {
|
||||
final String positiveLabel;
|
||||
final String negativeLabel;
|
||||
final VoidCallback onNegativeAction;
|
||||
final VoidCallback onPositiveAction;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
const ModalListActionButtonWidget({
|
||||
super.key,
|
||||
required this.positiveLabel,
|
||||
required this.negativeLabel,
|
||||
required this.onPositiveAction,
|
||||
required this.onNegativeAction,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget negativeButton = Container(
|
||||
constraints: const BoxConstraints(minWidth: 67),
|
||||
height: 48,
|
||||
child: ConfirmDialogButton(
|
||||
label: negativeLabel,
|
||||
onTapAction: onNegativeAction,
|
||||
),
|
||||
);
|
||||
|
||||
Widget positiveButton = Container(
|
||||
constraints: const BoxConstraints(minWidth: 153),
|
||||
height: 48,
|
||||
child: ConfirmDialogButton(
|
||||
label: positiveLabel,
|
||||
backgroundColor: AppColor.primaryMain,
|
||||
textColor: Colors.white,
|
||||
onTapAction: onPositiveAction,
|
||||
),
|
||||
);
|
||||
|
||||
Widget bodyWidget = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(child: negativeButton),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(child: positiveButton),
|
||||
],
|
||||
);
|
||||
|
||||
if (padding != null) {
|
||||
return Padding(padding: padding!, child: bodyWidget);
|
||||
} else {
|
||||
return bodyWidget;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,6 +382,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
flutter_hsvcolor_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_hsvcolor_picker
|
||||
sha256: f3480d7beaf4d3a3b76512a0c149a3b698076c8cf721bc515cded30de37ded6a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
flutter_image_compress:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -107,6 +107,8 @@ dependencies:
|
||||
|
||||
sentry_dio: 9.8.0
|
||||
|
||||
flutter_hsvcolor_picker: 1.5.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
Reference in New Issue
Block a user