From eef34caf27f0a63787da6295798d4dd081eaa5ce Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 18 Dec 2023 17:04:20 +0800 Subject: [PATCH] feat: revamp mobile database tab bar (#4160) --- .../util/database_test_op.dart | 3 +- .../presentation/base/mobile_view_page.dart | 6 +- .../database/board/mobile_board_content.dart | 69 ++- .../board/widgets/mobile_board_trailing.dart | 2 +- .../mobile_card_detail_screen.dart | 40 +- .../widgets/mobile_create_field_button.dart | 2 +- .../widgets/option_text_field.dart | 11 +- .../mobile_date_picker_screen.dart | 16 +- ...d.dart => mobile_field_bottom_sheets.dart} | 17 +- .../mobile_field_type_option_editor.dart | 29 +- ...et.dart => mobile_quick_field_editor.dart} | 22 +- .../database/view/database_field_list.dart | 245 ++++++++++ .../database/view/database_view_layout.dart | 116 +++++ .../database/view/database_view_list.dart | 164 +++++++ .../view/database_view_quick_actions.dart | 65 +++ .../view/edit_database_view_cubit.dart | 35 ++ .../view/edit_database_view_screen.dart | 441 ++++++++++++++++++ .../flowy_mobile_quick_action_button.dart | 37 ++ .../widgets/flowy_option_tile.dart | 172 ++++--- .../application/database_view_service.dart | 13 + .../application/layout/layout_bloc.dart | 12 +- .../application/layout/layout_service.dart | 20 - .../calendar/presentation/calendar_page.dart | 16 +- .../calendar/presentation/layout/sizes.dart | 4 +- .../grid/presentation/layout/sizes.dart | 6 +- .../grid/presentation/mobile_grid_page.dart | 100 +--- .../widgets/header/mobile_field_button.dart | 2 +- .../widgets/header/mobile_grid_header.dart | 2 +- .../tab_bar/desktop/tab_bar_header.dart | 8 +- .../tab_bar/mobile/mobile_tab_bar_header.dart | 232 +++++---- .../database_view/tab_bar/tab_bar_view.dart | 26 +- ...ton.dart => mobile_database_controls.dart} | 63 ++- .../setting/setting_property_list.dart | 135 +----- .../resources/flowy_icons/16x/card_view.svg | 3 + .../flowy_icons/16x/disorder_list.svg | 8 + frontend/resources/translations/en.json | 11 +- 36 files changed, 1566 insertions(+), 587 deletions(-) rename frontend/appflowy_flutter/lib/mobile/presentation/database/field/{bottom_sheet_create_field.dart => mobile_field_bottom_sheets.dart} (84%) rename frontend/appflowy_flutter/lib/mobile/presentation/database/field/{quick_edit_field_bottom_sheet.dart => mobile_quick_field_editor.dart} (89%) create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_field_list.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_layout.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_cubit.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart rename frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/{mobile_database_settings_button.dart => mobile_database_controls.dart} (62%) create mode 100644 frontend/resources/flowy_icons/16x/card_view.svg create mode 100644 frontend/resources/flowy_icons/16x/disorder_list.svg diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index 34600c5b6a..41f68c1076 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -1610,8 +1610,7 @@ extension AppFlowyDatabaseTest on WidgetTester { ) async { final field = find.byWidgetPredicate( (widget) => - widget is DesktopDatabasePropertyCell && - widget.fieldInfo.name == fieldName, + widget is DatabasePropertyCell && widget.fieldInfo.name == fieldName, ); final toggleVisibilityButton = find.descendant(of: field, matching: find.byType(FlowyIconButton)); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index bef066ebb9..8cfd7e8b0b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -106,9 +106,11 @@ class _MobileViewPageState extends State { Widget _buildApp(ViewPB? view, List actions, Widget child) { final icon = view?.icon.value; + final elevation = (view?.layout.isDatabaseView ?? false) ? 0.0 : null; return Scaffold( appBar: AppBar( titleSpacing: 0, + elevation: elevation, title: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -118,9 +120,9 @@ class _MobileViewPageState extends State { fontSize: 22.0, ), Expanded( - child: FlowyText.regular( + child: FlowyText.medium( view?.name ?? widget.title ?? '', - fontSize: 14.0, + fontSize: 15.0, overflow: TextOverflow.ellipsis, ), ), 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 index 665ebd46f6..b0934e1b05 100644 --- 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 @@ -4,7 +4,6 @@ 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/board/application/board_bloc.dart'; -import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.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'; @@ -57,7 +56,7 @@ class _MobileBoardContentState extends State { final config = AppFlowyBoardConfig( groupCornerRadius: 8, groupBackgroundColor: Theme.of(context).colorScheme.secondary, - groupMargin: const EdgeInsets.fromLTRB(4, 8, 4, 12), + groupMargin: const EdgeInsets.fromLTRB(4, 0, 4, 12), groupHeaderPadding: const EdgeInsets.all(8), groupBodyPadding: const EdgeInsets.all(4), groupFooterPadding: const EdgeInsets.all(8), @@ -82,47 +81,33 @@ class _MobileBoardContentState extends State { final showCreateGroupButton = context.read().groupingFieldType.canCreateNewGroup; final showHiddenGroups = state.hiddenGroups.isNotEmpty; - return Column( - children: [ - Divider( - height: 1, - thickness: 1, - indent: GridSize.leadingHeaderPadding / 2, - endIndent: GridSize.leadingHeaderPadding / 2, + 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, ), - Expanded( - child: 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, - ), - ), - ), - ], + ), + footerBuilder: _buildFooter, + cardBuilder: (_, column, columnItem) => _buildCard( + context: context, + afGroupData: column, + afGroupItem: columnItem, + cardMargin: config.cardMargin, + ), ); }, ), 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 index 430f085303..0e85bbdfba 100644 --- 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 @@ -25,7 +25,7 @@ class _MobileBoardTrailingState extends State { final style = Theme.of(context); return Container( - margin: const EdgeInsets.all(8), + margin: const EdgeInsets.symmetric(horizontal: 8), child: SizedBox( width: screenSize.width * 0.7, child: isEditing 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 81026beae0..4027605649 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 @@ -1,6 +1,7 @@ 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.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; @@ -141,13 +142,13 @@ class _MobileRowDetailPageState extends State { showMobileBottomSheet( context, backgroundColor: Theme.of(context).colorScheme.background, - padding: const EdgeInsets.only(top: 4, bottom: 32), + padding: const EdgeInsets.only(top: 8, bottom: 36), builder: (_) => Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: _CardActionButton( + child: MobileQuickActionButton( onTap: () { final rowId = _bloc.state.currentRowId; if (rowId == null) { @@ -169,7 +170,7 @@ class _MobileRowDetailPageState extends State { const Divider(height: 9), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: _CardActionButton( + child: MobileQuickActionButton( onTap: () { final rowId = _bloc.state.currentRowId; if (rowId == null) { @@ -196,39 +197,6 @@ class _MobileRowDetailPageState extends State { } } -class _CardActionButton extends StatelessWidget { - const _CardActionButton({ - required this.onTap, - required this.icon, - required this.text, - this.color, - }); - - final VoidCallback onTap; - final FlowySvgData icon; - final String text; - final Color? color; - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: Container( - height: 44, - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - children: [ - FlowySvg(icon, size: const Size.square(20), color: color), - const HSpace(8), - FlowyText(text, fontSize: 15, color: color), - ], - ), - ), - ); - } -} - class RowDetailFab extends StatelessWidget { const RowDetailFab({ super.key, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart index 7494267299..fda7944700 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/database/field/bottom_sheet_create_field.dart'; +import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/option_text_field.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/option_text_field.dart index 9f044a82f6..ac9e290aa5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/option_text_field.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/option_text_field.dart @@ -22,13 +22,10 @@ class OptionTextField extends StatelessWidget { controller: controller, textFieldPadding: const EdgeInsets.symmetric(horizontal: 12.0), onTextChanged: onTextChanged, - leftIcon: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: FlowySvg( - type.svgData, - size: const Size.square(36.0), - blendMode: null, - ), + leftIcon: FlowySvg( + type.svgData, + size: const Size.square(36.0), + blendMode: null, ), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart index e5c1cf206d..52b161f8d9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart @@ -180,24 +180,24 @@ class _DateCellEditBody extends StatelessWidget { showTopBorder: false, child: _IncludeTimePicker(), ), - _ColoredDivider(), + _Divider(), FlowyOptionDecorateBox( child: MobileDatePicker(), ), - _ColoredDivider(), + _Divider(), _EndDateSwitch(), _IncludeTimeSwitch(), - _ColoredDivider(), + _Divider(), _ClearDateButton(), - _ColoredDivider(), + _Divider(), ], ), ); } } -class _ColoredDivider extends StatelessWidget { - const _ColoredDivider(); +class _Divider extends StatelessWidget { + const _Divider(); @override Widget build(BuildContext context) { @@ -364,7 +364,7 @@ class _EndDateSwitch extends StatelessWidget { return BlocSelector( selector: (state) => state.isRange, builder: (context, isRange) { - return FlowyOptionTile.switcher( + return FlowyOptionTile.toggle( text: LocaleKeys.grid_field_isRange.tr(), isSelected: isRange, onValueChanged: (value) { @@ -386,7 +386,7 @@ class _IncludeTimeSwitch extends StatelessWidget { return BlocSelector( selector: (state) => state.includeTime, builder: (context, includeTime) { - return FlowyOptionTile.switcher( + return FlowyOptionTile.toggle( showTopBorder: false, text: LocaleKeys.grid_field_includeTime.tr(), isSelected: includeTime, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/bottom_sheet_create_field.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart similarity index 84% rename from frontend/appflowy_flutter/lib/mobile/presentation/database/field/bottom_sheet_create_field.dart rename to frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart index 33b43d4dc4..876a35fee9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/bottom_sheet_create_field.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart @@ -1,7 +1,4 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/database/field/mobile_field_type_grid.dart'; -import 'package:appflowy/mobile/presentation/database/field/mobile_field_type_option_editor.dart'; -import 'package:appflowy/mobile/presentation/database/field/quick_edit_field_bottom_sheet.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; import 'package:flutter/material.dart'; @@ -9,6 +6,9 @@ import 'package:go_router/go_router.dart'; import 'mobile_create_field_screen.dart'; import 'mobile_edit_field_screen.dart'; +import 'mobile_field_type_grid.dart'; +import 'mobile_field_type_option_editor.dart'; +import 'mobile_quick_field_editor.dart'; void showCreateFieldBottomSheet(BuildContext context, String viewId) { showMobileBottomSheet( @@ -95,13 +95,10 @@ void showQuickEditField( backgroundColor: Theme.of(context).colorScheme.secondaryContainer, resizeToAvoidBottomInset: true, builder: (context) { - return ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 500), - child: SingleChildScrollView( - child: QuickEditField( - viewId: viewId, - fieldInfo: fieldInfo, - ), + return SingleChildScrollView( + child: QuickEditField( + viewId: viewId, + fieldInfo: fieldInfo, ), ); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_type_option_editor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_type_option_editor.dart index f0329baf82..6bbf4c4471 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_type_option_editor.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_type_option_editor.dart @@ -440,12 +440,10 @@ class _DateOptionState extends State<_DateOption> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.symmetric( - vertical: 6.0, - horizontal: 16.0, - ), + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0), child: FlowyText( - LocaleKeys.grid_field_dateFormat.tr(), + LocaleKeys.grid_field_dateFormat.tr().toUpperCase(), + fontSize: 13, color: Theme.of(context).hintColor, ), ), @@ -496,12 +494,10 @@ class _TimeOptionState extends State<_TimeOption> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.symmetric( - vertical: 6.0, - horizontal: 16.0, - ), + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0), child: FlowyText( - LocaleKeys.grid_field_timeFormat.tr(), + LocaleKeys.grid_field_timeFormat.tr().toUpperCase(), + fontSize: 13, color: Theme.of(context).hintColor, ), ), @@ -629,12 +625,10 @@ class _SelectOption extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.symmetric( - vertical: 6.0, - horizontal: 16.0, - ), + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0), child: FlowyText( - LocaleKeys.grid_field_optionTitle.tr(), + LocaleKeys.grid_field_optionTitle.tr().toUpperCase(), + fontSize: 13, color: Theme.of(context).hintColor, ), ), @@ -770,7 +764,10 @@ class __SelectOptionTileState extends State<_SelectOptionTile> { textFieldHintText: LocaleKeys.grid_field_typeANewOption.tr(), showTopBorder: widget.showTopBorder, showBottomBorder: widget.showBottomBorder, - textFieldPadding: const EdgeInsets.symmetric(horizontal: 16.0), + textFieldPadding: const EdgeInsets.symmetric( + horizontal: 0.0, + vertical: 16.0, + ), trailing: _SelectOptionColor( color: option.color, onChanged: (color) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/quick_edit_field_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart similarity index 89% rename from frontend/appflowy_flutter/lib/mobile/presentation/database/field/quick_edit_field_bottom_sheet.dart rename to frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart index bc5ac6f429..b9143788d9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/quick_edit_field_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart @@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart'; -import 'package:appflowy/mobile/presentation/database/field/bottom_sheet_create_field.dart'; +import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/plugins/database_view/application/field/field_backend_service.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; @@ -82,15 +82,16 @@ class _QuickEditFieldState extends State { } }, ), - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.grid_field_hide.tr(), - leftIcon: const FlowySvg(FlowySvgs.hide_s), - onTap: () async { - context.pop(); - await service.hide(); - }, - ), + if (!widget.fieldInfo.isPrimary) + FlowyOptionTile.text( + showTopBorder: false, + text: LocaleKeys.grid_field_hide.tr(), + leftIcon: const FlowySvg(FlowySvgs.hide_s), + onTap: () async { + context.pop(); + await service.hide(); + }, + ), FlowyOptionTile.text( showTopBorder: false, text: LocaleKeys.grid_field_insertLeft.tr(), @@ -133,6 +134,7 @@ class _QuickEditFieldState extends State { }, ), ], + const VSpace(12), ], ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_field_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_field_list.dart new file mode 100644 index 0000000000..80fb33efec --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_field_list.dart @@ -0,0 +1,245 @@ +import 'dart:ui'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; +import 'package:appflowy/plugins/base/drag_handler.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/field/field_info.dart'; +import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/field_visibility_extension.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/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 '../field/mobile_field_bottom_sheets.dart'; + +class MobileDatabaseFieldList extends StatelessWidget { + const MobileDatabaseFieldList({ + super.key, + required this.databaseController, + required this.viewPB, + }); + + final DatabaseController databaseController; + final ViewPB viewPB; + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + expand: false, + snap: true, + initialChildSize: 1.0, + minChildSize: 0.0, + builder: (context, controller) { + return Material( + child: Column( + children: [ + const Center(child: DragHandler()), + const _MobileDatabaseFieldListHeader(), + Expanded( + child: SingleChildScrollView( + child: _MobileDatabaseFieldListBody( + databaseController: databaseController, + view: viewPB, + ), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _MobileDatabaseFieldListHeader extends StatelessWidget { + const _MobileDatabaseFieldListHeader(); + + @override + Widget build(BuildContext context) { + const iconWidth = 30.0; + return Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 8, 12), + child: Stack( + children: [ + Align( + alignment: Alignment.centerLeft, + child: FlowyIconButton( + icon: const FlowySvg( + FlowySvgs.arrow_left_m, + size: Size.square(iconWidth), + ), + width: iconWidth, + iconPadding: EdgeInsets.zero, + onPressed: () => Navigator.of(context).maybePop(), + ), + ), + Align( + alignment: Alignment.center, + child: FlowyText.medium( + LocaleKeys.grid_settings_properties.tr(), + fontSize: 16, + ), + ), + ], + ), + ); + } +} + +class _MobileDatabaseFieldListBody extends StatelessWidget { + const _MobileDatabaseFieldListBody({ + required this.databaseController, + required this.view, + }); + + final DatabaseController databaseController; + final ViewPB view; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => DatabasePropertyBloc( + viewId: view.id, + fieldController: databaseController.fieldController, + )..add(const DatabasePropertyEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final cells = state.fieldContexts + .mapIndexed( + (index, field) => DatabaseFieldListTile( + key: ValueKey(field.id), + viewId: view.id, + fieldController: databaseController.fieldController, + fieldInfo: field, + index: index, + showTopBorder: index == 0, + ), + ) + .toList(); + + return ReorderableListView.builder( + proxyDecorator: (_, index, anim) { + final field = state.fieldContexts[index]; + return AnimatedBuilder( + animation: anim, + builder: (BuildContext context, Widget? child) { + final double animValue = + Curves.easeInOut.transform(anim.value); + final double scale = lerpDouble(1, 1.05, animValue)!; + return Transform.scale( + scale: scale, + child: Material( + child: DatabaseFieldListTile( + key: ValueKey(field.id), + viewId: view.id, + fieldController: databaseController.fieldController, + fieldInfo: field, + index: index, + showTopBorder: true, + ), + ), + ); + }, + ); + }, + buildDefaultDragHandles: true, + shrinkWrap: true, + onReorder: (from, to) { + context + .read() + .add(DatabasePropertyEvent.moveField(from, to)); + }, + footer: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _divider(), + _NewDatabaseFieldTile(viewId: view.id), + ], + ), + itemCount: cells.length, + itemBuilder: (context, index) => cells[index], + ); + }, + ), + ); + } + + Widget _divider() => const VSpace(20); +} + +class DatabaseFieldListTile extends StatelessWidget { + const DatabaseFieldListTile({ + super.key, + required this.index, + required this.fieldInfo, + required this.viewId, + required this.fieldController, + required this.showTopBorder, + }); + + final int index; + final FieldInfo fieldInfo; + final String viewId; + final FieldController fieldController; + final bool showTopBorder; + + @override + Widget build(BuildContext context) { + if (fieldInfo.field.isPrimary) { + return FlowyOptionTile.text( + text: fieldInfo.name, + leftIcon: FlowySvg( + fieldInfo.fieldType.icon(), + size: const Size.square(20), + ), + showTopBorder: showTopBorder, + ); + } else { + return FlowyOptionTile.toggle( + isSelected: fieldInfo.visibility?.isVisibleState() ?? false, + text: fieldInfo.name, + leftIcon: FlowySvg( + fieldInfo.fieldType.icon(), + size: const Size.square(20), + ), + showTopBorder: showTopBorder, + onValueChanged: (value) { + final newVisibility = fieldInfo.visibility!.toggle(); + context.read().add( + DatabasePropertyEvent.setFieldVisibility( + fieldInfo.id, + newVisibility, + ), + ); + }, + ); + } + } +} + +class _NewDatabaseFieldTile extends StatelessWidget { + const _NewDatabaseFieldTile({required this.viewId}); + + final String viewId; + + @override + Widget build(BuildContext context) { + return FlowyOptionTile.text( + text: LocaleKeys.grid_field_newProperty.tr(), + leftIcon: FlowySvg( + FlowySvgs.add_s, + size: const Size.square(20), + color: Theme.of(context).hintColor, + ), + textColor: Theme.of(context).hintColor, + onTap: () => showCreateFieldBottomSheet(context, viewId), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_layout.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_layout.dart new file mode 100644 index 0000000000..c7dd66238f --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_layout.dart @@ -0,0 +1,116 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +/// [DatabaseViewLayoutPicker] is seen when changing the layout type of a +/// database view or creating a new database view. +class DatabaseViewLayoutPicker extends StatelessWidget { + const DatabaseViewLayoutPicker({ + super.key, + required this.selectedLayout, + required this.onSelect, + }); + + final DatabaseLayoutPB selectedLayout; + final void Function(DatabaseLayoutPB layout) onSelect; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildButton(DatabaseLayoutPB.Grid, true), + _buildButton(DatabaseLayoutPB.Board, false), + _buildButton(DatabaseLayoutPB.Calendar, false), + ], + ); + } + + Widget _buildButton(DatabaseLayoutPB layout, bool showTopBorder) { + return FlowyOptionTile.checkbox( + text: layout.layoutName, + leftIcon: FlowySvg(layout.icon, size: const Size.square(20)), + isSelected: selectedLayout == layout, + showTopBorder: showTopBorder, + onTap: () { + onSelect(layout); + }, + ); + } +} + +class MobileCalendarViewLayoutSettings extends StatelessWidget { + const MobileCalendarViewLayoutSettings({ + super.key, + required this.databaseController, + }); + + final DatabaseController databaseController; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _layoutField(), + _divider(), + ..._startWeek(context), + ], + ); + } + + Widget _layoutField() { + return FlowyOptionTile.text( + text: LocaleKeys.calendar_settings_layoutDateField.tr(), + ); + } + + Widget _divider() => const VSpace(20); + + List _startWeek(BuildContext context) { + final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols; + return [ + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0), + child: FlowyText( + LocaleKeys.calendar_settings_firstDayOfWeek.tr().toUpperCase(), + fontSize: 13, + color: Theme.of(context).hintColor, + ), + ), + FlowyOptionTile.checkbox( + text: symbols.WEEKDAYS[-1], + isSelected: true, + onTap: () {}, + ), + FlowyOptionTile.checkbox( + text: symbols.WEEKDAYS[0], + isSelected: false, + showTopBorder: false, + onTap: () {}, + ), + ]; + } +} + +class MobileBoardViewLayoutSettings extends StatelessWidget { + const MobileBoardViewLayoutSettings({ + super.key, + required this.databaseController, + }); + + final DatabaseController databaseController; + + @override + Widget build(BuildContext context) { + return FlowyOptionTile.text(text: LocaleKeys.board_groupBy.tr()); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart new file mode 100644 index 0000000000..f2e6896bcb --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart @@ -0,0 +1,164 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; +import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; + +import 'database_view_layout.dart'; + +/// [MobileDatabaseViewList] shows a list of all the views in the database and +/// adds a button to create a new database view. +class MobileDatabaseViewList extends StatelessWidget { + const MobileDatabaseViewList({super.key, required this.views}); + + final List views; + + @override + Widget build(BuildContext context) { + return Builder( + builder: (context) { + final children = [ + ...views.map((view) => MobileDatabaseViewListButton(view: view)), + const VSpace(20), + const MobileNewDatabaseViewButton(), + ]; + + return Column( + children: children, + ); + }, + ); + } +} + +@visibleForTesting +class MobileDatabaseViewListButton extends StatelessWidget { + const MobileDatabaseViewListButton({super.key, required this.view}); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + return FlowyOptionTile.text( + text: view.name, + onTap: () {}, + leftIcon: _buildViewIconButton(context, view), + trailing: FlowySvg( + FlowySvgs.three_dots_s, + size: const Size.square(20), + color: Theme.of(context).hintColor, + ), + ); + } + + Widget _buildViewIconButton(BuildContext context, ViewPB view) { + return view.icon.value.isNotEmpty + ? EmojiText( + emoji: view.icon.value, + fontSize: 16.0, + ) + : SizedBox.square( + dimension: 20.0, + child: view.defaultIcon(), + ); + } +} + +class MobileNewDatabaseViewButton extends StatelessWidget { + const MobileNewDatabaseViewButton({super.key}); + + @override + Widget build(BuildContext context) { + return FlowyOptionTile.text( + text: LocaleKeys.grid_settings_createView.tr(), + textColor: Theme.of(context).hintColor, + leftIcon: FlowySvg( + FlowySvgs.add_s, + size: const Size.square(20), + color: Theme.of(context).hintColor, + ), + trailing: FlowySvg( + FlowySvgs.three_dots_s, + size: const Size.square(20), + color: Theme.of(context).hintColor, + ), + onTap: () {}, + ); + } +} + +class MobileCreateDatabaseView extends StatefulWidget { + const MobileCreateDatabaseView({super.key}); + + @override + State createState() => + _MobileCreateDatabaseViewState(); +} + +class _MobileCreateDatabaseViewState extends State { + late final TextEditingController controller; + DatabaseLayoutPB layoutType = DatabaseLayoutPB.Grid; + String icon = ""; + + @override + void initState() { + super.initState(); + controller = + TextEditingController(text: LocaleKeys.grid_title_placeholder.tr()); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + FlowyOptionTile.textField( + controller: controller, + leftIcon: _buildViewIcon(), + ), + const VSpace(20), + DatabaseViewLayoutPicker( + selectedLayout: DatabaseLayoutPB.Grid, + onSelect: (layout) {}, + ), + ], + ); + } + + Widget _buildViewIcon() { + final viewIcon = icon.isNotEmpty + ? EmojiText( + emoji: icon, + fontSize: 16.0, + ) + : SizedBox.square( + dimension: 18.0, + child: FlowySvg(layoutType.icon), + ); + return InkWell( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(12)), + border: Border.fromBorderSide( + BorderSide(color: Theme.of(context).dividerColor), + ), + ), + width: 36, + height: 36, + child: Center(child: viewIcon), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart new file mode 100644 index 0000000000..827ab6f59e --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart @@ -0,0 +1,65 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +/// [MobileDatabaseViewQuickActions] is gives users to quickly edit a database +/// view from the [MobileDatabaseViewList] +class MobileDatabaseViewQuickActions extends StatelessWidget { + const MobileDatabaseViewQuickActions({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _actionButton(_Action.edit), + _divider(), + _actionButton(_Action.duplicate), + _divider(), + _actionButton(_Action.delete), + _divider(), + ], + ); + } + + Widget _actionButton(_Action action) { + return MobileQuickActionButton( + icon: action.icon, + text: action.label, + onTap: () {}, + ); + } + + Widget _divider() => const Divider(height: 9); +} + +enum _Action { + edit, + duplicate, + delete; + + String get label { + return switch (this) { + edit => LocaleKeys.grid_settings_editView.tr(), + duplicate => LocaleKeys.button_duplicate.tr(), + delete => LocaleKeys.button_delete.tr(), + }; + } + + FlowySvgData get icon { + return switch (this) { + edit => FlowySvgs.grid_s, + duplicate => FlowySvgs.copy_s, + delete => FlowySvgs.delete_s, + }; + } + + Color? color(BuildContext context) { + return switch (this) { + delete => Theme.of(context).colorScheme.error, + _ => null, + }; + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_cubit.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_cubit.dart new file mode 100644 index 0000000000..6c098bff9c --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_cubit.dart @@ -0,0 +1,35 @@ +import 'package:bloc/bloc.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'edit_database_view_cubit.freezed.dart'; + +class MobileEditDatabaseViewCubit extends Cubit { + MobileEditDatabaseViewCubit() + : super( + MobileDatabaseViewEditorState.initial(), + ); + + void changePage(MobileEditDatabaseViewPageEnum newPage) { + emit(MobileDatabaseViewEditorState(currentPage: newPage)); + } +} + +@freezed +class MobileDatabaseViewEditorState with _$MobileDatabaseViewEditorState { + factory MobileDatabaseViewEditorState({ + required MobileEditDatabaseViewPageEnum currentPage, + }) = _MobileDatabaseViewEditorState; + + factory MobileDatabaseViewEditorState.initial() => + MobileDatabaseViewEditorState( + currentPage: MobileEditDatabaseViewPageEnum.main, + ); +} + +enum MobileEditDatabaseViewPageEnum { + main, + fields, + filter, + sort, +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart new file mode 100644 index 0000000000..1bd930ff48 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart @@ -0,0 +1,441 @@ +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.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; +import 'package:appflowy/plugins/base/drag_handler.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/application/database_view_service.dart'; +import 'package:appflowy/plugins/database_view/application/layout/layout_service.dart'; +import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.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'; + +import 'database_field_list.dart'; +import 'database_view_layout.dart'; +import 'edit_database_view_cubit.dart'; + +/// [MobileEditDatabaseViewScreen] is the main widget used to edit a database +/// view. It contains multiple sub-pages, and the current page is managed by +/// [MobileEditDatabaseViewCubit] +class MobileEditDatabaseViewScreen extends StatefulWidget { + const MobileEditDatabaseViewScreen({ + super.key, + required this.databaseController, + required this.viewPB, + }); + + final DatabaseController databaseController; + final ViewPB viewPB; + + @override + State createState() => + _MobileEditDatabaseViewScreenState(); +} + +class _MobileEditDatabaseViewScreenState + extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => MobileEditDatabaseViewCubit(), + child: BlocBuilder( + builder: (context, state) { + return switch (state.currentPage) { + MobileEditDatabaseViewPageEnum.main => _EditDatabaseViewMainPage( + databaseController: widget.databaseController, + viewPB: widget.viewPB, + ), + MobileEditDatabaseViewPageEnum.fields => _wrapSubPage( + context, + MobileDatabaseFieldList( + databaseController: widget.databaseController, + viewPB: widget.viewPB, + ), + ), + _ => const SizedBox.shrink(), + }; + }, + ), + ); + } + + Widget _wrapSubPage(BuildContext context, Widget child) { + return PopScope( + canPop: false, + child: child, + onPopInvoked: (_) { + context + .read() + .changePage(MobileEditDatabaseViewPageEnum.main); + }, + ); + } +} + +class _EditDatabaseViewMainPage extends StatelessWidget { + const _EditDatabaseViewMainPage({ + required this.databaseController, + required this.viewPB, + }); + + final DatabaseController databaseController; + final ViewPB viewPB; + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + expand: false, + snap: true, + initialChildSize: 1.0, + minChildSize: 0.0, + builder: (context, controller) { + return Material( + child: Column( + children: [ + const Center(child: DragHandler()), + const _EditDatabaseViewHeader(), + Expanded( + child: SingleChildScrollView( + child: _EditDatabaseViewBody( + databaseController: databaseController, + view: viewPB, + ), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _EditDatabaseViewHeader extends StatelessWidget { + const _EditDatabaseViewHeader(); + + @override + Widget build(BuildContext context) { + const iconWidth = 30.0; + return Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 8, 12), + child: 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(), + ), + ), + Align( + alignment: Alignment.center, + child: FlowyText.medium( + LocaleKeys.grid_settings_editView.tr(), + fontSize: 16, + ), + ), + ], + ), + ); + } +} + +class _EditDatabaseViewBody extends StatelessWidget { + const _EditDatabaseViewBody({ + required this.databaseController, + required this.view, + }); + + final DatabaseController databaseController; + final ViewPB view; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + return ViewBloc(view: view)..add(const ViewEvent.initial()); + }, + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _NameAndIcon(view: state.view), + _divider(), + DatabaseViewSettingTile( + setting: DatabaseViewSettings.layout, + databaseController: databaseController, + view: state.view, + showTopBorder: true, + ), + DatabaseViewSettingTile( + setting: DatabaseViewSettings.fields, + databaseController: databaseController, + view: state.view, + ), + _divider(), + ], + ); + }, + ), + ); + } + + Widget _divider() => const VSpace(20); +} + +class _NameAndIcon extends StatefulWidget { + const _NameAndIcon({required this.view}); + + final ViewPB view; + + @override + State<_NameAndIcon> createState() => _NameAndIconState(); +} + +class _NameAndIconState extends State<_NameAndIcon> { + final TextEditingController textEditingController = TextEditingController(); + + @override + void initState() { + super.initState(); + textEditingController.text = widget.view.name; + } + + @override + Widget build(BuildContext context) { + return FlowyOptionTile.textField( + controller: textEditingController, + textFieldPadding: const EdgeInsets.symmetric(horizontal: 12.0), + onTextChanged: (text) { + context.read().add(ViewEvent.rename(text)); + }, + leftIcon: _buildViewIcon(), + ); + } + + Widget _buildViewIcon() { + final icon = widget.view.icon.value.isNotEmpty + ? EmojiText( + emoji: widget.view.icon.value, + fontSize: 16.0, + ) + : SizedBox.square( + dimension: 18.0, + child: widget.view.defaultIcon(), + ); + return InkWell( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(12)), + border: Border.fromBorderSide( + BorderSide(color: Theme.of(context).dividerColor), + ), + ), + width: 36, + height: 36, + child: Center(child: icon), + ), + ); + } +} + +enum DatabaseViewSettings { + layout, + fields, + filter, + sort, + board, + calendar, + duplicate, + delete; + + String get label { + return switch (this) { + layout => LocaleKeys.grid_settings_databaseLayout.tr(), + fields => LocaleKeys.grid_settings_properties.tr(), + filter => LocaleKeys.grid_settings_filter.tr(), + sort => LocaleKeys.grid_settings_sort.tr(), + board => LocaleKeys.grid_settings_boardSettings.tr(), + calendar => LocaleKeys.grid_settings_calendarSettings.tr(), + duplicate => LocaleKeys.grid_settings_duplicateView.tr(), + delete => LocaleKeys.grid_settings_deleteView.tr(), + }; + } + + FlowySvgData get icon { + return switch (this) { + layout => FlowySvgs.card_view_s, + fields => FlowySvgs.disorder_list_s, + filter => FlowySvgs.filter_s, + sort => FlowySvgs.sort_ascending_s, + board => FlowySvgs.board_s, + calendar => FlowySvgs.date_s, + duplicate => FlowySvgs.copy_s, + delete => FlowySvgs.delete_s, + }; + } + + MobileEditDatabaseViewPageEnum? get subPage { + return switch (this) { + fields => MobileEditDatabaseViewPageEnum.fields, + filter => MobileEditDatabaseViewPageEnum.filter, + sort => MobileEditDatabaseViewPageEnum.sort, + _ => null, + }; + } +} + +class DatabaseViewSettingTile extends StatelessWidget { + const DatabaseViewSettingTile({ + super.key, + required this.setting, + required this.databaseController, + required this.view, + this.showTopBorder = false, + }); + + final DatabaseViewSettings setting; + final DatabaseController databaseController; + final ViewPB view; + final bool showTopBorder; + + @override + Widget build(BuildContext context) { + return FlowyOptionTile.text( + text: setting.label, + leftIcon: FlowySvg(setting.icon, size: const Size.square(20)), + trailing: _trailing(context, setting, view, databaseController), + showTopBorder: showTopBorder, + onTap: () => _onTap(context), + ); + } + + Widget _trailing( + BuildContext context, + DatabaseViewSettings setting, + ViewPB view, + DatabaseController databaseController, + ) { + switch (setting) { + case DatabaseViewSettings.layout: + return Row( + children: [ + FlowyText( + databaseLayoutFromViewLayout(view.layout).layoutName, + color: Theme.of(context).hintColor, + ), + const HSpace(8), + const FlowySvg(FlowySvgs.arrow_right_s), + ], + ); + case DatabaseViewSettings.fields: + final numVisible = databaseController.fieldController.fieldInfos + .where((field) => field.visibility != FieldVisibility.AlwaysHidden) + .length; + return Row( + children: [ + FlowyText( + "$numVisible shown", + color: Theme.of(context).hintColor, + ), + const HSpace(8), + const FlowySvg(FlowySvgs.arrow_right_s), + ], + ); + default: + return const SizedBox.shrink(); + } + } + + void _onTap(BuildContext context) async { + final subPage = setting.subPage; + + if (subPage != null) { + context.read().changePage(subPage); + return; + } + + if (setting == DatabaseViewSettings.layout) { + final databaseLayout = databaseLayoutFromViewLayout(view.layout); + final newLayout = await showMobileBottomSheet( + context, + padding: EdgeInsets.zero, + resizeToAvoidBottomInset: false, + builder: (context) { + return Padding( + padding: const EdgeInsets.only(top: 24, bottom: 46), + child: DatabaseViewLayoutPicker( + selectedLayout: databaseLayout, + onSelect: (layout) { + Navigator.of(context).pop(layout); + }, + ), + ); + }, + ); + if (newLayout != null && newLayout != databaseLayout) { + DatabaseViewBackendService.updateLayout( + viewId: databaseController.viewId, + layout: newLayout, + ); + } + return; + } + + if (setting == DatabaseViewSettings.board) { + showMobileBottomSheet( + context, + padding: EdgeInsets.zero, + resizeToAvoidBottomInset: false, + builder: (context) { + return Padding( + padding: const EdgeInsets.only(top: 24, bottom: 46), + child: MobileBoardViewLayoutSettings( + databaseController: databaseController, + ), + ); + }, + ); + return; + } + + if (setting == DatabaseViewSettings.calendar) { + showMobileBottomSheet( + context, + padding: EdgeInsets.zero, + resizeToAvoidBottomInset: false, + builder: (context) { + return Padding( + padding: const EdgeInsets.only(top: 24, bottom: 46), + child: MobileCalendarViewLayoutSettings( + databaseController: databaseController, + ), + ); + }, + ); + return; + } + + if (setting == DatabaseViewSettings.delete) { + context.read().add(const ViewEvent.delete()); + context.pop(true); + return; + } + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart new file mode 100644 index 0000000000..0d6efbe42c --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart @@ -0,0 +1,37 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class MobileQuickActionButton extends StatelessWidget { + const MobileQuickActionButton({ + super.key, + required this.onTap, + required this.icon, + required this.text, + this.color, + }); + + final VoidCallback onTap; + final FlowySvgData icon; + final String text; + final Color? color; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: [ + FlowySvg(icon, size: const Size.square(20), color: color), + const HSpace(8), + FlowyText(text, fontSize: 15, color: color), + ], + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart index 4fc0ca020b..6a7b2b6063 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart @@ -1,16 +1,16 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; enum FlowyOptionTileType { text, textField, checkbox, + toggle, } -// used in cell editor - class FlowyOptionTile extends StatelessWidget { const FlowyOptionTile._({ required this.type, @@ -59,8 +59,8 @@ class FlowyOptionTile extends StatelessWidget { void Function(String value)? onTextChanged, void Function(String value)? onTextSubmitted, EdgeInsets textFieldPadding = const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 2.0, + horizontal: 0.0, + vertical: 16.0, ), bool showTopBorder = true, bool showBottomBorder = true, @@ -69,7 +69,7 @@ class FlowyOptionTile extends StatelessWidget { String? textFieldHintText, }) { return FlowyOptionTile._( - type: FlowyOptionTileType.text, + type: FlowyOptionTileType.textField, controller: controller, textFieldPadding: textFieldPadding, text: null, @@ -88,6 +88,7 @@ class FlowyOptionTile extends StatelessWidget { required String text, required bool isSelected, required VoidCallback? onTap, + Widget? leftIcon, bool showTopBorder = true, bool showBottomBorder = true, }) { @@ -98,6 +99,7 @@ class FlowyOptionTile extends StatelessWidget { onTap: onTap, showTopBorder: showTopBorder, showBottomBorder: showBottomBorder, + leading: leftIcon, trailing: isSelected ? const FlowySvg( FlowySvgs.blue_check_s, @@ -108,7 +110,7 @@ class FlowyOptionTile extends StatelessWidget { ); } - factory FlowyOptionTile.switcher({ + factory FlowyOptionTile.toggle({ required String text, required bool isSelected, required void Function(bool value) onValueChanged, @@ -117,14 +119,14 @@ class FlowyOptionTile extends StatelessWidget { Widget? leftIcon, }) { return FlowyOptionTile._( - type: FlowyOptionTileType.text, + type: FlowyOptionTileType.toggle, text: text, controller: null, - onTap: null, + onTap: () => onValueChanged(!isSelected), showTopBorder: showTopBorder, showBottomBorder: showBottomBorder, leading: leftIcon, - trailing: _Switcher(value: isSelected, onChanged: onValueChanged), + trailing: _Toggle(value: isSelected, onChanged: onValueChanged), ); } @@ -150,110 +152,101 @@ class FlowyOptionTile extends StatelessWidget { @override Widget build(BuildContext context) { + final leadingWidget = _buildLeading(); + final child = ColoredBox( color: Theme.of(context).colorScheme.surface, child: FlowyOptionDecorateBox( showTopBorder: showTopBorder, showBottomBorder: showBottomBorder, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _buildText(), - ..._buildTextField(), - if (controller == null) const Spacer(), - trailing ?? const SizedBox.shrink(), - const HSpace(12.0), - ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (leadingWidget != null) leadingWidget, + _buildText(), + _buildTextField(), + if (trailing != null) trailing!, + ], + ), ), ), ); if (type == FlowyOptionTileType.checkbox || + type == FlowyOptionTileType.toggle || type == FlowyOptionTileType.text) { - return FlowyButton( - expandText: true, - margin: EdgeInsets.zero, + return GestureDetector( onTap: onTap, - text: child, + child: child, ); } return child; } + Widget? _buildLeading() { + if (leading != null) { + return Center(child: leading); + } else { + return null; + } + } + Widget _buildText() { - if (text == null) { + if (text == null || type == FlowyOptionTileType.textField) { return const SizedBox.shrink(); } - switch (type) { - case FlowyOptionTileType.text: - return FlowyButton( - useIntrinsicWidth: true, - text: FlowyText( - text!, - color: textColor, - ), - margin: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 16.0, - ), - leftIcon: leading, - leftIconSize: const Size.square(24.0), - iconPadding: 8.0, - onTap: onTap, - ); - case FlowyOptionTileType.textField: - return const SizedBox.shrink(); - case FlowyOptionTileType.checkbox: - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 16.0, - ), - child: FlowyText( - text!, - ), - ); - } - } + final padding = EdgeInsets.symmetric( + horizontal: leading == null ? 0.0 : 8.0, + vertical: 16.0, + ); - List _buildTextField() { - if (controller == null) { - return [ - const SizedBox.shrink(), - ]; - } - - return [ - if (leading != null) leading!, - Expanded( - child: Container( - constraints: const BoxConstraints.tightFor( - height: 54.0, - ), - alignment: Alignment.center, - child: TextField( - controller: controller, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: textFieldPadding, - hintText: textFieldHintText, - ), - onChanged: onTextChanged, - onSubmitted: onTextSubmitted, - ), + return Expanded( + child: Padding( + padding: padding, + child: FlowyText( + text!, + fontSize: 15, + color: textColor, ), ), - ]; + ); + } + + Widget _buildTextField() { + if (controller == null) { + return const SizedBox.shrink(); + } + + return Expanded( + child: Container( + constraints: const BoxConstraints.tightFor( + height: 54.0, + ), + alignment: Alignment.center, + child: TextField( + controller: controller, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: textFieldPadding, + hintText: textFieldHintText, + ), + onChanged: onTextChanged, + onSubmitted: onTextSubmitted, + ), + ), + ); } } -class _Switcher extends StatelessWidget { - const _Switcher({ +class _Toggle extends StatelessWidget { + const _Toggle({ required this.value, required this.onChanged, }); @@ -263,13 +256,16 @@ class _Switcher extends StatelessWidget { @override Widget build(BuildContext context) { + // CupertinoSwitch adds a 8px margin all around. The original size of the + // switch is 38 x 22. return SizedBox( - width: 48, + width: 46, + height: 30, child: FittedBox( fit: BoxFit.fill, - child: Switch.adaptive( + child: CupertinoSwitch( value: value, - activeColor: const Color(0xFF00BCF0), + activeColor: Theme.of(context).colorScheme.primary, onChanged: onChanged, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart index b04412c2fd..d00efe9da9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart @@ -11,6 +11,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; +import 'layout/layout_service.dart'; + class DatabaseViewBackendService { final String viewId; DatabaseViewBackendService({ @@ -25,6 +27,17 @@ class DatabaseViewBackendService { .then((value) => value.leftMap((l) => l.value)); } + static Future> updateLayout({ + required String viewId, + required DatabaseLayoutPB layout, + }) { + final payload = UpdateViewPayloadPB.create() + ..viewId = viewId + ..layout = viewLayoutFromDatabaseLayout(layout); + + return FolderEventUpdateView(payload).send(); + } + Future> openDatabase() async { final payload = DatabaseViewIdPB(value: viewId); return DatabaseEventGetDatabase(payload).send(); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_bloc.dart index 1098a39f59..1694b9e70e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_bloc.dart @@ -2,25 +2,23 @@ import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.da import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'layout_service.dart'; +import '../database_view_service.dart'; + part 'layout_bloc.freezed.dart'; class DatabaseLayoutBloc extends Bloc { - final DatabaseLayoutBackendService layoutService; - DatabaseLayoutBloc({ required String viewId, required DatabaseLayoutPB databaseLayout, - }) : layoutService = DatabaseLayoutBackendService(viewId), - super(DatabaseLayoutState.initial(viewId, databaseLayout)) { + }) : super(DatabaseLayoutState.initial(viewId, databaseLayout)) { on( (event, emit) async { event.when( initial: () {}, updateLayout: (DatabaseLayoutPB layout) { - layoutService.updateLayout( - fieldId: viewId, + DatabaseViewBackendService.updateLayout( + viewId: viewId, layout: layout, ); emit(state.copyWith(databaseLayout: layout)); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart index 5d517945fb..3ca3427f71 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart @@ -1,25 +1,5 @@ -import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:dartz/dartz.dart'; - -class DatabaseLayoutBackendService { - final String viewId; - - DatabaseLayoutBackendService(this.viewId); - - Future> updateLayout({ - required String fieldId, - required DatabaseLayoutPB layout, - }) { - final payload = UpdateViewPayloadPB.create() - ..viewId = viewId - ..layout = viewLayoutFromDatabaseLayout(layout); - - return FolderEventUpdateView(payload).send(); - } -} ViewLayoutPB viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) { switch (databaseLayout) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart index fcbbe1a1c6..8d9302120f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart @@ -189,7 +189,7 @@ class _CalendarPageState extends State { return LayoutBuilder( // must specify MonthView width for useAvailableVerticalSpace to work properly builder: (context, constraints) { - Widget calendar = Padding( + return Padding( padding: PlatformExtension.isMobile ? CalendarSize.contentInsetsMobile : CalendarSize.contentInsets, @@ -210,20 +210,6 @@ class _CalendarPageState extends State { ), ), ); - if (PlatformExtension.isMobile) { - calendar = Column( - children: [ - Divider( - height: 1, - thickness: 1, - indent: GridSize.leadingHeaderPadding / 2, - endIndent: GridSize.leadingHeaderPadding / 2, - ), - Expanded(child: calendar), - ], - ); - } - return calendar; }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/layout/sizes.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/layout/sizes.dart index a6f92b0876..c2548db5d5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/layout/sizes.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/layout/sizes.dart @@ -15,9 +15,9 @@ class CalendarSize { static EdgeInsets get contentInsetsMobile => EdgeInsets.fromLTRB( GridSize.leadingHeaderPadding / 2, - CalendarSize.headerContainerPadding / 2, + 0, GridSize.leadingHeaderPadding / 2, - CalendarSize.headerContainerPadding / 2, + 0, ); static double get scrollBarSize => 8 * scale; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/sizes.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/sizes.dart index 5e8ded6cb2..e73ffe7e9a 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/sizes.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/sizes.dart @@ -38,8 +38,10 @@ class GridSize { static EdgeInsets get footerContentInsets => EdgeInsets.fromLTRB( GridSize.leadingHeaderPadding, GridSize.headerContainerPadding, - GridSize.headerContainerPadding, - GridSize.headerContainerPadding, + PlatformExtension.isMobile + ? GridSize.leadingHeaderPadding + : GridSize.headerContainerPadding, + PlatformExtension.isMobile ? 100 : GridSize.headerContainerPadding, ); static EdgeInsets get contentInsets => EdgeInsets.fromLTRB( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/mobile_grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/mobile_grid_page.dart index cd216bb235..5bff94f7f2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/mobile_grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/mobile_grid_page.dart @@ -2,25 +2,24 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.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_service.dart'; import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/shortcuts.dart'; import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; -import 'package:appflowy/plugins/database_view/widgets/setting/mobile_database_settings_button.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; -import 'grid_page.dart'; import 'grid_scroll.dart'; import 'layout/sizes.dart'; import 'widgets/header/mobile_grid_header.dart'; @@ -28,8 +27,6 @@ import 'widgets/mobile_fab.dart'; import 'widgets/row/mobile_row.dart'; class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder { - final _toggleExtension = ToggleExtensionNotifier(); - @override Widget content( BuildContext context, @@ -45,21 +42,15 @@ class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder { } @override - Widget settingBar(BuildContext context, DatabaseController controller) { - return MobileDatabaseSettingsButton( - key: _makeValueKey(controller), - controller: controller, - toggleExtension: _toggleExtension, - ); - } + Widget settingBar(BuildContext context, DatabaseController controller) => + const SizedBox.shrink(); @override Widget settingBarExtension( BuildContext context, DatabaseController controller, - ) { - return const SizedBox.shrink(); - } + ) => + const SizedBox.shrink(); ValueKey _makeValueKey(DatabaseController controller) { return ValueKey(controller.viewId); @@ -217,8 +208,7 @@ class _GridRows extends StatelessWidget { return BlocBuilder( buildWhen: (previous, current) => previous.fields != current.fields, builder: (context, state) { - final double contentWidth = - (state.fields.length + 1) * 200 + GridSize.leadingHeaderPadding; + final double contentWidth = _getContentWidth(state.fields); return Expanded( child: _WrapScrollView( scrollController: scrollController, @@ -248,6 +238,14 @@ class _GridRows extends StatelessWidget { ); } + double _getContentWidth(List fields) { + final visibleFields = fields.where( + (field) => + field.fieldSettings?.visibility != FieldVisibility.AlwaysHidden, + ); + return (visibleFields.length + 1) * 200 + GridSize.leadingHeaderPadding * 2; + } + Widget _renderList( BuildContext context, GridState state, @@ -281,20 +279,9 @@ class _GridRows extends StatelessWidget { }, itemCount: state.rowInfos.length, itemBuilder: (context, index) => children[index], - footer: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: GridSize.footerContentInsets, - child: _AddRowButton(), - ), - Container( - height: 30, - alignment: AlignmentDirectional.centerStart, - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: const _GridFooter(), - ), - ], + footer: Padding( + padding: GridSize.footerContentInsets, + child: _AddRowButton(), ), ); } @@ -360,56 +347,17 @@ class _WrapScrollView extends StatelessWidget { @override Widget build(BuildContext context) { - return ScrollbarListStack( - axis: Axis.vertical, - controller: scrollController.verticalController, - barSize: GridSize.scrollBarSize, - autoHideScrollbar: false, - child: StyledSingleChildScrollView( - autoHideScrollbar: false, - controller: scrollController.horizontalController, - axis: Axis.horizontal, - child: SizedBox( - width: contentWidth, - child: child, - ), + return SingleChildScrollView( + controller: scrollController.horizontalController, + scrollDirection: Axis.horizontal, + child: SizedBox( + width: contentWidth, + child: child, ), ); } } -class _GridFooter extends StatelessWidget { - const _GridFooter(); - - @override - Widget build(BuildContext context) { - return BlocSelector( - selector: (state) => state.rowCount, - builder: (context, rowCount) { - return Padding( - padding: GridSize.contentInsets, - child: RichText( - text: TextSpan( - text: "${LocaleKeys.grid_row_count.tr()} :", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).hintColor, - ), - children: [ - TextSpan( - text: ' $rowCount', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: AFThemeExtension.of(context).gridRowCountColor, - ), - ), - ], - ), - ), - ); - }, - ); - } -} - class _AddRowButton extends StatelessWidget { @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/mobile_field_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/mobile_field_button.dart index 6e1e32ee9e..1605364717 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/mobile_field_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/mobile_field_button.dart @@ -1,5 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/presentation/database/field/bottom_sheet_create_field.dart'; +import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/mobile_grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/mobile_grid_header.dart index d2c4dfccd3..038640f59e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/mobile_grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/mobile_grid_header.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/database/field/bottom_sheet_create_field.dart'; +import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/desktop/tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/desktop/tab_bar_header.dart index 656de93c01..b59515fcde 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/desktop/tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/desktop/tab_bar_header.dart @@ -37,12 +37,8 @@ class TabBarHeader extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - BlocBuilder( - builder: (context, state) { - return const Flexible( - child: DatabaseTabBar(), - ); - }, + const Flexible( + child: DatabaseTabBar(), ), BlocBuilder( builder: (context, state) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/mobile/mobile_tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/mobile/mobile_tab_bar_header.dart index 86c237a9f1..0816270b5f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/mobile/mobile_tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/mobile/mobile_tab_bar_header.dart @@ -1,18 +1,14 @@ -import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; -import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/plugins/database_view/application/tab_bar_bloc.dart'; -import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; -import 'package:appflowy/plugins/database_view/widgets/setting/mobile_database_settings_button.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/mobile_database_controls.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; + +import '../../grid/presentation/grid_page.dart'; class MobileTabBarHeader extends StatefulWidget { const MobileTabBarHeader({super.key}); @@ -22,13 +18,40 @@ class MobileTabBarHeader extends StatefulWidget { } class _MobileTabBarHeaderState extends State { - final controller = TextEditingController(); - @override - void dispose() { - controller.dispose(); - super.dispose(); + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 14), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Expanded(child: _DatabaseViewList()), + const HSpace(10), + BlocBuilder( + builder: (context, state) { + final currentView = state.tabBars.firstWhereIndexedOrNull( + (index, tabBar) => index == state.selectedIndex, + ); + + if (currentView == null) { + return const SizedBox.shrink(); + } + + return MobileDatabaseControls( + controller: state + .tabBarControllerByViewId[currentView.viewId]!.controller, + toggleExtension: ToggleExtensionNotifier(), + ); + }, + ), + ], + ), + ); } +} + +class _DatabaseViewList extends StatelessWidget { + const _DatabaseViewList(); @override Widget build(BuildContext context) { @@ -42,76 +65,127 @@ class _MobileTabBarHeaderState extends State { return const SizedBox.shrink(); } - controller.text = currentView.view.name; - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 14), - child: Row( - children: [ - _buildViewIconButton(currentView.view), - const HSpace(8.0), - Expanded( - child: FlowyTextField( - autoFocus: false, - maxLines: 1, - controller: controller, - textAlignVertical: TextAlignVertical.top, - textInputAction: TextInputAction.done, - decoration: const InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: EdgeInsets.zero, - ), - textStyle: Theme.of(context).textTheme.titleLarge, - onSubmitted: (value) { - if (value.isNotEmpty) { - context.read().add( - ViewEvent.rename(value), - ); - } - }, - onCanceled: () { - controller.text = currentView.view.name; - }, - ), - ), - MobileDatabaseSettingsButton( - controller: state - .tabBarControllerByViewId[currentView.viewId]!.controller, - toggleExtension: ToggleExtensionNotifier(), - ), - ], - ), - ); - }, - ); - } - - Widget _buildViewIconButton(ViewPB view) { - final icon = view.icon.value.isNotEmpty - ? EmojiText( - emoji: view.icon.value, - fontSize: 24.0, - ) - : SizedBox.square( - dimension: 26.0, - child: view.defaultIcon(), + final children = state.tabBars.mapIndexed((index, tabBar) { + return Padding( + padding: EdgeInsetsDirectional.only( + start: index == 0 ? 0 : 2, + end: 2, + ), + child: _DatabaseViewListItem( + tabBar: tabBar, + isSelected: currentView.viewId == tabBar.viewId, + ), ); - return FlowyButton( - text: icon, - useIntrinsicWidth: true, - onTap: () async { - final result = await context.push( - MobileEmojiPickerScreen.routeName, + }).toList(); + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row(children: children), ); - if (context.mounted && result != null) { - await ViewBackendService.updateViewIcon( - viewId: view.id, - viewIcon: result.emoji, - ); - } }, ); } } + +class _DatabaseViewListItem extends StatefulWidget { + const _DatabaseViewListItem({ + required this.tabBar, + required this.isSelected, + }); + + final DatabaseTabBar tabBar; + final bool isSelected; + + @override + State<_DatabaseViewListItem> createState() => _DatabaseViewListItemState(); +} + +class _DatabaseViewListItemState extends State<_DatabaseViewListItem> { + late final MaterialStatesController statesController; + + @override + void initState() { + super.initState(); + statesController = MaterialStatesController( + {if (widget.isSelected) MaterialState.selected}, + ); + } + + @override + void didUpdateWidget(covariant oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isSelected != oldWidget.isSelected) { + statesController.update(MaterialState.selected, widget.isSelected); + } + } + + @override + Widget build(BuildContext context) { + return TextButton( + statesController: statesController, + style: ButtonStyle( + padding: const MaterialStatePropertyAll( + EdgeInsets.symmetric(horizontal: 12, vertical: 7), + ), + maximumSize: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return const Size(150, 48); + } + return const Size(120, 48); + }), + minimumSize: const MaterialStatePropertyAll(Size(48, 0)), + shape: const MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ), + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return const Color(0x0F212729); + } + return Colors.transparent; + }), + overlayColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return Colors.transparent; + } + return Theme.of(context).colorScheme.secondary; + }), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildViewIconButton(context, widget.tabBar.view), + const HSpace(6), + Flexible( + child: FlowyText( + widget.tabBar.view.name, + fontSize: 14, + fontWeight: widget.isSelected ? FontWeight.w500 : FontWeight.w400, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + onPressed: () { + if (!widget.isSelected) { + context + .read() + .add(DatabaseTabBarEvent.selectView(widget.tabBar.viewId)); + } + }, + ); + } + + Widget _buildViewIconButton(BuildContext context, ViewPB view) { + return view.icon.value.isNotEmpty + ? EmojiText( + emoji: view.icon.value, + fontSize: 16.0, + ) + : SizedBox.square( + dimension: 16.0, + child: view.defaultIcon(), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/tab_bar_view.dart index 0ca6fc64cf..46f6f858cb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/tab_bar_view.dart @@ -1,7 +1,6 @@ import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/tab_bar_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database_view/tab_bar/mobile/mobile_tab_bar_header.dart'; import 'package:appflowy/plugins/database_view/widgets/share_button.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; @@ -15,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'desktop/tab_bar_header.dart'; +import 'mobile/mobile_tab_bar_header.dart'; abstract class DatabaseTabBarItemBuilder { const DatabaseTabBarItemBuilder(); @@ -102,14 +102,22 @@ class _DatabaseTabBarViewState extends State { return const SizedBox.shrink(); } - return Padding( - padding: EdgeInsets.symmetric( - horizontal: GridSize.leadingHeaderPadding, - ), - child: PlatformExtension.isMobile - ? const MobileTabBarHeader() - : const TabBarHeader(), - ); + if (PlatformExtension.isDesktop) { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: GridSize.leadingHeaderPadding, + ), + child: const TabBarHeader(), + ); + } else { + return Padding( + padding: EdgeInsets.only( + left: GridSize.leadingHeaderPadding, + right: 8, + ), + child: const MobileTabBarHeader(), + ); + } }, ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_database_settings_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_database_controls.dart similarity index 62% rename from frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_database_settings_button.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_database_controls.dart index f8e3fdc905..4b7dd0b3cf 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_database_settings_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_database_controls.dart @@ -1,17 +1,20 @@ 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.dart'; +import 'package:appflowy/mobile/presentation/database/view/edit_database_view_screen.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_paginated_bottom_sheet.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/database_settings_list.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class MobileDatabaseSettingsButton extends StatelessWidget { - const MobileDatabaseSettingsButton({ +class MobileDatabaseControls extends StatelessWidget { + const MobileDatabaseControls({ super.key, required this.controller, required this.toggleExtension, @@ -55,17 +58,26 @@ class MobileDatabaseSettingsButton extends StatelessWidget { return const SizedBox.shrink(); } - return SizedBox( - height: 24, - width: 24, - child: IconButton( - padding: EdgeInsets.zero, - onPressed: () => _showMobileSettings(context, controller), - icon: const FlowySvg( - FlowySvgs.m_setting_m, - size: Size.square(24), + return Row( + children: [ + _DatabaseControlButton( + icon: FlowySvgs.settings_s, + onTap: () { + showMobileBottomSheet( + context, + padding: EdgeInsets.zero, + builder: (_) => MobileEditDatabaseViewScreen( + databaseController: controller, + viewPB: context.read().state.view, + ), + ); + }, ), - ), + _DatabaseControlButton( + icon: FlowySvgs.align_left_s, + onTap: () => _showMobileSettings(context, controller), + ), + ], ); }, ), @@ -87,3 +99,30 @@ class MobileDatabaseSettingsButton extends StatelessWidget { ), ); } + +class _DatabaseControlButton extends StatelessWidget { + const _DatabaseControlButton({ + required this.onTap, + required this.icon, + }); + + final VoidCallback onTap; + final FlowySvgData icon; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 36, + width: 36, + child: IconButton( + splashRadius: 18, + padding: EdgeInsets.zero, + onPressed: onTap, + icon: FlowySvg( + icon, + size: const Size.square(20), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_property_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_property_list.dart index 8c96377f78..d00317c8d8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_property_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_property_list.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/presentation/database/field/bottom_sheet_create_field.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart'; @@ -9,10 +8,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.da import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/field_visibility_extension.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -63,20 +59,10 @@ class _DatabasePropertyListState extends State { fieldInfo: field, popoverMutex: _popoverMutex, index: index, - bloc: _bloc, ), ) .toList(); - if (PlatformExtension.isMobile) { - return ListView.separated( - shrinkWrap: true, - itemCount: cells.length, - itemBuilder: (_, index) => cells[index], - separatorBuilder: (_, __) => const VSpace(8), - ); - } - return ReorderableListView( proxyDecorator: (child, index, _) => Material( color: Colors.transparent, @@ -110,7 +96,7 @@ class _DatabasePropertyListState extends State { } @visibleForTesting -class DatabasePropertyCell extends StatelessWidget { +class DatabasePropertyCell extends StatefulWidget { const DatabasePropertyCell({ super.key, required this.fieldInfo, @@ -118,7 +104,6 @@ class DatabasePropertyCell extends StatelessWidget { required this.popoverMutex, required this.index, required this.fieldController, - required this.bloc, }); final FieldInfo fieldInfo; @@ -126,126 +111,12 @@ class DatabasePropertyCell extends StatelessWidget { final PopoverMutex popoverMutex; final int index; final FieldController fieldController; - final DatabasePropertyBloc bloc; @override - Widget build(BuildContext context) { - return PlatformExtension.isMobile - ? MobileDatabasePropertyCell( - fieldInfo: fieldInfo, - viewId: viewId, - fieldController: fieldController, - bloc: bloc, - ) - : DesktopDatabasePropertyCell( - fieldInfo: fieldInfo, - viewId: viewId, - popoverMutex: popoverMutex, - index: index, - fieldController: fieldController, - ); - } + State createState() => _DatabasePropertyCellState(); } -class MobileDatabasePropertyCell extends StatefulWidget { - const MobileDatabasePropertyCell({ - super.key, - required this.fieldInfo, - required this.viewId, - required this.fieldController, - required this.bloc, - }); - - final FieldInfo fieldInfo; - final String viewId; - final FieldController fieldController; - final DatabasePropertyBloc bloc; - - @override - State createState() => - _MobileDatabasePropertyCellState(); -} - -class _MobileDatabasePropertyCellState - extends State { - late bool isVisible = widget.fieldInfo.visibility?.isVisibleState() ?? false; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - border: Border.all(color: Theme.of(context).dividerColor), - borderRadius: BorderRadius.circular(6), - ), - child: InkWell( - borderRadius: BorderRadius.circular(6), - onTap: () => - showEditFieldScreen(context, widget.viewId, widget.fieldInfo), - child: Padding( - padding: const EdgeInsets.all(10), - child: Row( - children: [ - FlowySvg( - widget.fieldInfo.fieldType.icon(), - color: Theme.of(context).iconTheme.color, - size: const Size.square(24), - ), - const HSpace(8), - FlowyText.medium( - widget.fieldInfo.name, - color: AFThemeExtension.of(context).textColor, - ), - const Spacer(), - // Toggle Visibility - Toggle( - padding: EdgeInsets.zero, - value: isVisible, - style: ToggleStyle.mobile, - onChanged: (newValue) { - final newVisibility = widget.fieldInfo.visibility!.toggle(); - - context.read().add( - DatabasePropertyEvent.setFieldVisibility( - widget.fieldInfo.id, - newVisibility, - ), - ); - - setState(() => isVisible = !newValue); - }, - ), - ], - ), - ), - ), - ); - } -} - -@visibleForTesting -class DesktopDatabasePropertyCell extends StatefulWidget { - const DesktopDatabasePropertyCell({ - super.key, - required this.fieldController, - required this.fieldInfo, - required this.viewId, - required this.popoverMutex, - required this.index, - }); - - final FieldController fieldController; - final FieldInfo fieldInfo; - final String viewId; - final PopoverMutex popoverMutex; - final int index; - - @override - State createState() => - _DesktopDatabasePropertyCellState(); -} - -class _DesktopDatabasePropertyCellState - extends State { +class _DatabasePropertyCellState extends State { final PopoverController _popoverController = PopoverController(); @override diff --git a/frontend/resources/flowy_icons/16x/card_view.svg b/frontend/resources/flowy_icons/16x/card_view.svg new file mode 100644 index 0000000000..eca8252b57 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/card_view.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/disorder_list.svg b/frontend/resources/flowy_icons/16x/disorder_list.svg new file mode 100644 index 0000000000..22c0494837 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/disorder_list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 44773fe0ac..74eefc1aa4 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -452,7 +452,14 @@ "filterBy": "Filter by...", "typeAValue": "Type a value...", "layout": "Layout", - "databaseLayout": "Layout" + "databaseLayout": "Layout", + "editView": "Edit View", + "boardSettings": "Board settings", + "calendarSettings": "Calendar settings", + "createView": "New view", + "duplicateView": "Duplicate view", + "deleteView": "Delete view", + "numberOfVisibleFields": "{} shown" }, "textFilter": { "contains": "Contains", @@ -1162,4 +1169,4 @@ "addField": "Add field", "userIcon": "User icon" } -} +} \ No newline at end of file