From 6b4d4fef1514a6c8bafe831ca23968dc7200fa83 Mon Sep 17 00:00:00 2001 From: Yijing Huang Date: Wed, 29 Nov 2023 19:01:29 -0700 Subject: [PATCH] feat: mobile board view (#4030) --- .../lib/mobile/application/mobile_router.dart | 2 +- .../presentation/database/board/board.dart | 4 + .../database/board/mobile_board_content.dart | 284 +++++++++++++++ .../{ => board}/mobile_board_screen.dart | 0 .../board/widgets/group_card_header.dart | 175 ++++++++++ .../board/widgets/mobile_board_trailing.dart | 129 +++++++ .../widgets/mobile_hidden_groups_column.dart | 323 ++++++++++++++++++ .../database/board/widgets/widgets.dart | 3 + .../presentation/database/card/card.dart | 1 + .../card_content/card_cells/card_cells.dart | 9 + .../card_content/card_cells/checkbox.dart | 68 ++++ .../card_content/card_cells/checklist.dart | 137 ++++++++ .../card/card_content/card_cells/date.dart | 74 ++++ .../card/card_content/card_cells/number.dart | 76 +++++ .../card_cells/select_option.dart | 147 ++++++++ .../card/card_content/card_cells/style.dart | 44 +++ .../card/card_content/card_cells/text.dart | 75 ++++ .../card_content/card_cells/time_stamp.dart | 73 ++++ .../card/card_content/card_cells/url.dart | 63 ++++ .../card_content/mobile_card_content.dart | 118 +++++++ .../mobile_card_detail_screen.dart | 28 +- .../recent_folder/mobile_recent_view.dart | 2 +- .../board/application/board_bloc.dart | 15 +- .../board/presentation/board_page.dart | 75 ++-- .../database_view/widgets/card/card.dart | 5 +- .../widgets/card/card_cell_builder.dart | 113 +++++- .../widgets/card/cells/card_cell.dart | 2 +- .../lib/startup/tasks/generate_router.dart | 2 +- .../appearance/mobile_appearance.dart | 7 +- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 3 +- frontend/resources/translations/en.json | 10 +- 32 files changed, 2002 insertions(+), 69 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/board/board.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_content.dart rename frontend/appflowy_flutter/lib/mobile/presentation/database/{ => board}/mobile_board_screen.dart (100%) create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/group_card_header.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/widgets.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/card_cells.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/checkbox.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/checklist.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/date.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/number.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/select_option.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/style.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/text.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/time_stamp.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/url.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/mobile_card_content.dart diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart index 2a8a4ef75f..ace1233559 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/mobile/presentation/database/mobile_board_screen.dart'; +import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/board.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/board.dart new file mode 100644 index 0000000000..89ae2411e1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/board.dart @@ -0,0 +1,4 @@ +export 'mobile_board_screen.dart'; +export 'mobile_board_content.dart'; +export 'widgets/mobile_hidden_groups_column.dart'; +export 'widgets/mobile_board_trailing.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_content.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_content.dart new file mode 100644 index 0000000000..ac6968b8c3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_content.dart @@ -0,0 +1,284 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/database/board/board.dart'; +import 'package:appflowy/mobile/presentation/database/board/widgets/group_card_header.dart'; +import 'package:appflowy/mobile/presentation/database/card/card.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; +import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/card.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_board/appflowy_board.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 MobileBoardContent extends StatefulWidget { + const MobileBoardContent({ + super.key, + }); + + @override + State createState() => _MobileBoardContentState(); +} + +class _MobileBoardContentState extends State { + final renderHook = RowCardRenderHook(); + late final ScrollController scrollController; + late final AppFlowyBoardScrollController scrollManager; + + @override + void initState() { + super.initState(); + //mobile may not need this + //scroll to bottom when add a new card + scrollManager = AppFlowyBoardScrollController(); + scrollController = ScrollController(); + renderHook.addSelectOptionHook((options, groupId, _) { + // The cell should hide if the option id is equal to the groupId. + final isInGroup = + options.where((element) => element.id == groupId).isNotEmpty; + + if (isInGroup || options.isEmpty) { + return const SizedBox.shrink(); + } + + return null; + }); + } + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final config = AppFlowyBoardConfig( + groupCornerRadius: 8, + groupBackgroundColor: Theme.of(context).colorScheme.secondary, + groupMargin: const EdgeInsets.fromLTRB(4, 8, 4, 12), + groupHeaderPadding: const EdgeInsets.all(8), + groupBodyPadding: const EdgeInsets.all(4), + groupFooterPadding: const EdgeInsets.all(8), + cardMargin: const EdgeInsets.all(4), + ); + + return BlocListener( + listenWhen: (previous, current) => + previous.recentAddedRowMeta != current.recentAddedRowMeta, + listener: (context, state) { + // when add a new card + // it push to the card detail screen of the new card + final rowCache = context.read().getRowCache()!; + context.push( + MobileCardDetailScreen.routeName, + extra: { + MobileCardDetailScreen.argRowController: RowController( + rowMeta: state.recentAddedRowMeta!, + viewId: state.viewId, + rowCache: rowCache, + ), + MobileCardDetailScreen.argFieldController: + context.read().fieldController, + }, + ); + }, + child: BlocBuilder( + builder: (context, state) { + final showCreateGroupButton = + context.read().groupingFieldType.canCreateNewGroup; + final showHiddenGroups = state.hiddenGroups.isNotEmpty; + return AppFlowyBoard( + boardScrollController: scrollManager, + scrollController: scrollController, + controller: context.read().boardController, + groupConstraints: BoxConstraints.tightFor(width: screenWidth * 0.7), + config: config, + leading: showHiddenGroups + ? MobileHiddenGroupsColumn(padding: config.groupHeaderPadding) + : const HSpace(16), + trailing: showCreateGroupButton + ? const MobileBoardTrailing() + : const HSpace(16), + headerBuilder: (_, groupData) => BlocProvider.value( + value: context.read(), + child: GroupCardHeader( + groupData: groupData, + ), + ), + footerBuilder: _buildFooter, + cardBuilder: (_, column, columnItem) => _buildCard( + context: context, + afGroupData: column, + afGroupItem: columnItem, + cardMargin: config.cardMargin, + ), + ); + }, + ), + ); + } + + Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) { + final style = Theme.of(context); + + return SizedBox( + height: 42, + width: double.infinity, + child: TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.only(left: 8), + alignment: Alignment.centerLeft, + ), + icon: FlowySvg( + FlowySvgs.add_m, + color: style.colorScheme.onSurface, + ), + label: Text( + LocaleKeys.board_column_createNewCard.tr(), + style: style.textTheme.bodyMedium?.copyWith( + color: style.colorScheme.onSurface, + ), + ), + onPressed: () => context + .read() + .add(BoardEvent.createBottomRow(columnData.id)), + ), + ); + } + + Widget _buildCard({ + required BuildContext context, + required AppFlowyGroupData afGroupData, + required AppFlowyGroupItem afGroupItem, + required EdgeInsets cardMargin, + }) { + final boardBloc = context.read(); + final groupItem = afGroupItem as GroupItem; + final groupData = afGroupData.customData as GroupData; + final rowMeta = groupItem.row; + final rowCache = boardBloc.getRowCache(); + + /// Return placeholder widget if the rowCache is null. + if (rowCache == null) return SizedBox.shrink(key: ObjectKey(groupItem)); + final cellCache = rowCache.cellCache; + final fieldController = boardBloc.fieldController; + final viewId = boardBloc.viewId; + + final cellBuilder = CardCellBuilder(cellCache); + final isEditing = boardBloc.state.isEditingRow && + boardBloc.state.editingRow?.row.id == groupItem.row.id; + + final groupItemId = groupItem.row.id + groupData.group.groupId; + + return Container( + key: ValueKey(groupItemId), + margin: cardMargin, + decoration: _makeBoxDecoration(context), + child: RowCard( + rowMeta: rowMeta, + viewId: viewId, + rowCache: rowCache, + cardData: groupData.group.groupId, + groupingFieldId: groupItem.fieldInfo.id, + groupId: groupData.group.groupId, + isEditing: isEditing, + cellBuilder: cellBuilder, + renderHook: renderHook, + openCard: (context) => _openCard( + context: context, + viewId: viewId, + groupId: groupData.group.groupId, + fieldController: fieldController, + rowMeta: rowMeta, + rowCache: rowCache, + ), + onStartEditing: () => boardBloc + .add(BoardEvent.startEditingRow(groupData.group, groupItem.row)), + onEndEditing: () => + boardBloc.add(BoardEvent.endEditingRow(groupItem.row.id)), + styleConfiguration: const RowCardStyleConfiguration( + showAccessory: false, + hoverStyle: null, + cardPadding: EdgeInsets.all(8), + cellPadding: EdgeInsets.zero, + ), + ), + ); + } + + BoxDecoration _makeBoxDecoration(BuildContext context) { + return BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.all(Radius.circular(8)), + border: Border.fromBorderSide( + BorderSide( + color: Theme.of(context).colorScheme.outline.withOpacity(0.5), + ), + ), + boxShadow: [ + // card shadow + BoxShadow( + color: Theme.of(context).colorScheme.outline.withOpacity(0.5), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ); + } + + void _openCard({ + required BuildContext context, + required String viewId, + required String groupId, + required FieldController fieldController, + required RowMetaPB rowMeta, + required RowCache rowCache, + }) { + final rowInfo = RowInfo( + viewId: viewId, + fields: UnmodifiableListView(fieldController.fieldInfos), + rowMeta: rowMeta, + rowId: rowMeta.id, + ); + + final dataController = RowController( + rowMeta: rowInfo.rowMeta, + viewId: rowInfo.viewId, + rowCache: rowCache, + groupId: groupId, + ); + + showModalBottomSheet( + isScrollControlled: true, + context: context, + // To avoid the appbar of [MobileCardDetailScreen] being covered by status bar. + useSafeArea: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(8), + ), + ), + clipBehavior: Clip.antiAliasWithSaveLayer, + builder: (context) => DraggableScrollableSheet( + expand: false, + initialChildSize: 0.4, + minChildSize: 0.4, + snapSizes: const [0.4, 1], + snap: true, + builder: (context, scrollController) { + return MobileCardDetailScreen( + fieldController: fieldController, + rowController: dataController, + scrollController: scrollController, + isBottomSheet: true, + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_board_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_screen.dart similarity index 100% rename from frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_board_screen.dart rename to frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_screen.dart diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/group_card_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/group_card_header.dart new file mode 100644 index 0000000000..105eb9588d --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/group_card_header.dart @@ -0,0 +1,175 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart'; +import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart'; +import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; +import 'package:appflowy_board/appflowy_board.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'; + +// similar to [BoardColumnHeader] in Desktop +class GroupCardHeader extends StatefulWidget { + const GroupCardHeader({ + super.key, + required this.groupData, + }); + + final AppFlowyGroupData groupData; + + @override + State createState() => _GroupCardHeaderState(); +} + +class _GroupCardHeaderState extends State { + late final TextEditingController _controller = + TextEditingController.fromValue( + TextEditingValue( + selection: TextSelection.collapsed( + offset: widget.groupData.headerData.groupName.length, + ), + text: widget.groupData.headerData.groupName, + ), + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final boardCustomData = widget.groupData.customData as GroupData; + final titleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.w600, + ); + return BlocBuilder( + builder: (context, state) { + Widget title = Text( + widget.groupData.headerData.groupName, + style: titleTextStyle, + overflow: TextOverflow.ellipsis, + ); + + // header can be edited if it's not default group(no status) and the field type can be edited + if (!boardCustomData.group.isDefault && + boardCustomData.fieldType.canEditHeader) { + title = GestureDetector( + onTap: () => context + .read() + .add(BoardEvent.startEditingHeader(widget.groupData.id)), + child: Text( + widget.groupData.headerData.groupName, + style: titleTextStyle, + overflow: TextOverflow.ellipsis, + ), + ); + } + + if (state.isEditingHeader && + state.editingHeaderId == widget.groupData.id) { + title = TextField( + controller: _controller, + autofocus: true, + onEditingComplete: () => context.read().add( + BoardEvent.endEditingHeader( + widget.groupData.id, + _controller.text, + ), + ), + maxLines: 1, + style: titleTextStyle, + ); + } + + return Padding( + padding: const EdgeInsets.only(left: 16), + child: SizedBox( + height: 42, + child: Row( + children: [ + _buildHeaderIcon(boardCustomData), + Expanded(child: title), + IconButton( + icon: Icon( + Icons.more_horiz_rounded, + color: Theme.of(context).colorScheme.onSurface, + ), + splashRadius: 5, + onPressed: () => showFlowyMobileBottomSheet( + context, + title: LocaleKeys.board_column_groupActions.tr(), + builder: (_) { + return Row( + children: [ + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.edit_s, + text: LocaleKeys.board_column_renameColumn.tr(), + onTap: () { + context.read().add( + BoardEvent.startEditingHeader( + widget.groupData.id, + ), + ); + context.pop(); + }, + ), + ), + const HSpace(8), + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.hide_s, + text: LocaleKeys.board_column_hideColumn.tr(), + onTap: () { + context.read().add( + BoardEvent.toggleGroupVisibility( + widget.groupData.customData.group + as GroupPB, + false, + ), + ); + context.pop(); + }, + ), + ), + ], + ); + }, + ), + ), + IconButton( + icon: Icon( + Icons.add, + color: Theme.of(context).colorScheme.onSurface, + ), + splashRadius: 5, + onPressed: () => context.read().add( + BoardEvent.createHeaderRow(widget.groupData.id), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildHeaderIcon(GroupData customData) => + switch (customData.fieldType) { + FieldType.Checkbox => FlowySvg( + customData.asCheckboxGroup()!.isCheck + ? FlowySvgs.check_filled_s + : FlowySvgs.uncheck_s, + blendMode: BlendMode.dst, + ), + _ => const SizedBox.shrink(), + }; +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart new file mode 100644 index 0000000000..430f085303 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart @@ -0,0 +1,129 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// Add new group +class MobileBoardTrailing extends StatefulWidget { + const MobileBoardTrailing({ + super.key, + }); + + @override + State createState() => _MobileBoardTrailingState(); +} + +class _MobileBoardTrailingState extends State { + final TextEditingController _textController = TextEditingController(); + + bool isEditing = false; + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final style = Theme.of(context); + + return Container( + margin: const EdgeInsets.all(8), + child: SizedBox( + width: screenSize.width * 0.7, + child: isEditing + ? DecoratedBox( + decoration: BoxDecoration( + color: style.colorScheme.secondary, + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _textController, + autofocus: true, + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon( + Icons.close, + color: style.colorScheme.onBackground, + ), + onPressed: () => _textController.clear(), + ), + isDense: true, + ), + onEditingComplete: () { + context.read().add( + BoardEvent.createGroup( + _textController.text, + ), + ); + _textController.clear(); + setState(() { + isEditing = false; + }); + }, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + child: Text( + LocaleKeys.button_cancel.tr(), + style: style.textTheme.titleSmall?.copyWith( + color: style.colorScheme.onBackground, + ), + ), + onPressed: () { + setState(() { + isEditing = false; + }); + }, + ), + TextButton( + child: Text( + LocaleKeys.button_add.tr(), + style: style.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + color: style.colorScheme.onBackground, + ), + ), + onPressed: () { + context.read().add( + BoardEvent.createGroup( + _textController.text, + ), + ); + _textController.clear(); + setState(() { + isEditing = false; + }); + }, + ), + ], + ), + ], + ), + ), + ) + : ElevatedButton.icon( + style: ElevatedButton.styleFrom( + foregroundColor: style.colorScheme.onBackground, + backgroundColor: style.colorScheme.secondary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.add), + label: Text( + LocaleKeys.board_column_newGroup.tr(), + ), + onPressed: () => setState( + () => isEditing = true, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart new file mode 100644 index 0000000000..5dcb248931 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart @@ -0,0 +1,323 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/database/card/card.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; +import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.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 MobileHiddenGroupsColumn extends StatelessWidget { + const MobileHiddenGroupsColumn({super.key, required this.padding}); + + final EdgeInsets padding; + + @override + Widget build(BuildContext context) { + final databaseController = context.read().databaseController; + return BlocSelector( + selector: (state) => state.layoutSettings, + builder: (context, layoutSettings) { + if (layoutSettings == null) { + return const SizedBox.shrink(); + } + final isCollapsed = layoutSettings.collapseHiddenGroups; + return Container( + padding: padding, + child: AnimatedSize( + alignment: AlignmentDirectional.topStart, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 150), + child: isCollapsed + ? SizedBox( + height: 50, + child: _collapseExpandIcon(context, isCollapsed), + ) + : SizedBox( + width: 180, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Spacer(), + _collapseExpandIcon(context, isCollapsed), + ], + ), + Text( + LocaleKeys.board_hiddenGroupSection_sectionTitle.tr(), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: Theme.of(context).colorScheme.tertiary, + ), + ), + const VSpace(8), + Expanded( + child: MobileHiddenGroupList( + databaseController: databaseController, + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _collapseExpandIcon(BuildContext context, bool isCollapsed) { + return CircleAvatar( + radius: 20, + backgroundColor: Theme.of(context).colorScheme.secondary, + child: IconButton( + icon: FlowySvg( + isCollapsed + ? FlowySvgs.hamburger_s_s + : FlowySvgs.pull_left_outlined_s, + size: isCollapsed ? const Size.square(12) : const Size.square(40), + ), + onPressed: () => context + .read() + .add(BoardEvent.toggleHiddenSectionVisibility(!isCollapsed)), + ), + ); + } +} + +class MobileHiddenGroupList extends StatelessWidget { + const MobileHiddenGroupList({ + super.key, + required this.databaseController, + }); + + final DatabaseController databaseController; + + @override + Widget build(BuildContext context) { + final bloc = context.read(); + return BlocBuilder( + builder: (_, state) => ReorderableListView.builder( + itemCount: state.hiddenGroups.length, + itemBuilder: (_, index) => MobileHiddenGroup( + key: ValueKey(state.hiddenGroups[index].groupId), + group: state.hiddenGroups[index], + index: index, + bloc: bloc, + ), + physics: const ClampingScrollPhysics(), + onReorder: (oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex--; + } + final fromGroupId = state.hiddenGroups[oldIndex].groupId; + final toGroupId = state.hiddenGroups[newIndex].groupId; + bloc.add(BoardEvent.reorderGroup(fromGroupId, toGroupId)); + }, + ), + ); + } +} + +class MobileHiddenGroup extends StatelessWidget { + const MobileHiddenGroup({ + super.key, + required this.group, + required this.index, + required this.bloc, + }); + + final GroupPB group; + final BoardBloc bloc; + final int index; + + @override + Widget build(BuildContext context) { + final databaseController = bloc.databaseController; + final primaryField = databaseController.fieldController.fieldInfos + .firstWhereOrNull((element) => element.isPrimary)!; + + return BlocProvider.value( + value: bloc, + child: BlocBuilder( + builder: (context, state) { + final group = state.hiddenGroups.firstWhereOrNull( + (g) => g.groupId == this.group.groupId, + ); + if (group == null) { + return const SizedBox.shrink(); + } + + return ExpansionTile( + tilePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.zero, + title: Row( + children: [ + Expanded( + child: Text( + group.groupName, + style: Theme.of(context).textTheme.bodyMedium, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + GestureDetector( + child: const Padding( + padding: EdgeInsets.all(4), + child: FlowySvg( + FlowySvgs.hide_m, + size: Size.square(20), + ), + ), + onTap: () => showFlowyMobileConfirmDialog( + context, + title: LocaleKeys.board_mobile_unhideGroup.tr(), + content: LocaleKeys.board_mobile_unhideGroupContent.tr(), + actionButtonTitle: LocaleKeys.button_yes.tr(), + actionButtonColor: Theme.of(context).colorScheme.primary, + onActionButtonPressed: () => context.read().add( + BoardEvent.toggleGroupVisibility( + group, + true, + ), + ), + ), + ), + ], + ), + children: [ + MobileHiddenGroupItemList( + bloc: bloc, + viewId: databaseController.viewId, + groupId: group.groupId, + primaryField: primaryField, + rowCache: databaseController.rowCache, + ), + ], + ); + }, + ), + ); + } +} + +class MobileHiddenGroupItemList extends StatelessWidget { + const MobileHiddenGroupItemList({ + required this.bloc, + required this.groupId, + required this.viewId, + required this.primaryField, + required this.rowCache, + super.key, + }); + + final BoardBloc bloc; + final String groupId; + final String viewId; + final FieldInfo primaryField; + final RowCache rowCache; + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: bloc, + child: BlocBuilder( + builder: (context, state) { + final group = state.hiddenGroups.firstWhereOrNull( + (g) => g.groupId == groupId, + ); + if (group == null) { + return const SizedBox.shrink(); + } + + final cells = [ + ...group.rows.map( + (item) { + final cellContext = rowCache.loadCells(item)[primaryField.id]!; + final rowController = RowController( + rowMeta: item, + viewId: viewId, + rowCache: rowCache, + ); + final renderHook = RowCardRenderHook(); + renderHook.addTextCellHook((cellData, _, __) { + return BlocBuilder( + builder: (context, state) { + final text = cellData.isEmpty + ? LocaleKeys.grid_row_titlePlaceholder.tr() + : cellData; + + if (text.isEmpty) { + return const SizedBox.shrink(); + } + + return Row( + children: [ + if (!cellContext.rowMeta.isDocumentEmpty) ...[ + const FlowySvg(FlowySvgs.notes_s), + const HSpace(4), + ], + Expanded( + child: Text( + text, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + }, + ); + }); + + return TextButton( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.bodyMedium, + foregroundColor: Theme.of(context).colorScheme.onBackground, + visualDensity: VisualDensity.compact, + ), + child: CardCellBuilder(rowController.cellCache) + .buildCell( + cellContext: cellContext, + renderHook: renderHook, + hasNotes: !cellContext.rowMeta.isDocumentEmpty, + ), + onPressed: () { + context.push( + MobileCardDetailScreen.routeName, + extra: { + MobileCardDetailScreen.argRowController: rowController, + MobileCardDetailScreen.argFieldController: + context.read().fieldController, + }, + ); + }, + ); + }, + ), + ]; + + return ListView.builder( + itemBuilder: (context, index) => cells[index], + itemCount: cells.length, + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/widgets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/widgets.dart new file mode 100644 index 0000000000..0b1d45ab65 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/widgets.dart @@ -0,0 +1,3 @@ +export 'group_card_header.dart'; +export 'mobile_board_trailing.dart'; +export 'mobile_hidden_groups_column.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card.dart index 47370a8d95..d76eca81a4 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card.dart @@ -1 +1,2 @@ export 'card_detail/mobile_card_detail_screen.dart'; +export 'card_content/mobile_card_content.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/card_cells.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/card_cells.dart new file mode 100644 index 0000000000..10b81feabd --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/card_cells.dart @@ -0,0 +1,9 @@ +export 'checkbox.dart'; +export 'date.dart'; +export 'style.dart'; +export 'time_stamp.dart'; +export 'number.dart'; +export 'text.dart'; +export 'select_option.dart'; +export 'url.dart'; +export 'checklist.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/checkbox.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/checkbox.dart new file mode 100644 index 0000000000..cd1919b3b3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/checkbox.dart @@ -0,0 +1,68 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileCheckboxCardCell extends CardCell { + const MobileCheckboxCardCell({ + super.key, + required this.cellControllerBuilder, + }); + + final CellControllerBuilder cellControllerBuilder; + + @override + State createState() => _CheckboxCellState(); +} + +class _CheckboxCellState extends State { + late final CheckboxCellBloc _cellBloc; + + @override + void initState() { + super.initState(); + final cellController = + widget.cellControllerBuilder.build() as CheckboxCellController; + _cellBloc = CheckboxCellBloc(cellController: cellController) + ..add(const CheckboxCellEvent.initial()); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) => + previous.isSelected != current.isSelected, + builder: (context, state) { + return Align( + alignment: Alignment.centerLeft, + child: IconButton( + padding: EdgeInsets.zero, + alignment: Alignment.centerLeft, + visualDensity: VisualDensity.compact, + icon: FlowySvg( + state.isSelected + ? FlowySvgs.check_filled_s + : FlowySvgs.uncheck_s, + blendMode: BlendMode.dst, + size: const Size.square(24), + ), + onPressed: () => context + .read() + .add(const CheckboxCellEvent.select()), + ), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/checklist.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/checklist.dart new file mode 100644 index 0000000000..b3194f437c --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/checklist.dart @@ -0,0 +1,137 @@ +import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/card_cells.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:percent_indicator/linear_percent_indicator.dart'; + +class MobileChecklistCardCell extends CardCell { + const MobileChecklistCardCell({ + super.key, + required this.cellControllerBuilder, + }); + + final CellControllerBuilder cellControllerBuilder; + + @override + State createState() => _ChecklistCellState(); +} + +class _ChecklistCellState extends State { + late final ChecklistCellBloc _cellBloc; + + @override + void initState() { + super.initState(); + final cellController = + widget.cellControllerBuilder.build() as ChecklistCellController; + _cellBloc = ChecklistCellBloc(cellController: cellController) + ..add(const ChecklistCellEvent.initial()); + } + + @override + void dispose() { + _cellBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cellStyle = MobileCardCellStyle(context); + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + if (state.tasks.isEmpty) { + return const SizedBox.shrink(); + } + return Padding( + padding: cellStyle.padding, + child: MobileChecklistProgressBar( + tasks: state.tasks, + percent: state.percent, + ), + ); + }, + ), + ); + } +} + +class MobileChecklistProgressBar extends StatefulWidget { + const MobileChecklistProgressBar({ + super.key, + required this.tasks, + required this.percent, + }); + + final List tasks; + final double percent; + final int segmentLimit = 5; + + @override + State createState() => + _MobileChecklistProgresssBarState(); +} + +class _MobileChecklistProgresssBarState + extends State { + @override + Widget build(BuildContext context) { + final cellStyle = MobileCardCellStyle(context); + final numFinishedTasks = widget.tasks.where((e) => e.isSelected).length; + final completedTaskColor = numFinishedTasks == widget.tasks.length + ? AFThemeExtension.of(context).success + : Theme.of(context).colorScheme.primary; + + return Row( + children: [ + Expanded( + child: Row( + children: [ + if (widget.tasks.isNotEmpty && + widget.tasks.length <= widget.segmentLimit) + ...List.generate( + widget.tasks.length, + (index) => Flexible( + child: Container( + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all(Radius.circular(2)), + color: index < numFinishedTasks + ? completedTaskColor + : AFThemeExtension.of(context).progressBarBGColor, + ), + margin: const EdgeInsets.symmetric(horizontal: 1), + height: 6.0, + ), + ), + ) + else + Expanded( + child: LinearPercentIndicator( + lineHeight: 6.0, + percent: widget.percent, + padding: EdgeInsets.zero, + progressColor: completedTaskColor, + backgroundColor: + AFThemeExtension.of(context).progressBarBGColor, + barRadius: const Radius.circular(2), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(left: 8), + child: Text( + "${(widget.percent * 100).round()}%", + style: cellStyle.secondaryTextStyle(), + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/date.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/date.dart new file mode 100644 index 0000000000..8d9ab488f3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/date.dart @@ -0,0 +1,74 @@ +import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileDateCardCell extends CardCell { + const MobileDateCardCell({ + super.key, + required this.cellControllerBuilder, + this.renderHook, + }); + + final CellControllerBuilder cellControllerBuilder; + final CellRenderHook? renderHook; + + @override + State createState() => _DateCellState(); +} + +class _DateCellState extends State { + late final DateCellBloc _cellBloc; + + @override + void initState() { + super.initState(); + final cellController = + widget.cellControllerBuilder.build() as DateCellController; + + _cellBloc = DateCellBloc(cellController: cellController) + ..add(const DateCellEvent.initial()); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cellStyle = MobileCardCellStyle(context); + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) => previous.dateStr != current.dateStr, + builder: (context, state) { + if (state.dateStr.isEmpty) { + return const SizedBox(); + } else { + final Widget? custom = widget.renderHook?.call( + state.data, + widget.cardData, + context, + ); + if (custom != null) { + return custom; + } + + return Container( + alignment: Alignment.centerLeft, + padding: cellStyle.padding, + child: Text( + state.dateStr, + style: cellStyle.secondaryTextStyle(), + ), + ); + } + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/number.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/number.dart new file mode 100644 index 0000000000..1b6388a360 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/number.dart @@ -0,0 +1,76 @@ +import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/number_cell/number_cell_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileNumberCardCell extends CardCell { + const MobileNumberCardCell({ + super.key, + required this.cellControllerBuilder, + CustomCardData? cardData, + this.renderHook, + }); + + final CellRenderHook? renderHook; + final CellControllerBuilder cellControllerBuilder; + + @override + State createState() => _NumberCellState(); +} + +class _NumberCellState extends State { + late final NumberCellBloc _cellBloc; + + @override + void initState() { + super.initState(); + final cellController = + widget.cellControllerBuilder.build() as NumberCellController; + + _cellBloc = NumberCellBloc(cellController: cellController) + ..add(const NumberCellEvent.initial()); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cellStyle = MobileCardCellStyle(context); + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) => + previous.cellContent != current.cellContent, + builder: (context, state) { + if (state.cellContent.isEmpty) { + return const SizedBox(); + } else { + final Widget? custom = widget.renderHook?.call( + state.cellContent, + widget.cardData, + context, + ); + if (custom != null) { + return custom; + } + + return Container( + alignment: Alignment.centerLeft, + padding: cellStyle.padding, + child: Text( + state.cellContent, + style: cellStyle.primaryTextStyle(), + ), + ); + } + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/select_option.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/select_option.dart new file mode 100644 index 0000000000..d1a889725c --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/select_option.dart @@ -0,0 +1,147 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.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_cell_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileSelectOptionCardCell extends CardCell { + const MobileSelectOptionCardCell({ + super.key, + required this.cellControllerBuilder, + required CustomCardData? cardData, + this.renderHook, + }); + + final CellControllerBuilder cellControllerBuilder; + final CellRenderHook, CustomCardData>? renderHook; + + @override + State createState() => _SelectOptionCellState(); +} + +class _SelectOptionCellState extends State { + late final SelectOptionCellBloc _cellBloc; + + @override + void initState() { + super.initState(); + final cellController = + widget.cellControllerBuilder.build() as SelectOptionCellController; + _cellBloc = SelectOptionCellBloc(cellController: cellController) + ..add(const SelectOptionCellEvent.initial()); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cellStyle = MobileCardCellStyle(context); + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) { + return previous.selectedOptions != current.selectedOptions; + }, + builder: (context, state) { + final Widget? custom = widget.renderHook?.call( + state.selectedOptions, + widget.cardData, + context, + ); + if (custom != null) { + return custom; + } + + final children = state.selectedOptions + .map( + (option) => MobileSelectOptionTag.fromOption( + context: context, + option: option, + ), + ) + .toList(); + + return IntrinsicHeight( + child: Padding( + padding: cellStyle.padding, + child: SizedBox.expand( + child: Wrap(spacing: 4, runSpacing: 2, children: children), + ), + ), + ); + }, + ), + ); + } +} + +class MobileSelectOptionTag extends StatelessWidget { + const MobileSelectOptionTag({ + super.key, + required this.name, + required this.color, + this.onSelected, + this.onRemove, + }); + + factory MobileSelectOptionTag.fromOption({ + required BuildContext context, + required SelectOptionPB option, + VoidCallback? onSelected, + Function(String)? onRemove, + }) { + return MobileSelectOptionTag( + name: option.name, + color: option.color.toColor(context), + onSelected: onSelected, + onRemove: onRemove, + ); + } + + final String name; + final Color color; + final VoidCallback? onSelected; + final void Function(String)? onRemove; + + @override + Widget build(BuildContext context) { + final cellStyle = MobileCardCellStyle(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + name, + style: cellStyle.tagTextStyle(), + ), + ), + if (onRemove != null) ...[ + const HSpace(2), + IconButton( + onPressed: () => onRemove?.call(name), + icon: const FlowySvg( + FlowySvgs.close_s, + ), + ), + ], + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/style.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/style.dart new file mode 100644 index 0000000000..af07701369 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/style.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class MobileCardCellStyle { + MobileCardCellStyle(this.context); + + BuildContext context; + + EdgeInsets get padding => const EdgeInsets.symmetric( + vertical: 4, + ); + + TextStyle? primaryTextStyle() { + final theme = Theme.of(context); + return theme.textTheme.bodyMedium?.copyWith( + fontSize: 16, + color: theme.colorScheme.onBackground, + ); + } + + TextStyle? secondaryTextStyle() { + final theme = Theme.of(context); + return theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.tertiary, + fontSize: 14, + ); + } + + TextStyle? tagTextStyle() { + final theme = Theme.of(context); + return theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onBackground, + fontSize: 12, + ); + } + + TextStyle? urlTextStyle() { + final theme = Theme.of(context); + return theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.primary, + fontSize: 16, + decoration: TextDecoration.underline, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/text.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/text.dart new file mode 100644 index 0000000000..6be4c74cfa --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/text.dart @@ -0,0 +1,75 @@ +import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileTextCardCell extends CardCell { + const MobileTextCardCell({ + super.key, + required this.cellControllerBuilder, + CustomCardData? cardData, + this.renderHook, + }); + + final CellRenderHook? renderHook; + final CellControllerBuilder cellControllerBuilder; + + @override + State createState() => _NumberCellState(); +} + +class _NumberCellState extends State { + late final TextCellBloc _cellBloc; + + @override + void initState() { + super.initState(); + final cellController = + widget.cellControllerBuilder.build() as TextCellController; + + _cellBloc = TextCellBloc(cellController: cellController) + ..add(const TextCellEvent.initial()); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cellStyle = MobileCardCellStyle(context); + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) => previous.content != current.content, + builder: (context, state) { + if (state.content.isEmpty) { + return const SizedBox(); + } else { + final Widget? custom = widget.renderHook?.call( + state.content, + widget.cardData, + context, + ); + if (custom != null) { + return custom; + } + + return Container( + alignment: Alignment.centerLeft, + padding: cellStyle.padding, + child: Text( + state.content, + style: cellStyle.primaryTextStyle(), + ), + ); + } + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/time_stamp.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/time_stamp.dart new file mode 100644 index 0000000000..b92275bdf2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/time_stamp.dart @@ -0,0 +1,73 @@ +import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileTimestampCardCell extends CardCell { + const MobileTimestampCardCell({ + super.key, + required this.cellControllerBuilder, + this.renderHook, + }); + + final CellControllerBuilder cellControllerBuilder; + final CellRenderHook? renderHook; + + @override + State createState() => _TimestampCellState(); +} + +class _TimestampCellState extends State { + late final TimestampCellBloc _cellBloc; + + @override + void initState() { + super.initState(); + final cellController = + widget.cellControllerBuilder.build() as TimestampCellController; + + _cellBloc = TimestampCellBloc(cellController: cellController) + ..add(const TimestampCellEvent.initial()); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cellStyle = MobileCardCellStyle(context); + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) => previous.dateStr != current.dateStr, + builder: (context, state) { + if (state.dateStr.isEmpty) { + return const SizedBox.shrink(); + } + final Widget? custom = widget.renderHook?.call( + state.data, + widget.cardData, + context, + ); + if (custom != null) { + return custom; + } + + return Container( + alignment: Alignment.centerLeft, + padding: cellStyle.padding, + child: Text( + state.dateStr, + style: cellStyle.secondaryTextStyle(), + ), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/url.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/url.dart new file mode 100644 index 0000000000..0a12c58dbb --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/card_cells/url.dart @@ -0,0 +1,63 @@ +import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileURLCardCell extends CardCell { + const MobileURLCardCell({ + super.key, + required this.cellControllerBuilder, + }); + + final CellControllerBuilder cellControllerBuilder; + + @override + State createState() => _URLCellState(); +} + +class _URLCellState extends State { + late final URLCellBloc _cellBloc; + + @override + void initState() { + super.initState(); + final cellController = + widget.cellControllerBuilder.build() as URLCellController; + + _cellBloc = URLCellBloc(cellController: cellController) + ..add(const URLCellEvent.initial()); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cellStyle = MobileCardCellStyle(context); + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) => previous.content != current.content, + builder: (context, state) { + if (state.content.isEmpty) { + return const SizedBox(); + } else { + return Container( + alignment: Alignment.centerLeft, + padding: cellStyle.padding, + child: Text( + state.content, + style: cellStyle.urlTextStyle(), + ), + ); + } + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/mobile_card_content.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/mobile_card_content.dart new file mode 100644 index 0000000000..2aaed1e585 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_content/mobile_card_content.dart @@ -0,0 +1,118 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/card.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileCardContent extends StatelessWidget { + const MobileCardContent({ + super.key, + required this.cellBuilder, + required this.cells, + required this.cardData, + required this.styleConfiguration, + this.renderHook, + }); + + final CardCellBuilder cellBuilder; + + final List cells; + final RowCardRenderHook? renderHook; + final CustomCardData? cardData; + final RowCardStyleConfiguration styleConfiguration; + + @override + Widget build(BuildContext context) { + return Padding( + padding: styleConfiguration.cardPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + children: _makeCells(context, cells), + ), + ); + } + + List _makeCells( + BuildContext context, + List cells, + ) { + final List children = []; + + cells.asMap().forEach((int index, DatabaseCellContext cellContext) { + Widget child; + if (index == 0) { + // The title cell UI is different with a normal text cell. + // Use render hook to customize its UI + child = _buildTitleCell(cellContext); + } else { + child = Padding( + key: cellContext.key(), + padding: styleConfiguration.cellPadding, + child: cellBuilder.buildCell( + cellContext: cellContext, + cardData: cardData, + renderHook: renderHook, + hasNotes: !cellContext.rowMeta.isDocumentEmpty, + ), + ); + } + + children.add(child); + }); + return children; + } + + Widget _buildTitleCell( + DatabaseCellContext cellContext, + ) { + final renderHook = RowCardRenderHook(); + renderHook.addTextCellHook((cellData, _, __) { + return BlocBuilder( + builder: (context, state) { + final text = cellData.isEmpty + ? LocaleKeys.grid_row_titlePlaceholder.tr() + : cellData; + + final textStyle = Theme.of(context).textTheme.bodyMedium?.copyWith( + color: cellData.isEmpty + ? Theme.of(context).colorScheme.onSecondary + : Theme.of(context).colorScheme.onBackground, + fontSize: 20, + ); + + return Row( + children: [ + if (!cellContext.rowMeta.isDocumentEmpty) ...[ + const FlowySvg(FlowySvgs.notes_s), + const HSpace(4), + ], + Expanded( + child: Text( + text, + style: textStyle, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + }, + ); + }); + + return Padding( + key: cellContext.key(), + padding: styleConfiguration.cellPadding, + child: CardCellBuilder(cellBuilder.cellCache).buildCell( + cellContext: cellContext, + renderHook: renderHook, + hasNotes: !cellContext.rowMeta.isDocumentEmpty, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart index 4626973ded..4030740081 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart @@ -25,6 +25,8 @@ class MobileCardDetailScreen extends StatefulWidget { const MobileCardDetailScreen({ super.key, required this.rowController, + this.scrollController, + this.isBottomSheet = false, required this.fieldController, }); @@ -34,6 +36,8 @@ class MobileCardDetailScreen extends StatefulWidget { static const argFieldController = 'fieldController'; final RowController rowController; + final ScrollController? scrollController; + final bool isBottomSheet; final FieldController fieldController; @override @@ -41,24 +45,16 @@ class MobileCardDetailScreen extends StatefulWidget { } class _MobileCardDetailScreenState extends State { - late final ScrollController _scrollController; late final GridCellBuilder _cellBuilder; @override void initState() { super.initState(); - _scrollController = ScrollController(); _cellBuilder = GridCellBuilder( cellCache: widget.rowController.cellCache, ); } - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { // TODO(yijing): fix context issue when navigating in bottom navigation bar @@ -66,10 +62,18 @@ class _MobileCardDetailScreenState extends State { create: (context) => RowDetailBloc(rowController: widget.rowController) ..add(const RowDetailEvent.initial()), child: Scaffold( - // appbar with duplicate and delete card features appBar: AppBar( + leading: IconButton( + onPressed: () { + context.pop(); + }, + icon: Icon( + widget.isBottomSheet ? Icons.close : Icons.arrow_back, + ), + ), title: Text(LocaleKeys.board_cardDetail.tr()), actions: [ + // appbar with duplicate and delete card features BlocProvider( create: (context) => RowActionSheetBloc( viewId: widget.rowController.viewId, @@ -135,9 +139,9 @@ class _MobileCardDetailScreenState extends State { ], ), body: Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), child: ListView( - controller: _scrollController, + controller: widget.scrollController, children: [ BlocProvider( create: (context) => RowBannerBloc( @@ -181,7 +185,7 @@ class _MobileCardDetailScreenState extends State { RowDocument( viewId: widget.rowController.viewId, rowId: widget.rowController.rowId, - scrollController: _scrollController, + scrollController: widget.scrollController ?? ScrollController(), ), ], ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart index 10f457d823..4d04e08acd 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart @@ -83,7 +83,7 @@ class _MobileRecentViewState extends State { color: theme.colorScheme.background, borderRadius: BorderRadius.circular(8), border: Border.all( - color: theme.colorScheme.outline.withOpacity(0.5), + color: theme.colorScheme.outline, ), ), child: Stack( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index eea5388cb9..b389f3c630 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -85,7 +85,12 @@ class BoardBloc extends Bloc { startRowId: startRowId, ); - result.fold((_) {}, (err) => Log.error(err)); + result.fold( + (rowMeta) { + emit(state.copyWith(recentAddedRowMeta: rowMeta)); + }, + (err) => Log.error(err), + ); }, createHeaderRow: (String groupId) async { final result = await databaseController.createRow( @@ -93,7 +98,12 @@ class BoardBloc extends Bloc { fromBeginning: true, ); - result.fold((_) {}, (err) => Log.error(err)); + result.fold( + (rowMeta) { + emit(state.copyWith(recentAddedRowMeta: rowMeta)); + }, + (err) => Log.error(err), + ); }, createGroup: (name) async { final result = await groupBackendSvc.createGroup(name: name); @@ -527,6 +537,7 @@ class BoardState with _$BoardState { required BoardLayoutSettingPB? layoutSettings, String? editingHeaderId, BoardEditingRow? editingRow, + RowMetaPB? recentAddedRowMeta, required List hiddenGroups, }) = _BoardState; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index 32705c4f20..fc30408ea6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -2,7 +2,8 @@ import 'dart:collection'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/database/card/card.dart'; +import 'package:appflowy/mobile/presentation/database/board/mobile_board_content.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; @@ -23,7 +24,6 @@ import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart' hide Card; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; import '../../widgets/card/card.dart'; import '../../widgets/card/card_cell_builder.dart'; @@ -89,11 +89,19 @@ class BoardPage extends StatelessWidget { child: CircularProgressIndicator.adaptive(), ), finish: (result) => result.successOrFail.fold( - (_) => BoardContent(onEditStateChanged: onEditStateChanged), - (err) => FlowyErrorPage.message( - err.toString(), - howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), - ), + (_) => PlatformExtension.isMobile + ? const MobileBoardContent() + : DesktopBoardContent(onEditStateChanged: onEditStateChanged), + (err) => PlatformExtension.isMobile + ? FlowyMobileStateContainer.error( + emoji: '🛸', + title: LocaleKeys.board_mobile_faildToLoad.tr(), + errorMsg: err.toString(), + ) + : FlowyErrorPage.message( + err.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), ), ), ), @@ -101,8 +109,8 @@ class BoardPage extends StatelessWidget { } } -class BoardContent extends StatefulWidget { - const BoardContent({ +class DesktopBoardContent extends StatefulWidget { + const DesktopBoardContent({ super.key, this.onEditStateChanged, }); @@ -110,18 +118,18 @@ class BoardContent extends StatefulWidget { final VoidCallback? onEditStateChanged; @override - State createState() => _BoardContentState(); + State createState() => _DesktopBoardContentState(); } -class _BoardContentState extends State { +class _DesktopBoardContentState extends State { final renderHook = RowCardRenderHook(); late final ScrollController scrollController; late final AppFlowyBoardScrollController scrollManager; final config = const AppFlowyBoardConfig( groupBackgroundColor: Color(0xffF7F8FC), - headerPadding: EdgeInsets.symmetric(horizontal: 8), - cardPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 3), + groupHeaderPadding: EdgeInsets.symmetric(horizontal: 8), + cardMargin: EdgeInsets.symmetric(horizontal: 4, vertical: 3), ); @override @@ -162,12 +170,12 @@ class _BoardContentState extends State { controller: context.read().boardController, groupConstraints: const BoxConstraints.tightFor(width: 256), config: const AppFlowyBoardConfig( - groupPadding: EdgeInsets.symmetric(horizontal: 4), - groupItemPadding: EdgeInsets.symmetric(horizontal: 4), - footerPadding: EdgeInsets.fromLTRB(4, 14, 4, 4), + groupMargin: EdgeInsets.symmetric(horizontal: 4), + groupBodyPadding: EdgeInsets.symmetric(horizontal: 4), + groupFooterPadding: EdgeInsets.fromLTRB(4, 14, 4, 4), stretchGroupHeight: false, ), - leading: HiddenGroupsColumn(margin: config.headerPadding), + leading: HiddenGroupsColumn(margin: config.groupHeaderPadding), trailing: showCreateGroupButton ? BoardTrailing(scrollController: scrollController) : null, @@ -175,7 +183,7 @@ class _BoardContentState extends State { value: context.read(), child: BoardColumnHeader( groupData: groupData, - margin: config.headerPadding, + margin: config.groupHeaderPadding, ), ), footerBuilder: _buildFooter, @@ -204,7 +212,7 @@ class _BoardContentState extends State { Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) { return AppFlowyGroupFooter( height: 36, - margin: config.footerPadding, + margin: config.groupFooterPadding, icon: FlowySvg( FlowySvgs.add_s, color: Theme.of(context).hintColor, @@ -244,7 +252,7 @@ class _BoardContentState extends State { return AppFlowyGroupCard( key: ValueKey(groupItemId), - margin: config.cardPadding, + margin: config.cardMargin, decoration: _makeBoxDecoration(context), child: RowCard( rowMeta: rowMeta, @@ -329,25 +337,14 @@ class _BoardContentState extends State { groupId: groupId, ); - // navigate to card detail screen when it is in mobile - if (PlatformExtension.isMobile) { - context.push( - MobileCardDetailScreen.routeName, - extra: { - MobileCardDetailScreen.argRowController: dataController, - MobileCardDetailScreen.argFieldController: fieldController, - }, - ); - } else { - FlowyOverlay.show( - context: context, - builder: (_) => RowDetailPage( - fieldController: fieldController, - cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), - rowController: dataController, - ), - ); - } + FlowyOverlay.show( + context: context, + builder: (_) => RowDetailPage( + fieldController: fieldController, + cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), + rowController: dataController, + ), + ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart index ea8212a934..01a7781908 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/mobile/presentation/database/card/card.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart'; @@ -120,14 +121,12 @@ class _RowCardState extends State> { }, builder: (context, state) { if (PlatformExtension.isMobile) { - // TODO(yijing): refactor it in mobile to display card in database view return RowCardContainer( buildAccessoryWhen: () => state.isEditing == false, accessories: const [], openAccessory: (p0) {}, openCard: (context) => widget.openCard(context), - child: _CardContent( - rowNotifier: rowNotifier, + child: MobileCardContent( cellBuilder: widget.cellBuilder, styleConfiguration: widget.styleConfiguration, cells: state.cells, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart index 695244eb2e..5f42e8abb7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart @@ -1,4 +1,7 @@ +import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/card_cells.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/timestamp_card_cell.dart'; +import 'package:appflowy/util/platform_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter/material.dart'; @@ -10,7 +13,6 @@ import 'cells/date_card_cell.dart'; import 'cells/number_card_cell.dart'; import 'cells/select_option_card_cell.dart'; import 'cells/text_card_cell.dart'; -import 'cells/timestamp_card_cell.dart'; import 'cells/url_card_cell.dart'; // T represents as the Generic card data @@ -34,6 +36,40 @@ class CardCellBuilder { final key = cellContext.key(); final style = styles?[cellContext.fieldType]; + + return PlatformExtension.isMobile + ? _getMobileCardCellWidget( + key: key, + cellContext: cellContext, + cellControllerBuilder: cellControllerBuilder, + style: style, + cardData: cardData, + cellNotifier: cellNotifier, + renderHook: renderHook, + hasNotes: hasNotes, + ) + : _getDesktopCardCellWidget( + key: key, + cellContext: cellContext, + cellControllerBuilder: cellControllerBuilder, + style: style, + cardData: cardData, + cellNotifier: cellNotifier, + renderHook: renderHook, + hasNotes: hasNotes, + ); + } + + Widget _getDesktopCardCellWidget({ + required Key key, + required DatabaseCellContext cellContext, + required CellControllerBuilder cellControllerBuilder, + CardCellStyle? style, + CustomCardData? cardData, + EditableCardNotifier? cellNotifier, + RowCardRenderHook? renderHook, + required bool hasNotes, + }) { switch (cellContext.fieldType) { case FieldType.Checkbox: return CheckboxCardCell( @@ -104,4 +140,79 @@ class CardCellBuilder { } throw UnimplementedError; } + + Widget _getMobileCardCellWidget({ + required Key key, + required DatabaseCellContext cellContext, + required CellControllerBuilder cellControllerBuilder, + CardCellStyle? style, + CustomCardData? cardData, + EditableCardNotifier? cellNotifier, + RowCardRenderHook? renderHook, + required bool hasNotes, + }) { + switch (cellContext.fieldType) { + case FieldType.Checkbox: + return MobileCheckboxCardCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.DateTime: + return MobileDateCardCell( + renderHook: renderHook?.renderHook[FieldType.DateTime], + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.LastEditedTime: + return MobileTimestampCardCell( + renderHook: renderHook?.renderHook[FieldType.LastEditedTime], + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.CreatedTime: + return MobileTimestampCardCell( + renderHook: renderHook?.renderHook[FieldType.CreatedTime], + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.SingleSelect: + return MobileSelectOptionCardCell( + renderHook: renderHook?.renderHook[FieldType.SingleSelect], + cellControllerBuilder: cellControllerBuilder, + cardData: cardData, + key: key, + ); + case FieldType.MultiSelect: + return MobileSelectOptionCardCell( + renderHook: renderHook?.renderHook[FieldType.MultiSelect], + cellControllerBuilder: cellControllerBuilder, + cardData: cardData, + key: key, + ); + case FieldType.Checklist: + return MobileChecklistCardCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.Number: + return MobileNumberCardCell( + renderHook: renderHook?.renderHook[FieldType.Number], + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.RichText: + return MobileTextCardCell( + key: key, + cardData: cardData, + renderHook: renderHook?.renderHook[FieldType.RichText], + cellControllerBuilder: cellControllerBuilder, + ); + case FieldType.URL: + return MobileURLCardCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + } + throw UnimplementedError; + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart index ec798c50c3..8aa72c2e0d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart @@ -15,7 +15,7 @@ typedef CellRenderHook = Widget? Function( typedef RenderHookByFieldType = Map>; /// The [RowCardRenderHook] is used to customize the rendering of the -/// card cell. Each cell has itw own field type. So the [renderHook] +/// card cell. Each cell has its own field type. So the [renderHook] /// is a map of [FieldType] to [CellRenderHook]. class RowCardRenderHook { final RenderHookByFieldType renderHook = {}; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index ef3e8cae6d..ae72ab74d4 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -1,8 +1,8 @@ +import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart'; import 'package:appflowy/mobile/presentation/database/card/card.dart'; import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart'; import 'package:appflowy/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart'; import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart'; -import 'package:appflowy/mobile/presentation/database/mobile_board_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_calendar_events_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; 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 d42f9ccee4..5339ef6dca 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 @@ -37,15 +37,18 @@ class MobileAppearance extends BaseAppearance { brightness: brightness, primary: _primaryColor, onPrimary: Colors.white, + // group card header background color + primaryContainer: const Color(0xffF1F1F4), // primary 20 // group card & property edit background color secondary: const Color(0xfff7f8fc), // shade 10 onSecondary: _onSecondaryColor, + // hidden group title & card text color + tertiary: const Color(0xff858585), // for light text error: const Color(0xffFB006D), onError: const Color(0xffFB006D), background: Colors.white, onBackground: _onBackgroundColor, - outline: const Color(0xffE3E3E3), //caption - // outline: const Color(0xffBDC0C5), //caption + outline: const Color(0xffe3e3e3), outlineVariant: const Color(0xffCBD5E0).withOpacity(0.24), //Snack bar surface: Colors.white, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 2c6809b4e3..f106573656 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -45,8 +45,8 @@ packages: dependency: "direct main" description: path: "." - ref: "2de4fe0" - resolved-ref: "2de4fe0b0245dcdf2c2bf43410661c28acbcc687" + ref: "93a1b70" + resolved-ref: "93a1b70858801583b5a7edb0b2c06308d7982054" url: "https://github.com/AppFlowy-IO/appflowy-board.git" source: git version: "0.1.1" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 947574b8ca..8665d42778 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -40,10 +40,9 @@ dependencies: flowy_svg: path: packages/flowy_svg appflowy_board: - # path: packages/appflowy_board git: url: https://github.com/AppFlowy-IO/appflowy-board.git - ref: 2de4fe0 + ref: 93a1b70 appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 9ba42db731..96c47d4400 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -228,7 +228,8 @@ "addToFavorites": "Add to favorites", "rename": "Rename", "helpCenter": "Help Center", - "OK": "Ok", + "add": "Add", + "yes": "Yes", "Done": "Done", "Cancel": "Cancel" }, @@ -813,6 +814,8 @@ "addToColumnTopTooltip": "Add a new card at the top", "renameColumn": "Rename", "hideColumn": "Hide", + "groupActions": "Group Actions", + "newGroup": "New Group", "deleteColumn": "Delete", "deleteColumnConfirmation": "This will delete this group and all the cards in it.\nAre you sure you want to continue?" }, @@ -836,7 +839,10 @@ "groupBy": "Group by", "referencedBoardPrefix": "View of", "mobile": { - "editURL": "Edit URL" + "editURL": "Edit URL", + "unhideGroup": "Unhide group", + "unhideGroupContent": "Are you sure you want to show this group on the board?", + "faildToLoad": "Failed to load board view" } }, "calendar": {