TF-4178 Add color picker modal
This commit is contained in:
@@ -389,6 +389,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
version: "5.0.2"
|
||||||
|
flutter_hsvcolor_picker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_hsvcolor_picker
|
||||||
|
sha256: f3480d7beaf4d3a3b76512a0c149a3b698076c8cf721bc515cded30de37ded6a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.1"
|
||||||
flutter_image_compress:
|
flutter_image_compress:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export 'presentation/views/responsive/responsive_widget.dart';
|
|||||||
export 'presentation/views/list/tree_view.dart';
|
export 'presentation/views/list/tree_view.dart';
|
||||||
export 'presentation/views/button/icon_button_web.dart';
|
export 'presentation/views/button/icon_button_web.dart';
|
||||||
export 'presentation/views/button/tmail_button_widget.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/image/avatar_builder.dart';
|
||||||
export 'presentation/views/list/sliver_grid_delegate_fixed_height.dart';
|
export 'presentation/views/list/sliver_grid_delegate_fixed_height.dart';
|
||||||
export 'presentation/views/list/no_stretch_scroll_behavior.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/confirmation_dialog_builder.dart';
|
||||||
export 'presentation/views/dialog/edit_text_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/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_widget.dart';
|
||||||
export 'presentation/views/html_viewer/html_content_viewer_on_web_widget.dart';
|
export 'presentation/views/html_viewer/html_content_viewer_on_web_widget.dart';
|
||||||
export 'presentation/views/floating_button/scrolling_floating_button_animated.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/tooltip/iframe_tooltip_overlay.dart';
|
||||||
export 'presentation/views/color/color_circle_widget.dart';
|
export 'presentation/views/color/color_circle_widget.dart';
|
||||||
export 'presentation/views/color/colors_map_widget.dart';
|
export 'presentation/views/color/colors_map_widget.dart';
|
||||||
|
export 'presentation/views/color/color_picker_modal.dart';
|
||||||
|
|
||||||
// Resources
|
// Resources
|
||||||
export 'presentation/resources/assets_paths.dart';
|
export 'presentation/resources/assets_paths.dart';
|
||||||
|
|||||||
@@ -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/resources/image_paths.dart';
|
||||||
import 'package:core/presentation/views/button/tmail_button_widget.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_circle_widget.dart';
|
||||||
|
import 'package:core/presentation/views/color/color_picker_modal.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
class ColorsMapWidget extends StatefulWidget {
|
class ColorsMapWidget extends StatefulWidget {
|
||||||
final Color? initialColor;
|
final ImagePaths imagePaths;
|
||||||
|
final Color? customColor;
|
||||||
|
final VoidCallback? onOpenColorPicker;
|
||||||
|
final OnSelectColorCallback? onSelectColorCallback;
|
||||||
|
|
||||||
const ColorsMapWidget({
|
const ColorsMapWidget({
|
||||||
super.key,
|
super.key,
|
||||||
this.initialColor,
|
required this.imagePaths,
|
||||||
|
this.customColor,
|
||||||
|
this.onOpenColorPicker,
|
||||||
|
this.onSelectColorCallback,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -41,14 +47,21 @@ class _ColorsMapWidgetState extends State<ColorsMapWidget> {
|
|||||||
Color(0xFF646580),
|
Color(0xFF646580),
|
||||||
];
|
];
|
||||||
|
|
||||||
final _imagePaths = Get.find<ImagePaths>();
|
|
||||||
|
|
||||||
final ValueNotifier<Color?> _selectedColor = ValueNotifier(null);
|
final ValueNotifier<Color?> _selectedColor = ValueNotifier(null);
|
||||||
|
List<Color> _colorList = <Color>[];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_selectedColor.value = widget.initialColor;
|
_setUpColorList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant ColorsMapWidget oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.customColor != widget.customColor) {
|
||||||
|
_setUpColorList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -58,7 +71,7 @@ class _ColorsMapWidgetState extends State<ColorsMapWidget> {
|
|||||||
runSpacing: 10,
|
runSpacing: 10,
|
||||||
children: [
|
children: [
|
||||||
TMailButtonWidget.fromIcon(
|
TMailButtonWidget.fromIcon(
|
||||||
icon: _imagePaths.icCloseDialog,
|
icon: widget.imagePaths.icCloseDialog,
|
||||||
iconColor: AppColor.m3SurfaceBackground.withValues(alpha: 0.48),
|
iconColor: AppColor.m3SurfaceBackground.withValues(alpha: 0.48),
|
||||||
iconSize: 18.46,
|
iconSize: 18.46,
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -68,7 +81,7 @@ class _ColorsMapWidgetState extends State<ColorsMapWidget> {
|
|||||||
border: Border.all(width: 2, color: AppColor.grayCDCDCD),
|
border: Border.all(width: 2, color: AppColor.grayCDCDCD),
|
||||||
onTapActionCallback: _clearColor,
|
onTapActionCallback: _clearColor,
|
||||||
),
|
),
|
||||||
..._defaultColors
|
..._colorList
|
||||||
.map(
|
.map(
|
||||||
(color) => ValueListenableBuilder(
|
(color) => ValueListenableBuilder(
|
||||||
valueListenable: _selectedColor,
|
valueListenable: _selectedColor,
|
||||||
@@ -77,7 +90,7 @@ class _ColorsMapWidgetState extends State<ColorsMapWidget> {
|
|||||||
color: color,
|
color: color,
|
||||||
isSelected: color == value,
|
isSelected: color == value,
|
||||||
onTap: () => _selectColor(color),
|
onTap: () => _selectColor(color),
|
||||||
imagePaths: _imagePaths,
|
imagePaths: widget.imagePaths,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -99,30 +112,36 @@ class _ColorsMapWidgetState extends State<ColorsMapWidget> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: TMailButtonWidget.fromIcon(
|
child: TMailButtonWidget.fromIcon(
|
||||||
icon: _imagePaths.icColorPicker,
|
icon: widget.imagePaths.icColorPicker,
|
||||||
iconColor: AppColor.textSecondary,
|
iconColor: AppColor.textSecondary,
|
||||||
iconSize: 18.46,
|
iconSize: 18.46,
|
||||||
width: 40,
|
width: 38,
|
||||||
height: 40,
|
height: 38,
|
||||||
borderRadius: 100,
|
borderRadius: 100,
|
||||||
backgroundColor: Colors.white,
|
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) {
|
void _selectColor(Color color) {
|
||||||
_selectedColor.value = color;
|
_selectedColor.value = color;
|
||||||
|
widget.onSelectColorCallback?.call(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearColor() {
|
void _clearColor() {
|
||||||
_selectedColor.value = null;
|
_selectedColor.value = null;
|
||||||
|
widget.onSelectColorCallback?.call(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openColorPicker() {}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_selectedColor.dispose();
|
_selectedColor.dispose();
|
||||||
|
|||||||
@@ -382,6 +382,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
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:
|
flutter_image_compress:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ dependencies:
|
|||||||
|
|
||||||
sentry_dio: 9.8.0
|
sentry_dio: 9.8.0
|
||||||
|
|
||||||
|
flutter_hsvcolor_picker: 1.5.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
@@ -381,6 +381,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
|
flutter_hsvcolor_picker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_hsvcolor_picker
|
||||||
|
sha256: f3480d7beaf4d3a3b76512a0c149a3b698076c8cf721bc515cded30de37ded6a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.1"
|
||||||
flutter_image_compress:
|
flutter_image_compress:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
+1
-1
@@ -1,12 +1,12 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:core/presentation/utils/theme_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/checkbox/custom_icon_labeled_checkbox.dart';
|
import 'package:core/presentation/views/checkbox/custom_icon_labeled_checkbox.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:tmail_ui_user/features/base/model/filter_filter.dart';
|
import 'package:tmail_ui_user/features/base/model/filter_filter.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/default_field/default_autocomplete_input_field_widget.dart';
|
import 'package:tmail_ui_user/features/base/widget/default_field/default_autocomplete_input_field_widget.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/default_field/default_close_button_widget.dart';
|
|
||||||
import 'package:tmail_ui_user/features/base/widget/default_field/default_date_drop_down_field_widget.dart';
|
import 'package:tmail_ui_user/features/base/widget/default_field/default_date_drop_down_field_widget.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/default_field/default_input_field_with_tab_key_widget.dart';
|
import 'package:tmail_ui_user/features/base/widget/default_field/default_input_field_with_tab_key_widget.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/default_field/default_label_field_widget.dart';
|
import 'package:tmail_ui_user/features/base/widget/default_field/default_label_field_widget.dart';
|
||||||
|
|||||||
+1
-1
@@ -1,12 +1,12 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:core/presentation/extensions/color_extension.dart';
|
import 'package:core/presentation/extensions/color_extension.dart';
|
||||||
|
import 'package:core/presentation/views/button/default_close_button_widget.dart';
|
||||||
import 'package:core/utils/platform_info.dart';
|
import 'package:core/utils/platform_info.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:rich_text_composer/views/keyboard_rich_text.dart';
|
import 'package:rich_text_composer/views/keyboard_rich_text.dart';
|
||||||
import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart';
|
import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/default_field/default_close_button_widget.dart';
|
|
||||||
import 'package:tmail_ui_user/features/base/widget/pop_back_barrier_widget.dart';
|
import 'package:tmail_ui_user/features/base/widget/pop_back_barrier_widget.dart';
|
||||||
import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart';
|
import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart';
|
||||||
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_creator_form_bottom_view.dart';
|
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_creator_form_bottom_view.dart';
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import 'package:core/presentation/extensions/color_extension.dart';
|
|||||||
import 'package:core/presentation/resources/image_paths.dart';
|
import 'package:core/presentation/resources/image_paths.dart';
|
||||||
import 'package:core/presentation/utils/responsive_utils.dart';
|
import 'package:core/presentation/utils/responsive_utils.dart';
|
||||||
import 'package:core/presentation/utils/theme_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/color/color_picker_modal.dart';
|
||||||
import 'package:core/presentation/views/color/colors_map_widget.dart';
|
import 'package:core/presentation/views/color/colors_map_widget.dart';
|
||||||
|
import 'package:core/presentation/views/dialog/modal_list_action_button_widget.dart';
|
||||||
import 'package:core/utils/platform_info.dart';
|
import 'package:core/utils/platform_info.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:labels/extensions/list_label_extension.dart';
|
import 'package:labels/extensions/list_label_extension.dart';
|
||||||
import 'package:labels/model/label.dart';
|
import 'package:labels/model/label.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/default_field/default_close_button_widget.dart';
|
|
||||||
import 'package:tmail_ui_user/features/base/widget/label_input_field_builder.dart';
|
import 'package:tmail_ui_user/features/base/widget/label_input_field_builder.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/modal_list_action_button_widget.dart';
|
|
||||||
import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/duplicate_name_validator.dart';
|
import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/duplicate_name_validator.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/empty_name_validator.dart';
|
import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/empty_name_validator.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/name_with_space_only_validator.dart';
|
import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/name_with_space_only_validator.dart';
|
||||||
@@ -37,11 +38,14 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
|
|||||||
final _imagePaths = Get.find<ImagePaths>();
|
final _imagePaths = Get.find<ImagePaths>();
|
||||||
final _verifyNameInteractor = Get.find<VerifyNameInteractor>();
|
final _verifyNameInteractor = Get.find<VerifyNameInteractor>();
|
||||||
|
|
||||||
final ValueNotifier<String?> _labelNameErrorText = ValueNotifier(null);
|
final ValueNotifier<String?> _labelNameErrorTextNotifier =
|
||||||
|
ValueNotifier(null);
|
||||||
|
final ValueNotifier<Color?> _labelSelectedColorNotifier = ValueNotifier(null);
|
||||||
final TextEditingController _nameInputController = TextEditingController();
|
final TextEditingController _nameInputController = TextEditingController();
|
||||||
final FocusNode _nameInputFocusNode = FocusNode();
|
final FocusNode _nameInputFocusNode = FocusNode();
|
||||||
|
|
||||||
List<String> _labelDisplayNameList = <String>[];
|
List<String> _labelDisplayNameList = <String>[];
|
||||||
|
Color? _selectedColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -107,10 +111,10 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
|
|||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsetsDirectional.only(
|
padding: const EdgeInsetsDirectional.only(
|
||||||
start: 32,
|
start: 32,
|
||||||
end: 32,
|
end: 32,
|
||||||
bottom: isMobile ? 32 : 48,
|
bottom: 24,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
appLocalizations.organizeYourInboxWithACustomCategory,
|
appLocalizations.organizeYourInboxWithACustomCategory,
|
||||||
@@ -137,7 +141,7 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ValueListenableBuilder(
|
ValueListenableBuilder(
|
||||||
valueListenable: _labelNameErrorText,
|
valueListenable: _labelNameErrorTextNotifier,
|
||||||
builder: (_, errorText, __) {
|
builder: (_, errorText, __) {
|
||||||
return LabelInputFieldBuilder(
|
return LabelInputFieldBuilder(
|
||||||
label: appLocalizations.labelName,
|
label: appLocalizations.labelName,
|
||||||
@@ -185,7 +189,18 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
|
|||||||
start: 32,
|
start: 32,
|
||||||
end: 16,
|
end: 16,
|
||||||
),
|
),
|
||||||
child: const ColorsMapWidget(),
|
child: ValueListenableBuilder(
|
||||||
|
valueListenable: _labelSelectedColorNotifier,
|
||||||
|
builder: (_, value, __) {
|
||||||
|
return ColorsMapWidget(
|
||||||
|
imagePaths: _imagePaths,
|
||||||
|
customColor: value,
|
||||||
|
onOpenColorPicker: () =>
|
||||||
|
_openColorPickerModal(appLocalizations),
|
||||||
|
onSelectColorCallback: _updateLabelColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ModalListActionButtonWidget(
|
ModalListActionButtonWidget(
|
||||||
@@ -230,8 +245,13 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onLabelNameInputChanged(
|
void _onLabelNameInputChanged(
|
||||||
AppLocalizations appLocalizations, String value) {
|
AppLocalizations appLocalizations,
|
||||||
_labelNameErrorText.value = _verifyLabelName(appLocalizations, value);
|
String value,
|
||||||
|
) {
|
||||||
|
_labelNameErrorTextNotifier.value = _verifyLabelName(
|
||||||
|
appLocalizations,
|
||||||
|
value,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _verifyLabelName(AppLocalizations appLocalizations, String value) {
|
String? _verifyLabelName(AppLocalizations appLocalizations, String value) {
|
||||||
@@ -269,11 +289,35 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
|
|||||||
popBack();
|
popBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateLabelColor(Color? color) {
|
||||||
|
_selectedColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLabelColorChanged(Color? color) {
|
||||||
|
_updateLabelColor(color);
|
||||||
|
_labelSelectedColorNotifier.value = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openColorPickerModal(AppLocalizations appLocalizations) async {
|
||||||
|
await Get.generalDialog(
|
||||||
|
barrierDismissible: true,
|
||||||
|
barrierLabel: 'color-picker-modal',
|
||||||
|
pageBuilder: (_, __, ___) => ColorPickerModal(
|
||||||
|
imagePaths: _imagePaths,
|
||||||
|
modalTitle: appLocalizations.chooseCustomColour,
|
||||||
|
modalSubtitle: appLocalizations.chooseAColourForThisLabel,
|
||||||
|
initialColor: _selectedColor,
|
||||||
|
onSelectColorCallback: _onLabelColorChanged,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_nameInputFocusNode.dispose();
|
_nameInputFocusNode.dispose();
|
||||||
_nameInputController.dispose();
|
_nameInputController.dispose();
|
||||||
_labelNameErrorText.dispose();
|
_labelNameErrorTextNotifier.dispose();
|
||||||
|
_labelSelectedColorNotifier.dispose();
|
||||||
_labelDisplayNameList = [];
|
_labelDisplayNameList = [];
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import 'dart:math' as math;
|
|||||||
import 'package:core/presentation/extensions/color_extension.dart';
|
import 'package:core/presentation/extensions/color_extension.dart';
|
||||||
import 'package:core/presentation/utils/responsive_utils.dart';
|
import 'package:core/presentation/utils/responsive_utils.dart';
|
||||||
import 'package:core/presentation/utils/theme_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:core/utils/platform_info.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/default_field/default_button_arrow_down_field_widget.dart';
|
import 'package:tmail_ui_user/features/base/widget/default_field/default_button_arrow_down_field_widget.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/default_field/default_close_button_widget.dart';
|
|
||||||
import 'package:tmail_ui_user/features/base/widget/label_input_field_builder.dart';
|
import 'package:tmail_ui_user/features/base/widget/label_input_field_builder.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/modal_list_action_button_widget.dart';
|
|
||||||
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart';
|
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_icon_widget.dart';
|
import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_icon_widget.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_controller.dart';
|
import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_controller.dart';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import 'package:core/presentation/views/button/default_close_button_widget.dart';
|
||||||
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
||||||
import 'package:core/utils/platform_info.dart';
|
import 'package:core/utils/platform_info.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/default_field/default_close_button_widget.dart';
|
|
||||||
import 'package:tmail_ui_user/features/quotas/domain/extensions/quota_extensions.dart';
|
import 'package:tmail_ui_user/features/quotas/domain/extensions/quota_extensions.dart';
|
||||||
import 'package:tmail_ui_user/features/quotas/presentation/quotas_controller.dart';
|
import 'package:tmail_ui_user/features/quotas/presentation/quotas_controller.dart';
|
||||||
import 'package:tmail_ui_user/features/quotas/presentation/styles/quotas_banner_styles.dart';
|
import 'package:tmail_ui_user/features/quotas/presentation/styles/quotas_banner_styles.dart';
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:core/core.dart';
|
|||||||
import 'package:core/presentation/views/dialog/confirm_dialog_button.dart';
|
import 'package:core/presentation/views/dialog/confirm_dialog_button.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/default_field/default_close_button_widget.dart';
|
|
||||||
import 'package:tmail_ui_user/features/base/widget/label_input_field_builder.dart';
|
import 'package:tmail_ui_user/features/base/widget/label_input_field_builder.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/pop_back_barrier_widget.dart';
|
import 'package:tmail_ui_user/features/base/widget/pop_back_barrier_widget.dart';
|
||||||
import 'package:tmail_ui_user/features/rules_filter_creator/presentation/extensions/handle_toggle_preview_rule_filter_extension.dart';
|
import 'package:tmail_ui_user/features/rules_filter_creator/presentation/extensions/handle_toggle_preview_rule_filter_extension.dart';
|
||||||
|
|||||||
@@ -5243,5 +5243,17 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders_order": [],
|
"placeholders_order": [],
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"chooseCustomColour": "Choose custom colour",
|
||||||
|
"@chooseCustomColour": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders_order": [],
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"chooseAColourForThisLabel": "Choose a colour for this label",
|
||||||
|
"@chooseAColourForThisLabel": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders_order": [],
|
||||||
|
"placeholders": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5569,4 +5569,18 @@ class AppLocalizations {
|
|||||||
name: 'tagNameAlreadyExists',
|
name: 'tagNameAlreadyExists',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get chooseCustomColour {
|
||||||
|
return Intl.message(
|
||||||
|
'Choose custom colour',
|
||||||
|
name: 'chooseCustomColour',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get chooseAColourForThisLabel {
|
||||||
|
return Intl.message(
|
||||||
|
'Choose a colour for this label',
|
||||||
|
name: 'chooseAColourForThisLabel',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -389,6 +389,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
version: "5.0.2"
|
||||||
|
flutter_hsvcolor_picker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_hsvcolor_picker
|
||||||
|
sha256: f3480d7beaf4d3a3b76512a0c149a3b698076c8cf721bc515cded30de37ded6a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.1"
|
||||||
flutter_image_compress:
|
flutter_image_compress:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -789,6 +789,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
flutter_hsvcolor_picker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_hsvcolor_picker
|
||||||
|
sha256: f3480d7beaf4d3a3b76512a0c149a3b698076c8cf721bc515cded30de37ded6a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.1"
|
||||||
flutter_image_compress:
|
flutter_image_compress:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user