feat: mobile checklist (#4088)

* fix: grid header new property font size on desktop

* feat: checklist cell and editor on mobile
This commit is contained in:
Richard Shiue 2023-12-05 13:39:51 +08:00 committed by GitHub
parent 2d7a373d77
commit 25e94da7e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 503 additions and 12 deletions

View File

@ -86,6 +86,8 @@ class FieldOptionValues {
return MultiSelectTypeOptionPB(
options: selectOption,
).writeToBuffer();
case FieldType.Checklist:
return ChecklistTypeOptionPB().writeToBuffer();
default:
throw UnimplementedError();
}

View File

@ -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(

View File

@ -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<MobileChecklistCell> createState() =>
_MobileChecklistCellState();
}
class _MobileChecklistCellState extends GridCellState<MobileChecklistCell> {
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<ChecklistCellBloc, ChecklistCellState>(
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() {}
}

View File

@ -242,7 +242,7 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
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,
),

View File

@ -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,

View File

@ -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,
});
}

View File

@ -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<ChecklistItem> createState() => _ChecklistItemState();

View File

@ -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<ChecklistSelectOption> 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<ChecklistProgressBar> createState() => _ChecklistProgressBarState();
@ -66,13 +69,15 @@ class _ChecklistProgressBarState extends State<ChecklistProgressBar> {
),
),
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,
),
),
),

View File

@ -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<MobileChecklistCellEditScreen> createState() =>
_MobileChecklistCellEditScreenState();
}
class _MobileChecklistCellEditScreenState
extends State<MobileChecklistCellEditScreen> {
@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<ChecklistCellBloc, ChecklistCellState>(
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<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
final cells = <Widget>[];
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<ChecklistCellBloc>()
.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<ChecklistCellBloc>().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<ChecklistCellBloc>().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<ChecklistCellBloc>()
.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),
],
),
),
),
);
}
}

View File

@ -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(),
};
}