From 7da759c662aa9bb8c6276427384b750a508dca5e Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 29 Nov 2023 09:38:53 +0800 Subject: [PATCH] feat: support select / multi select option (#4039) * feat: implement select option cell * feat: support adding new select option * feat: support selecting / deselecting option * feat: clear search field after adding new property * feat: support updating option name --- .../show_mobile_bottom_sheet.dart | 2 + .../database/field/edit_select_field.dart | 49 ++ .../lib/plugins/base/drag_handler.dart | 20 + .../mobile_select_option_editor.dart | 638 ++++++++++++++++++ .../select_option_cell.dart | 71 +- .../appearance/mobile_appearance.dart | 6 +- .../lib/style_widget/text_field.dart | 11 +- .../flowy_icons/16x/m_checkbox_checked.svg | 4 + .../flowy_icons/16x/m_checkbox_uncheck.svg | 3 + 9 files changed, 778 insertions(+), 26 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/field/edit_select_field.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/base/drag_handler.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/mobile_select_option_editor.dart create mode 100644 frontend/resources/flowy_icons/16x/m_checkbox_checked.svg create mode 100644 frontend/resources/flowy_icons/16x/m_checkbox_uncheck.svg diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart index 6799dbac53..7960a0a9f5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart @@ -11,12 +11,14 @@ Future showMobileBottomSheet({ required BuildContext context, required WidgetBuilder builder, bool isDragEnabled = true, + ShapeBorder? shape, }) async { showModalBottomSheet( context: context, isScrollControlled: true, enableDrag: isDragEnabled, useSafeArea: true, + shape: shape, builder: builder, ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/edit_select_field.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/edit_select_field.dart new file mode 100644 index 0000000000..33e6b05301 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/edit_select_field.dart @@ -0,0 +1,49 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +// include single select and multiple select +class EditSelectField extends StatefulWidget { + const EditSelectField({super.key}); + + @override + State createState() => _EditSelectFieldState(); +} + +class _EditSelectFieldState extends State { + @override + Widget build(BuildContext context) { + return Column( + children: [ + _SearchField( + hintText: LocaleKeys.grid_selectOption_searchOrCreateOption.tr(), + ), + ], + ); + } +} + +class _SearchField extends StatelessWidget { + const _SearchField({ + required this.hintText, + }); + + final String hintText; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: SizedBox( + height: 44, // the height is fixed. + child: FlowyTextField( + hintText: hintText, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/drag_handler.dart b/frontend/appflowy_flutter/lib/plugins/base/drag_handler.dart new file mode 100644 index 0000000000..28e7e96af5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/drag_handler.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class DragHandler extends StatelessWidget { + const DragHandler({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: 4, + width: 40, + margin: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.shade400, + borderRadius: BorderRadius.circular(2), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/mobile_select_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/mobile_select_option_editor.dart new file mode 100644 index 0000000000..0eddf5c4e0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/mobile_select_option_editor.dart @@ -0,0 +1,638 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/drag_handler.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:easy_localization/easy_localization.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:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:protobuf/protobuf.dart'; + +// include single select and multiple select +class MobileSelectOptionEditor extends StatefulWidget { + const MobileSelectOptionEditor({ + super.key, + required this.cellController, + }); + + final SelectOptionCellController cellController; + + @override + State createState() => + _MobileSelectOptionEditorState(); +} + +class _MobileSelectOptionEditorState extends State { + final searchController = TextEditingController(); + final renameController = TextEditingController(); + + String typingOption = ''; + FieldType get fieldType => widget.cellController.fieldType; + + bool showMoreOptions = false; + SelectOptionPB? option; + + @override + void dispose() { + searchController.dispose(); + renameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + constraints: const BoxConstraints.tightFor(height: 420), + child: BlocProvider( + create: (context) => SelectOptionCellEditorBloc( + cellController: widget.cellController, + )..add(const SelectOptionEditorEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const DragHandler(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: _buildHeader( + context, + showSaveButton: state.createOption + .fold(() => false, (a) => a.isNotEmpty) || + showMoreOptions, + ), + ), + const Divider(), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: showMoreOptions ? 0.0 : 16.0, + ), + child: _buildBody(context), + ), + ), + ], + ); + }, + ), + ), + ); + } + + Widget _buildHeader(BuildContext context, {required bool showSaveButton}) { + const iconWidth = 36.0; + const height = 44.0; + return Stack( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Align( + alignment: Alignment.centerLeft, + child: FlowyIconButton( + icon: const FlowySvg( + FlowySvgs.close_s, + size: Size.square(iconWidth), + ), + width: iconWidth, + iconPadding: EdgeInsets.zero, + onPressed: () => _popOrBack(), + ), + ), + SizedBox( + height: 44.0, + child: Align( + alignment: Alignment.center, + child: FlowyText.medium( + _headerTitle(), + fontSize: 18, + ), + ), + ), + Align( + alignment: Alignment.centerRight, + child: !showSaveButton + ? const HSpace(iconWidth) + : Container( + padding: const EdgeInsets.symmetric( + vertical: 2.0, + horizontal: 8.0, + ), + decoration: const BoxDecoration( + color: Color(0xFF00bcf0), + borderRadius: Corners.s10Border, + ), + child: FlowyButton( + text: FlowyText( + LocaleKeys.button_save.tr(), + color: Colors.white, + ), + useIntrinsicWidth: true, + onTap: () { + if (showMoreOptions) { + final option = this.option; + if (option == null) { + return; + } + option.freeze(); + context.read().add( + SelectOptionEditorEvent.updateOption( + option.rebuild((p0) { + if (p0.name != renameController.text) { + p0.name = renameController.text; + } + }), + ), + ); + _popOrBack(); + } else if (typingOption.isNotEmpty) { + context.read().add( + SelectOptionEditorEvent.trySelectOption( + typingOption, + ), + ); + searchController.clear(); + } + }, + ), + ), + ), + ].map((e) => SizedBox(height: height, child: e)).toList(), + ); + } + + Widget _buildBody(BuildContext context) { + if (showMoreOptions && option != null) { + return _MoreOptions( + option: option!, + controller: renameController..text = option!.name, + onDelete: () { + context + .read() + .add(SelectOptionEditorEvent.deleteOption(option!)); + context.pop(); + }, + onUpdate: (name, color) { + final option = this.option; + if (option == null) { + return; + } + option.freeze(); + context.read().add( + SelectOptionEditorEvent.updateOption( + option.rebuild((p0) { + if (name != null) { + p0.name = name; + } + if (color != null) { + p0.color = color; + } + }), + ), + ); + _popOrBack(); + }, + ); + } + + return SingleChildScrollView( + child: Column( + children: [ + _SearchField( + controller: searchController, + hintText: LocaleKeys.grid_selectOption_searchOrCreateOption.tr(), + onSubmitted: (option) { + context + .read() + .add(SelectOptionEditorEvent.trySelectOption(option)); + searchController.clear(); + }, + onChanged: (value) { + typingOption = value; + context.read().add( + SelectOptionEditorEvent.selectMultipleOptions( + [], + value, + ), + ); + }, + ), + _OptionList( + onCreateOption: (optionName) { + context + .read() + .add(SelectOptionEditorEvent.newOption(optionName)); + searchController.clear(); + }, + onCheck: (option, value) { + if (value) { + context + .read() + .add(SelectOptionEditorEvent.selectOption(option.id)); + } else { + context + .read() + .add(SelectOptionEditorEvent.unSelectOption(option.id)); + } + }, + onMoreOptions: (option) { + setState(() { + this.option = option; + showMoreOptions = true; + }); + }, + ), + ], + ), + ); + } + + String _headerTitle() { + switch (fieldType) { + case FieldType.SingleSelect: + return LocaleKeys.grid_field_singleSelectFieldName.tr(); + case FieldType.MultiSelect: + return LocaleKeys.grid_field_multiSelectFieldName.tr(); + default: + throw UnimplementedError(); + } + } + + void _popOrBack() { + if (showMoreOptions) { + setState(() { + showMoreOptions = false; + option = null; + }); + } else { + context.pop(); + } + } +} + +class _SearchField extends StatelessWidget { + const _SearchField({ + this.hintText, + required this.onChanged, + required this.onSubmitted, + required this.controller, + }); + + final String? hintText; + final void Function(String value) onChanged; + final void Function(String value) onSubmitted; + final TextEditingController controller; + + @override + Widget build(BuildContext context) { + final textStyle = Theme.of(context).textTheme.bodyMedium; + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + child: SizedBox( + height: 44, // the height is fixed. + child: FlowyTextField( + autoFocus: false, + hintText: hintText, + textStyle: textStyle, + hintStyle: textStyle?.copyWith(color: Theme.of(context).hintColor), + onChanged: onChanged, + onSubmitted: onSubmitted, + controller: controller, + ), + ), + ); + } +} + +class _OptionList extends StatelessWidget { + const _OptionList({ + required this.onCreateOption, + required this.onCheck, + required this.onMoreOptions, + }); + + final void Function(String optionName) onCreateOption; + final void Function(SelectOptionPB option, bool value) onCheck; + final void Function(SelectOptionPB option) onMoreOptions; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + // existing options + final List cells = []; + + // create an option cell + state.createOption.fold( + () => null, + (createOption) { + cells.add( + _CreateOptionCell( + optionName: createOption, + onTap: () => onCreateOption(createOption), + ), + ); + }, + ); + + cells.addAll( + state.options.map( + (option) => _SelectOption( + option: option, + checked: state.selectedOptions.contains(option), + onCheck: (value) => onCheck(option, value), + onMoreOptions: () => onMoreOptions(option), + ), + ), + ); + + return ListView.separated( + shrinkWrap: true, + itemCount: cells.length, + separatorBuilder: (_, __) => + VSpace(GridSize.typeOptionSeparatorHeight), + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (_, int index) => cells[index], + padding: const EdgeInsets.only(bottom: 12.0), + ); + }, + ); + } +} + +class _SelectOption extends StatelessWidget { + const _SelectOption({ + required this.option, + required this.checked, + required this.onCheck, + required this.onMoreOptions, + }); + + final SelectOptionPB option; + final bool checked; + final void Function(bool value) onCheck; + final VoidCallback onMoreOptions; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 44, + child: GestureDetector( + // no need to add click effect, so using gesture detector + behavior: HitTestBehavior.translucent, + onTap: () => onCheck(!checked), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // check icon + FlowySvg( + checked + ? FlowySvgs.m_checkbox_checked_s + : FlowySvgs.m_checkbox_uncheck_s, + size: const Size.square(24.0), + blendMode: null, + ), + // padding + const HSpace(12), + // option tag + _SelectOptionTag( + optionName: option.name, + color: option.color.toColor(context), + ), + const Spacer(), + // more options + FlowyIconButton( + icon: const FlowySvg(FlowySvgs.three_dots_s), + onPressed: onMoreOptions, + ), + ], + ), + ), + ); + } +} + +class _CreateOptionCell extends StatelessWidget { + const _CreateOptionCell({ + required this.optionName, + required this.onTap, + }); + + final String optionName; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 44, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: onTap, + child: Row( + children: [ + FlowyText.medium( + LocaleKeys.grid_selectOption_create.tr(), + color: Theme.of(context).hintColor, + ), + const HSpace(8), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: _SelectOptionTag( + optionName: optionName, + color: Theme.of(context).colorScheme.surfaceVariant, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _SelectOptionTag extends StatelessWidget { + const _SelectOptionTag({ + required this.optionName, + required this.color, + }); + + final String optionName; + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: 6.0, + horizontal: 12.0, + ), + decoration: BoxDecoration( + color: color, + borderRadius: Corners.s12Border, + ), + child: FlowyText.regular( + optionName, + fontSize: 16, + overflow: TextOverflow.ellipsis, + color: AFThemeExtension.of(context).textColor, + ), + ); + } +} + +class _MoreOptions extends StatelessWidget { + const _MoreOptions({ + required this.option, + required this.onDelete, + required this.onUpdate, + required this.controller, + }); + + final SelectOptionPB option; + final VoidCallback onDelete; + final void Function(String? name, SelectOptionColorPB? color) onUpdate; + final TextEditingController controller; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const VSpace(8.0), + _buildRenameTextField(context), + const VSpace(16.0), + _buildDeleteButton(context), + const VSpace(16.0), + Padding( + padding: const EdgeInsets.only(left: 12.0), + child: FlowyText( + LocaleKeys.grid_field_optionTitle.tr(), + color: Theme.of(context).hintColor, + ), + ), + const VSpace(4.0), + _buildColorOptions(context), + ], + ), + ); + } + + Widget _buildRenameTextField(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints.tightFor(height: 52.0), + child: _DefaultDecorateBox( + showTopBorder: true, + showBottomBorder: true, + child: TextField( + controller: controller, + decoration: const InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: EdgeInsets.only(left: 16.0), + ), + onChanged: (value) {}, + onSubmitted: (value) => onUpdate(value, null), + ), + ), + ); + } + + Widget _buildDeleteButton(BuildContext context) { + return _DefaultDecorateBox( + showTopBorder: true, + showBottomBorder: true, + child: FlowyButton( + text: FlowyText( + LocaleKeys.button_delete.tr(), + fontSize: 16.0, + ), + margin: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 16.0, + ), + leftIcon: const FlowySvg(FlowySvgs.delete_s), + leftIconSize: const Size.square(24.0), + iconPadding: 8.0, + onTap: onDelete, + ), + ); + } + + Widget _buildColorOptions(BuildContext context) { + return _DefaultDecorateBox( + showTopBorder: true, + showBottomBorder: true, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: GridView.count( + crossAxisCount: 6, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: SelectOptionColorPB.values.map( + (colorPB) { + final color = colorPB.toColor(context); + return GestureDetector( + onTap: () => onUpdate(null, colorPB), + child: Container( + margin: const EdgeInsets.all( + 10.0, + ), + decoration: BoxDecoration( + color: color, + borderRadius: Corners.s12Border, + border: Border.all( + color: option.color.value == colorPB.value + ? const Color(0xff00C6F1) + : Theme.of(context).dividerColor, + ), + ), + ), + ); + }, + ).toList(), + ), + ), + ); + } +} + +class _DefaultDecorateBox extends StatelessWidget { + const _DefaultDecorateBox({ + this.showTopBorder = true, + this.showBottomBorder = true, + required this.child, + }); + + final bool showTopBorder; + final bool showBottomBorder; + final Widget child; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + border: Border( + top: BorderSide( + color: Theme.of(context).dividerColor, + ), + bottom: BorderSide( + color: Theme.of(context).dividerColor, + ), + ), + ), + child: child, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart index cf814e2a9a..781163339b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart @@ -1,6 +1,10 @@ +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/mobile_select_option_editor.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -171,31 +175,56 @@ class SelectOptionWrap extends StatefulWidget { class _SelectOptionWrapState extends State { @override Widget build(BuildContext context) { - final Widget child = _buildOptions(context); - final constraints = BoxConstraints.loose( Size(SelectOptionCellEditor.editorPanelWidth, 300), ); - return AppFlowyPopover( - controller: widget.popoverController, - constraints: constraints, - margin: EdgeInsets.zero, - direction: PopoverDirection.bottomWithLeftAligned, - popupBuilder: (BuildContext popoverContext) { - WidgetsBinding.instance.addPostFrameCallback((_) { - widget.onCellEditing(true); - }); - return SelectOptionCellEditor( - cellController: widget.cellControllerBuilder.build() - as SelectOptionCellController, - ); - }, - onClose: () => widget.onCellEditing(false), - child: Padding( - padding: widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets, - child: child, - ), + final cellController = + widget.cellControllerBuilder.build() as SelectOptionCellController; + + Widget child = Padding( + padding: widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets, + child: _buildOptions(context), ); + + if (PlatformExtension.isDesktopOrWeb) { + child = AppFlowyPopover( + controller: widget.popoverController, + constraints: constraints, + margin: EdgeInsets.zero, + direction: PopoverDirection.bottomWithLeftAligned, + popupBuilder: (BuildContext popoverContext) { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onCellEditing(true); + }); + return SelectOptionCellEditor( + cellController: cellController, + ); + }, + onClose: () => widget.onCellEditing(false), + child: child, + ); + } else { + child = FlowyButton( + text: child, + onTap: () { + showMobileBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Corners.s12Radius, + ), + ), + builder: (context) { + return MobileSelectOptionEditor( + cellController: cellController, + ); + }, + ); + }, + ); + } + + return child; } Widget _buildOptions(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 8446512456..de1ffc175b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -70,12 +70,16 @@ class MobileAppearance extends BaseAppearance { onSurface: const Color(0xffC5C6C7), // text/body color ); + final hintColor = brightness == Brightness.light + ? const Color(0xff89909B) + : const Color(0xff96989C); + return ThemeData( // color primaryColor: colorTheme.primary, //primary 100 primaryColorLight: const Color(0xFF57B5F8), //primary 80 dividerColor: colorTheme.outline, //caption - hintColor: colorTheme.outline, + hintColor: hintColor, disabledColor: colorTheme.outline, scaffoldBackgroundColor: colorTheme.background, appBarTheme: AppBarTheme( diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index 0d40af1f8a..dc07eab03c 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart @@ -27,6 +27,7 @@ class FlowyTextField extends StatefulWidget { final BoxConstraints? prefixIconConstraints; final BoxConstraints? suffixIconConstraints; final BoxConstraints? hintTextConstraints; + final TextStyle? hintStyle; const FlowyTextField({ super.key, @@ -52,6 +53,7 @@ class FlowyTextField extends StatefulWidget { this.prefixIconConstraints, this.suffixIconConstraints, this.hintTextConstraints, + this.hintStyle, }); @override @@ -153,10 +155,11 @@ class FlowyTextFieldState extends State { .textTheme .bodySmall! .copyWith(color: Theme.of(context).colorScheme.error), - hintStyle: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Theme.of(context).hintColor), + hintStyle: widget.hintStyle ?? + Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Theme.of(context).hintColor), suffixText: widget.showCounter ? _suffixText() : "", counterText: "", focusedBorder: OutlineInputBorder( diff --git a/frontend/resources/flowy_icons/16x/m_checkbox_checked.svg b/frontend/resources/flowy_icons/16x/m_checkbox_checked.svg new file mode 100644 index 0000000000..a99a87cccb --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_checkbox_checked.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/m_checkbox_uncheck.svg b/frontend/resources/flowy_icons/16x/m_checkbox_uncheck.svg new file mode 100644 index 0000000000..217cbcef0b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_checkbox_uncheck.svg @@ -0,0 +1,3 @@ + + +