From 771dd9979fb3b1e7ed31446bdaff5a5eeddd5f87 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Mon, 27 Nov 2023 06:36:10 +0200 Subject: [PATCH] feat: mobile database settings rework (#4016) * feat: mobile database settings rework * chore: clean --- .../util/database_test_op.dart | 13 +- .../bottom_sheet_database_field_editor.dart | 9 +- .../show_mobile_bottom_sheet.dart | 3 +- .../mobile_create_row_field_screen.dart | 5 - .../card_property_edit_screen.dart | 5 - .../mobile_field_editor.dart | 64 ++-- .../widgets/mobile_setting_item_widget.dart | 25 +- .../widgets/flowy_paginated_bottom_sheet.dart | 230 ++++++++++++ .../toolbar/calendar_layout_setting.dart | 156 ++++---- .../grid/presentation/mobile_grid_page.dart | 4 +- .../widgets/toolbar/grid_layout.dart | 105 ------ .../tab_bar/mobile/mobile_tab_bar_header.dart | 4 +- .../widgets/group/database_group.dart | 11 +- .../setting/database_layout_selector.dart | 158 ++++++++ .../setting/database_setting_action.dart | 198 ++++++++++ .../setting/database_settings_list.dart | 76 ++++ .../setting/field_visibility_extension.dart | 16 + .../setting/mobile_calendar_settings.dart | 349 ++++++++++++++++++ .../mobile_database_property_editor.dart | 122 ++++++ .../mobile_database_settings_button.dart} | 25 +- .../widgets/setting/setting_button.dart | 211 ++--------- .../setting/setting_property_list.dart | 212 +++++++++-- .../presentation/widgets/toggle/toggle.dart | 6 +- .../widgets/toggle/toggle_style.dart | 3 + frontend/resources/translations/en.json | 6 +- 25 files changed, 1517 insertions(+), 499 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_paginated_bottom_sheet.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_layout_selector.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting_action.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_settings_list.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/field_visibility_extension.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_calendar_settings.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_database_property_editor.dart rename frontend/appflowy_flutter/lib/plugins/database_view/{grid/presentation/widgets/toolbar/mobile_grid_setting.dart => widgets/setting/mobile_database_settings_button.dart} (74%) 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 4cc9222681..3137235a01 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -33,7 +33,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/or import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart'; -import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/database_layout_selector.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart'; import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_add_button.dart'; import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_header.dart'; @@ -51,6 +51,8 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/database_setting_action.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/database_settings_list.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; @@ -1138,7 +1140,7 @@ extension AppFlowyDatabaseTest on WidgetTester { /// Should call [tapDatabaseSettingButton] first. Future tapViewPropertiesButton() async { - final findSettingItem = find.byType(DatabaseSettingListPopover); + final findSettingItem = find.byType(DatabaseSettingsList); final findLayoutButton = find.byWidgetPredicate( (widget) => widget is FlowyText && @@ -1155,7 +1157,7 @@ extension AppFlowyDatabaseTest on WidgetTester { /// Should call [tapDatabaseSettingButton] first. Future tapDatabaseLayoutButton() async { - final findSettingItem = find.byType(DatabaseSettingListPopover); + final findSettingItem = find.byType(DatabaseSettingsList); final findLayoutButton = find.byWidgetPredicate( (widget) => widget is FlowyText && @@ -1171,7 +1173,7 @@ extension AppFlowyDatabaseTest on WidgetTester { } Future tapCalendarLayoutSettingButton() async { - final findSettingItem = find.byType(DatabaseSettingListPopover); + final findSettingItem = find.byType(DatabaseSettingsList); final findLayoutButton = find.byWidgetPredicate( (widget) => widget is FlowyText && @@ -1593,7 +1595,8 @@ extension AppFlowyDatabaseTest on WidgetTester { ) async { final field = find.byWidgetPredicate( (widget) => - widget is DatabasePropertyCell && widget.fieldInfo.name == fieldName, + widget is DesktopDatabasePropertyCell && + widget.fieldInfo.name == fieldName, ); final toggleVisibilityButton = find.descendant(of: field, matching: find.byType(FlowyIconButton)); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_database_field_editor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_database_field_editor.dart index 2db2b26c2e..61fd5c6294 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_database_field_editor.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_database_field_editor.dart @@ -158,9 +158,7 @@ class MobileDBFieldBottomSheetBody extends StatelessWidget { BottomSheetActionWidget( svg: FlowySvgs.date_s, text: LocaleKeys.grid_field_editProperty.tr(), - onTap: () { - onAction(MobileDBBottomSheetGeneralAction.typeOption); - }, + onTap: () => onAction(MobileDBBottomSheetGeneralAction.typeOption), ), const VSpace(8), Row( @@ -170,9 +168,8 @@ class MobileDBFieldBottomSheetBody extends StatelessWidget { child: BottomSheetActionWidget( svg: FlowySvgs.hide_m, text: LocaleKeys.grid_field_hide.tr(), - onTap: () { - onAction(MobileDBBottomSheetGeneralAction.toggleVisibility); - }, + onTap: () => + onAction(MobileDBBottomSheetGeneralAction.toggleVisibility), ), ), const HSpace(8), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart index c8116ff380..6799dbac53 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart @@ -10,11 +10,12 @@ import 'package:go_router/go_router.dart'; Future showMobileBottomSheet({ required BuildContext context, required WidgetBuilder builder, + bool isDragEnabled = true, }) async { showModalBottomSheet( context: context, isScrollControlled: true, - enableDrag: true, + enableDrag: isDragEnabled, useSafeArea: true, builder: builder, ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart index cb15f38ba2..8ad4417f31 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart @@ -1,7 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; -import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -53,10 +52,6 @@ class _MobileCreateRowFieldScreenState ), body: MobileFieldEditor( viewId: widget.viewId, - typeOptionLoader: FieldTypeOptionLoader( - viewId: widget.viewId, - field: widget.typeOption.field_2, - ), fieldController: widget.fieldController, field: widget.typeOption.field_2, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart index 21659db4a4..ca7171e124 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart @@ -4,7 +4,6 @@ import 'package:appflowy/mobile/presentation/database/card/card_property_edit/mo import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; -import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -57,10 +56,6 @@ class CardPropertyEditScreen extends StatelessWidget { ), body: MobileFieldEditor( viewId: cellContext.viewId, - typeOptionLoader: FieldTypeOptionLoader( - viewId: cellContext.viewId, - field: cellContext.fieldInfo.field, - ), fieldController: fieldController, field: cellContext.fieldInfo.field, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart index 0d80903c93..f16a5cba6f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart @@ -6,6 +6,9 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.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:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; @@ -17,18 +20,21 @@ class MobileFieldEditor extends StatelessWidget { const MobileFieldEditor({ super.key, required this.viewId, - required this.typeOptionLoader, required this.field, required this.fieldController, }); final String viewId; final FieldController fieldController; - final FieldTypeOptionLoader typeOptionLoader; final FieldPB field; @override Widget build(BuildContext context) { + final typeOptionLoader = FieldTypeOptionLoader( + viewId: viewId, + field: field, + ); + return BlocProvider( create: (context) { return FieldEditorBloc( @@ -49,42 +55,35 @@ class MobileFieldEditor extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // field name // TODO(yijing): improve hint text PropertyTitle(LocaleKeys.settings_user_name.tr()), BlocSelector( selector: (state) => state.field.name, - builder: (context, fieldName) { - return MobileFieldNameTextField( - text: fieldName, - ); - }, + builder: (context, fieldName) => + MobileFieldNameTextField(text: fieldName), ), Row( children: [ Expanded( - child: - PropertyTitle(LocaleKeys.grid_field_visibility.tr()), + child: PropertyTitle( + LocaleKeys.grid_field_visibility.tr(), + ), ), VisibilitySwitch( - isFieldHidden: state.field.visibility == - FieldVisibility.AlwaysHidden, - onChanged: () { - context.read().add( - RowDetailEvent.toggleFieldVisibility( - state.field.id, - ), - ); - }, + isVisible: + state.field.visibility?.isVisibleState() ?? false, + onChanged: () => context.read().add( + RowDetailEvent.toggleFieldVisibility( + state.field.id, + ), + ), ), ], ), const VSpace(8), // edit property type and settings if (!typeOptionLoader.field.isPrimary) - MobileFieldTypeOptionEditor( - dataController: dataController, - ), + MobileFieldTypeOptionEditor(dataController: dataController), ], ), ); @@ -97,11 +96,11 @@ class MobileFieldEditor extends StatelessWidget { class VisibilitySwitch extends StatefulWidget { const VisibilitySwitch({ super.key, - required this.isFieldHidden, + required this.isVisible, this.onChanged, }); - final bool isFieldHidden; + final bool isVisible; final Function? onChanged; @override @@ -109,18 +108,17 @@ class VisibilitySwitch extends StatefulWidget { } class _VisibilitySwitchState extends State { - late bool _isFieldHidden = widget.isFieldHidden; + late bool _isVisible = widget.isVisible; @override Widget build(BuildContext context) { - return Switch.adaptive( - activeColor: Theme.of(context).colorScheme.primary, - value: !_isFieldHidden, - onChanged: (bool value) { - setState(() { - _isFieldHidden = !_isFieldHidden; - widget.onChanged?.call(); - }); + return Toggle( + padding: EdgeInsets.zero, + value: _isVisible, + style: ToggleStyle.mobile, + onChanged: (newValue) { + widget.onChanged?.call(); + setState(() => _isVisible = newValue); }, ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart index 81e91b0b56..2d5dcd0481 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart @@ -5,23 +5,36 @@ class MobileSettingItem extends StatelessWidget { const MobileSettingItem({ super.key, required this.name, + this.padding = const EdgeInsets.only(bottom: 4), + this.trailing, + this.leadingIcon, this.subtitle, - required this.trailing, this.onTap, }); + final String name; + final EdgeInsets padding; + final Widget? trailing; + final Widget? leadingIcon; final Widget? subtitle; - final Widget trailing; final VoidCallback? onTap; @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.only(bottom: 4), + padding: padding, child: ListTile( - title: FlowyText.medium( - name, - fontSize: 14.0, + title: Row( + children: [ + if (leadingIcon != null) ...[ + leadingIcon!, + const HSpace(8), + ], + FlowyText.medium( + name, + fontSize: 14.0, + ), + ], ), subtitle: subtitle, trailing: trailing, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_paginated_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_paginated_bottom_sheet.dart new file mode 100644 index 0000000000..377d3ed4ea --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_paginated_bottom_sheet.dart @@ -0,0 +1,230 @@ +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class SheetPage { + const SheetPage({ + required this.title, + required this.body, + }); + + final String title; + final Widget body; +} + +void showPaginatedBottomSheet(BuildContext context, {required SheetPage page}) { + showMobileBottomSheet( + context: context, + // Workaround for not causing drag to rebuild + isDragEnabled: false, + builder: (context) => FlowyBottomSheet(root: page), + ); +} + +typedef SheetNotifier = ValueNotifier<(SheetPage, bool)>; + +class FlowyBottomSheet extends StatelessWidget { + FlowyBottomSheet({ + super.key, + required this.root, + }) : _notifier = ValueNotifier((root, true)); + + final SheetPage root; + final SheetNotifier _notifier; + + @override + Widget build(BuildContext context) { + return FlowyBottomSheetController( + key: UniqueKey(), + root: root, + onPageChanged: (page, isRoot) => _notifier.value = (page, isRoot), + child: _FlowyBottomSheetHandler( + root: root, + notifier: _notifier, + ), + ); + } +} + +class _FlowyBottomSheetHandler extends StatefulWidget { + const _FlowyBottomSheetHandler({ + required this.root, + required this.notifier, + }); + + final SheetPage root; + final ValueNotifier<(SheetPage, bool)> notifier; + + @override + State<_FlowyBottomSheetHandler> createState() => + _FlowyBottomSheetHandlerState(); +} + +class _FlowyBottomSheetHandlerState extends State<_FlowyBottomSheetHandler> { + late SheetPage currentPage; + late bool isRoot; + + @override + void initState() { + super.initState(); + widget.notifier.addListener(_onPageChanged); + isRoot = true; + currentPage = widget.root; + + WidgetsBinding.instance.addPostFrameCallback((_) { + currentPage = FlowyBottomSheetController.of(context)!.currentPage; + }); + } + + @override + void dispose() { + widget.notifier.removeListener(_onPageChanged); + super.dispose(); + } + + void _onPageChanged() { + final (page, root) = widget.notifier.value; + + if (mounted) { + setState(() { + currentPage = page; + isRoot = root; + }); + } + } + + @override + Widget build(BuildContext context) { + return AnimatedSize( + duration: const Duration(milliseconds: 150), + child: FlowyBottomSheetPage( + isRoot: isRoot, + title: currentPage.title, + child: currentPage.body, + ), + ); + } +} + +class FlowyBottomSheetPage extends StatelessWidget { + const FlowyBottomSheetPage({ + super.key, + required this.title, + required this.child, + this.isRoot = false, + }); + + final String title; + final Widget child; + final bool isRoot; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _SheetTopBar(title: title, isRoot: isRoot), + child, + ], + ), + ); + } +} + +class _SheetTopBar extends StatelessWidget { + const _SheetTopBar({ + required this.title, + this.isRoot = false, + }); + + final String title; + final bool isRoot; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Row( + children: [ + if (!isRoot) ...[ + IconButton( + onPressed: () => FlowyBottomSheetController.of(context)!.pop(), + icon: const Icon(Icons.arrow_back_ios), + ), + const HSpace(6), + ], + Text( + title, + style: theme.textTheme.labelSmall, + ), + const Spacer(), + IconButton( + icon: Icon( + Icons.close, + color: theme.hintColor, + ), + onPressed: () => context.pop(), + ), + ], + ); + } +} + +class FlowyBottomSheetController extends InheritedWidget { + FlowyBottomSheetController({ + super.key, + required SheetPage root, + this.onPageChanged, + required super.child, + FlowyBottomSheetControllerImpl? controller, + }) : _controller = controller ?? FlowyBottomSheetControllerImpl(root: root); + + final Function(SheetPage page, bool isRoot)? onPageChanged; + + final FlowyBottomSheetControllerImpl _controller; + SheetPage get currentPage => _controller.page; + + @override + bool updateShouldNotify(covariant FlowyBottomSheetController oldWidget) { + return child != oldWidget.child || + _controller.length != oldWidget._controller.length; + } + + static FlowyBottomSheetController? of(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType(); + } + + void push(SheetPage page) { + _controller.push(page); + onPageChanged?.call(_controller.page, _controller.isRoot); + } + + void pop() { + _controller.pop(); + onPageChanged?.call(_controller.page, _controller.isRoot); + } +} + +class FlowyBottomSheetControllerImpl { + FlowyBottomSheetControllerImpl({ + required SheetPage root, + }) : _pages = [root]; + + final List _pages; + SheetPage get page => _pages.last; + bool get isRoot => _pages.length == 1; + + int get length => _pages.length; + + void push(SheetPage page) { + _pages.add(page); + } + + void pop() { + _pages.remove(page); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart index d9149b9966..54411330b1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; @@ -9,13 +11,13 @@ import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.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:protobuf/protobuf.dart'; abstract class ICalendarSetting { + const ICalendarSetting(); + /// Returns the current layout settings for the calendar view. CalendarLayoutSettingPB? getLayoutSetting(); @@ -42,25 +44,15 @@ class CalendarLayoutSetting extends StatefulWidget { } class _CalendarLayoutSettingState extends State { - late final PopoverMutex popoverMutex; - - @override - void initState() { - popoverMutex = PopoverMutex(); - super.initState(); - } + late final PopoverMutex popoverMutex = PopoverMutex(); @override Widget build(BuildContext context) { return BlocProvider( - create: (context) { - return CalendarSettingBloc( - viewId: widget.viewId, - layoutSettings: widget.calendarSettingController.getLayoutSetting(), - )..add( - const CalendarSettingEvent.init(), - ); - }, + create: (context) => CalendarSettingBloc( + viewId: widget.viewId, + layoutSettings: widget.calendarSettingController.getLayoutSetting(), + )..add(const CalendarSettingEvent.init()), child: BlocBuilder( builder: (context, state) { final CalendarLayoutSettingPB? settings = state.layoutSetting @@ -69,39 +61,34 @@ class _CalendarLayoutSettingState extends State { if (settings == null) { return const CircularProgressIndicator(); } + final availableSettings = _availableCalendarSettings(settings); final items = availableSettings.map((setting) { switch (setting) { case CalendarLayoutSettingAction.showWeekNumber: return ShowWeekNumber( showWeekNumbers: settings.showWeekNumbers, - onUpdated: (showWeekNumbers) { - _updateLayoutSettings( - context, - showWeekNumbers: showWeekNumbers, - ); - }, + onUpdated: (showWeekNumbers) => _updateLayoutSettings( + context, + showWeekNumbers: showWeekNumbers, + ), ); case CalendarLayoutSettingAction.showWeekends: return ShowWeekends( showWeekends: settings.showWeekends, - onUpdated: (showWeekends) { - _updateLayoutSettings( - context, - showWeekends: showWeekends, - ); - }, + onUpdated: (showWeekends) => _updateLayoutSettings( + context, + showWeekends: showWeekends, + ), ); case CalendarLayoutSettingAction.firstDayOfWeek: return FirstDayOfWeek( firstDayOfWeek: settings.firstDayOfWeek, popoverMutex: popoverMutex, - onUpdated: (firstDayOfWeek) { - _updateLayoutSettings( - context, - firstDayOfWeek: firstDayOfWeek, - ); - }, + onUpdated: (firstDayOfWeek) => _updateLayoutSettings( + context, + firstDayOfWeek: firstDayOfWeek, + ), ); case CalendarLayoutSettingAction.layoutField: return LayoutDateField( @@ -109,15 +96,13 @@ class _CalendarLayoutSettingState extends State { viewId: widget.viewId, fieldId: settings.fieldId, popoverMutex: popoverMutex, - onUpdated: (fieldId) { - _updateLayoutSettings( - context, - layoutFieldId: fieldId, - ); - }, + onUpdated: (fieldId) => _updateLayoutSettings( + context, + layoutFieldId: fieldId, + ), ); default: - return const SizedBox(); + return const SizedBox.shrink(); } }).toList(); @@ -127,10 +112,10 @@ class _CalendarLayoutSettingState extends State { shrinkWrap: true, controller: ScrollController(), itemCount: items.length, - separatorBuilder: (context, index) => + separatorBuilder: (_, __) => VSpace(GridSize.typeOptionSeparatorHeight), physics: StyledScrollPhysics(), - itemBuilder: (BuildContext context, int index) => items[index], + itemBuilder: (_, int index) => items[index], padding: const EdgeInsets.all(6.0), ), ); @@ -144,28 +129,14 @@ class _CalendarLayoutSettingState extends State { ) { final List settings = [ CalendarLayoutSettingAction.layoutField, - // CalendarLayoutSettingAction.layoutType, - // CalendarLayoutSettingAction.showWeekNumber, ]; switch (layoutSettings.layoutTy) { case CalendarLayoutPB.DayLayout: - // settings.add(CalendarLayoutSettingAction.showTimeLine); break; case CalendarLayoutPB.MonthLayout: - settings.addAll([ - // CalendarLayoutSettingAction.showWeekends, - // if (layoutSettings.showWeekends) - CalendarLayoutSettingAction.firstDayOfWeek, - ]); - break; case CalendarLayoutPB.WeekLayout: - settings.addAll([ - // CalendarLayoutSettingAction.showWeekends, - // if (layoutSettings.showWeekends) - CalendarLayoutSettingAction.firstDayOfWeek, - // CalendarLayoutSettingAction.showTimeLine, - ]); + settings.add(CalendarLayoutSettingAction.firstDayOfWeek); break; } @@ -189,16 +160,20 @@ class _CalendarLayoutSettingState extends State { if (showWeekends != null) { setting.showWeekends = !showWeekends; } + if (showWeekNumbers != null) { setting.showWeekNumbers = !showWeekNumbers; } + if (firstDayOfWeek != null) { setting.firstDayOfWeek = firstDayOfWeek; } + if (layoutFieldId != null) { setting.fieldId = layoutFieldId; } }); + context .read() .add(CalendarSettingEvent.updateLayoutSetting(setting)); @@ -208,21 +183,21 @@ class _CalendarLayoutSettingState extends State { } class LayoutDateField extends StatelessWidget { - final String fieldId; - final String viewId; - final FieldController fieldController; - final PopoverMutex popoverMutex; - final Function(String fieldId) onUpdated; - const LayoutDateField({ + super.key, required this.fieldId, required this.fieldController, required this.viewId, required this.popoverMutex, required this.onUpdated, - super.key, }); + final String fieldId; + final String viewId; + final FieldController fieldController; + final PopoverMutex popoverMutex; + final Function(String fieldId) onUpdated; + @override Widget build(BuildContext context) { return AppFlowyPopover( @@ -264,8 +239,8 @@ class LayoutDateField extends StatelessWidget { width: 200, child: ListView.separated( shrinkWrap: true, - itemBuilder: (context, index) => items[index], - separatorBuilder: (context, index) => + itemBuilder: (_, index) => items[index], + separatorBuilder: (_, __) => VSpace(GridSize.typeOptionSeparatorHeight), itemCount: items.length, ), @@ -288,21 +263,19 @@ class LayoutDateField extends StatelessWidget { } class ShowWeekNumber extends StatelessWidget { - final bool showWeekNumbers; - final Function(bool showWeekNumbers) onUpdated; - const ShowWeekNumber({ + super.key, required this.showWeekNumbers, required this.onUpdated, - super.key, }); + final bool showWeekNumbers; + final Function(bool showWeekNumbers) onUpdated; + @override Widget build(BuildContext context) { return _toggleItem( - onToggle: (showWeekNumbers) { - onUpdated(!showWeekNumbers); - }, + onToggle: (showWeekNumbers) => onUpdated(!showWeekNumbers), value: showWeekNumbers, text: LocaleKeys.calendar_settings_showWeekNumbers.tr(), ); @@ -310,20 +283,19 @@ class ShowWeekNumber extends StatelessWidget { } class ShowWeekends extends StatelessWidget { - final bool showWeekends; - final Function(bool showWeekends) onUpdated; const ShowWeekends({ super.key, required this.showWeekends, required this.onUpdated, }); + final bool showWeekends; + final Function(bool showWeekends) onUpdated; + @override Widget build(BuildContext context) { return _toggleItem( - onToggle: (showWeekends) { - onUpdated(!showWeekends); - }, + onToggle: (showWeekends) => onUpdated(!showWeekends), value: showWeekends, text: LocaleKeys.calendar_settings_showWeekends.tr(), ); @@ -331,16 +303,17 @@ class ShowWeekends extends StatelessWidget { } class FirstDayOfWeek extends StatelessWidget { - final int firstDayOfWeek; - final PopoverMutex popoverMutex; - final Function(int firstDayOfWeek) onUpdated; const FirstDayOfWeek({ super.key, required this.firstDayOfWeek, - required this.onUpdated, required this.popoverMutex, + required this.onUpdated, }); + final int firstDayOfWeek; + final PopoverMutex popoverMutex; + final Function(int firstDayOfWeek) onUpdated; + @override Widget build(BuildContext context) { return AppFlowyPopover( @@ -370,8 +343,8 @@ class FirstDayOfWeek extends StatelessWidget { width: 100, child: ListView.separated( shrinkWrap: true, - itemBuilder: (context, index) => items[index], - separatorBuilder: (context, index) => + itemBuilder: (_, index) => items[index], + separatorBuilder: (_, __) => VSpace(GridSize.typeOptionSeparatorHeight), itemCount: len, ), @@ -425,18 +398,19 @@ enum CalendarLayoutSettingAction { } class StartFromButton extends StatelessWidget { - final int dayIndex; - final String title; - final bool isSelected; - final void Function(int) onTap; const StartFromButton({ + super.key, required this.title, required this.dayIndex, required this.onTap, required this.isSelected, - super.key, }); + final String title; + final int dayIndex; + final void Function(int) onTap; + final bool isSelected; + @override Widget build(BuildContext context) { return SizedBox( 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 8f0d8f5a1b..ff3e49cb2d 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 @@ -28,7 +28,7 @@ import 'widgets/footer/grid_footer.dart'; import 'widgets/header/grid_header.dart'; import 'widgets/row/row.dart'; import 'widgets/shortcuts.dart'; -import 'widgets/toolbar/mobile_grid_setting.dart'; +import '../../widgets/setting/mobile_database_settings_button.dart'; class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder { final _toggleExtension = ToggleExtensionNotifier(); @@ -49,7 +49,7 @@ class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder { @override Widget settingBar(BuildContext context, DatabaseController controller) { - return MobileGridSettingButton( + return MobileDatabaseSettingsButton( key: _makeValueKey(controller), controller: controller, toggleExtension: _toggleExtension, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart deleted file mode 100644 index cb865b9288..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database_view/application/layout/layout_bloc.dart'; -import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; - -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:styled_widget/styled_widget.dart'; - -import '../../layout/sizes.dart'; - -class DatabaseLayoutList extends StatefulWidget { - final String viewId; - final DatabaseLayoutPB currentLayout; - const DatabaseLayoutList({ - required this.viewId, - required this.currentLayout, - Key? key, - }) : super(key: key); - - @override - State createState() => _DatabaseLayoutListState(); -} - -class _DatabaseLayoutListState extends State { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DatabaseLayoutBloc( - viewId: widget.viewId, - databaseLayout: widget.currentLayout, - )..add(const DatabaseLayoutEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - final cells = DatabaseLayoutPB.values.map((layout) { - final isSelected = state.databaseLayout == layout; - return DatabaseViewLayoutCell( - databaseLayout: layout, - isSelected: isSelected, - onTap: (selectedLayout) { - context - .read() - .add(DatabaseLayoutEvent.updateLayout(selectedLayout)); - }, - ); - }).toList(); - - return ListView.separated( - controller: ScrollController(), - shrinkWrap: true, - itemCount: cells.length, - itemBuilder: (BuildContext context, int index) => cells[index], - separatorBuilder: (BuildContext context, int index) => - VSpace(GridSize.typeOptionSeparatorHeight), - padding: const EdgeInsets.symmetric(vertical: 6.0), - ); - }, - ), - ); - } -} - -class DatabaseViewLayoutCell extends StatelessWidget { - final bool isSelected; - final DatabaseLayoutPB databaseLayout; - final void Function(DatabaseLayoutPB) onTap; - const DatabaseViewLayoutCell({ - required this.databaseLayout, - required this.isSelected, - required this.onTap, - super.key, - }); - - @override - Widget build(BuildContext context) { - Widget? checkmark; - if (isSelected) { - checkmark = const FlowySvg(FlowySvgs.check_s); - } - - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText.medium( - databaseLayout.layoutName, - color: AFThemeExtension.of(context).textColor, - ), - leftIcon: FlowySvg( - databaseLayout.icon, - color: Theme.of(context).iconTheme.color, - ), - rightIcon: checkmark, - onTap: () => onTap(databaseLayout), - ).padding(horizontal: 6.0), - ); - } -} 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 b19bb3e6ea..d95231dcc5 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,6 +1,6 @@ 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/grid/presentation/widgets/toolbar/mobile_grid_setting.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/mobile_database_settings_button.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -34,7 +34,7 @@ class MobileTabBarHeader extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - MobileGridSettingButton( + MobileDatabaseSettingsButton( controller: state .tabBarControllerByViewId[currentView.viewId]! .controller, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/group/database_group.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/group/database_group.dart index 20699de28f..631562d772 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/group/database_group.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/group/database_group.dart @@ -22,15 +22,16 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:protobuf/protobuf.dart' hide FieldInfo; class DatabaseGroupList extends StatelessWidget { - final String viewId; - final DatabaseController databaseController; - final VoidCallback onDismissed; const DatabaseGroupList({ + super.key, required this.viewId, required this.databaseController, required this.onDismissed, - Key? key, - }) : super(key: key); + }); + + final String viewId; + final DatabaseController databaseController; + final VoidCallback onDismissed; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_layout_selector.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_layout_selector.dart new file mode 100644 index 0000000000..226786b550 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_layout_selector.dart @@ -0,0 +1,158 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart'; +import 'package:appflowy/plugins/database_view/application/layout/layout_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; +import 'package:appflowy/util/platform_extension.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; + +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../grid/presentation/layout/sizes.dart'; + +class DatabaseLayoutSelector extends StatefulWidget { + const DatabaseLayoutSelector({ + super.key, + required this.viewId, + required this.currentLayout, + }); + + final String viewId; + final DatabaseLayoutPB currentLayout; + + @override + State createState() => _DatabaseLayoutSelectorState(); +} + +class _DatabaseLayoutSelectorState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DatabaseLayoutBloc( + viewId: widget.viewId, + databaseLayout: widget.currentLayout, + )..add(const DatabaseLayoutEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final cells = DatabaseLayoutPB.values + .map( + (layout) => DatabaseViewLayoutCell( + databaseLayout: layout, + isSelected: state.databaseLayout == layout, + onTap: (selectedLayout) => context + .read() + .add(DatabaseLayoutEvent.updateLayout(selectedLayout)), + ), + ) + .toList(); + + return ListView.separated( + controller: ScrollController(), + shrinkWrap: true, + itemCount: cells.length, + padding: const EdgeInsets.symmetric(vertical: 6.0), + itemBuilder: (_, int index) => cells[index], + separatorBuilder: (_, __) => + VSpace(GridSize.typeOptionSeparatorHeight), + ); + }, + ), + ); + } +} + +class DatabaseViewLayoutCell extends StatelessWidget { + const DatabaseViewLayoutCell({ + super.key, + required this.isSelected, + required this.databaseLayout, + required this.onTap, + }); + + final bool isSelected; + final DatabaseLayoutPB databaseLayout; + final void Function(DatabaseLayoutPB) onTap; + + @override + Widget build(BuildContext context) { + if (PlatformExtension.isMobile) { + return MobileDatabaseViewLayoutCell( + isSelected: isSelected, + databaseLayout: databaseLayout, + onTap: onTap, + ); + } + + return DesktopDatabaseViewLayoutCell( + isSelected: isSelected, + databaseLayout: databaseLayout, + onTap: onTap, + ); + } +} + +class DesktopDatabaseViewLayoutCell extends StatelessWidget { + const DesktopDatabaseViewLayoutCell({ + super.key, + required this.isSelected, + required this.databaseLayout, + required this.onTap, + }); + + final bool isSelected; + final DatabaseLayoutPB databaseLayout; + final void Function(DatabaseLayoutPB) onTap; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + databaseLayout.layoutName, + color: AFThemeExtension.of(context).textColor, + ), + leftIcon: FlowySvg( + databaseLayout.icon, + color: Theme.of(context).iconTheme.color, + ), + rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null, + onTap: () => onTap(databaseLayout), + ), + ), + ); + } +} + +class MobileDatabaseViewLayoutCell extends StatelessWidget { + const MobileDatabaseViewLayoutCell({ + super.key, + required this.isSelected, + required this.databaseLayout, + required this.onTap, + }); + + final bool isSelected; + final DatabaseLayoutPB databaseLayout; + final void Function(DatabaseLayoutPB) onTap; + + @override + Widget build(BuildContext context) { + return MobileSettingItem( + padding: EdgeInsets.zero, + name: databaseLayout.layoutName, + trailing: isSelected ? const FlowySvg(FlowySvgs.check_s) : null, + leadingIcon: FlowySvg( + databaseLayout.icon, + color: Theme.of(context).iconTheme.color, + size: const Size.square(18), + ), + onTap: () => onTap(databaseLayout), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting_action.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting_action.dart new file mode 100644 index 0000000000..8102967502 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting_action.dart @@ -0,0 +1,198 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_item_widget.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/calendar/presentation/toolbar/calendar_layout_setting.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/database_layout_selector.dart'; +import 'package:appflowy/plugins/database_view/widgets/group/database_group.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/mobile_calendar_settings.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart'; +import 'package:appflowy/util/platform_extension.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.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:flutter/material.dart'; + +enum DatabaseSettingAction { + showProperties, + showLayout, + showGroup, + showCalendarLayout, +} + +extension DatabaseSettingActionExtension on DatabaseSettingAction { + FlowySvgData iconData() { + switch (this) { + case DatabaseSettingAction.showProperties: + return FlowySvgs.properties_s; + case DatabaseSettingAction.showLayout: + return FlowySvgs.database_layout_m; + case DatabaseSettingAction.showGroup: + return FlowySvgs.group_s; + case DatabaseSettingAction.showCalendarLayout: + return FlowySvgs.calendar_layout_m; + } + } + + String title() { + switch (this) { + case DatabaseSettingAction.showProperties: + return LocaleKeys.grid_settings_properties.tr(); + case DatabaseSettingAction.showLayout: + return LocaleKeys.grid_settings_databaseLayout.tr(); + case DatabaseSettingAction.showGroup: + return LocaleKeys.grid_settings_group.tr(); + case DatabaseSettingAction.showCalendarLayout: + return LocaleKeys.calendar_settings_name.tr(); + } + } + + Widget build( + BuildContext context, + DatabaseController databaseController, + PopoverMutex popoverMutex, + ) { + final popover = switch (this) { + DatabaseSettingAction.showLayout => DatabaseLayoutSelector( + viewId: databaseController.viewId, + currentLayout: databaseController.databaseLayout, + ), + DatabaseSettingAction.showGroup => DatabaseGroupList( + viewId: databaseController.viewId, + databaseController: databaseController, + onDismissed: () {}, + ), + DatabaseSettingAction.showProperties => DatabasePropertyList( + viewId: databaseController.viewId, + fieldController: databaseController.fieldController, + ), + DatabaseSettingAction.showCalendarLayout => CalendarLayoutSetting( + viewId: databaseController.viewId, + fieldController: databaseController.fieldController, + calendarSettingController: ICalendarSettingImpl( + databaseController, + ), + ), + }; + + return AppFlowyPopover( + triggerActions: PlatformExtension.isMobile + ? PopoverTriggerFlags.none + : PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + direction: PopoverDirection.leftWithTopAligned, + mutex: popoverMutex, + margin: EdgeInsets.zero, + offset: const Offset(-14, 0), + child: PlatformExtension.isMobile + ? MobileSettingItem( + name: title(), + trailing: _trailingFromSetting( + context, + databaseController.databaseLayout, + ), + leadingIcon: FlowySvg( + iconData(), + size: const Size.square(18), + color: Theme.of(context).iconTheme.color, + ), + onTap: _actionFromSetting(context, databaseController), + ) + : SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + onTap: null, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + title(), + color: AFThemeExtension.of(context).textColor, + ), + leftIcon: FlowySvg( + iconData(), + color: Theme.of(context).iconTheme.color, + ), + ), + ), + popupBuilder: (context) => popover, + ); + } + + VoidCallback? _actionFromSetting( + BuildContext context, + DatabaseController databaseController, + ) => + switch (this) { + DatabaseSettingAction.showLayout => () => + _showLayoutSettings(context, databaseController), + DatabaseSettingAction.showProperties => () => + _showPropertiesSettings(context, databaseController), + DatabaseSettingAction.showCalendarLayout => () => + _showCalendarSettings(context, databaseController), + // Group Settings + _ => null, + }; + + void _showLayoutSettings( + BuildContext context, + DatabaseController databaseController, + ) => + FlowyBottomSheetController.of(context)!.push( + SheetPage( + title: LocaleKeys.settings_mobile_selectLayout.tr(), + body: DatabaseLayoutSelector( + viewId: databaseController.viewId, + currentLayout: databaseController.databaseLayout, + ), + ), + ); + + void _showPropertiesSettings( + BuildContext context, + DatabaseController databaseController, + ) => + FlowyBottomSheetController.of(context)!.push( + SheetPage( + title: LocaleKeys.grid_settings_properties.tr(), + body: DatabasePropertyList( + viewId: databaseController.viewId, + fieldController: databaseController.fieldController, + ), + ), + ); + + void _showCalendarSettings( + BuildContext context, + DatabaseController databaseController, + ) => + FlowyBottomSheetController.of(context)!.push( + SheetPage( + title: LocaleKeys.calendar_settings_name.tr(), + body: MobileCalendarLayoutSetting( + viewId: databaseController.viewId, + fieldController: databaseController.fieldController, + calendarSettingController: ICalendarSettingImpl( + databaseController, + ), + ), + ), + ); + + Widget? _trailingFromSetting(BuildContext context, DatabaseLayoutPB layout) => + switch (this) { + DatabaseSettingAction.showLayout => Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyText( + layout.name, + color: Theme.of(context).colorScheme.onSurface, + ), + const Icon(Icons.chevron_right), + ], + ), + _ => null, + }; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_settings_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_settings_list.dart new file mode 100644 index 0000000000..d5f00c1505 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_settings_list.dart @@ -0,0 +1,76 @@ +import 'package:flutter/widgets.dart'; + +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/database_setting_action.dart'; +import 'package:appflowy/util/platform_extension.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; + +class DatabaseSettingsList extends StatefulWidget { + const DatabaseSettingsList({ + super.key, + required this.databaseController, + }); + + final DatabaseController databaseController; + + @override + State createState() => _DatabaseSettingsListState(); +} + +class _DatabaseSettingsListState extends State { + late final PopoverMutex popoverMutex = PopoverMutex(); + + @override + Widget build(BuildContext context) { + final cells = + actionsForDatabaseLayout(widget.databaseController.databaseLayout) + .map( + (action) => action.build( + context, + widget.databaseController, + popoverMutex, + ), + ) + .toList(); + + return ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + controller: ScrollController(), + itemCount: cells.length, + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) => cells[index], + ); + } +} + +/// Returns the list of actions that should be shown for the given database layout. +List actionsForDatabaseLayout(DatabaseLayoutPB? layout) { + switch (layout) { + case DatabaseLayoutPB.Board: + return [ + DatabaseSettingAction.showProperties, + DatabaseSettingAction.showLayout, + if (!PlatformExtension.isMobile) DatabaseSettingAction.showGroup, + ]; + case DatabaseLayoutPB.Calendar: + return [ + DatabaseSettingAction.showProperties, + DatabaseSettingAction.showLayout, + DatabaseSettingAction.showCalendarLayout, + ]; + case DatabaseLayoutPB.Grid: + return [ + DatabaseSettingAction.showProperties, + DatabaseSettingAction.showLayout, + ]; + default: + return []; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/field_visibility_extension.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/field_visibility_extension.dart new file mode 100644 index 0000000000..9fe9061205 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/field_visibility_extension.dart @@ -0,0 +1,16 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart'; + +extension ToggleVisibility on FieldVisibility { + FieldVisibility toggle() => switch (this) { + FieldVisibility.AlwaysShown => FieldVisibility.AlwaysHidden, + FieldVisibility.AlwaysHidden => FieldVisibility.AlwaysShown, + _ => FieldVisibility.AlwaysHidden, + }; + + bool isVisibleState() => switch (this) { + FieldVisibility.AlwaysShown => true, + FieldVisibility.HideWhenEmpty => true, + FieldVisibility.AlwaysHidden => false, + _ => false, + }; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_calendar_settings.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_calendar_settings.dart new file mode 100644 index 0000000000..e89c74c813 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_calendar_settings.dart @@ -0,0 +1,349 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/presentation.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_paginated_bottom_sheet.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; +import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart'; +import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:protobuf/protobuf.dart' hide FieldInfo; + +class MobileCalendarLayoutSetting extends StatefulWidget { + const MobileCalendarLayoutSetting({ + super.key, + required this.viewId, + required this.fieldController, + required this.calendarSettingController, + }); + + final String viewId; + final FieldController fieldController; + final ICalendarSetting calendarSettingController; + + @override + State createState() => + _MobileCalendarLayoutSettingState(); +} + +class _MobileCalendarLayoutSettingState + extends State { + late final PopoverMutex popoverMutex = PopoverMutex(); + late final CalendarSettingBloc calendarSettingBloc; + + @override + void initState() { + super.initState(); + calendarSettingBloc = CalendarSettingBloc( + viewId: widget.viewId, + layoutSettings: widget.calendarSettingController.getLayoutSetting(), + )..add(const CalendarSettingEvent.init()); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: calendarSettingBloc, + child: BlocBuilder( + builder: (context, state) { + final CalendarLayoutSettingPB? settings = state.layoutSetting + .foldLeft(null, (previous, settings) => settings); + + if (settings == null) { + return const CircularProgressIndicator(); + } + + final availableSettings = _availableCalendarSettings(settings); + final items = availableSettings.map((setting) { + switch (setting) { + case CalendarLayoutSettingAction.firstDayOfWeek: + return MobileFirstDayOfWeekSetting( + selectedDay: settings.firstDayOfWeek, + onUpdated: (firstDayOfWeek) => _updateLayoutSettings( + context, + firstDayOfWeek: firstDayOfWeek, + ), + ); + case CalendarLayoutSettingAction.layoutField: + return MobileLayoutDateField( + fieldController: widget.fieldController, + viewId: widget.viewId, + fieldId: settings.fieldId, + popoverMutex: popoverMutex, + onUpdated: (fieldId) => _updateLayoutSettings( + context, + layoutFieldId: fieldId, + ), + ); + default: + return const SizedBox.shrink(); + } + }).toList(); + + return ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: items.length, + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + physics: StyledScrollPhysics(), + itemBuilder: (_, int index) => items[index], + padding: const EdgeInsets.all(6.0), + ); + }, + ), + ); + } + + List _availableCalendarSettings( + CalendarLayoutSettingPB layoutSettings, + ) { + final List settings = [ + CalendarLayoutSettingAction.layoutField, + ]; + + switch (layoutSettings.layoutTy) { + case CalendarLayoutPB.DayLayout: + break; + case CalendarLayoutPB.MonthLayout: + case CalendarLayoutPB.WeekLayout: + settings.add(CalendarLayoutSettingAction.firstDayOfWeek); + break; + } + + return settings; + } + + void _updateLayoutSettings( + BuildContext context, { + bool? showWeekends, + bool? showWeekNumbers, + int? firstDayOfWeek, + String? layoutFieldId, + }) { + CalendarLayoutSettingPB setting = calendarSettingBloc.state.layoutSetting + .foldLeft(null, (previous, settings) => settings)!; + setting.freeze(); + setting = setting.rebuild((setting) { + if (showWeekends != null) { + setting.showWeekends = !showWeekends; + } + if (showWeekNumbers != null) { + setting.showWeekNumbers = !showWeekNumbers; + } + if (firstDayOfWeek != null) { + setting.firstDayOfWeek = firstDayOfWeek; + } + if (layoutFieldId != null) { + setting.fieldId = layoutFieldId; + } + }); + + calendarSettingBloc.add(CalendarSettingEvent.updateLayoutSetting(setting)); + widget.calendarSettingController.updateLayoutSettings(setting); + } +} + +class MobileLayoutDateField extends StatelessWidget { + const MobileLayoutDateField({ + super.key, + required this.fieldId, + required this.fieldController, + required this.viewId, + required this.popoverMutex, + required this.onUpdated, + }); + + final String fieldId; + final String viewId; + final FieldController fieldController; + final PopoverMutex popoverMutex; + final Function(String fieldId) onUpdated; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DatabasePropertyBloc( + viewId: viewId, + fieldController: fieldController, + )..add(const DatabasePropertyEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final items = state.fieldContexts + .where((field) => field.fieldType == FieldType.DateTime) + .toList(); + final selected = items.firstWhere((field) => field.id == fieldId); + + return MobileSettingItem( + padding: EdgeInsets.zero, + name: LocaleKeys.calendar_settings_layoutDateField.tr(), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: FlowyText( + selected.name, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const Icon(Icons.chevron_right), + ], + ), + onTap: () => _showSelector(context, items), + ); + }, + ), + ); + } + + void _showSelector(BuildContext context, List items) => + FlowyBottomSheetController.of(context)!.push( + SheetPage( + title: LocaleKeys.settings_mobile_selectStartingDay.tr(), + body: MobileCalendarLayoutSelector( + fieldId: fieldId, + items: items, + onUpdated: onUpdated, + ), + ), + ); +} + +class MobileCalendarLayoutSelector extends StatefulWidget { + const MobileCalendarLayoutSelector({ + super.key, + required this.fieldId, + required this.items, + required this.onUpdated, + }); + + final String fieldId; + final List items; + final Function(String fieldId) onUpdated; + + @override + State createState() => + _MobileCalendarLayoutSelectorState(); +} + +class _MobileCalendarLayoutSelectorState + extends State { + late String _selectedField = widget.fieldId; + + @override + Widget build(BuildContext context) { + return ListView.separated( + shrinkWrap: true, + itemCount: widget.items.length, + separatorBuilder: (_, __) => const VSpace(4), + itemBuilder: (_, index) => MobileSettingItem( + name: widget.items[index].name, + trailing: _selectedField == widget.items[index].id + ? const FlowySvg(FlowySvgs.check_s) + : null, + onTap: () { + final selected = widget.items[index].id; + widget.onUpdated(selected); + setState(() => _selectedField = selected); + }, + ), + ); + } +} + +const weekdayOptions = 2; + +class MobileFirstDayOfWeekSetting extends StatelessWidget { + const MobileFirstDayOfWeekSetting({ + super.key, + required this.selectedDay, + required this.onUpdated, + }); + + final int selectedDay; + final Function(int firstDayOfWeek) onUpdated; + + @override + Widget build(BuildContext context) { + final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols; + final weekdays = symbols.WEEKDAYS.take(weekdayOptions).toList(); + + return MobileSettingItem( + padding: EdgeInsets.zero, + name: LocaleKeys.calendar_settings_firstDayOfWeek.tr(), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: FlowyText( + weekdays[selectedDay], + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const Icon(Icons.chevron_right), + ], + ), + onTap: () => _showSelector(context), + ); + } + + void _showSelector(BuildContext context) => + FlowyBottomSheetController.of(context)!.push( + SheetPage( + title: LocaleKeys.calendar_settings_layoutDateField.tr(), + body: MobileFirstDayOfWeekSelector( + selectedDay: selectedDay, + onUpdated: onUpdated, + ), + ), + ); +} + +class MobileFirstDayOfWeekSelector extends StatefulWidget { + const MobileFirstDayOfWeekSelector({ + super.key, + required this.selectedDay, + required this.onUpdated, + }); + + final int selectedDay; + final Function(int firstDayOfWeek) onUpdated; + + @override + State createState() => + _MobileFirstDayOfWeekSelectorState(); +} + +class _MobileFirstDayOfWeekSelectorState + extends State { + late int _selectedDay = widget.selectedDay; + + @override + Widget build(BuildContext context) { + final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols; + final weekdays = symbols.WEEKDAYS.take(weekdayOptions).toList(); + + return ListView.separated( + shrinkWrap: true, + itemCount: weekdayOptions, + separatorBuilder: (_, __) => const VSpace(4), + itemBuilder: (_, index) => MobileSettingItem( + name: weekdays[index], + trailing: + _selectedDay == index ? const FlowySvg(FlowySvgs.check_s) : null, + onTap: () { + widget.onUpdated(index); + setState(() => _selectedDay = index); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_database_property_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_database_property_editor.dart new file mode 100644 index 0000000000..dd36c6b42e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_database_property_editor.dart @@ -0,0 +1,122 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/mobile_field_name_text_field.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_property_edit/mobile_field_type_option_editor.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_property_edit/widgets/property_title.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/application/setting/property_bloc.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/field_settings_entities.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileDatabasePropertyEditor extends StatefulWidget { + const MobileDatabasePropertyEditor({ + super.key, + required this.viewId, + required this.fieldInfo, + required this.fieldController, + required this.bloc, + }); + + final String viewId; + final FieldInfo fieldInfo; + final FieldController fieldController; + final DatabasePropertyBloc bloc; + + @override + State createState() => + _MobileDatabasePropertyEditorState(); +} + +class _MobileDatabasePropertyEditorState + extends State { + late FieldVisibility _visibility = + widget.fieldInfo.visibility ?? FieldVisibility.AlwaysShown; + + @override + Widget build(BuildContext context) { + final typeOptionLoader = FieldTypeOptionLoader( + viewId: widget.viewId, + field: widget.fieldInfo.field, + ); + + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: widget.bloc), + BlocProvider( + create: (context) => FieldEditorBloc( + viewId: widget.viewId, + loader: typeOptionLoader, + field: widget.fieldInfo.field, + fieldController: widget.fieldController, + )..add(const FieldEditorEvent.initial()), + ), + ], + child: BlocBuilder( + builder: (context, _) { + return BlocBuilder( + builder: (context, state) { + final dataController = + context.read().typeOptionController; + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // TODO(yijing): improve hint text + PropertyTitle(LocaleKeys.settings_user_name.tr()), + BlocSelector( + selector: (state) => state.field.name, + builder: (context, fieldName) => MobileFieldNameTextField( + text: fieldName, + ), + ), + Row( + children: [ + Expanded( + child: PropertyTitle( + LocaleKeys.grid_field_visibility.tr(), + ), + ), + Toggle( + padding: EdgeInsets.zero, + value: _visibility.isVisibleState(), + style: ToggleStyle.mobile, + onChanged: (newValue) { + final newVisibility = _visibility.toggle(); + + context.read().add( + DatabasePropertyEvent.setFieldVisibility( + widget.fieldInfo.id, + newVisibility, + ), + ); + + setState(() => _visibility = newVisibility); + }, + ), + ], + ), + const VSpace(8), + if (!typeOptionLoader.field.isPrimary) + MobileFieldTypeOptionEditor( + dataController: dataController, + ), + ], + ), + ); + }, + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/mobile_grid_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_database_settings_button.dart similarity index 74% rename from frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/mobile_grid_setting.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_database_settings_button.dart index 5d0bda4d13..f8e3fdc905 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/mobile_grid_setting.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/mobile_database_settings_button.dart @@ -1,13 +1,17 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.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:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class MobileGridSettingButton extends StatelessWidget { - const MobileGridSettingButton({ +class MobileDatabaseSettingsButton extends StatelessWidget { + const MobileDatabaseSettingsButton({ super.key, required this.controller, required this.toggleExtension, @@ -56,8 +60,7 @@ class MobileGridSettingButton extends StatelessWidget { width: 24, child: IconButton( padding: EdgeInsets.zero, - // TODO(Xazin): Database Settings - onPressed: () {}, + onPressed: () => _showMobileSettings(context, controller), icon: const FlowySvg( FlowySvgs.m_setting_m, size: Size.square(24), @@ -69,4 +72,18 @@ class MobileGridSettingButton extends StatelessWidget { ), ); } + + void _showMobileSettings( + BuildContext context, + DatabaseController controller, + ) => + showPaginatedBottomSheet( + context, + page: SheetPage( + title: LocaleKeys.settings_title.tr(), + body: DatabaseSettingsList( + databaseController: controller, + ), + ), + ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart index f1bfc48767..6c9a70300b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart @@ -1,27 +1,24 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart'; -import 'package:appflowy/plugins/database_view/widgets/group/database_group.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/database_settings_list.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import '../../grid/presentation/layout/sizes.dart'; -import '../../grid/presentation/widgets/toolbar/grid_layout.dart'; -import 'setting_property_list.dart'; class SettingButton extends StatefulWidget { - final DatabaseController databaseController; const SettingButton({ + super.key, required this.databaseController, - Key? key, - }) : super(key: key); + }); + + final DatabaseController databaseController; @override State createState() => _SettingButtonState(); @@ -47,193 +44,27 @@ class _SettingButtonState extends State { hoverColor: AFThemeExtension.of(context).lightGreyHover, padding: GridSize.toolbarSettingButtonInsets, radius: Corners.s4Border, - onPressed: () => _popoverController.show(), + onPressed: _popoverController.show, + ), + popupBuilder: (BuildContext context) => DatabaseSettingsList( + databaseController: widget.databaseController, ), - popupBuilder: (BuildContext context) { - return DatabaseSettingListPopover( - databaseController: widget.databaseController, - ); - }, - ); - } -} - -class DatabaseSettingListPopover extends StatefulWidget { - final DatabaseController databaseController; - - const DatabaseSettingListPopover({ - required this.databaseController, - Key? key, - }) : super(key: key); - - @override - State createState() => _DatabaseSettingListPopoverState(); -} - -class _DatabaseSettingListPopoverState - extends State { - late final PopoverMutex popoverMutex; - - @override - void initState() { - super.initState(); - popoverMutex = PopoverMutex(); - } - - @override - Widget build(BuildContext context) { - final cells = - actionsForDatabaseLayout(widget.databaseController.databaseLayout) - .map( - (action) => action.build( - context, - widget.databaseController, - popoverMutex, - ), - ) - .toList(); - - return ListView.separated( - shrinkWrap: true, - padding: EdgeInsets.zero, - controller: ScrollController(), - itemCount: cells.length, - separatorBuilder: (context, index) => - VSpace(GridSize.typeOptionSeparatorHeight), - physics: StyledScrollPhysics(), - itemBuilder: (BuildContext context, int index) { - return cells[index]; - }, ); } } class ICalendarSettingImpl extends ICalendarSetting { + const ICalendarSettingImpl(this._databaseController); + final DatabaseController _databaseController; - ICalendarSettingImpl(this._databaseController); + @override + void updateLayoutSettings(CalendarLayoutSettingPB layoutSettings) => + _databaseController.updateLayoutSetting( + calendarLayoutSetting: layoutSettings, + ); @override - void updateLayoutSettings(CalendarLayoutSettingPB layoutSettings) { - _databaseController.updateLayoutSetting( - calendarLayoutSetting: layoutSettings, - ); - } - - @override - CalendarLayoutSettingPB? getLayoutSetting() { - return _databaseController.databaseLayoutSetting?.calendar; - } -} - -enum DatabaseSettingAction { - showProperties, - showLayout, - showGroup, - showCalendarLayout, -} - -extension DatabaseSettingActionExtension on DatabaseSettingAction { - FlowySvgData iconData() { - switch (this) { - case DatabaseSettingAction.showProperties: - return FlowySvgs.properties_s; - case DatabaseSettingAction.showLayout: - return FlowySvgs.database_layout_m; - case DatabaseSettingAction.showGroup: - return FlowySvgs.group_s; - case DatabaseSettingAction.showCalendarLayout: - return FlowySvgs.calendar_layout_m; - } - } - - String title() { - switch (this) { - case DatabaseSettingAction.showProperties: - return LocaleKeys.grid_settings_properties.tr(); - case DatabaseSettingAction.showLayout: - return LocaleKeys.grid_settings_databaseLayout.tr(); - case DatabaseSettingAction.showGroup: - return LocaleKeys.grid_settings_group.tr(); - case DatabaseSettingAction.showCalendarLayout: - return LocaleKeys.calendar_settings_name.tr(); - } - } - - Widget build( - BuildContext context, - DatabaseController databaseController, - PopoverMutex popoverMutex, - ) { - final popover = switch (this) { - DatabaseSettingAction.showLayout => DatabaseLayoutList( - viewId: databaseController.viewId, - currentLayout: databaseController.databaseLayout, - ), - DatabaseSettingAction.showGroup => DatabaseGroupList( - viewId: databaseController.viewId, - databaseController: databaseController, - onDismissed: () {}, - ), - DatabaseSettingAction.showProperties => DatabasePropertyList( - viewId: databaseController.viewId, - fieldController: databaseController.fieldController, - ), - DatabaseSettingAction.showCalendarLayout => CalendarLayoutSetting( - viewId: databaseController.viewId, - fieldController: databaseController.fieldController, - calendarSettingController: ICalendarSettingImpl( - databaseController, - ), - ), - }; - - return AppFlowyPopover( - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - direction: PopoverDirection.leftWithTopAligned, - mutex: popoverMutex, - margin: EdgeInsets.zero, - offset: const Offset(-14, 0), - child: SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText.medium( - title(), - color: AFThemeExtension.of(context).textColor, - ), - leftIcon: FlowySvg( - iconData(), - color: Theme.of(context).iconTheme.color, - ), - ), - ), - popupBuilder: (context) => popover, - ); - } -} - -/// Returns the list of actions that should be shown for the given database layout. -List actionsForDatabaseLayout(DatabaseLayoutPB? layout) { - switch (layout) { - case DatabaseLayoutPB.Board: - return [ - DatabaseSettingAction.showProperties, - DatabaseSettingAction.showLayout, - DatabaseSettingAction.showGroup, - ]; - case DatabaseLayoutPB.Calendar: - return [ - DatabaseSettingAction.showProperties, - DatabaseSettingAction.showLayout, - DatabaseSettingAction.showCalendarLayout, - ]; - case DatabaseLayoutPB.Grid: - return [ - DatabaseSettingAction.showProperties, - DatabaseSettingAction.showLayout, - ]; - default: - return []; - } + CalendarLayoutSettingPB? getLayoutSetting() => + _databaseController.databaseLayoutSetting?.calendar; } 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 17e3e97e2e..9f6c686167 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,15 +1,23 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_paginated_bottom_sheet.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/layout/sizes.dart'; 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/plugins/database_view/widgets/setting/mobile_database_property_editor.dart'; +import 'package:appflowy/util/platform_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_popover/appflowy_popover.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:flutter/material.dart'; @@ -32,26 +40,45 @@ class DatabasePropertyList extends StatefulWidget { class _DatabasePropertyListState extends State { final PopoverMutex _popoverMutex = PopoverMutex(); + late final DatabasePropertyBloc _bloc; + + @override + void initState() { + super.initState(); + _bloc = DatabasePropertyBloc( + viewId: widget.viewId, + fieldController: widget.fieldController, + )..add(const DatabasePropertyEvent.initial()); + } @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DatabasePropertyBloc( - viewId: widget.viewId, - fieldController: widget.fieldController, - )..add(const DatabasePropertyEvent.initial()), + return BlocProvider.value( + value: _bloc, child: BlocBuilder( builder: (context, state) { - final cells = state.fieldContexts.mapIndexed((index, field) { - return DatabasePropertyCell( - key: ValueKey(field.id), - viewId: widget.viewId, - fieldController: widget.fieldController, - fieldInfo: field, - popoverMutex: _popoverMutex, - index: index, + final cells = state.fieldContexts + .mapIndexed( + (index, field) => DatabasePropertyCell( + key: ValueKey(field.id), + viewId: widget.viewId, + fieldController: widget.fieldController, + 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), ); - }).toList(); + } return ReorderableListView( proxyDecorator: (child, index, _) => Material( @@ -90,13 +117,7 @@ class _DatabasePropertyListState extends State { } @visibleForTesting -class DatabasePropertyCell extends StatefulWidget { - final FieldController fieldController; - final FieldInfo fieldInfo; - final String viewId; - final PopoverMutex popoverMutex; - final int index; - +class DatabasePropertyCell extends StatelessWidget { const DatabasePropertyCell({ super.key, required this.fieldInfo, @@ -104,13 +125,145 @@ class DatabasePropertyCell extends StatefulWidget { required this.popoverMutex, required this.index, required this.fieldController, + required this.bloc, }); + final FieldInfo fieldInfo; + final String viewId; + final PopoverMutex popoverMutex; + final int index; + final FieldController fieldController; + final DatabasePropertyBloc bloc; + @override - State createState() => _DatabasePropertyCellState(); + Widget build(BuildContext context) { + if (PlatformExtension.isMobile) { + return MobileDatabasePropertyCell( + fieldInfo: fieldInfo, + viewId: viewId, + fieldController: fieldController, + bloc: bloc, + ); + } + + return DesktopDatabasePropertyCell( + fieldInfo: fieldInfo, + viewId: viewId, + popoverMutex: popoverMutex, + index: index, + fieldController: fieldController, + ); + } } -class _DatabasePropertyCellState extends State { +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: () => FlowyBottomSheetController.of(context)!.push( + SheetPage( + title: LocaleKeys.grid_field_editProperty.tr(), + body: MobileDatabasePropertyEditor( + viewId: widget.viewId, + fieldInfo: widget.fieldInfo, + fieldController: widget.fieldController, + bloc: widget.bloc, + ), + ), + ), + 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 { final PopoverController _popoverController = PopoverController(); @override @@ -173,9 +326,8 @@ class _DatabasePropertyCellState extends State { return; } - final newVisiblity = _newFieldVisibility( - widget.fieldInfo.fieldSettings!.visibility, - ); + final newVisiblity = + widget.fieldInfo.fieldSettings!.visibility.toggle(); context.read().add( DatabasePropertyEvent.setFieldVisibility( widget.fieldInfo.id, @@ -197,12 +349,4 @@ class _DatabasePropertyCellState extends State { }, ); } - - FieldVisibility _newFieldVisibility(FieldVisibility current) { - return switch (current) { - FieldVisibility.AlwaysShown => FieldVisibility.AlwaysHidden, - FieldVisibility.AlwaysHidden => FieldVisibility.AlwaysShown, - _ => FieldVisibility.AlwaysHidden, - }; - } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart index 80335c66fe..a81db54d89 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart @@ -12,7 +12,7 @@ class Toggle extends StatelessWidget { final EdgeInsets padding; const Toggle({ - Key? key, + super.key, required this.value, required this.onChanged, required this.style, @@ -20,7 +20,7 @@ class Toggle extends StatelessWidget { this.activeBackgroundColor, this.inactiveBackgroundColor, this.padding = const EdgeInsets.all(8.0), - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -28,7 +28,7 @@ class Toggle extends StatelessWidget { ? activeBackgroundColor ?? Theme.of(context).colorScheme.primary : activeBackgroundColor ?? AFThemeExtension.of(context).toggleOffFill; return GestureDetector( - onTap: (() => onChanged(value)), + onTap: () => onChanged(value), child: Padding( padding: padding, child: Stack( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle_style.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle_style.dart index 62664d83c0..165f0ad7bc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle_style.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle_style.dart @@ -15,4 +15,7 @@ class ToggleStyle { static ToggleStyle get small => ToggleStyle(height: 10, width: 16, thumbRadius: 8); + + static ToggleStyle get mobile => + ToggleStyle(height: 24, width: 42, thumbRadius: 18); } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index bb64bbf5e4..d6ba54d815 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -424,7 +424,9 @@ "privacyPolicy": "Privacy Policy", "userAgreement": "User Agreement", "userprofileError": "Failed to load user profile", - "userprofileErrorDescription": "Please try to log out and log back in to check if the issue still persists." + "userprofileErrorDescription": "Please try to log out and log back in to check if the issue still persists.", + "selectLayout": "Select layout", + "selectStartingDay": "Select starting day" } }, "grid": { @@ -852,7 +854,7 @@ "other": "{} unscheduled events" }, "clickToAdd": "Click to add to the calendar", - "name": "Calendar layout" + "name": "Calendar settings" }, "referencedCalendarPrefix": "View of", "quickJumpYear": "Jump to"