diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/_field_options_eidtor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/_field_options_eidtor.dart index af4798e460..5754d6c928 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/_field_options_eidtor.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/_field_options_eidtor.dart @@ -86,6 +86,8 @@ class FieldOptionValues { return MultiSelectTypeOptionPB( options: selectOption, ).writeToBuffer(); + case FieldType.Checklist: + return ChecklistTypeOptionPB().writeToBuffer(); default: throw UnimplementedError(); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart index 5437242f2d..5af3e2f3a9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart @@ -129,8 +129,8 @@ GridCellStyle? _customCellStyle(FieldType fieldType) { case FieldType.Checklist: return ChecklistCellStyle( placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), - cellPadding: EdgeInsets.zero, - showTasksInline: true, + cellPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), + useRoundedBorders: true, ); case FieldType.Number: return GridNumberCellStyle( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_checklist_cell.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_checklist_cell.dart new file mode 100644 index 0000000000..bcdc0e3e05 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_checklist_cell.dart @@ -0,0 +1,150 @@ +import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.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/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/mobile_checklist_cell_editor.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileChecklistCell extends GridCellWidget { + final CellControllerBuilder cellControllerBuilder; + late final ChecklistCellStyle cellStyle; + MobileChecklistCell({ + required this.cellControllerBuilder, + GridCellStyle? style, + super.key, + }) { + if (style != null) { + cellStyle = (style as ChecklistCellStyle); + } else { + cellStyle = const ChecklistCellStyle(); + } + } + + @override + GridCellState createState() => + _MobileChecklistCellState(); +} + +class _MobileChecklistCellState extends GridCellState { + late ChecklistCellBloc _cellBloc; + + @override + void initState() { + super.initState(); + final cellController = + widget.cellControllerBuilder.build() as ChecklistCellController; + _cellBloc = ChecklistCellBloc(cellController: cellController) + ..add(const ChecklistCellEvent.initial()); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + if (widget.cellStyle.useRoundedBorders) { + return InkWell( + borderRadius: const BorderRadius.all(Radius.circular(14)), + onTap: () => showMobileBottomSheet( + context, + padding: EdgeInsets.zero, + backgroundColor: Theme.of(context).colorScheme.background, + builder: (context) { + return MobileChecklistCellEditScreen( + cellController: widget.cellControllerBuilder.build() + as ChecklistCellController, + ); + }, + ), + child: Container( + constraints: const BoxConstraints( + minHeight: 48, + minWidth: double.infinity, + ), + decoration: BoxDecoration( + border: Border.fromBorderSide( + BorderSide(color: Theme.of(context).colorScheme.outline), + ), + borderRadius: const BorderRadius.all(Radius.circular(14)), + ), + child: Padding( + padding: widget.cellStyle.cellPadding ?? EdgeInsets.zero, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Row( + children: [ + Expanded( + child: state.tasks.isEmpty + ? FlowyText( + widget.cellStyle.placeholder, + fontSize: 15, + color: Theme.of(context).hintColor, + ) + : ChecklistProgressBar( + tasks: state.tasks, + percent: state.percent, + fontSize: 15, + ), + ), + const HSpace(6), + RotatedBox( + quarterTurns: 3, + child: Icon( + Icons.chevron_left, + color: Theme.of(context).hintColor, + ), + ), + const HSpace(2), + ], + ), + ), + ), + ), + ); + } else { + return FlowyButton( + radius: BorderRadius.zero, + hoverColor: Colors.transparent, + text: Container( + alignment: Alignment.centerLeft, + padding: + widget.cellStyle.cellPadding ?? GridSize.cellContentInsets, + child: state.tasks.isEmpty + ? FlowyText( + widget.cellStyle.placeholder, + fontSize: 15, + color: Theme.of(context).hintColor, + ) + : ChecklistProgressBar( + tasks: state.tasks, + percent: state.percent, + fontSize: 15, + ), + ), + onTap: () => showMobileBottomSheet( + context, + padding: EdgeInsets.zero, + backgroundColor: Theme.of(context).colorScheme.background, + builder: (context) { + return MobileChecklistCellEditScreen( + cellController: widget.cellControllerBuilder.build() + as ChecklistCellController, + ); + }, + ), + ); + } + }, + ), + ); + } + + @override + void requestBeginFocus() {} +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart index 73495af305..903bfcfde4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart @@ -242,7 +242,7 @@ class _CreateFieldButtonState extends State { radius: BorderRadius.zero, text: FlowyText( LocaleKeys.grid_field_newProperty.tr(), - fontSize: 15, + fontSize: PlatformExtension.isDesktop ? null : 15, overflow: TextOverflow.ellipsis, color: PlatformExtension.isDesktop ? null : Theme.of(context).hintColor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart index 26d505012e..3b6176fed0 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart @@ -3,6 +3,7 @@ import 'package:appflowy/mobile/presentation/database/card/card_detail/cells/num import 'package:appflowy/mobile/presentation/database/card/card_detail/cells/text_cell.dart'; import 'package:appflowy/mobile/presentation/database/card/card_detail/cells/url_cell.dart'; import 'package:appflowy/mobile/presentation/database/card/row/cells/cells.dart'; +import 'package:appflowy/mobile/presentation/database/card/row/cells/mobile_checklist_cell.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/util/platform_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; @@ -182,7 +183,7 @@ class GridCellBuilder { key: key, ); case FieldType.Checklist: - return GridChecklistCell( + return MobileChecklistCell( cellControllerBuilder: cellControllerBuilder, style: style, key: key, @@ -261,7 +262,7 @@ class MobileRowDetailPageCellBuilder { key: key, ); case FieldType.Checklist: - return GridChecklistCell( + return MobileChecklistCell( cellControllerBuilder: cellControllerBuilder, style: style, key: key, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart index 0de6876f73..e1faa5f216 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart @@ -19,11 +19,13 @@ class ChecklistCellStyle extends GridCellStyle { final String placeholder; final EdgeInsets? cellPadding; final bool showTasksInline; + final bool useRoundedBorders; const ChecklistCellStyle({ this.placeholder = "", this.cellPadding, this.showTasksInline = false, + this.useRoundedBorders = false, }); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart index ffebbf7538..2e7c771445 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart @@ -155,11 +155,11 @@ class ChecklistItem extends StatefulWidget { final VoidCallback? onSubmitted; final bool autofocus; const ChecklistItem({ + super.key, required this.task, - Key? key, this.onSubmitted, this.autofocus = false, - }) : super(key: key); + }); @override State createState() => _ChecklistItemState(); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart index 6641a89b1d..1fac096364 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -8,12 +9,14 @@ class ChecklistProgressBar extends StatefulWidget { final List tasks; final double percent; final int segmentLimit = 5; + final double fontSize; const ChecklistProgressBar({ + super.key, required this.tasks, required this.percent, - Key? key, - }) : super(key: key); + this.fontSize = 11, + }); @override State createState() => _ChecklistProgressBarState(); @@ -66,13 +69,15 @@ class _ChecklistProgressBarState extends State { ), ), SizedBox( - width: 36, + width: PlatformExtension.isDesktop ? 36 : 45, child: Align( alignment: AlignmentDirectional.centerEnd, child: FlowyText.regular( "${(widget.percent * 100).round()}%", - fontSize: 11, - color: Theme.of(context).hintColor, + fontSize: widget.fontSize, + color: PlatformExtension.isDesktop + ? Theme.of(context).hintColor + : null, ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/mobile_checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/mobile_checklist_cell_editor.dart new file mode 100644 index 0000000000..0cd965fc31 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/mobile_checklist_cell_editor.dart @@ -0,0 +1,330 @@ +import 'dart:async'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.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/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart'; +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.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'; + +class MobileChecklistCellEditScreen extends StatefulWidget { + const MobileChecklistCellEditScreen({ + super.key, + required this.cellController, + }); + + final ChecklistCellController cellController; + + @override + State createState() => + _MobileChecklistCellEditScreenState(); +} + +class _MobileChecklistCellEditScreenState + extends State { + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints.tightFor(height: 420), + child: BlocProvider( + create: (context) => ChecklistCellBloc( + cellController: widget.cellController, + )..add(const ChecklistCellEvent.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), + ), + const Divider(), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 0.0), + child: _buildBody(context), + ), + ), + ], + ); + }, + ), + ), + ); + } + + Widget _buildHeader(BuildContext context) { + const iconWidth = 36.0; + const height = 44.0; + return Stack( + children: [ + Align( + alignment: Alignment.centerLeft, + child: FlowyIconButton( + icon: const FlowySvg( + FlowySvgs.close_s, + size: Size.square(iconWidth), + ), + width: iconWidth, + iconPadding: EdgeInsets.zero, + onPressed: () => context.pop(), + ), + ), + SizedBox( + height: 44.0, + child: Align( + alignment: Alignment.center, + child: FlowyText.medium( + LocaleKeys.grid_field_checklistFieldName.tr(), + fontSize: 18, + ), + ), + ), + ].map((e) => SizedBox(height: height, child: e)).toList(), + ); + } + + Widget _buildBody(BuildContext context) { + return _TaskList( + onCreateOption: (optionName) {}, + ); + } +} + +class _TaskList extends StatelessWidget { + const _TaskList({ + required this.onCreateOption, + }); + + final void Function(String optionName) onCreateOption; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final cells = []; + cells.addAll( + state.tasks + .mapIndexed( + (index, task) => _ChecklistItem( + task: task, + autofocus: state.newTask && index == state.tasks.length - 1, + ), + ) + .toList(), + ); + cells.add(const _NewTaskButton()); + + return ListView.separated( + shrinkWrap: true, + itemCount: cells.length, + separatorBuilder: (_, __) => const VSpace(8), + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (_, int index) => cells[index], + padding: const EdgeInsets.only(bottom: 12.0), + ); + }, + ); + } +} + +class _ChecklistItem extends StatefulWidget { + const _ChecklistItem({required this.task, required this.autofocus}); + + final ChecklistSelectOption task; + final bool autofocus; + + @override + State<_ChecklistItem> createState() => _ChecklistItemState(); +} + +class _ChecklistItemState extends State<_ChecklistItem> { + late final TextEditingController _textController; + final FocusNode _focusNode = FocusNode(); + Timer? _debounceOnChanged; + + @override + void initState() { + super.initState(); + _textController = TextEditingController(text: widget.task.data.name); + if (widget.autofocus) { + _focusNode.requestFocus(); + } + } + + @override + void didUpdateWidget(covariant oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.task.data.name != oldWidget.task.data.name && + !_focusNode.hasFocus) { + _textController.text = widget.task.data.name; + } + } + + @override + void dispose() { + _textController.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(left: 5, right: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + InkWell( + borderRadius: BorderRadius.circular(22), + onTap: () => context + .read() + .add(ChecklistCellEvent.selectTask(widget.task.data)), + child: SizedBox.square( + dimension: 44, + child: Center( + child: FlowySvg( + widget.task.isSelected + ? FlowySvgs.check_filled_s + : FlowySvgs.uncheck_s, + size: const Size.square(20.0), + blendMode: BlendMode.dst, + ), + ), + ), + ), + Expanded( + child: TextField( + controller: _textController, + focusNode: _focusNode, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontSize: 15), + maxLines: 1, + decoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + isCollapsed: true, + isDense: true, + contentPadding: const EdgeInsets.symmetric(vertical: 12), + hintText: LocaleKeys.grid_checklist_taskHint.tr(), + ), + onChanged: _debounceOnChangedText, + onSubmitted: (description) { + _submitUpdateTaskDescription(description); + }, + ), + ), + InkWell( + borderRadius: BorderRadius.circular(22), + onTap: () => showMobileBottomSheet( + context, + padding: const EdgeInsets.only(top: 8, bottom: 32), + builder: (_) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: InkWell( + onTap: () { + context.read().add( + ChecklistCellEvent.deleteTask(widget.task.data), + ); + context.pop(); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: [ + FlowySvg( + FlowySvgs.m_delete_m, + size: const Size.square(20), + color: Theme.of(context).colorScheme.error, + ), + const HSpace(8), + FlowyText( + LocaleKeys.button_delete.tr(), + fontSize: 15, + color: Theme.of(context).colorScheme.error, + ), + ], + ), + ), + ), + ), + const Divider(height: 9), + ], + ), + ), + child: SizedBox.square( + dimension: 44, + child: Center( + child: FlowySvg( + FlowySvgs.three_dots_s, + color: Theme.of(context).hintColor, + ), + ), + ), + ), + ], + ), + ); + } + + void _debounceOnChangedText(String text) { + _debounceOnChanged?.cancel(); + _debounceOnChanged = Timer(const Duration(milliseconds: 300), () { + _submitUpdateTaskDescription(text); + }); + } + + void _submitUpdateTaskDescription(String description) { + context.read().add( + ChecklistCellEvent.updateTaskName( + widget.task.data, + description.trim(), + ), + ); + } +} + +class _NewTaskButton extends StatelessWidget { + const _NewTaskButton(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + context + .read() + .add(const ChecklistCellEvent.createNewTask("")); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 13), + child: Row( + children: [ + const FlowySvg(FlowySvgs.add_s, size: Size.square(20)), + const HSpace(11), + FlowyText(LocaleKeys.grid_checklist_addNew.tr(), fontSize: 15), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/util/field_type_extension.dart b/frontend/appflowy_flutter/lib/util/field_type_extension.dart index e24565d0dd..5c8c88b5af 100644 --- a/frontend/appflowy_flutter/lib/util/field_type_extension.dart +++ b/frontend/appflowy_flutter/lib/util/field_type_extension.dart @@ -41,6 +41,7 @@ extension FieldTypeExtension on FieldType { FieldType.MultiSelect => FlowySvgs.field_option_select_s, FieldType.Checkbox => FlowySvgs.field_option_checkbox_s, FieldType.URL => FlowySvgs.field_option_url_s, + FieldType.Checklist => FlowySvgs.checklist_s, _ => throw UnimplementedError(), }; }