chore: revamp database setting action sheets (#4570)

* chore: revamp database setting action sheets

* chore: field picker improvement

* chore: fix comments
This commit is contained in:
Richard Shiue 2024-02-01 15:41:02 +08:00 committed by GitHub
parent 9d28360887
commit 20391bfec4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 339 additions and 532 deletions

View File

@ -85,6 +85,39 @@ class AppBarDoneButton extends StatelessWidget {
} }
} }
class AppBarFilledDoneButton extends StatelessWidget {
const AppBarFilledDoneButton({super.key, required this.onTap});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 8),
child: TextButton(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 0,
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
enableFeedback: true,
backgroundColor: Theme.of(context).primaryColor,
),
onPressed: onTap,
child: FlowyText.medium(
LocaleKeys.button_done.tr(),
fontSize: 16,
color: Theme.of(context).colorScheme.onPrimary,
overflow: TextOverflow.ellipsis,
),
),
);
}
}
class AppBarMoreButton extends StatelessWidget { class AppBarMoreButton extends StatelessWidget {
const AppBarMoreButton({ const AppBarMoreButton({
super.key, super.key,

View File

@ -4,7 +4,6 @@ export 'bottom_sheet_drag_handler.dart';
export 'bottom_sheet_rename_widget.dart'; export 'bottom_sheet_rename_widget.dart';
export 'bottom_sheet_view_item.dart'; export 'bottom_sheet_view_item.dart';
export 'bottom_sheet_view_item_body.dart'; export 'bottom_sheet_view_item_body.dart';
export 'bottom_sheet_view_item_header.dart';
export 'bottom_sheet_view_page.dart'; export 'bottom_sheet_view_page.dart';
export 'default_mobile_action_pane.dart'; export 'default_mobile_action_pane.dart';
export 'show_mobile_bottom_sheet.dart'; export 'show_mobile_bottom_sheet.dart';

View File

@ -1,59 +0,0 @@
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MobileViewItemBottomSheetHeader extends StatelessWidget {
const MobileViewItemBottomSheetHeader({
super.key,
required this.view,
required this.showBackButton,
required this.onBack,
});
final ViewPB view;
final bool showBackButton;
final VoidCallback onBack;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// back button,
showBackButton
? InkWell(
onTap: onBack,
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Icon(
Icons.arrow_back_ios_new_rounded,
size: 24.0,
),
),
)
: FlowyButton(
useIntrinsicWidth: true,
text: const Icon(
Icons.close,
),
margin: EdgeInsets.zero,
onTap: () {
context.pop();
},
),
// title
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.6,
),
child: FlowyText.medium(
view.name,
overflow: TextOverflow.ellipsis,
),
),
const HSpace(24.0),
],
);
}
}

View File

@ -7,10 +7,12 @@ import 'package:flutter/material.dart';
Future<T?> showMobileBottomSheet<T>( Future<T?> showMobileBottomSheet<T>(
BuildContext context, { BuildContext context, {
required WidgetBuilder builder, required WidgetBuilder builder,
bool useSafeArea = true,
bool isDragEnabled = true, bool isDragEnabled = true,
bool showDragHandle = false, bool showDragHandle = false,
bool showHeader = false, bool showHeader = false,
// this field is only used if showHeader is true // this field is only used if showHeader is true
bool showBackButton = false,
bool showCloseButton = false, bool showCloseButton = false,
// this field is only used if showHeader is true // this field is only used if showHeader is true
String title = '', String title = '',
@ -20,7 +22,7 @@ Future<T?> showMobileBottomSheet<T>(
bool useRootNavigator = false, bool useRootNavigator = false,
ShapeBorder? shape, ShapeBorder? shape,
// the padding of the content, the padding of the header area is fixed // the padding of the content, the padding of the header area is fixed
EdgeInsets padding = const EdgeInsets.all(0.0), EdgeInsets padding = EdgeInsets.zero,
Color? backgroundColor, Color? backgroundColor,
BoxConstraints? constraints, BoxConstraints? constraints,
Color? barrierColor, Color? barrierColor,
@ -32,10 +34,11 @@ Future<T?> showMobileBottomSheet<T>(
double maxChildSize = 0.8, double maxChildSize = 0.8,
double initialChildSize = 0.51, double initialChildSize = 0.51,
}) async { }) async {
assert(() { assert(
if (showCloseButton || title.isNotEmpty) assert(showHeader); showHeader ||
return true; title.isEmpty && !showCloseButton && !showBackButton && !showDoneButton,
}()); );
assert(!(showCloseButton && showBackButton));
shape ??= const RoundedRectangleBorder( shape ??= const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical( borderRadius: BorderRadius.vertical(
@ -83,6 +86,7 @@ Future<T?> showMobileBottomSheet<T>(
children.add( children.add(
_Header( _Header(
showCloseButton: showCloseButton, showCloseButton: showCloseButton,
showBackButton: showBackButton,
showDoneButton: showDoneButton, showDoneButton: showDoneButton,
title: title, title: title,
), ),
@ -147,23 +151,30 @@ Future<T?> showMobileBottomSheet<T>(
VSpace(MediaQuery.of(context).padding.bottom == 0 ? 28.0 : 16.0), VSpace(MediaQuery.of(context).padding.bottom == 0 ? 28.0 : 16.0),
); );
return SafeArea( return useSafeArea
child: Column( ? SafeArea(
mainAxisSize: MainAxisSize.min, child: Column(
children: children, mainAxisSize: MainAxisSize.min,
), children: children,
); ),
)
: Column(
mainAxisSize: MainAxisSize.min,
children: children,
);
}, },
); );
} }
class _Header extends StatelessWidget { class _Header extends StatelessWidget {
const _Header({ const _Header({
required this.showBackButton,
required this.showCloseButton, required this.showCloseButton,
required this.title, required this.title,
required this.showDoneButton, required this.showDoneButton,
}); });
final bool showBackButton;
final bool showCloseButton; final bool showCloseButton;
final String title; final String title;
final bool showDoneButton; final bool showDoneButton;
@ -176,6 +187,11 @@ class _Header extends StatelessWidget {
height: 44.0, // the height of the header area is fixed height: 44.0, // the height of the header area is fixed
child: Stack( child: Stack(
children: [ children: [
if (showBackButton)
const Align(
alignment: Alignment.centerLeft,
child: AppBarBackButton(),
),
if (showCloseButton) if (showCloseButton)
const Align( const Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,

View File

@ -128,14 +128,17 @@ void showQuickEditField(
/// Display a list of fields in the current database that users can choose from. /// Display a list of fields in the current database that users can choose from.
Future<String?> showFieldPicker( Future<String?> showFieldPicker(
BuildContext context, BuildContext context,
String title,
String? selectedFieldId, String? selectedFieldId,
FieldController fieldController, FieldController fieldController,
bool Function(FieldInfo fieldInfo) filterBy, bool Function(FieldInfo fieldInfo) filterBy,
) { ) {
return showMobileBottomSheet<String>( return showMobileBottomSheet<String>(
context, context,
showDivider: false,
builder: (context) { builder: (context) {
return MobileFieldPickerList( return MobileFieldPickerList(
title: title,
selectedFieldId: selectedFieldId, selectedFieldId: selectedFieldId,
fieldController: fieldController, fieldController: fieldController,
filterBy: filterBy, filterBy: filterBy,

View File

@ -1,11 +1,10 @@
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/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/base/drag_handler.dart'; import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/util/field_type_extension.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/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -13,11 +12,13 @@ import 'package:go_router/go_router.dart';
class MobileFieldPickerList extends StatefulWidget { class MobileFieldPickerList extends StatefulWidget {
MobileFieldPickerList({ MobileFieldPickerList({
super.key, super.key,
required this.title,
required this.selectedFieldId, required this.selectedFieldId,
required FieldController fieldController, required FieldController fieldController,
required bool Function(FieldInfo fieldInfo) filterBy, required bool Function(FieldInfo fieldInfo) filterBy,
}) : fields = fieldController.fieldInfos.where(filterBy).toList(); }) : fields = fieldController.fieldInfos.where(filterBy).toList();
final String title;
final String? selectedFieldId; final String? selectedFieldId;
final List<FieldInfo> fields; final List<FieldInfo> fields;
@ -36,87 +37,79 @@ class _MobileFieldPickerListState extends State<MobileFieldPickerList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return DraggableScrollableSheet(
children: [ expand: false,
const Center(child: DragHandler()), snap: true,
_Header(newFieldId: newFieldId), initialChildSize: 0.98,
Expanded( minChildSize: 0.98,
child: ListView.builder( maxChildSize: 0.98,
itemCount: widget.fields.length, builder: (context, scrollController) {
itemBuilder: (context, index) => _FieldButton( return Column(
field: widget.fields[index], mainAxisSize: MainAxisSize.min,
showTopBorder: index == 0,
isSelected: widget.fields[index].id == newFieldId,
onSelect: (fieldId) => setState(() => newFieldId = fieldId),
),
),
),
],
);
}
}
class _Header extends StatelessWidget {
const _Header({required this.newFieldId});
final String? newFieldId;
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
SizedBox.square( const DragHandler(),
dimension: 36, _Header(
child: IconButton( title: widget.title,
highlightColor: Colors.transparent, onDone: (context) => context.pop(newFieldId),
splashColor: Colors.transparent,
padding: EdgeInsets.zero,
onPressed: () => context.pop(),
icon: const FlowySvg(
FlowySvgs.arrow_left_s,
size: Size.square(20),
),
),
), ),
Padding( SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 8), controller: scrollController,
child: TextButton( child: ListView.builder(
style: TextButton.styleFrom( shrinkWrap: true,
padding: const EdgeInsets.symmetric( itemCount: widget.fields.length,
horizontal: 12, itemBuilder: (context, index) => _FieldButton(
vertical: 5, field: widget.fields[index],
), showTopBorder: index == 0,
shape: RoundedRectangleBorder( isSelected: widget.fields[index].id == newFieldId,
borderRadius: BorderRadius.circular(10), onSelect: (fieldId) => setState(() => newFieldId = fieldId),
),
elevation: 0,
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
enableFeedback: true,
backgroundColor: Theme.of(context).primaryColor,
),
onPressed: () => context.pop(newFieldId),
child: FlowyText.medium(
LocaleKeys.button_done.tr(),
fontSize: 16,
color: Theme.of(context).colorScheme.onPrimary,
overflow: TextOverflow.ellipsis,
), ),
), ),
), ),
], ],
);
},
);
}
}
/// Same header as the one in showMobileBottomSheet, but allows popping the
/// sheet with a value.
class _Header extends StatelessWidget {
const _Header({
required this.title,
required this.onDone,
});
final String title;
final void Function(BuildContext context) onDone;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: SizedBox(
height: 44.0,
child: Stack(
children: [
const Align(
alignment: Alignment.centerLeft,
child: AppBarBackButton(),
),
Align(
child: FlowyText.medium(
title,
fontSize: 16.0,
),
),
Align(
alignment: Alignment.centerRight,
child: AppBarDoneButton(
onTap: () => onDone(context),
),
),
],
), ),
Center( ),
child: FlowyText.medium(
LocaleKeys.calendar_settings_changeLayoutDateField.tr(),
fontSize: 16,
),
),
],
); );
} }
} }

View File

@ -3,7 +3,6 @@ import 'dart:ui';
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/mobile/presentation/widgets/flowy_option_tile.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart';
@ -11,7 +10,6 @@ import 'package:appflowy/plugins/database/application/setting/property_bloc.dart
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart'; import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.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';
@ -30,61 +28,9 @@ class MobileDatabaseFieldList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DraggableScrollableSheet( return _MobileDatabaseFieldListBody(
expand: false, databaseController: databaseController,
snap: true, viewId: context.read<ViewBloc>().state.view.id,
initialChildSize: 1.0,
minChildSize: 0.0,
builder: (context, controller) {
return Material(
child: Column(
children: [
const Center(child: DragHandler()),
const _MobileDatabaseFieldListHeader(),
Expanded(
child: SingleChildScrollView(
child: _MobileDatabaseFieldListBody(
databaseController: databaseController,
view: context.read<ViewBloc>().state.view,
),
),
),
],
),
);
},
);
}
}
class _MobileDatabaseFieldListHeader extends StatelessWidget {
const _MobileDatabaseFieldListHeader();
@override
Widget build(BuildContext context) {
const iconWidth = 30.0;
return Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
child: Stack(
children: [
Align(
alignment: Alignment.centerLeft,
child: FlowyIconButton(
icon: const FlowySvg(
FlowySvgs.arrow_left_m,
size: Size.square(iconWidth),
),
onPressed: () => Navigator.of(context).maybePop(),
),
),
Align(
child: FlowyText.medium(
LocaleKeys.grid_settings_properties.tr(),
fontSize: 16,
),
),
],
),
); );
} }
} }
@ -92,17 +38,17 @@ class _MobileDatabaseFieldListHeader extends StatelessWidget {
class _MobileDatabaseFieldListBody extends StatelessWidget { class _MobileDatabaseFieldListBody extends StatelessWidget {
const _MobileDatabaseFieldListBody({ const _MobileDatabaseFieldListBody({
required this.databaseController, required this.databaseController,
required this.view, required this.viewId,
}); });
final DatabaseController databaseController; final DatabaseController databaseController;
final ViewPB view; final String viewId;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<DatabasePropertyBloc>( return BlocProvider<DatabasePropertyBloc>(
create: (_) => DatabasePropertyBloc( create: (_) => DatabasePropertyBloc(
viewId: view.id, viewId: viewId,
fieldController: databaseController.fieldController, fieldController: databaseController.fieldController,
)..add(const DatabasePropertyEvent.initial()), )..add(const DatabasePropertyEvent.initial()),
child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>( child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(
@ -114,7 +60,7 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
final firstField = fields.removeAt(0); final firstField = fields.removeAt(0);
final firstCell = DatabaseFieldListTile( final firstCell = DatabaseFieldListTile(
key: ValueKey(firstField.id), key: ValueKey(firstField.id),
viewId: view.id, viewId: viewId,
fieldController: databaseController.fieldController, fieldController: databaseController.fieldController,
fieldInfo: firstField, fieldInfo: firstField,
showTopBorder: true, showTopBorder: true,
@ -123,7 +69,7 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
.mapIndexed( .mapIndexed(
(index, field) => DatabaseFieldListTile( (index, field) => DatabaseFieldListTile(
key: ValueKey(field.id), key: ValueKey(field.id),
viewId: view.id, viewId: viewId,
fieldController: databaseController.fieldController, fieldController: databaseController.fieldController,
fieldInfo: field, fieldInfo: field,
index: index, index: index,
@ -133,6 +79,7 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
.toList(); .toList();
return ReorderableListView.builder( return ReorderableListView.builder(
padding: EdgeInsets.zero,
proxyDecorator: (_, index, anim) { proxyDecorator: (_, index, anim) {
final field = fields[index]; final field = fields[index];
return AnimatedBuilder( return AnimatedBuilder(
@ -146,7 +93,7 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
child: Material( child: Material(
child: DatabaseFieldListTile( child: DatabaseFieldListTile(
key: ValueKey(field.id), key: ValueKey(field.id),
viewId: view.id, viewId: viewId,
fieldController: databaseController.fieldController, fieldController: databaseController.fieldController,
fieldInfo: field, fieldInfo: field,
index: index, index: index,
@ -170,7 +117,7 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_divider(), _divider(),
_NewDatabaseFieldTile(viewId: view.id), _NewDatabaseFieldTile(viewId: viewId),
], ],
), ),
itemCount: cells.length, itemCount: cells.length,

View File

@ -168,6 +168,7 @@ class _CalendarLayoutField extends StatelessWidget {
onTap: () async { onTap: () async {
final newFieldId = await showFieldPicker( final newFieldId = await showFieldPicker(
context, context,
LocaleKeys.calendar_settings_changeLayoutDateField.tr(),
selectedFieldId, selectedFieldId,
databaseController.fieldController, databaseController.fieldController,
(field) => field.fieldType == FieldType.DateTime, (field) => field.fieldType == FieldType.DateTime,

View File

@ -1,5 +1,6 @@
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/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/base/drag_handler.dart'; import 'package:appflowy/plugins/base/drag_handler.dart';
@ -26,28 +27,110 @@ class MobileDatabaseViewList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<ViewBloc, ViewState>( return DraggableScrollableSheet(
builder: (context, state) { expand: false,
final views = [state.view, ...state.view.childViews]; snap: true,
final children = [ initialChildSize: 0.98,
...views.mapIndexed( minChildSize: 0.98,
(index, view) => MobileDatabaseViewListButton( maxChildSize: 0.98,
view: view, builder: (context, scrollController) {
showTopBorder: index == 0, return BlocBuilder<ViewBloc, ViewState>(
), builder: (context, state) {
), final views = [state.view, ...state.view.childViews];
const VSpace(20),
const MobileNewDatabaseViewButton(),
];
return Column( return Column(
children: children, children: [
const DragHandler(),
_Header(
title: LocaleKeys.grid_settings_viewList.plural(
context.watch<DatabaseTabBarBloc>().state.tabBars.length,
namedArgs: {
'count':
'${context.watch<DatabaseTabBarBloc>().state.tabBars.length}',
},
),
showBackButton: false,
useFilledDoneButton: false,
onDone: (context) => Navigator.pop(context),
),
SingleChildScrollView(
controller: scrollController,
child: Column(
children: [
...views.mapIndexed(
(index, view) => MobileDatabaseViewListButton(
view: view,
showTopBorder: index == 0,
),
),
const VSpace(20),
const MobileNewDatabaseViewButton(),
],
),
),
],
);
},
); );
}, },
); );
} }
} }
/// Same header as the one in showMobileBottomSheet, but allows popping the
/// sheet with a value.
class _Header extends StatelessWidget {
const _Header({
required this.title,
required this.showBackButton,
required this.useFilledDoneButton,
required this.onDone,
});
final String title;
final bool showBackButton;
final bool useFilledDoneButton;
final void Function(BuildContext context) onDone;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: SizedBox(
height: 44.0,
child: Stack(
children: [
if (showBackButton)
const Align(
alignment: Alignment.centerLeft,
child: AppBarBackButton(),
),
Align(
child: FlowyText.medium(
title,
fontSize: 16.0,
),
),
useFilledDoneButton
? Align(
alignment: Alignment.centerRight,
child: AppBarFilledDoneButton(
onTap: () => onDone(context),
),
)
: Align(
alignment: Alignment.centerRight,
child: AppBarDoneButton(
onTap: () => onDone(context),
),
),
],
),
),
);
}
}
@visibleForTesting @visibleForTesting
class MobileDatabaseViewListButton extends StatelessWidget { class MobileDatabaseViewListButton extends StatelessWidget {
const MobileDatabaseViewListButton({ const MobileDatabaseViewListButton({
@ -155,6 +238,7 @@ class MobileNewDatabaseViewButton extends StatelessWidget {
onTap: () async { onTap: () async {
final result = await showMobileBottomSheet<(DatabaseLayoutPB, String)>( final result = await showMobileBottomSheet<(DatabaseLayoutPB, String)>(
context, context,
showDragHandle: true,
builder: (_) { builder: (_) {
return const MobileCreateDatabaseView(); return const MobileCreateDatabaseView();
}, },
@ -199,12 +283,13 @@ class _MobileCreateDatabaseViewState extends State<MobileCreateDatabaseView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
const Center(child: DragHandler()), _Header(
_CreateViewHeader( title: LocaleKeys.grid_settings_createView.tr(),
textController: controller, showBackButton: true,
selectedLayout: layoutType, useFilledDoneButton: true,
onDone: (context) =>
context.pop((layoutType, controller.text.trim())),
), ),
const VSpace(4.0),
FlowyOptionTile.textField( FlowyOptionTile.textField(
autofocus: true, autofocus: true,
controller: controller, controller: controller,
@ -220,74 +305,3 @@ class _MobileCreateDatabaseViewState extends State<MobileCreateDatabaseView> {
); );
} }
} }
class _CreateViewHeader extends StatelessWidget {
const _CreateViewHeader({
required this.textController,
required this.selectedLayout,
});
final TextEditingController textController;
final DatabaseLayoutPB selectedLayout;
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox.square(
dimension: 36,
child: IconButton(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
padding: EdgeInsets.zero,
onPressed: () => context.pop(),
icon: const FlowySvg(
FlowySvgs.arrow_left_s,
size: Size.square(24),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 8),
child: TextButton(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 5,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 0,
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
enableFeedback: true,
backgroundColor: Theme.of(context).primaryColor,
),
onPressed: () {
context.pop((selectedLayout, textController.text.trim()));
},
child: FlowyText.medium(
LocaleKeys.button_done.tr(),
fontSize: 16,
color: Theme.of(context).colorScheme.onPrimary,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
Center(
child: FlowyText.medium(
LocaleKeys.grid_settings_createView.tr(),
fontSize: 16,
),
),
],
);
}
}

View File

@ -1,5 +1,6 @@
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/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart';
@ -13,7 +14,7 @@ import 'edit_database_view_screen.dart';
/// [MobileDatabaseViewQuickActions] is gives users to quickly edit a database /// [MobileDatabaseViewQuickActions] is gives users to quickly edit a database
/// view from the [MobileDatabaseViewList] /// view from the [MobileDatabaseViewList]
class MobileDatabaseViewQuickActions extends StatefulWidget { class MobileDatabaseViewQuickActions extends StatelessWidget {
const MobileDatabaseViewQuickActions({ const MobileDatabaseViewQuickActions({
super.key, super.key,
required this.view, required this.view,
@ -23,49 +24,46 @@ class MobileDatabaseViewQuickActions extends StatefulWidget {
final ViewPB view; final ViewPB view;
final DatabaseController databaseController; final DatabaseController databaseController;
@override
State<MobileDatabaseViewQuickActions> createState() =>
_MobileDatabaseViewQuickActionsState();
}
class _MobileDatabaseViewQuickActionsState
extends State<MobileDatabaseViewQuickActions> {
bool isEditing = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return isEditing
? MobileEditDatabaseViewScreen(
databaseController: widget.databaseController,
)
: _quickActions(context, widget.view);
}
Widget _quickActions(BuildContext context, ViewPB view) {
final isInline = view.childViews.isNotEmpty; final isInline = view.childViews.isNotEmpty;
return Padding( return Column(
padding: const EdgeInsets.only(top: 8), mainAxisSize: MainAxisSize.min,
child: Column( children: [
mainAxisSize: MainAxisSize.min, _actionButton(context, _Action.edit, () {
children: [ final bloc = context.read<ViewBloc>();
_actionButton(context, _Action.edit, () { context.pop();
setState(() => isEditing = true); showMobileBottomSheet(
context,
showHeader: true,
showDoneButton: true,
title: LocaleKeys.grid_settings_editView.tr(),
enableDraggableScrollable: true,
initialChildSize: 0.98,
minChildSize: 0.98,
maxChildSize: 0.98,
builder: (_) => BlocProvider.value(
value: bloc,
child: MobileEditDatabaseViewScreen(
databaseController: databaseController,
),
),
);
}),
if (!isInline) ...[
_divider(),
_actionButton(context, _Action.duplicate, () {
context.read<ViewBloc>().add(const ViewEvent.duplicate());
context.pop();
}), }),
if (!isInline) ...[ _divider(),
_divider(), _actionButton(context, _Action.delete, () {
_actionButton(context, _Action.duplicate, () { context.read<ViewBloc>().add(const ViewEvent.delete());
context.read<ViewBloc>().add(const ViewEvent.duplicate()); context.pop();
context.pop(); }),
}), _divider(),
_divider(),
_actionButton(context, _Action.delete, () {
context.read<ViewBloc>().add(const ViewEvent.delete());
context.pop();
}),
_divider(),
],
], ],
), ],
); );
} }

View File

@ -1,35 +0,0 @@
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'edit_database_view_cubit.freezed.dart';
class MobileEditDatabaseViewCubit extends Cubit<MobileDatabaseViewEditorState> {
MobileEditDatabaseViewCubit()
: super(
MobileDatabaseViewEditorState.initial(),
);
void changePage(MobileEditDatabaseViewPageEnum newPage) {
emit(MobileDatabaseViewEditorState(currentPage: newPage));
}
}
@freezed
class MobileDatabaseViewEditorState with _$MobileDatabaseViewEditorState {
factory MobileDatabaseViewEditorState({
required MobileEditDatabaseViewPageEnum currentPage,
}) = _MobileDatabaseViewEditorState;
factory MobileDatabaseViewEditorState.initial() =>
MobileDatabaseViewEditorState(
currentPage: MobileEditDatabaseViewPageEnum.main,
);
}
enum MobileEditDatabaseViewPageEnum {
main,
fields,
filter,
sort,
}

View File

@ -2,7 +2,6 @@ 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/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/database_view_service.dart'; import 'package:appflowy/plugins/database/application/database_view_service.dart';
import 'package:appflowy/plugins/database/application/layout/layout_service.dart'; import 'package:appflowy/plugins/database/application/layout/layout_service.dart';
@ -18,12 +17,11 @@ import 'package:go_router/go_router.dart';
import 'database_field_list.dart'; import 'database_field_list.dart';
import 'database_view_layout.dart'; import 'database_view_layout.dart';
import 'edit_database_view_cubit.dart';
/// [MobileEditDatabaseViewScreen] is the main widget used to edit a database /// [MobileEditDatabaseViewScreen] is the main widget used to edit a database
/// view. It contains multiple sub-pages, and the current page is managed by /// view. It contains multiple sub-pages, and the current page is managed by
/// [MobileEditDatabaseViewCubit] /// [MobileEditDatabaseViewCubit]
class MobileEditDatabaseViewScreen extends StatefulWidget { class MobileEditDatabaseViewScreen extends StatelessWidget {
const MobileEditDatabaseViewScreen({ const MobileEditDatabaseViewScreen({
super.key, super.key,
required this.databaseController, required this.databaseController,
@ -31,130 +29,12 @@ class MobileEditDatabaseViewScreen extends StatefulWidget {
final DatabaseController databaseController; final DatabaseController databaseController;
@override
State<MobileEditDatabaseViewScreen> createState() =>
_MobileEditDatabaseViewScreenState();
}
class _MobileEditDatabaseViewScreenState
extends State<MobileEditDatabaseViewScreen> {
@override
Widget build(BuildContext context) {
return BlocProvider<MobileEditDatabaseViewCubit>(
create: (context) => MobileEditDatabaseViewCubit(),
child: BlocBuilder<MobileEditDatabaseViewCubit,
MobileDatabaseViewEditorState>(
builder: (context, state) {
return switch (state.currentPage) {
MobileEditDatabaseViewPageEnum.main => _EditDatabaseViewMainPage(
databaseController: widget.databaseController,
),
MobileEditDatabaseViewPageEnum.fields => _wrapSubPage(
context,
MobileDatabaseFieldList(
databaseController: widget.databaseController,
),
),
_ => const SizedBox.shrink(),
};
},
),
);
}
Widget _wrapSubPage(BuildContext context, Widget child) {
return PopScope(
canPop: false,
child: child,
onPopInvoked: (_) {
context
.read<MobileEditDatabaseViewCubit>()
.changePage(MobileEditDatabaseViewPageEnum.main);
},
);
}
}
class _EditDatabaseViewMainPage extends StatelessWidget {
const _EditDatabaseViewMainPage({
required this.databaseController,
});
final DatabaseController databaseController;
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
expand: false,
snap: true,
initialChildSize: 1.0,
minChildSize: 0.0,
builder: (context, controller) {
return Material(
child: Column(
children: [
const Center(child: DragHandler()),
const _EditDatabaseViewHeader(),
Expanded(
child: SingleChildScrollView(
child: _EditDatabaseViewBody(
databaseController: databaseController,
),
),
),
],
),
);
},
);
}
}
class _EditDatabaseViewHeader extends StatelessWidget {
const _EditDatabaseViewHeader();
@override
Widget build(BuildContext context) {
const iconWidth = 30.0;
return Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
child: Stack(
children: [
Align(
alignment: Alignment.centerLeft,
child: FlowyIconButton(
icon: const FlowySvg(
FlowySvgs.close_s,
size: Size.square(iconWidth),
),
onPressed: () => context.pop(),
),
),
Align(
child: FlowyText.medium(
LocaleKeys.grid_settings_editView.tr(),
fontSize: 16,
),
),
],
),
);
}
}
class _EditDatabaseViewBody extends StatelessWidget {
const _EditDatabaseViewBody({
required this.databaseController,
});
final DatabaseController databaseController;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<ViewBloc, ViewState>( return BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) { builder: (context, state) {
return Column( return ListView(
mainAxisSize: MainAxisSize.min, shrinkWrap: true,
children: [ children: [
_NameAndIcon(view: state.view), _NameAndIcon(view: state.view),
_divider(), _divider(),
@ -207,6 +87,7 @@ class _NameAndIconState extends State<_NameAndIcon> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FlowyOptionTile.textField( return FlowyOptionTile.textField(
autofocus: true, autofocus: true,
showTopBorder: false,
controller: textEditingController, controller: textEditingController,
onTextChanged: (text) { onTextChanged: (text) {
context.read<ViewBloc>().add(ViewEvent.rename(text)); context.read<ViewBloc>().add(ViewEvent.rename(text));
@ -250,15 +131,6 @@ enum DatabaseViewSettings {
delete => FlowySvgs.delete_s, delete => FlowySvgs.delete_s,
}; };
} }
MobileEditDatabaseViewPageEnum? get subPage {
return switch (this) {
fields => MobileEditDatabaseViewPageEnum.fields,
filter => MobileEditDatabaseViewPageEnum.filter,
sort => MobileEditDatabaseViewPageEnum.sort,
_ => null,
};
}
} }
class DatabaseViewSettingTile extends StatelessWidget { class DatabaseViewSettingTile extends StatelessWidget {
@ -325,25 +197,18 @@ class DatabaseViewSettingTile extends StatelessWidget {
} }
void _onTap(BuildContext context) async { void _onTap(BuildContext context) async {
final subPage = setting.subPage;
if (subPage != null) {
context.read<MobileEditDatabaseViewCubit>().changePage(subPage);
return;
}
if (setting == DatabaseViewSettings.layout) { if (setting == DatabaseViewSettings.layout) {
final databaseLayout = databaseLayoutFromViewLayout(view.layout); final databaseLayout = databaseLayoutFromViewLayout(view.layout);
final newLayout = await showMobileBottomSheet<DatabaseLayoutPB>( final newLayout = await showMobileBottomSheet<DatabaseLayoutPB>(
context, context,
resizeToAvoidBottomInset: false,
showDragHandle: true, showDragHandle: true,
showHeader: true,
showDivider: false,
title: LocaleKeys.grid_settings_layout.tr(),
builder: (context) { builder: (context) {
return DatabaseViewLayoutPicker( return DatabaseViewLayoutPicker(
selectedLayout: databaseLayout, selectedLayout: databaseLayout,
onSelect: (layout) { onSelect: (layout) => Navigator.of(context).pop(layout),
Navigator.of(context).pop(layout);
},
); );
}, },
); );
@ -356,6 +221,32 @@ class DatabaseViewSettingTile extends StatelessWidget {
return; return;
} }
if (setting == DatabaseViewSettings.fields) {
await showMobileBottomSheet(
context,
useSafeArea: false,
resizeToAvoidBottomInset: false,
showDragHandle: true,
showHeader: true,
showBackButton: true,
title: LocaleKeys.grid_settings_properties.tr(),
showDivider: true,
enableDraggableScrollable: true,
initialChildSize: 0.98,
minChildSize: 0.98,
maxChildSize: 0.98,
builder: (_) {
return BlocProvider.value(
value: context.read<ViewBloc>(),
child: MobileDatabaseFieldList(
databaseController: databaseController,
),
);
},
);
return;
}
if (setting == DatabaseViewSettings.board) { if (setting == DatabaseViewSettings.board) {
await showMobileBottomSheet<DatabaseLayoutPB>( await showMobileBottomSheet<DatabaseLayoutPB>(
context, context,
@ -375,13 +266,13 @@ class DatabaseViewSettingTile extends StatelessWidget {
if (setting == DatabaseViewSettings.calendar) { if (setting == DatabaseViewSettings.calendar) {
await showMobileBottomSheet<DatabaseLayoutPB>( await showMobileBottomSheet<DatabaseLayoutPB>(
context, context,
resizeToAvoidBottomInset: false, showDragHandle: true,
showHeader: true,
showDivider: false,
title: LocaleKeys.calendar_settings_name.tr(),
builder: (context) { builder: (context) {
return Padding( return MobileCalendarViewLayoutSettings(
padding: const EdgeInsets.only(top: 24, bottom: 46), databaseController: databaseController,
child: MobileCalendarViewLayoutSettings(
databaseController: databaseController,
),
); );
}, },
); );

View File

@ -65,7 +65,13 @@ class MobileDatabaseControls extends StatelessWidget {
onTap: () { onTap: () {
showMobileBottomSheet( showMobileBottomSheet(
context, context,
padding: EdgeInsets.zero, showHeader: true,
showDoneButton: true,
title: LocaleKeys.grid_settings_editView.tr(),
enableDraggableScrollable: true,
initialChildSize: 0.98,
minChildSize: 0.98,
maxChildSize: 0.98,
builder: (_) { builder: (_) {
return BlocProvider<ViewBloc>( return BlocProvider<ViewBloc>(
create: (_) { create: (_) {
@ -90,11 +96,7 @@ class MobileDatabaseControls extends StatelessWidget {
onTap: () { onTap: () {
showMobileBottomSheet( showMobileBottomSheet(
context, context,
showHeader: true,
showCloseButton: true,
showDragHandle: true,
showDivider: false, showDivider: false,
title: LocaleKeys.grid_settings_viewList.tr(),
builder: (_) { builder: (_) {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [

View File

@ -480,7 +480,11 @@
"typeAValue": "Type a value...", "typeAValue": "Type a value...",
"layout": "Layout", "layout": "Layout",
"databaseLayout": "Layout", "databaseLayout": "Layout",
"viewList": "Database Views", "viewList": {
"zero": "0 views",
"one": "{count} view",
"other": "{count} views"
},
"editView": "Edit View", "editView": "Edit View",
"boardSettings": "Board settings", "boardSettings": "Board settings",
"calendarSettings": "Calendar settings", "calendarSettings": "Calendar settings",
@ -1250,4 +1254,4 @@
"userIcon": "User icon" "userIcon": "User icon"
}, },
"noLogFiles": "There're no log files" "noLogFiles": "There're no log files"
} }