feat: mobile database settings rework (#4016)

* feat: mobile database settings rework

* chore: clean
This commit is contained in:
Mathias Mogensen 2023-11-27 06:36:10 +02:00 committed by GitHub
parent 929508df16
commit 771dd9979f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1517 additions and 499 deletions

View File

@ -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_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_menu.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/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/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_add_button.dart';
import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_header.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_detail.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_document.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/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_button.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
@ -1138,7 +1140,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Should call [tapDatabaseSettingButton] first. /// Should call [tapDatabaseSettingButton] first.
Future<void> tapViewPropertiesButton() async { Future<void> tapViewPropertiesButton() async {
final findSettingItem = find.byType(DatabaseSettingListPopover); final findSettingItem = find.byType(DatabaseSettingsList);
final findLayoutButton = find.byWidgetPredicate( final findLayoutButton = find.byWidgetPredicate(
(widget) => (widget) =>
widget is FlowyText && widget is FlowyText &&
@ -1155,7 +1157,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Should call [tapDatabaseSettingButton] first. /// Should call [tapDatabaseSettingButton] first.
Future<void> tapDatabaseLayoutButton() async { Future<void> tapDatabaseLayoutButton() async {
final findSettingItem = find.byType(DatabaseSettingListPopover); final findSettingItem = find.byType(DatabaseSettingsList);
final findLayoutButton = find.byWidgetPredicate( final findLayoutButton = find.byWidgetPredicate(
(widget) => (widget) =>
widget is FlowyText && widget is FlowyText &&
@ -1171,7 +1173,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
} }
Future<void> tapCalendarLayoutSettingButton() async { Future<void> tapCalendarLayoutSettingButton() async {
final findSettingItem = find.byType(DatabaseSettingListPopover); final findSettingItem = find.byType(DatabaseSettingsList);
final findLayoutButton = find.byWidgetPredicate( final findLayoutButton = find.byWidgetPredicate(
(widget) => (widget) =>
widget is FlowyText && widget is FlowyText &&
@ -1593,7 +1595,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
) async { ) async {
final field = find.byWidgetPredicate( final field = find.byWidgetPredicate(
(widget) => (widget) =>
widget is DatabasePropertyCell && widget.fieldInfo.name == fieldName, widget is DesktopDatabasePropertyCell &&
widget.fieldInfo.name == fieldName,
); );
final toggleVisibilityButton = final toggleVisibilityButton =
find.descendant(of: field, matching: find.byType(FlowyIconButton)); find.descendant(of: field, matching: find.byType(FlowyIconButton));

View File

@ -158,9 +158,7 @@ class MobileDBFieldBottomSheetBody extends StatelessWidget {
BottomSheetActionWidget( BottomSheetActionWidget(
svg: FlowySvgs.date_s, svg: FlowySvgs.date_s,
text: LocaleKeys.grid_field_editProperty.tr(), text: LocaleKeys.grid_field_editProperty.tr(),
onTap: () { onTap: () => onAction(MobileDBBottomSheetGeneralAction.typeOption),
onAction(MobileDBBottomSheetGeneralAction.typeOption);
},
), ),
const VSpace(8), const VSpace(8),
Row( Row(
@ -170,9 +168,8 @@ class MobileDBFieldBottomSheetBody extends StatelessWidget {
child: BottomSheetActionWidget( child: BottomSheetActionWidget(
svg: FlowySvgs.hide_m, svg: FlowySvgs.hide_m,
text: LocaleKeys.grid_field_hide.tr(), text: LocaleKeys.grid_field_hide.tr(),
onTap: () { onTap: () =>
onAction(MobileDBBottomSheetGeneralAction.toggleVisibility); onAction(MobileDBBottomSheetGeneralAction.toggleVisibility),
},
), ),
), ),
const HSpace(8), const HSpace(8),

View File

@ -10,11 +10,12 @@ import 'package:go_router/go_router.dart';
Future<void> showMobileBottomSheet({ Future<void> showMobileBottomSheet({
required BuildContext context, required BuildContext context,
required WidgetBuilder builder, required WidgetBuilder builder,
bool isDragEnabled = true,
}) async { }) async {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
enableDrag: true, enableDrag: isDragEnabled,
useSafeArea: true, useSafeArea: true,
builder: builder, builder: builder,
); );

View File

@ -1,7 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart'; 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/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/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:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -53,10 +52,6 @@ class _MobileCreateRowFieldScreenState
), ),
body: MobileFieldEditor( body: MobileFieldEditor(
viewId: widget.viewId, viewId: widget.viewId,
typeOptionLoader: FieldTypeOptionLoader(
viewId: widget.viewId,
field: widget.typeOption.field_2,
),
fieldController: widget.fieldController, fieldController: widget.fieldController,
field: widget.typeOption.field_2, field: widget.typeOption.field_2,
), ),

View File

@ -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/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/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.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:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -57,10 +56,6 @@ class CardPropertyEditScreen extends StatelessWidget {
), ),
body: MobileFieldEditor( body: MobileFieldEditor(
viewId: cellContext.viewId, viewId: cellContext.viewId,
typeOptionLoader: FieldTypeOptionLoader(
viewId: cellContext.viewId,
field: cellContext.fieldInfo.field,
),
fieldController: fieldController, fieldController: fieldController,
field: cellContext.fieldInfo.field, field: cellContext.fieldInfo.field,
), ),

View File

@ -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/field_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.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/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:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
@ -17,18 +20,21 @@ class MobileFieldEditor extends StatelessWidget {
const MobileFieldEditor({ const MobileFieldEditor({
super.key, super.key,
required this.viewId, required this.viewId,
required this.typeOptionLoader,
required this.field, required this.field,
required this.fieldController, required this.fieldController,
}); });
final String viewId; final String viewId;
final FieldController fieldController; final FieldController fieldController;
final FieldTypeOptionLoader typeOptionLoader;
final FieldPB field; final FieldPB field;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final typeOptionLoader = FieldTypeOptionLoader(
viewId: viewId,
field: field,
);
return BlocProvider( return BlocProvider(
create: (context) { create: (context) {
return FieldEditorBloc( return FieldEditorBloc(
@ -49,42 +55,35 @@ class MobileFieldEditor extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// field name
// TODO(yijing): improve hint text // TODO(yijing): improve hint text
PropertyTitle(LocaleKeys.settings_user_name.tr()), PropertyTitle(LocaleKeys.settings_user_name.tr()),
BlocSelector<FieldEditorBloc, FieldEditorState, String>( BlocSelector<FieldEditorBloc, FieldEditorState, String>(
selector: (state) => state.field.name, selector: (state) => state.field.name,
builder: (context, fieldName) { builder: (context, fieldName) =>
return MobileFieldNameTextField( MobileFieldNameTextField(text: fieldName),
text: fieldName,
);
},
), ),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: child: PropertyTitle(
PropertyTitle(LocaleKeys.grid_field_visibility.tr()), LocaleKeys.grid_field_visibility.tr(),
),
), ),
VisibilitySwitch( VisibilitySwitch(
isFieldHidden: state.field.visibility == isVisible:
FieldVisibility.AlwaysHidden, state.field.visibility?.isVisibleState() ?? false,
onChanged: () { onChanged: () => context.read<RowDetailBloc>().add(
context.read<RowDetailBloc>().add( RowDetailEvent.toggleFieldVisibility(
RowDetailEvent.toggleFieldVisibility( state.field.id,
state.field.id, ),
), ),
);
},
), ),
], ],
), ),
const VSpace(8), const VSpace(8),
// edit property type and settings // edit property type and settings
if (!typeOptionLoader.field.isPrimary) if (!typeOptionLoader.field.isPrimary)
MobileFieldTypeOptionEditor( MobileFieldTypeOptionEditor(dataController: dataController),
dataController: dataController,
),
], ],
), ),
); );
@ -97,11 +96,11 @@ class MobileFieldEditor extends StatelessWidget {
class VisibilitySwitch extends StatefulWidget { class VisibilitySwitch extends StatefulWidget {
const VisibilitySwitch({ const VisibilitySwitch({
super.key, super.key,
required this.isFieldHidden, required this.isVisible,
this.onChanged, this.onChanged,
}); });
final bool isFieldHidden; final bool isVisible;
final Function? onChanged; final Function? onChanged;
@override @override
@ -109,18 +108,17 @@ class VisibilitySwitch extends StatefulWidget {
} }
class _VisibilitySwitchState extends State<VisibilitySwitch> { class _VisibilitySwitchState extends State<VisibilitySwitch> {
late bool _isFieldHidden = widget.isFieldHidden; late bool _isVisible = widget.isVisible;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Switch.adaptive( return Toggle(
activeColor: Theme.of(context).colorScheme.primary, padding: EdgeInsets.zero,
value: !_isFieldHidden, value: _isVisible,
onChanged: (bool value) { style: ToggleStyle.mobile,
setState(() { onChanged: (newValue) {
_isFieldHidden = !_isFieldHidden; widget.onChanged?.call();
widget.onChanged?.call(); setState(() => _isVisible = newValue);
});
}, },
); );
} }

View File

@ -5,23 +5,36 @@ class MobileSettingItem extends StatelessWidget {
const MobileSettingItem({ const MobileSettingItem({
super.key, super.key,
required this.name, required this.name,
this.padding = const EdgeInsets.only(bottom: 4),
this.trailing,
this.leadingIcon,
this.subtitle, this.subtitle,
required this.trailing,
this.onTap, this.onTap,
}); });
final String name; final String name;
final EdgeInsets padding;
final Widget? trailing;
final Widget? leadingIcon;
final Widget? subtitle; final Widget? subtitle;
final Widget trailing;
final VoidCallback? onTap; final VoidCallback? onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 4), padding: padding,
child: ListTile( child: ListTile(
title: FlowyText.medium( title: Row(
name, children: [
fontSize: 14.0, if (leadingIcon != null) ...[
leadingIcon!,
const HSpace(8),
],
FlowyText.medium(
name,
fontSize: 14.0,
),
],
), ),
subtitle: subtitle, subtitle: subtitle,
trailing: trailing, trailing: trailing,

View File

@ -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<FlowyBottomSheetController>();
}
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<SheetPage> _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);
}
}

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.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_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.dart';
abstract class ICalendarSetting { abstract class ICalendarSetting {
const ICalendarSetting();
/// Returns the current layout settings for the calendar view. /// Returns the current layout settings for the calendar view.
CalendarLayoutSettingPB? getLayoutSetting(); CalendarLayoutSettingPB? getLayoutSetting();
@ -42,25 +44,15 @@ class CalendarLayoutSetting extends StatefulWidget {
} }
class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> { class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
late final PopoverMutex popoverMutex; late final PopoverMutex popoverMutex = PopoverMutex();
@override
void initState() {
popoverMutex = PopoverMutex();
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) { create: (context) => CalendarSettingBloc(
return CalendarSettingBloc( viewId: widget.viewId,
viewId: widget.viewId, layoutSettings: widget.calendarSettingController.getLayoutSetting(),
layoutSettings: widget.calendarSettingController.getLayoutSetting(), )..add(const CalendarSettingEvent.init()),
)..add(
const CalendarSettingEvent.init(),
);
},
child: BlocBuilder<CalendarSettingBloc, CalendarSettingState>( child: BlocBuilder<CalendarSettingBloc, CalendarSettingState>(
builder: (context, state) { builder: (context, state) {
final CalendarLayoutSettingPB? settings = state.layoutSetting final CalendarLayoutSettingPB? settings = state.layoutSetting
@ -69,39 +61,34 @@ class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
if (settings == null) { if (settings == null) {
return const CircularProgressIndicator(); return const CircularProgressIndicator();
} }
final availableSettings = _availableCalendarSettings(settings); final availableSettings = _availableCalendarSettings(settings);
final items = availableSettings.map((setting) { final items = availableSettings.map((setting) {
switch (setting) { switch (setting) {
case CalendarLayoutSettingAction.showWeekNumber: case CalendarLayoutSettingAction.showWeekNumber:
return ShowWeekNumber( return ShowWeekNumber(
showWeekNumbers: settings.showWeekNumbers, showWeekNumbers: settings.showWeekNumbers,
onUpdated: (showWeekNumbers) { onUpdated: (showWeekNumbers) => _updateLayoutSettings(
_updateLayoutSettings( context,
context, showWeekNumbers: showWeekNumbers,
showWeekNumbers: showWeekNumbers, ),
);
},
); );
case CalendarLayoutSettingAction.showWeekends: case CalendarLayoutSettingAction.showWeekends:
return ShowWeekends( return ShowWeekends(
showWeekends: settings.showWeekends, showWeekends: settings.showWeekends,
onUpdated: (showWeekends) { onUpdated: (showWeekends) => _updateLayoutSettings(
_updateLayoutSettings( context,
context, showWeekends: showWeekends,
showWeekends: showWeekends, ),
);
},
); );
case CalendarLayoutSettingAction.firstDayOfWeek: case CalendarLayoutSettingAction.firstDayOfWeek:
return FirstDayOfWeek( return FirstDayOfWeek(
firstDayOfWeek: settings.firstDayOfWeek, firstDayOfWeek: settings.firstDayOfWeek,
popoverMutex: popoverMutex, popoverMutex: popoverMutex,
onUpdated: (firstDayOfWeek) { onUpdated: (firstDayOfWeek) => _updateLayoutSettings(
_updateLayoutSettings( context,
context, firstDayOfWeek: firstDayOfWeek,
firstDayOfWeek: firstDayOfWeek, ),
);
},
); );
case CalendarLayoutSettingAction.layoutField: case CalendarLayoutSettingAction.layoutField:
return LayoutDateField( return LayoutDateField(
@ -109,15 +96,13 @@ class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
viewId: widget.viewId, viewId: widget.viewId,
fieldId: settings.fieldId, fieldId: settings.fieldId,
popoverMutex: popoverMutex, popoverMutex: popoverMutex,
onUpdated: (fieldId) { onUpdated: (fieldId) => _updateLayoutSettings(
_updateLayoutSettings( context,
context, layoutFieldId: fieldId,
layoutFieldId: fieldId, ),
);
},
); );
default: default:
return const SizedBox(); return const SizedBox.shrink();
} }
}).toList(); }).toList();
@ -127,10 +112,10 @@ class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
shrinkWrap: true, shrinkWrap: true,
controller: ScrollController(), controller: ScrollController(),
itemCount: items.length, itemCount: items.length,
separatorBuilder: (context, index) => separatorBuilder: (_, __) =>
VSpace(GridSize.typeOptionSeparatorHeight), VSpace(GridSize.typeOptionSeparatorHeight),
physics: StyledScrollPhysics(), physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) => items[index], itemBuilder: (_, int index) => items[index],
padding: const EdgeInsets.all(6.0), padding: const EdgeInsets.all(6.0),
), ),
); );
@ -144,28 +129,14 @@ class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
) { ) {
final List<CalendarLayoutSettingAction> settings = [ final List<CalendarLayoutSettingAction> settings = [
CalendarLayoutSettingAction.layoutField, CalendarLayoutSettingAction.layoutField,
// CalendarLayoutSettingAction.layoutType,
// CalendarLayoutSettingAction.showWeekNumber,
]; ];
switch (layoutSettings.layoutTy) { switch (layoutSettings.layoutTy) {
case CalendarLayoutPB.DayLayout: case CalendarLayoutPB.DayLayout:
// settings.add(CalendarLayoutSettingAction.showTimeLine);
break; break;
case CalendarLayoutPB.MonthLayout: case CalendarLayoutPB.MonthLayout:
settings.addAll([
// CalendarLayoutSettingAction.showWeekends,
// if (layoutSettings.showWeekends)
CalendarLayoutSettingAction.firstDayOfWeek,
]);
break;
case CalendarLayoutPB.WeekLayout: case CalendarLayoutPB.WeekLayout:
settings.addAll([ settings.add(CalendarLayoutSettingAction.firstDayOfWeek);
// CalendarLayoutSettingAction.showWeekends,
// if (layoutSettings.showWeekends)
CalendarLayoutSettingAction.firstDayOfWeek,
// CalendarLayoutSettingAction.showTimeLine,
]);
break; break;
} }
@ -189,16 +160,20 @@ class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
if (showWeekends != null) { if (showWeekends != null) {
setting.showWeekends = !showWeekends; setting.showWeekends = !showWeekends;
} }
if (showWeekNumbers != null) { if (showWeekNumbers != null) {
setting.showWeekNumbers = !showWeekNumbers; setting.showWeekNumbers = !showWeekNumbers;
} }
if (firstDayOfWeek != null) { if (firstDayOfWeek != null) {
setting.firstDayOfWeek = firstDayOfWeek; setting.firstDayOfWeek = firstDayOfWeek;
} }
if (layoutFieldId != null) { if (layoutFieldId != null) {
setting.fieldId = layoutFieldId; setting.fieldId = layoutFieldId;
} }
}); });
context context
.read<CalendarSettingBloc>() .read<CalendarSettingBloc>()
.add(CalendarSettingEvent.updateLayoutSetting(setting)); .add(CalendarSettingEvent.updateLayoutSetting(setting));
@ -208,21 +183,21 @@ class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
} }
class LayoutDateField extends StatelessWidget { class LayoutDateField extends StatelessWidget {
final String fieldId;
final String viewId;
final FieldController fieldController;
final PopoverMutex popoverMutex;
final Function(String fieldId) onUpdated;
const LayoutDateField({ const LayoutDateField({
super.key,
required this.fieldId, required this.fieldId,
required this.fieldController, required this.fieldController,
required this.viewId, required this.viewId,
required this.popoverMutex, required this.popoverMutex,
required this.onUpdated, required this.onUpdated,
super.key,
}); });
final String fieldId;
final String viewId;
final FieldController fieldController;
final PopoverMutex popoverMutex;
final Function(String fieldId) onUpdated;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppFlowyPopover( return AppFlowyPopover(
@ -264,8 +239,8 @@ class LayoutDateField extends StatelessWidget {
width: 200, width: 200,
child: ListView.separated( child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,
itemBuilder: (context, index) => items[index], itemBuilder: (_, index) => items[index],
separatorBuilder: (context, index) => separatorBuilder: (_, __) =>
VSpace(GridSize.typeOptionSeparatorHeight), VSpace(GridSize.typeOptionSeparatorHeight),
itemCount: items.length, itemCount: items.length,
), ),
@ -288,21 +263,19 @@ class LayoutDateField extends StatelessWidget {
} }
class ShowWeekNumber extends StatelessWidget { class ShowWeekNumber extends StatelessWidget {
final bool showWeekNumbers;
final Function(bool showWeekNumbers) onUpdated;
const ShowWeekNumber({ const ShowWeekNumber({
super.key,
required this.showWeekNumbers, required this.showWeekNumbers,
required this.onUpdated, required this.onUpdated,
super.key,
}); });
final bool showWeekNumbers;
final Function(bool showWeekNumbers) onUpdated;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _toggleItem( return _toggleItem(
onToggle: (showWeekNumbers) { onToggle: (showWeekNumbers) => onUpdated(!showWeekNumbers),
onUpdated(!showWeekNumbers);
},
value: showWeekNumbers, value: showWeekNumbers,
text: LocaleKeys.calendar_settings_showWeekNumbers.tr(), text: LocaleKeys.calendar_settings_showWeekNumbers.tr(),
); );
@ -310,20 +283,19 @@ class ShowWeekNumber extends StatelessWidget {
} }
class ShowWeekends extends StatelessWidget { class ShowWeekends extends StatelessWidget {
final bool showWeekends;
final Function(bool showWeekends) onUpdated;
const ShowWeekends({ const ShowWeekends({
super.key, super.key,
required this.showWeekends, required this.showWeekends,
required this.onUpdated, required this.onUpdated,
}); });
final bool showWeekends;
final Function(bool showWeekends) onUpdated;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _toggleItem( return _toggleItem(
onToggle: (showWeekends) { onToggle: (showWeekends) => onUpdated(!showWeekends),
onUpdated(!showWeekends);
},
value: showWeekends, value: showWeekends,
text: LocaleKeys.calendar_settings_showWeekends.tr(), text: LocaleKeys.calendar_settings_showWeekends.tr(),
); );
@ -331,16 +303,17 @@ class ShowWeekends extends StatelessWidget {
} }
class FirstDayOfWeek extends StatelessWidget { class FirstDayOfWeek extends StatelessWidget {
final int firstDayOfWeek;
final PopoverMutex popoverMutex;
final Function(int firstDayOfWeek) onUpdated;
const FirstDayOfWeek({ const FirstDayOfWeek({
super.key, super.key,
required this.firstDayOfWeek, required this.firstDayOfWeek,
required this.onUpdated,
required this.popoverMutex, required this.popoverMutex,
required this.onUpdated,
}); });
final int firstDayOfWeek;
final PopoverMutex popoverMutex;
final Function(int firstDayOfWeek) onUpdated;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppFlowyPopover( return AppFlowyPopover(
@ -370,8 +343,8 @@ class FirstDayOfWeek extends StatelessWidget {
width: 100, width: 100,
child: ListView.separated( child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,
itemBuilder: (context, index) => items[index], itemBuilder: (_, index) => items[index],
separatorBuilder: (context, index) => separatorBuilder: (_, __) =>
VSpace(GridSize.typeOptionSeparatorHeight), VSpace(GridSize.typeOptionSeparatorHeight),
itemCount: len, itemCount: len,
), ),
@ -425,18 +398,19 @@ enum CalendarLayoutSettingAction {
} }
class StartFromButton extends StatelessWidget { class StartFromButton extends StatelessWidget {
final int dayIndex;
final String title;
final bool isSelected;
final void Function(int) onTap;
const StartFromButton({ const StartFromButton({
super.key,
required this.title, required this.title,
required this.dayIndex, required this.dayIndex,
required this.onTap, required this.onTap,
required this.isSelected, required this.isSelected,
super.key,
}); });
final String title;
final int dayIndex;
final void Function(int) onTap;
final bool isSelected;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(

View File

@ -28,7 +28,7 @@ import 'widgets/footer/grid_footer.dart';
import 'widgets/header/grid_header.dart'; import 'widgets/header/grid_header.dart';
import 'widgets/row/row.dart'; import 'widgets/row/row.dart';
import 'widgets/shortcuts.dart'; import 'widgets/shortcuts.dart';
import 'widgets/toolbar/mobile_grid_setting.dart'; import '../../widgets/setting/mobile_database_settings_button.dart';
class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder { class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
final _toggleExtension = ToggleExtensionNotifier(); final _toggleExtension = ToggleExtensionNotifier();
@ -49,7 +49,7 @@ class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
@override @override
Widget settingBar(BuildContext context, DatabaseController controller) { Widget settingBar(BuildContext context, DatabaseController controller) {
return MobileGridSettingButton( return MobileDatabaseSettingsButton(
key: _makeValueKey(controller), key: _makeValueKey(controller),
controller: controller, controller: controller,
toggleExtension: _toggleExtension, toggleExtension: _toggleExtension,

View File

@ -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<StatefulWidget> createState() => _DatabaseLayoutListState();
}
class _DatabaseLayoutListState extends State<DatabaseLayoutList> {
@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<DatabaseLayoutBloc, DatabaseLayoutState>(
builder: (context, state) {
final cells = DatabaseLayoutPB.values.map((layout) {
final isSelected = state.databaseLayout == layout;
return DatabaseViewLayoutCell(
databaseLayout: layout,
isSelected: isSelected,
onTap: (selectedLayout) {
context
.read<DatabaseLayoutBloc>()
.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),
);
}
}

View File

@ -1,6 +1,6 @@
import 'package:appflowy/plugins/database_view/application/tab_bar_bloc.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/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:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -34,7 +34,7 @@ class MobileTabBarHeader extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
MobileGridSettingButton( MobileDatabaseSettingsButton(
controller: state controller: state
.tabBarControllerByViewId[currentView.viewId]! .tabBarControllerByViewId[currentView.viewId]!
.controller, .controller,

View File

@ -22,15 +22,16 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo; import 'package:protobuf/protobuf.dart' hide FieldInfo;
class DatabaseGroupList extends StatelessWidget { class DatabaseGroupList extends StatelessWidget {
final String viewId;
final DatabaseController databaseController;
final VoidCallback onDismissed;
const DatabaseGroupList({ const DatabaseGroupList({
super.key,
required this.viewId, required this.viewId,
required this.databaseController, required this.databaseController,
required this.onDismissed, required this.onDismissed,
Key? key, });
}) : super(key: key);
final String viewId;
final DatabaseController databaseController;
final VoidCallback onDismissed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -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<StatefulWidget> createState() => _DatabaseLayoutSelectorState();
}
class _DatabaseLayoutSelectorState extends State<DatabaseLayoutSelector> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DatabaseLayoutBloc(
viewId: widget.viewId,
databaseLayout: widget.currentLayout,
)..add(const DatabaseLayoutEvent.initial()),
child: BlocBuilder<DatabaseLayoutBloc, DatabaseLayoutState>(
builder: (context, state) {
final cells = DatabaseLayoutPB.values
.map(
(layout) => DatabaseViewLayoutCell(
databaseLayout: layout,
isSelected: state.databaseLayout == layout,
onTap: (selectedLayout) => context
.read<DatabaseLayoutBloc>()
.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),
);
}
}

View File

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

View File

@ -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<StatefulWidget> createState() => _DatabaseSettingsListState();
}
class _DatabaseSettingsListState extends State<DatabaseSettingsList> {
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<DatabaseSettingAction> 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 [];
}
}

View File

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

View File

@ -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<MobileCalendarLayoutSetting> createState() =>
_MobileCalendarLayoutSettingState();
}
class _MobileCalendarLayoutSettingState
extends State<MobileCalendarLayoutSetting> {
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<CalendarSettingBloc>.value(
value: calendarSettingBloc,
child: BlocBuilder<CalendarSettingBloc, CalendarSettingState>(
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<CalendarLayoutSettingAction> _availableCalendarSettings(
CalendarLayoutSettingPB layoutSettings,
) {
final List<CalendarLayoutSettingAction> 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<DatabasePropertyBloc, DatabasePropertyState>(
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<FieldInfo> 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<FieldInfo> items;
final Function(String fieldId) onUpdated;
@override
State<MobileCalendarLayoutSelector> createState() =>
_MobileCalendarLayoutSelectorState();
}
class _MobileCalendarLayoutSelectorState
extends State<MobileCalendarLayoutSelector> {
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<MobileFirstDayOfWeekSelector> createState() =>
_MobileFirstDayOfWeekSelectorState();
}
class _MobileFirstDayOfWeekSelectorState
extends State<MobileFirstDayOfWeekSelector> {
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);
},
),
);
}
}

View File

@ -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<MobileDatabasePropertyEditor> createState() =>
_MobileDatabasePropertyEditorState();
}
class _MobileDatabasePropertyEditorState
extends State<MobileDatabasePropertyEditor> {
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<DatabasePropertyBloc>.value(value: widget.bloc),
BlocProvider<FieldEditorBloc>(
create: (context) => FieldEditorBloc(
viewId: widget.viewId,
loader: typeOptionLoader,
field: widget.fieldInfo.field,
fieldController: widget.fieldController,
)..add(const FieldEditorEvent.initial()),
),
],
child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(
builder: (context, _) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) {
final dataController =
context.read<FieldEditorBloc>().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<FieldEditorBloc, FieldEditorState, String>(
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<DatabasePropertyBloc>().add(
DatabasePropertyEvent.setFieldVisibility(
widget.fieldInfo.id,
newVisibility,
),
);
setState(() => _visibility = newVisibility);
},
),
],
),
const VSpace(8),
if (!typeOptionLoader.field.isPrimary)
MobileFieldTypeOptionEditor(
dataController: dataController,
),
],
),
);
},
);
},
),
);
}
}

View File

@ -1,13 +1,17 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; 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/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/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/application/sort/sort_menu_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class MobileGridSettingButton extends StatelessWidget { class MobileDatabaseSettingsButton extends StatelessWidget {
const MobileGridSettingButton({ const MobileDatabaseSettingsButton({
super.key, super.key,
required this.controller, required this.controller,
required this.toggleExtension, required this.toggleExtension,
@ -56,8 +60,7 @@ class MobileGridSettingButton extends StatelessWidget {
width: 24, width: 24,
child: IconButton( child: IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
// TODO(Xazin): Database Settings onPressed: () => _showMobileSettings(context, controller),
onPressed: () {},
icon: const FlowySvg( icon: const FlowySvg(
FlowySvgs.m_setting_m, FlowySvgs.m_setting_m,
size: Size.square(24), 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,
),
),
);
} }

View File

@ -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/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.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/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/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.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 { class SettingButton extends StatefulWidget {
final DatabaseController databaseController;
const SettingButton({ const SettingButton({
super.key,
required this.databaseController, required this.databaseController,
Key? key, });
}) : super(key: key);
final DatabaseController databaseController;
@override @override
State<SettingButton> createState() => _SettingButtonState(); State<SettingButton> createState() => _SettingButtonState();
@ -47,193 +44,27 @@ class _SettingButtonState extends State<SettingButton> {
hoverColor: AFThemeExtension.of(context).lightGreyHover, hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.toolbarSettingButtonInsets, padding: GridSize.toolbarSettingButtonInsets,
radius: Corners.s4Border, 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<StatefulWidget> createState() => _DatabaseSettingListPopoverState();
}
class _DatabaseSettingListPopoverState
extends State<DatabaseSettingListPopover> {
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 { class ICalendarSettingImpl extends ICalendarSetting {
const ICalendarSettingImpl(this._databaseController);
final DatabaseController _databaseController; final DatabaseController _databaseController;
ICalendarSettingImpl(this._databaseController); @override
void updateLayoutSettings(CalendarLayoutSettingPB layoutSettings) =>
_databaseController.updateLayoutSetting(
calendarLayoutSetting: layoutSettings,
);
@override @override
void updateLayoutSettings(CalendarLayoutSettingPB layoutSettings) { CalendarLayoutSettingPB? getLayoutSetting() =>
_databaseController.updateLayoutSetting( _databaseController.databaseLayoutSetting?.calendar;
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<DatabaseSettingAction> 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 [];
}
} }

View File

@ -1,15 +1,23 @@
import 'dart:io'; import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart'; 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_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.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/application/setting/property_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.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_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.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_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -32,26 +40,45 @@ class DatabasePropertyList extends StatefulWidget {
class _DatabasePropertyListState extends State<DatabasePropertyList> { class _DatabasePropertyListState extends State<DatabasePropertyList> {
final PopoverMutex _popoverMutex = PopoverMutex(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider<DatabasePropertyBloc>.value(
create: (context) => DatabasePropertyBloc( value: _bloc,
viewId: widget.viewId,
fieldController: widget.fieldController,
)..add(const DatabasePropertyEvent.initial()),
child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>( child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(
builder: (context, state) { builder: (context, state) {
final cells = state.fieldContexts.mapIndexed((index, field) { final cells = state.fieldContexts
return DatabasePropertyCell( .mapIndexed(
key: ValueKey(field.id), (index, field) => DatabasePropertyCell(
viewId: widget.viewId, key: ValueKey(field.id),
fieldController: widget.fieldController, viewId: widget.viewId,
fieldInfo: field, fieldController: widget.fieldController,
popoverMutex: _popoverMutex, fieldInfo: field,
index: index, 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( return ReorderableListView(
proxyDecorator: (child, index, _) => Material( proxyDecorator: (child, index, _) => Material(
@ -90,13 +117,7 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
} }
@visibleForTesting @visibleForTesting
class DatabasePropertyCell extends StatefulWidget { class DatabasePropertyCell extends StatelessWidget {
final FieldController fieldController;
final FieldInfo fieldInfo;
final String viewId;
final PopoverMutex popoverMutex;
final int index;
const DatabasePropertyCell({ const DatabasePropertyCell({
super.key, super.key,
required this.fieldInfo, required this.fieldInfo,
@ -104,13 +125,145 @@ class DatabasePropertyCell extends StatefulWidget {
required this.popoverMutex, required this.popoverMutex,
required this.index, required this.index,
required this.fieldController, 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 @override
State<DatabasePropertyCell> 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<DatabasePropertyCell> { 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<MobileDatabasePropertyCell> createState() =>
_MobileDatabasePropertyCellState();
}
class _MobileDatabasePropertyCellState
extends State<MobileDatabasePropertyCell> {
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<DatabasePropertyBloc>().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<DesktopDatabasePropertyCell> createState() =>
_DesktopDatabasePropertyCellState();
}
class _DesktopDatabasePropertyCellState
extends State<DesktopDatabasePropertyCell> {
final PopoverController _popoverController = PopoverController(); final PopoverController _popoverController = PopoverController();
@override @override
@ -173,9 +326,8 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
return; return;
} }
final newVisiblity = _newFieldVisibility( final newVisiblity =
widget.fieldInfo.fieldSettings!.visibility, widget.fieldInfo.fieldSettings!.visibility.toggle();
);
context.read<DatabasePropertyBloc>().add( context.read<DatabasePropertyBloc>().add(
DatabasePropertyEvent.setFieldVisibility( DatabasePropertyEvent.setFieldVisibility(
widget.fieldInfo.id, widget.fieldInfo.id,
@ -197,12 +349,4 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
}, },
); );
} }
FieldVisibility _newFieldVisibility(FieldVisibility current) {
return switch (current) {
FieldVisibility.AlwaysShown => FieldVisibility.AlwaysHidden,
FieldVisibility.AlwaysHidden => FieldVisibility.AlwaysShown,
_ => FieldVisibility.AlwaysHidden,
};
}
} }

View File

@ -12,7 +12,7 @@ class Toggle extends StatelessWidget {
final EdgeInsets padding; final EdgeInsets padding;
const Toggle({ const Toggle({
Key? key, super.key,
required this.value, required this.value,
required this.onChanged, required this.onChanged,
required this.style, required this.style,
@ -20,7 +20,7 @@ class Toggle extends StatelessWidget {
this.activeBackgroundColor, this.activeBackgroundColor,
this.inactiveBackgroundColor, this.inactiveBackgroundColor,
this.padding = const EdgeInsets.all(8.0), this.padding = const EdgeInsets.all(8.0),
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -28,7 +28,7 @@ class Toggle extends StatelessWidget {
? activeBackgroundColor ?? Theme.of(context).colorScheme.primary ? activeBackgroundColor ?? Theme.of(context).colorScheme.primary
: activeBackgroundColor ?? AFThemeExtension.of(context).toggleOffFill; : activeBackgroundColor ?? AFThemeExtension.of(context).toggleOffFill;
return GestureDetector( return GestureDetector(
onTap: (() => onChanged(value)), onTap: () => onChanged(value),
child: Padding( child: Padding(
padding: padding, padding: padding,
child: Stack( child: Stack(

View File

@ -15,4 +15,7 @@ class ToggleStyle {
static ToggleStyle get small => static ToggleStyle get small =>
ToggleStyle(height: 10, width: 16, thumbRadius: 8); ToggleStyle(height: 10, width: 16, thumbRadius: 8);
static ToggleStyle get mobile =>
ToggleStyle(height: 24, width: 42, thumbRadius: 18);
} }

View File

@ -424,7 +424,9 @@
"privacyPolicy": "Privacy Policy", "privacyPolicy": "Privacy Policy",
"userAgreement": "User Agreement", "userAgreement": "User Agreement",
"userprofileError": "Failed to load user profile", "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": { "grid": {
@ -852,7 +854,7 @@
"other": "{} unscheduled events" "other": "{} unscheduled events"
}, },
"clickToAdd": "Click to add to the calendar", "clickToAdd": "Click to add to the calendar",
"name": "Calendar layout" "name": "Calendar settings"
}, },
"referencedCalendarPrefix": "View of", "referencedCalendarPrefix": "View of",
"quickJumpYear": "Jump to" "quickJumpYear": "Jump to"