feat: revamp mobile database tab bar (#4160)

This commit is contained in:
Richard Shiue 2023-12-18 17:04:20 +08:00 committed by GitHub
parent 5ef9d55dca
commit eef34caf27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1566 additions and 587 deletions

View File

@ -1610,8 +1610,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
) async {
final field = find.byWidgetPredicate(
(widget) =>
widget is DesktopDatabasePropertyCell &&
widget.fieldInfo.name == fieldName,
widget is DatabasePropertyCell && widget.fieldInfo.name == fieldName,
);
final toggleVisibilityButton =
find.descendant(of: field, matching: find.byType(FlowyIconButton));

View File

@ -106,9 +106,11 @@ class _MobileViewPageState extends State<MobileViewPage> {
Widget _buildApp(ViewPB? view, List<Widget> actions, Widget child) {
final icon = view?.icon.value;
final elevation = (view?.layout.isDatabaseView ?? false) ? 0.0 : null;
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
elevation: elevation,
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -118,9 +120,9 @@ class _MobileViewPageState extends State<MobileViewPage> {
fontSize: 22.0,
),
Expanded(
child: FlowyText.regular(
child: FlowyText.medium(
view?.name ?? widget.title ?? '',
fontSize: 14.0,
fontSize: 15.0,
overflow: TextOverflow.ellipsis,
),
),

View File

@ -4,7 +4,6 @@ import 'package:appflowy/mobile/presentation/database/board/board.dart';
import 'package:appflowy/mobile/presentation/database/board/widgets/group_card_header.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
@ -57,7 +56,7 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
final config = AppFlowyBoardConfig(
groupCornerRadius: 8,
groupBackgroundColor: Theme.of(context).colorScheme.secondary,
groupMargin: const EdgeInsets.fromLTRB(4, 8, 4, 12),
groupMargin: const EdgeInsets.fromLTRB(4, 0, 4, 12),
groupHeaderPadding: const EdgeInsets.all(8),
groupBodyPadding: const EdgeInsets.all(4),
groupFooterPadding: const EdgeInsets.all(8),
@ -82,47 +81,33 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
final showCreateGroupButton =
context.read<BoardBloc>().groupingFieldType.canCreateNewGroup;
final showHiddenGroups = state.hiddenGroups.isNotEmpty;
return Column(
children: [
Divider(
height: 1,
thickness: 1,
indent: GridSize.leadingHeaderPadding / 2,
endIndent: GridSize.leadingHeaderPadding / 2,
return AppFlowyBoard(
boardScrollController: scrollManager,
scrollController: scrollController,
controller: context.read<BoardBloc>().boardController,
groupConstraints: BoxConstraints.tightFor(width: screenWidth * 0.7),
config: config,
leading: showHiddenGroups
? MobileHiddenGroupsColumn(
padding: config.groupHeaderPadding,
)
: const HSpace(16),
trailing: showCreateGroupButton
? const MobileBoardTrailing()
: const HSpace(16),
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(),
child: GroupCardHeader(
groupData: groupData,
),
Expanded(
child: AppFlowyBoard(
boardScrollController: scrollManager,
scrollController: scrollController,
controller: context.read<BoardBloc>().boardController,
groupConstraints:
BoxConstraints.tightFor(width: screenWidth * 0.7),
config: config,
leading: showHiddenGroups
? MobileHiddenGroupsColumn(
padding: config.groupHeaderPadding,
)
: const HSpace(16),
trailing: showCreateGroupButton
? const MobileBoardTrailing()
: const HSpace(16),
headerBuilder: (_, groupData) =>
BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(),
child: GroupCardHeader(
groupData: groupData,
),
),
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context: context,
afGroupData: column,
afGroupItem: columnItem,
cardMargin: config.cardMargin,
),
),
),
],
),
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context: context,
afGroupData: column,
afGroupItem: columnItem,
cardMargin: config.cardMargin,
),
);
},
),

View File

@ -25,7 +25,7 @@ class _MobileBoardTrailingState extends State<MobileBoardTrailing> {
final style = Theme.of(context);
return Container(
margin: const EdgeInsets.all(8),
margin: const EdgeInsets.symmetric(horizontal: 8),
child: SizedBox(
width: screenSize.width * 0.7,
child: isEditing

View File

@ -1,6 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
@ -141,13 +142,13 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
showMobileBottomSheet(
context,
backgroundColor: Theme.of(context).colorScheme.background,
padding: const EdgeInsets.only(top: 4, bottom: 32),
padding: const EdgeInsets.only(top: 8, bottom: 36),
builder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _CardActionButton(
child: MobileQuickActionButton(
onTap: () {
final rowId = _bloc.state.currentRowId;
if (rowId == null) {
@ -169,7 +170,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
const Divider(height: 9),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _CardActionButton(
child: MobileQuickActionButton(
onTap: () {
final rowId = _bloc.state.currentRowId;
if (rowId == null) {
@ -196,39 +197,6 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
}
}
class _CardActionButton extends StatelessWidget {
const _CardActionButton({
required this.onTap,
required this.icon,
required this.text,
this.color,
});
final VoidCallback onTap;
final FlowySvgData icon;
final String text;
final Color? color;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: [
FlowySvg(icon, size: const Size.square(20), color: color),
const HSpace(8),
FlowyText(text, fontSize: 15, color: color),
],
),
),
);
}
}
class RowDetailFab extends StatelessWidget {
const RowDetailFab({
super.key,

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/field/bottom_sheet_create_field.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';

View File

@ -22,13 +22,10 @@ class OptionTextField extends StatelessWidget {
controller: controller,
textFieldPadding: const EdgeInsets.symmetric(horizontal: 12.0),
onTextChanged: onTextChanged,
leftIcon: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: FlowySvg(
type.svgData,
size: const Size.square(36.0),
blendMode: null,
),
leftIcon: FlowySvg(
type.svgData,
size: const Size.square(36.0),
blendMode: null,
),
);
}

View File

@ -180,24 +180,24 @@ class _DateCellEditBody extends StatelessWidget {
showTopBorder: false,
child: _IncludeTimePicker(),
),
_ColoredDivider(),
_Divider(),
FlowyOptionDecorateBox(
child: MobileDatePicker(),
),
_ColoredDivider(),
_Divider(),
_EndDateSwitch(),
_IncludeTimeSwitch(),
_ColoredDivider(),
_Divider(),
_ClearDateButton(),
_ColoredDivider(),
_Divider(),
],
),
);
}
}
class _ColoredDivider extends StatelessWidget {
const _ColoredDivider();
class _Divider extends StatelessWidget {
const _Divider();
@override
Widget build(BuildContext context) {
@ -364,7 +364,7 @@ class _EndDateSwitch extends StatelessWidget {
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
selector: (state) => state.isRange,
builder: (context, isRange) {
return FlowyOptionTile.switcher(
return FlowyOptionTile.toggle(
text: LocaleKeys.grid_field_isRange.tr(),
isSelected: isRange,
onValueChanged: (value) {
@ -386,7 +386,7 @@ class _IncludeTimeSwitch extends StatelessWidget {
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
selector: (state) => state.includeTime,
builder: (context, includeTime) {
return FlowyOptionTile.switcher(
return FlowyOptionTile.toggle(
showTopBorder: false,
text: LocaleKeys.grid_field_includeTime.tr(),
isSelected: includeTime,

View File

@ -1,7 +1,4 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_field_type_grid.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_field_type_option_editor.dart';
import 'package:appflowy/mobile/presentation/database/field/quick_edit_field_bottom_sheet.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:flutter/material.dart';
@ -9,6 +6,9 @@ import 'package:go_router/go_router.dart';
import 'mobile_create_field_screen.dart';
import 'mobile_edit_field_screen.dart';
import 'mobile_field_type_grid.dart';
import 'mobile_field_type_option_editor.dart';
import 'mobile_quick_field_editor.dart';
void showCreateFieldBottomSheet(BuildContext context, String viewId) {
showMobileBottomSheet(
@ -95,13 +95,10 @@ void showQuickEditField(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
resizeToAvoidBottomInset: true,
builder: (context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 500),
child: SingleChildScrollView(
child: QuickEditField(
viewId: viewId,
fieldInfo: fieldInfo,
),
return SingleChildScrollView(
child: QuickEditField(
viewId: viewId,
fieldInfo: fieldInfo,
),
);
},

View File

@ -440,12 +440,10 @@ class _DateOptionState extends State<_DateOption> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 6.0,
horizontal: 16.0,
),
padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0),
child: FlowyText(
LocaleKeys.grid_field_dateFormat.tr(),
LocaleKeys.grid_field_dateFormat.tr().toUpperCase(),
fontSize: 13,
color: Theme.of(context).hintColor,
),
),
@ -496,12 +494,10 @@ class _TimeOptionState extends State<_TimeOption> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 6.0,
horizontal: 16.0,
),
padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0),
child: FlowyText(
LocaleKeys.grid_field_timeFormat.tr(),
LocaleKeys.grid_field_timeFormat.tr().toUpperCase(),
fontSize: 13,
color: Theme.of(context).hintColor,
),
),
@ -629,12 +625,10 @@ class _SelectOption extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 6.0,
horizontal: 16.0,
),
padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0),
child: FlowyText(
LocaleKeys.grid_field_optionTitle.tr(),
LocaleKeys.grid_field_optionTitle.tr().toUpperCase(),
fontSize: 13,
color: Theme.of(context).hintColor,
),
),
@ -770,7 +764,10 @@ class __SelectOptionTileState extends State<_SelectOptionTile> {
textFieldHintText: LocaleKeys.grid_field_typeANewOption.tr(),
showTopBorder: widget.showTopBorder,
showBottomBorder: widget.showBottomBorder,
textFieldPadding: const EdgeInsets.symmetric(horizontal: 16.0),
textFieldPadding: const EdgeInsets.symmetric(
horizontal: 0.0,
vertical: 16.0,
),
trailing: _SelectOptionColor(
color: option.color,
onChanged: (color) {

View File

@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart';
import 'package:appflowy/mobile/presentation/database/field/bottom_sheet_create_field.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database_view/application/field/field_backend_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
@ -82,15 +82,16 @@ class _QuickEditFieldState extends State<QuickEditField> {
}
},
),
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.grid_field_hide.tr(),
leftIcon: const FlowySvg(FlowySvgs.hide_s),
onTap: () async {
context.pop();
await service.hide();
},
),
if (!widget.fieldInfo.isPrimary)
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.grid_field_hide.tr(),
leftIcon: const FlowySvg(FlowySvgs.hide_s),
onTap: () async {
context.pop();
await service.hide();
},
),
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.grid_field_insertLeft.tr(),
@ -133,6 +134,7 @@ class _QuickEditFieldState extends State<QuickEditField> {
},
),
],
const VSpace(12),
],
);
}

View File

@ -0,0 +1,245 @@
import 'dart:ui';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/field_visibility_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../field/mobile_field_bottom_sheets.dart';
class MobileDatabaseFieldList extends StatelessWidget {
const MobileDatabaseFieldList({
super.key,
required this.databaseController,
required this.viewPB,
});
final DatabaseController databaseController;
final ViewPB viewPB;
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
expand: false,
snap: true,
initialChildSize: 1.0,
minChildSize: 0.0,
builder: (context, controller) {
return Material(
child: Column(
children: [
const Center(child: DragHandler()),
const _MobileDatabaseFieldListHeader(),
Expanded(
child: SingleChildScrollView(
child: _MobileDatabaseFieldListBody(
databaseController: databaseController,
view: viewPB,
),
),
),
],
),
);
},
);
}
}
class _MobileDatabaseFieldListHeader extends StatelessWidget {
const _MobileDatabaseFieldListHeader();
@override
Widget build(BuildContext context) {
const iconWidth = 30.0;
return Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
child: Stack(
children: [
Align(
alignment: Alignment.centerLeft,
child: FlowyIconButton(
icon: const FlowySvg(
FlowySvgs.arrow_left_m,
size: Size.square(iconWidth),
),
width: iconWidth,
iconPadding: EdgeInsets.zero,
onPressed: () => Navigator.of(context).maybePop(),
),
),
Align(
alignment: Alignment.center,
child: FlowyText.medium(
LocaleKeys.grid_settings_properties.tr(),
fontSize: 16,
),
),
],
),
);
}
}
class _MobileDatabaseFieldListBody extends StatelessWidget {
const _MobileDatabaseFieldListBody({
required this.databaseController,
required this.view,
});
final DatabaseController databaseController;
final ViewPB view;
@override
Widget build(BuildContext context) {
return BlocProvider<DatabasePropertyBloc>(
create: (_) => DatabasePropertyBloc(
viewId: view.id,
fieldController: databaseController.fieldController,
)..add(const DatabasePropertyEvent.initial()),
child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(
builder: (context, state) {
final cells = state.fieldContexts
.mapIndexed(
(index, field) => DatabaseFieldListTile(
key: ValueKey(field.id),
viewId: view.id,
fieldController: databaseController.fieldController,
fieldInfo: field,
index: index,
showTopBorder: index == 0,
),
)
.toList();
return ReorderableListView.builder(
proxyDecorator: (_, index, anim) {
final field = state.fieldContexts[index];
return AnimatedBuilder(
animation: anim,
builder: (BuildContext context, Widget? child) {
final double animValue =
Curves.easeInOut.transform(anim.value);
final double scale = lerpDouble(1, 1.05, animValue)!;
return Transform.scale(
scale: scale,
child: Material(
child: DatabaseFieldListTile(
key: ValueKey(field.id),
viewId: view.id,
fieldController: databaseController.fieldController,
fieldInfo: field,
index: index,
showTopBorder: true,
),
),
);
},
);
},
buildDefaultDragHandles: true,
shrinkWrap: true,
onReorder: (from, to) {
context
.read<DatabasePropertyBloc>()
.add(DatabasePropertyEvent.moveField(from, to));
},
footer: Column(
mainAxisSize: MainAxisSize.min,
children: [
_divider(),
_NewDatabaseFieldTile(viewId: view.id),
],
),
itemCount: cells.length,
itemBuilder: (context, index) => cells[index],
);
},
),
);
}
Widget _divider() => const VSpace(20);
}
class DatabaseFieldListTile extends StatelessWidget {
const DatabaseFieldListTile({
super.key,
required this.index,
required this.fieldInfo,
required this.viewId,
required this.fieldController,
required this.showTopBorder,
});
final int index;
final FieldInfo fieldInfo;
final String viewId;
final FieldController fieldController;
final bool showTopBorder;
@override
Widget build(BuildContext context) {
if (fieldInfo.field.isPrimary) {
return FlowyOptionTile.text(
text: fieldInfo.name,
leftIcon: FlowySvg(
fieldInfo.fieldType.icon(),
size: const Size.square(20),
),
showTopBorder: showTopBorder,
);
} else {
return FlowyOptionTile.toggle(
isSelected: fieldInfo.visibility?.isVisibleState() ?? false,
text: fieldInfo.name,
leftIcon: FlowySvg(
fieldInfo.fieldType.icon(),
size: const Size.square(20),
),
showTopBorder: showTopBorder,
onValueChanged: (value) {
final newVisibility = fieldInfo.visibility!.toggle();
context.read<DatabasePropertyBloc>().add(
DatabasePropertyEvent.setFieldVisibility(
fieldInfo.id,
newVisibility,
),
);
},
);
}
}
}
class _NewDatabaseFieldTile extends StatelessWidget {
const _NewDatabaseFieldTile({required this.viewId});
final String viewId;
@override
Widget build(BuildContext context) {
return FlowyOptionTile.text(
text: LocaleKeys.grid_field_newProperty.tr(),
leftIcon: FlowySvg(
FlowySvgs.add_s,
size: const Size.square(20),
color: Theme.of(context).hintColor,
),
textColor: Theme.of(context).hintColor,
onTap: () => showCreateFieldBottomSheet(context, viewId),
);
}
}

View File

@ -0,0 +1,116 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
/// [DatabaseViewLayoutPicker] is seen when changing the layout type of a
/// database view or creating a new database view.
class DatabaseViewLayoutPicker extends StatelessWidget {
const DatabaseViewLayoutPicker({
super.key,
required this.selectedLayout,
required this.onSelect,
});
final DatabaseLayoutPB selectedLayout;
final void Function(DatabaseLayoutPB layout) onSelect;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildButton(DatabaseLayoutPB.Grid, true),
_buildButton(DatabaseLayoutPB.Board, false),
_buildButton(DatabaseLayoutPB.Calendar, false),
],
);
}
Widget _buildButton(DatabaseLayoutPB layout, bool showTopBorder) {
return FlowyOptionTile.checkbox(
text: layout.layoutName,
leftIcon: FlowySvg(layout.icon, size: const Size.square(20)),
isSelected: selectedLayout == layout,
showTopBorder: showTopBorder,
onTap: () {
onSelect(layout);
},
);
}
}
class MobileCalendarViewLayoutSettings extends StatelessWidget {
const MobileCalendarViewLayoutSettings({
super.key,
required this.databaseController,
});
final DatabaseController databaseController;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_layoutField(),
_divider(),
..._startWeek(context),
],
);
}
Widget _layoutField() {
return FlowyOptionTile.text(
text: LocaleKeys.calendar_settings_layoutDateField.tr(),
);
}
Widget _divider() => const VSpace(20);
List<Widget> _startWeek(BuildContext context) {
final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
return [
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0),
child: FlowyText(
LocaleKeys.calendar_settings_firstDayOfWeek.tr().toUpperCase(),
fontSize: 13,
color: Theme.of(context).hintColor,
),
),
FlowyOptionTile.checkbox(
text: symbols.WEEKDAYS[-1],
isSelected: true,
onTap: () {},
),
FlowyOptionTile.checkbox(
text: symbols.WEEKDAYS[0],
isSelected: false,
showTopBorder: false,
onTap: () {},
),
];
}
}
class MobileBoardViewLayoutSettings extends StatelessWidget {
const MobileBoardViewLayoutSettings({
super.key,
required this.databaseController,
});
final DatabaseController databaseController;
@override
Widget build(BuildContext context) {
return FlowyOptionTile.text(text: LocaleKeys.board_groupBy.tr());
}
}

View File

@ -0,0 +1,164 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'database_view_layout.dart';
/// [MobileDatabaseViewList] shows a list of all the views in the database and
/// adds a button to create a new database view.
class MobileDatabaseViewList extends StatelessWidget {
const MobileDatabaseViewList({super.key, required this.views});
final List<ViewPB> views;
@override
Widget build(BuildContext context) {
return Builder(
builder: (context) {
final children = [
...views.map((view) => MobileDatabaseViewListButton(view: view)),
const VSpace(20),
const MobileNewDatabaseViewButton(),
];
return Column(
children: children,
);
},
);
}
}
@visibleForTesting
class MobileDatabaseViewListButton extends StatelessWidget {
const MobileDatabaseViewListButton({super.key, required this.view});
final ViewPB view;
@override
Widget build(BuildContext context) {
return FlowyOptionTile.text(
text: view.name,
onTap: () {},
leftIcon: _buildViewIconButton(context, view),
trailing: FlowySvg(
FlowySvgs.three_dots_s,
size: const Size.square(20),
color: Theme.of(context).hintColor,
),
);
}
Widget _buildViewIconButton(BuildContext context, ViewPB view) {
return view.icon.value.isNotEmpty
? EmojiText(
emoji: view.icon.value,
fontSize: 16.0,
)
: SizedBox.square(
dimension: 20.0,
child: view.defaultIcon(),
);
}
}
class MobileNewDatabaseViewButton extends StatelessWidget {
const MobileNewDatabaseViewButton({super.key});
@override
Widget build(BuildContext context) {
return FlowyOptionTile.text(
text: LocaleKeys.grid_settings_createView.tr(),
textColor: Theme.of(context).hintColor,
leftIcon: FlowySvg(
FlowySvgs.add_s,
size: const Size.square(20),
color: Theme.of(context).hintColor,
),
trailing: FlowySvg(
FlowySvgs.three_dots_s,
size: const Size.square(20),
color: Theme.of(context).hintColor,
),
onTap: () {},
);
}
}
class MobileCreateDatabaseView extends StatefulWidget {
const MobileCreateDatabaseView({super.key});
@override
State<MobileCreateDatabaseView> createState() =>
_MobileCreateDatabaseViewState();
}
class _MobileCreateDatabaseViewState extends State<MobileCreateDatabaseView> {
late final TextEditingController controller;
DatabaseLayoutPB layoutType = DatabaseLayoutPB.Grid;
String icon = "";
@override
void initState() {
super.initState();
controller =
TextEditingController(text: LocaleKeys.grid_title_placeholder.tr());
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
FlowyOptionTile.textField(
controller: controller,
leftIcon: _buildViewIcon(),
),
const VSpace(20),
DatabaseViewLayoutPicker(
selectedLayout: DatabaseLayoutPB.Grid,
onSelect: (layout) {},
),
],
);
}
Widget _buildViewIcon() {
final viewIcon = icon.isNotEmpty
? EmojiText(
emoji: icon,
fontSize: 16.0,
)
: SizedBox.square(
dimension: 18.0,
child: FlowySvg(layoutType.icon),
);
return InkWell(
onTap: () {},
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
),
),
width: 36,
height: 36,
child: Center(child: viewIcon),
),
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
/// [MobileDatabaseViewQuickActions] is gives users to quickly edit a database
/// view from the [MobileDatabaseViewList]
class MobileDatabaseViewQuickActions extends StatelessWidget {
const MobileDatabaseViewQuickActions({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_actionButton(_Action.edit),
_divider(),
_actionButton(_Action.duplicate),
_divider(),
_actionButton(_Action.delete),
_divider(),
],
);
}
Widget _actionButton(_Action action) {
return MobileQuickActionButton(
icon: action.icon,
text: action.label,
onTap: () {},
);
}
Widget _divider() => const Divider(height: 9);
}
enum _Action {
edit,
duplicate,
delete;
String get label {
return switch (this) {
edit => LocaleKeys.grid_settings_editView.tr(),
duplicate => LocaleKeys.button_duplicate.tr(),
delete => LocaleKeys.button_delete.tr(),
};
}
FlowySvgData get icon {
return switch (this) {
edit => FlowySvgs.grid_s,
duplicate => FlowySvgs.copy_s,
delete => FlowySvgs.delete_s,
};
}
Color? color(BuildContext context) {
return switch (this) {
delete => Theme.of(context).colorScheme.error,
_ => null,
};
}
}

View File

@ -0,0 +1,35 @@
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'edit_database_view_cubit.freezed.dart';
class MobileEditDatabaseViewCubit extends Cubit<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

@ -0,0 +1,441 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/database_view_service.dart';
import 'package:appflowy/plugins/database_view/application/layout/layout_service.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'database_field_list.dart';
import 'database_view_layout.dart';
import 'edit_database_view_cubit.dart';
/// [MobileEditDatabaseViewScreen] is the main widget used to edit a database
/// view. It contains multiple sub-pages, and the current page is managed by
/// [MobileEditDatabaseViewCubit]
class MobileEditDatabaseViewScreen extends StatefulWidget {
const MobileEditDatabaseViewScreen({
super.key,
required this.databaseController,
required this.viewPB,
});
final DatabaseController databaseController;
final ViewPB viewPB;
@override
State<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,
viewPB: widget.viewPB,
),
MobileEditDatabaseViewPageEnum.fields => _wrapSubPage(
context,
MobileDatabaseFieldList(
databaseController: widget.databaseController,
viewPB: widget.viewPB,
),
),
_ => const SizedBox.shrink(),
};
},
),
);
}
Widget _wrapSubPage(BuildContext context, Widget child) {
return PopScope(
canPop: false,
child: child,
onPopInvoked: (_) {
context
.read<MobileEditDatabaseViewCubit>()
.changePage(MobileEditDatabaseViewPageEnum.main);
},
);
}
}
class _EditDatabaseViewMainPage extends StatelessWidget {
const _EditDatabaseViewMainPage({
required this.databaseController,
required this.viewPB,
});
final DatabaseController databaseController;
final ViewPB viewPB;
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
expand: false,
snap: true,
initialChildSize: 1.0,
minChildSize: 0.0,
builder: (context, controller) {
return Material(
child: Column(
children: [
const Center(child: DragHandler()),
const _EditDatabaseViewHeader(),
Expanded(
child: SingleChildScrollView(
child: _EditDatabaseViewBody(
databaseController: databaseController,
view: viewPB,
),
),
),
],
),
);
},
);
}
}
class _EditDatabaseViewHeader extends StatelessWidget {
const _EditDatabaseViewHeader();
@override
Widget build(BuildContext context) {
const iconWidth = 30.0;
return Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
child: Stack(
children: [
Align(
alignment: Alignment.centerLeft,
child: FlowyIconButton(
icon: const FlowySvg(
FlowySvgs.close_s,
size: Size.square(iconWidth),
),
width: iconWidth,
iconPadding: EdgeInsets.zero,
onPressed: () => context.pop(),
),
),
Align(
alignment: Alignment.center,
child: FlowyText.medium(
LocaleKeys.grid_settings_editView.tr(),
fontSize: 16,
),
),
],
),
);
}
}
class _EditDatabaseViewBody extends StatelessWidget {
const _EditDatabaseViewBody({
required this.databaseController,
required this.view,
});
final DatabaseController databaseController;
final ViewPB view;
@override
Widget build(BuildContext context) {
return BlocProvider<ViewBloc>(
create: (context) {
return ViewBloc(view: view)..add(const ViewEvent.initial());
},
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_NameAndIcon(view: state.view),
_divider(),
DatabaseViewSettingTile(
setting: DatabaseViewSettings.layout,
databaseController: databaseController,
view: state.view,
showTopBorder: true,
),
DatabaseViewSettingTile(
setting: DatabaseViewSettings.fields,
databaseController: databaseController,
view: state.view,
),
_divider(),
],
);
},
),
);
}
Widget _divider() => const VSpace(20);
}
class _NameAndIcon extends StatefulWidget {
const _NameAndIcon({required this.view});
final ViewPB view;
@override
State<_NameAndIcon> createState() => _NameAndIconState();
}
class _NameAndIconState extends State<_NameAndIcon> {
final TextEditingController textEditingController = TextEditingController();
@override
void initState() {
super.initState();
textEditingController.text = widget.view.name;
}
@override
Widget build(BuildContext context) {
return FlowyOptionTile.textField(
controller: textEditingController,
textFieldPadding: const EdgeInsets.symmetric(horizontal: 12.0),
onTextChanged: (text) {
context.read<ViewBloc>().add(ViewEvent.rename(text));
},
leftIcon: _buildViewIcon(),
);
}
Widget _buildViewIcon() {
final icon = widget.view.icon.value.isNotEmpty
? EmojiText(
emoji: widget.view.icon.value,
fontSize: 16.0,
)
: SizedBox.square(
dimension: 18.0,
child: widget.view.defaultIcon(),
);
return InkWell(
onTap: () {},
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
),
),
width: 36,
height: 36,
child: Center(child: icon),
),
);
}
}
enum DatabaseViewSettings {
layout,
fields,
filter,
sort,
board,
calendar,
duplicate,
delete;
String get label {
return switch (this) {
layout => LocaleKeys.grid_settings_databaseLayout.tr(),
fields => LocaleKeys.grid_settings_properties.tr(),
filter => LocaleKeys.grid_settings_filter.tr(),
sort => LocaleKeys.grid_settings_sort.tr(),
board => LocaleKeys.grid_settings_boardSettings.tr(),
calendar => LocaleKeys.grid_settings_calendarSettings.tr(),
duplicate => LocaleKeys.grid_settings_duplicateView.tr(),
delete => LocaleKeys.grid_settings_deleteView.tr(),
};
}
FlowySvgData get icon {
return switch (this) {
layout => FlowySvgs.card_view_s,
fields => FlowySvgs.disorder_list_s,
filter => FlowySvgs.filter_s,
sort => FlowySvgs.sort_ascending_s,
board => FlowySvgs.board_s,
calendar => FlowySvgs.date_s,
duplicate => FlowySvgs.copy_s,
delete => FlowySvgs.delete_s,
};
}
MobileEditDatabaseViewPageEnum? get subPage {
return switch (this) {
fields => MobileEditDatabaseViewPageEnum.fields,
filter => MobileEditDatabaseViewPageEnum.filter,
sort => MobileEditDatabaseViewPageEnum.sort,
_ => null,
};
}
}
class DatabaseViewSettingTile extends StatelessWidget {
const DatabaseViewSettingTile({
super.key,
required this.setting,
required this.databaseController,
required this.view,
this.showTopBorder = false,
});
final DatabaseViewSettings setting;
final DatabaseController databaseController;
final ViewPB view;
final bool showTopBorder;
@override
Widget build(BuildContext context) {
return FlowyOptionTile.text(
text: setting.label,
leftIcon: FlowySvg(setting.icon, size: const Size.square(20)),
trailing: _trailing(context, setting, view, databaseController),
showTopBorder: showTopBorder,
onTap: () => _onTap(context),
);
}
Widget _trailing(
BuildContext context,
DatabaseViewSettings setting,
ViewPB view,
DatabaseController databaseController,
) {
switch (setting) {
case DatabaseViewSettings.layout:
return Row(
children: [
FlowyText(
databaseLayoutFromViewLayout(view.layout).layoutName,
color: Theme.of(context).hintColor,
),
const HSpace(8),
const FlowySvg(FlowySvgs.arrow_right_s),
],
);
case DatabaseViewSettings.fields:
final numVisible = databaseController.fieldController.fieldInfos
.where((field) => field.visibility != FieldVisibility.AlwaysHidden)
.length;
return Row(
children: [
FlowyText(
"$numVisible shown",
color: Theme.of(context).hintColor,
),
const HSpace(8),
const FlowySvg(FlowySvgs.arrow_right_s),
],
);
default:
return const SizedBox.shrink();
}
}
void _onTap(BuildContext context) async {
final subPage = setting.subPage;
if (subPage != null) {
context.read<MobileEditDatabaseViewCubit>().changePage(subPage);
return;
}
if (setting == DatabaseViewSettings.layout) {
final databaseLayout = databaseLayoutFromViewLayout(view.layout);
final newLayout = await showMobileBottomSheet<DatabaseLayoutPB>(
context,
padding: EdgeInsets.zero,
resizeToAvoidBottomInset: false,
builder: (context) {
return Padding(
padding: const EdgeInsets.only(top: 24, bottom: 46),
child: DatabaseViewLayoutPicker(
selectedLayout: databaseLayout,
onSelect: (layout) {
Navigator.of(context).pop(layout);
},
),
);
},
);
if (newLayout != null && newLayout != databaseLayout) {
DatabaseViewBackendService.updateLayout(
viewId: databaseController.viewId,
layout: newLayout,
);
}
return;
}
if (setting == DatabaseViewSettings.board) {
showMobileBottomSheet<DatabaseLayoutPB>(
context,
padding: EdgeInsets.zero,
resizeToAvoidBottomInset: false,
builder: (context) {
return Padding(
padding: const EdgeInsets.only(top: 24, bottom: 46),
child: MobileBoardViewLayoutSettings(
databaseController: databaseController,
),
);
},
);
return;
}
if (setting == DatabaseViewSettings.calendar) {
showMobileBottomSheet<DatabaseLayoutPB>(
context,
padding: EdgeInsets.zero,
resizeToAvoidBottomInset: false,
builder: (context) {
return Padding(
padding: const EdgeInsets.only(top: 24, bottom: 46),
child: MobileCalendarViewLayoutSettings(
databaseController: databaseController,
),
);
},
);
return;
}
if (setting == DatabaseViewSettings.delete) {
context.read<ViewBloc>().add(const ViewEvent.delete());
context.pop(true);
return;
}
}
}

View File

@ -0,0 +1,37 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class MobileQuickActionButton extends StatelessWidget {
const MobileQuickActionButton({
super.key,
required this.onTap,
required this.icon,
required this.text,
this.color,
});
final VoidCallback onTap;
final FlowySvgData icon;
final String text;
final Color? color;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: [
FlowySvg(icon, size: const Size.square(20), color: color),
const HSpace(8),
FlowyText(text, fontSize: 15, color: color),
],
),
),
);
}
}

View File

@ -1,16 +1,16 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
enum FlowyOptionTileType {
text,
textField,
checkbox,
toggle,
}
// used in cell editor
class FlowyOptionTile extends StatelessWidget {
const FlowyOptionTile._({
required this.type,
@ -59,8 +59,8 @@ class FlowyOptionTile extends StatelessWidget {
void Function(String value)? onTextChanged,
void Function(String value)? onTextSubmitted,
EdgeInsets textFieldPadding = const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 2.0,
horizontal: 0.0,
vertical: 16.0,
),
bool showTopBorder = true,
bool showBottomBorder = true,
@ -69,7 +69,7 @@ class FlowyOptionTile extends StatelessWidget {
String? textFieldHintText,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.text,
type: FlowyOptionTileType.textField,
controller: controller,
textFieldPadding: textFieldPadding,
text: null,
@ -88,6 +88,7 @@ class FlowyOptionTile extends StatelessWidget {
required String text,
required bool isSelected,
required VoidCallback? onTap,
Widget? leftIcon,
bool showTopBorder = true,
bool showBottomBorder = true,
}) {
@ -98,6 +99,7 @@ class FlowyOptionTile extends StatelessWidget {
onTap: onTap,
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
leading: leftIcon,
trailing: isSelected
? const FlowySvg(
FlowySvgs.blue_check_s,
@ -108,7 +110,7 @@ class FlowyOptionTile extends StatelessWidget {
);
}
factory FlowyOptionTile.switcher({
factory FlowyOptionTile.toggle({
required String text,
required bool isSelected,
required void Function(bool value) onValueChanged,
@ -117,14 +119,14 @@ class FlowyOptionTile extends StatelessWidget {
Widget? leftIcon,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.text,
type: FlowyOptionTileType.toggle,
text: text,
controller: null,
onTap: null,
onTap: () => onValueChanged(!isSelected),
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
leading: leftIcon,
trailing: _Switcher(value: isSelected, onChanged: onValueChanged),
trailing: _Toggle(value: isSelected, onChanged: onValueChanged),
);
}
@ -150,110 +152,101 @@ class FlowyOptionTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final leadingWidget = _buildLeading();
final child = ColoredBox(
color: Theme.of(context).colorScheme.surface,
child: FlowyOptionDecorateBox(
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildText(),
..._buildTextField(),
if (controller == null) const Spacer(),
trailing ?? const SizedBox.shrink(),
const HSpace(12.0),
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (leadingWidget != null) leadingWidget,
_buildText(),
_buildTextField(),
if (trailing != null) trailing!,
],
),
),
),
);
if (type == FlowyOptionTileType.checkbox ||
type == FlowyOptionTileType.toggle ||
type == FlowyOptionTileType.text) {
return FlowyButton(
expandText: true,
margin: EdgeInsets.zero,
return GestureDetector(
onTap: onTap,
text: child,
child: child,
);
}
return child;
}
Widget? _buildLeading() {
if (leading != null) {
return Center(child: leading);
} else {
return null;
}
}
Widget _buildText() {
if (text == null) {
if (text == null || type == FlowyOptionTileType.textField) {
return const SizedBox.shrink();
}
switch (type) {
case FlowyOptionTileType.text:
return FlowyButton(
useIntrinsicWidth: true,
text: FlowyText(
text!,
color: textColor,
),
margin: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 16.0,
),
leftIcon: leading,
leftIconSize: const Size.square(24.0),
iconPadding: 8.0,
onTap: onTap,
);
case FlowyOptionTileType.textField:
return const SizedBox.shrink();
case FlowyOptionTileType.checkbox:
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 16.0,
),
child: FlowyText(
text!,
),
);
}
}
final padding = EdgeInsets.symmetric(
horizontal: leading == null ? 0.0 : 8.0,
vertical: 16.0,
);
List<Widget> _buildTextField() {
if (controller == null) {
return [
const SizedBox.shrink(),
];
}
return [
if (leading != null) leading!,
Expanded(
child: Container(
constraints: const BoxConstraints.tightFor(
height: 54.0,
),
alignment: Alignment.center,
child: TextField(
controller: controller,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: textFieldPadding,
hintText: textFieldHintText,
),
onChanged: onTextChanged,
onSubmitted: onTextSubmitted,
),
return Expanded(
child: Padding(
padding: padding,
child: FlowyText(
text!,
fontSize: 15,
color: textColor,
),
),
];
);
}
Widget _buildTextField() {
if (controller == null) {
return const SizedBox.shrink();
}
return Expanded(
child: Container(
constraints: const BoxConstraints.tightFor(
height: 54.0,
),
alignment: Alignment.center,
child: TextField(
controller: controller,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: textFieldPadding,
hintText: textFieldHintText,
),
onChanged: onTextChanged,
onSubmitted: onTextSubmitted,
),
),
);
}
}
class _Switcher extends StatelessWidget {
const _Switcher({
class _Toggle extends StatelessWidget {
const _Toggle({
required this.value,
required this.onChanged,
});
@ -263,13 +256,16 @@ class _Switcher extends StatelessWidget {
@override
Widget build(BuildContext context) {
// CupertinoSwitch adds a 8px margin all around. The original size of the
// switch is 38 x 22.
return SizedBox(
width: 48,
width: 46,
height: 30,
child: FittedBox(
fit: BoxFit.fill,
child: Switch.adaptive(
child: CupertinoSwitch(
value: value,
activeColor: const Color(0xFF00BCF0),
activeColor: Theme.of(context).colorScheme.primary,
onChanged: onChanged,
),
),

View File

@ -11,6 +11,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
import 'layout/layout_service.dart';
class DatabaseViewBackendService {
final String viewId;
DatabaseViewBackendService({
@ -25,6 +27,17 @@ class DatabaseViewBackendService {
.then((value) => value.leftMap((l) => l.value));
}
static Future<Either<ViewPB, FlowyError>> updateLayout({
required String viewId,
required DatabaseLayoutPB layout,
}) {
final payload = UpdateViewPayloadPB.create()
..viewId = viewId
..layout = viewLayoutFromDatabaseLayout(layout);
return FolderEventUpdateView(payload).send();
}
Future<Either<DatabasePB, FlowyError>> openDatabase() async {
final payload = DatabaseViewIdPB(value: viewId);
return DatabaseEventGetDatabase(payload).send();

View File

@ -2,25 +2,23 @@ import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.da
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'layout_service.dart';
import '../database_view_service.dart';
part 'layout_bloc.freezed.dart';
class DatabaseLayoutBloc
extends Bloc<DatabaseLayoutEvent, DatabaseLayoutState> {
final DatabaseLayoutBackendService layoutService;
DatabaseLayoutBloc({
required String viewId,
required DatabaseLayoutPB databaseLayout,
}) : layoutService = DatabaseLayoutBackendService(viewId),
super(DatabaseLayoutState.initial(viewId, databaseLayout)) {
}) : super(DatabaseLayoutState.initial(viewId, databaseLayout)) {
on<DatabaseLayoutEvent>(
(event, emit) async {
event.when(
initial: () {},
updateLayout: (DatabaseLayoutPB layout) {
layoutService.updateLayout(
fieldId: viewId,
DatabaseViewBackendService.updateLayout(
viewId: viewId,
layout: layout,
);
emit(state.copyWith(databaseLayout: layout));

View File

@ -1,25 +1,5 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:dartz/dartz.dart';
class DatabaseLayoutBackendService {
final String viewId;
DatabaseLayoutBackendService(this.viewId);
Future<Either<ViewPB, FlowyError>> updateLayout({
required String fieldId,
required DatabaseLayoutPB layout,
}) {
final payload = UpdateViewPayloadPB.create()
..viewId = viewId
..layout = viewLayoutFromDatabaseLayout(layout);
return FolderEventUpdateView(payload).send();
}
}
ViewLayoutPB viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
switch (databaseLayout) {

View File

@ -189,7 +189,7 @@ class _CalendarPageState extends State<CalendarPage> {
return LayoutBuilder(
// must specify MonthView width for useAvailableVerticalSpace to work properly
builder: (context, constraints) {
Widget calendar = Padding(
return Padding(
padding: PlatformExtension.isMobile
? CalendarSize.contentInsetsMobile
: CalendarSize.contentInsets,
@ -210,20 +210,6 @@ class _CalendarPageState extends State<CalendarPage> {
),
),
);
if (PlatformExtension.isMobile) {
calendar = Column(
children: [
Divider(
height: 1,
thickness: 1,
indent: GridSize.leadingHeaderPadding / 2,
endIndent: GridSize.leadingHeaderPadding / 2,
),
Expanded(child: calendar),
],
);
}
return calendar;
},
);
}

View File

@ -15,9 +15,9 @@ class CalendarSize {
static EdgeInsets get contentInsetsMobile => EdgeInsets.fromLTRB(
GridSize.leadingHeaderPadding / 2,
CalendarSize.headerContainerPadding / 2,
0,
GridSize.leadingHeaderPadding / 2,
CalendarSize.headerContainerPadding / 2,
0,
);
static double get scrollBarSize => 8 * scale;

View File

@ -38,8 +38,10 @@ class GridSize {
static EdgeInsets get footerContentInsets => EdgeInsets.fromLTRB(
GridSize.leadingHeaderPadding,
GridSize.headerContainerPadding,
GridSize.headerContainerPadding,
GridSize.headerContainerPadding,
PlatformExtension.isMobile
? GridSize.leadingHeaderPadding
: GridSize.headerContainerPadding,
PlatformExtension.isMobile ? 100 : GridSize.headerContainerPadding,
);
static EdgeInsets get contentInsets => EdgeInsets.fromLTRB(

View File

@ -2,25 +2,24 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/shortcuts.dart';
import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/mobile_database_settings_button.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
import 'grid_page.dart';
import 'grid_scroll.dart';
import 'layout/sizes.dart';
import 'widgets/header/mobile_grid_header.dart';
@ -28,8 +27,6 @@ import 'widgets/mobile_fab.dart';
import 'widgets/row/mobile_row.dart';
class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
final _toggleExtension = ToggleExtensionNotifier();
@override
Widget content(
BuildContext context,
@ -45,21 +42,15 @@ class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
}
@override
Widget settingBar(BuildContext context, DatabaseController controller) {
return MobileDatabaseSettingsButton(
key: _makeValueKey(controller),
controller: controller,
toggleExtension: _toggleExtension,
);
}
Widget settingBar(BuildContext context, DatabaseController controller) =>
const SizedBox.shrink();
@override
Widget settingBarExtension(
BuildContext context,
DatabaseController controller,
) {
return const SizedBox.shrink();
}
) =>
const SizedBox.shrink();
ValueKey _makeValueKey(DatabaseController controller) {
return ValueKey(controller.viewId);
@ -217,8 +208,7 @@ class _GridRows extends StatelessWidget {
return BlocBuilder<GridBloc, GridState>(
buildWhen: (previous, current) => previous.fields != current.fields,
builder: (context, state) {
final double contentWidth =
(state.fields.length + 1) * 200 + GridSize.leadingHeaderPadding;
final double contentWidth = _getContentWidth(state.fields);
return Expanded(
child: _WrapScrollView(
scrollController: scrollController,
@ -248,6 +238,14 @@ class _GridRows extends StatelessWidget {
);
}
double _getContentWidth(List<FieldInfo> fields) {
final visibleFields = fields.where(
(field) =>
field.fieldSettings?.visibility != FieldVisibility.AlwaysHidden,
);
return (visibleFields.length + 1) * 200 + GridSize.leadingHeaderPadding * 2;
}
Widget _renderList(
BuildContext context,
GridState state,
@ -281,20 +279,9 @@ class _GridRows extends StatelessWidget {
},
itemCount: state.rowInfos.length,
itemBuilder: (context, index) => children[index],
footer: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: GridSize.footerContentInsets,
child: _AddRowButton(),
),
Container(
height: 30,
alignment: AlignmentDirectional.centerStart,
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: const _GridFooter(),
),
],
footer: Padding(
padding: GridSize.footerContentInsets,
child: _AddRowButton(),
),
);
}
@ -360,56 +347,17 @@ class _WrapScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ScrollbarListStack(
axis: Axis.vertical,
controller: scrollController.verticalController,
barSize: GridSize.scrollBarSize,
autoHideScrollbar: false,
child: StyledSingleChildScrollView(
autoHideScrollbar: false,
controller: scrollController.horizontalController,
axis: Axis.horizontal,
child: SizedBox(
width: contentWidth,
child: child,
),
return SingleChildScrollView(
controller: scrollController.horizontalController,
scrollDirection: Axis.horizontal,
child: SizedBox(
width: contentWidth,
child: child,
),
);
}
}
class _GridFooter extends StatelessWidget {
const _GridFooter();
@override
Widget build(BuildContext context) {
return BlocSelector<GridBloc, GridState, int>(
selector: (state) => state.rowCount,
builder: (context, rowCount) {
return Padding(
padding: GridSize.contentInsets,
child: RichText(
text: TextSpan(
text: "${LocaleKeys.grid_row_count.tr()} :",
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).hintColor,
),
children: [
TextSpan(
text: ' $rowCount',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: AFThemeExtension.of(context).gridRowCountColor,
),
),
],
),
),
);
},
);
}
}
class _AddRowButton extends StatelessWidget {
@override
Widget build(BuildContext context) {

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/database/field/bottom_sheet_create_field.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/field/bottom_sheet_create_field.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';

View File

@ -37,12 +37,8 @@ class TabBarHeader extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {
return const Flexible(
child: DatabaseTabBar(),
);
},
const Flexible(
child: DatabaseTabBar(),
),
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {

View File

@ -1,18 +1,14 @@
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/database_view/application/tab_bar_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/mobile_database_settings_button.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/mobile_database_controls.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../grid/presentation/grid_page.dart';
class MobileTabBarHeader extends StatefulWidget {
const MobileTabBarHeader({super.key});
@ -22,13 +18,40 @@ class MobileTabBarHeader extends StatefulWidget {
}
class _MobileTabBarHeaderState extends State<MobileTabBarHeader> {
final controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 14),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Expanded(child: _DatabaseViewList()),
const HSpace(10),
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {
final currentView = state.tabBars.firstWhereIndexedOrNull(
(index, tabBar) => index == state.selectedIndex,
);
if (currentView == null) {
return const SizedBox.shrink();
}
return MobileDatabaseControls(
controller: state
.tabBarControllerByViewId[currentView.viewId]!.controller,
toggleExtension: ToggleExtensionNotifier(),
);
},
),
],
),
);
}
}
class _DatabaseViewList extends StatelessWidget {
const _DatabaseViewList();
@override
Widget build(BuildContext context) {
@ -42,76 +65,127 @@ class _MobileTabBarHeaderState extends State<MobileTabBarHeader> {
return const SizedBox.shrink();
}
controller.text = currentView.view.name;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 14),
child: Row(
children: [
_buildViewIconButton(currentView.view),
const HSpace(8.0),
Expanded(
child: FlowyTextField(
autoFocus: false,
maxLines: 1,
controller: controller,
textAlignVertical: TextAlignVertical.top,
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
textStyle: Theme.of(context).textTheme.titleLarge,
onSubmitted: (value) {
if (value.isNotEmpty) {
context.read<ViewBloc>().add(
ViewEvent.rename(value),
);
}
},
onCanceled: () {
controller.text = currentView.view.name;
},
),
),
MobileDatabaseSettingsButton(
controller: state
.tabBarControllerByViewId[currentView.viewId]!.controller,
toggleExtension: ToggleExtensionNotifier(),
),
],
),
);
},
);
}
Widget _buildViewIconButton(ViewPB view) {
final icon = view.icon.value.isNotEmpty
? EmojiText(
emoji: view.icon.value,
fontSize: 24.0,
)
: SizedBox.square(
dimension: 26.0,
child: view.defaultIcon(),
final children = state.tabBars.mapIndexed((index, tabBar) {
return Padding(
padding: EdgeInsetsDirectional.only(
start: index == 0 ? 0 : 2,
end: 2,
),
child: _DatabaseViewListItem(
tabBar: tabBar,
isSelected: currentView.viewId == tabBar.viewId,
),
);
return FlowyButton(
text: icon,
useIntrinsicWidth: true,
onTap: () async {
final result = await context.push<EmojiPickerResult>(
MobileEmojiPickerScreen.routeName,
}).toList();
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: children),
);
if (context.mounted && result != null) {
await ViewBackendService.updateViewIcon(
viewId: view.id,
viewIcon: result.emoji,
);
}
},
);
}
}
class _DatabaseViewListItem extends StatefulWidget {
const _DatabaseViewListItem({
required this.tabBar,
required this.isSelected,
});
final DatabaseTabBar tabBar;
final bool isSelected;
@override
State<_DatabaseViewListItem> createState() => _DatabaseViewListItemState();
}
class _DatabaseViewListItemState extends State<_DatabaseViewListItem> {
late final MaterialStatesController statesController;
@override
void initState() {
super.initState();
statesController = MaterialStatesController(
<MaterialState>{if (widget.isSelected) MaterialState.selected},
);
}
@override
void didUpdateWidget(covariant oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isSelected != oldWidget.isSelected) {
statesController.update(MaterialState.selected, widget.isSelected);
}
}
@override
Widget build(BuildContext context) {
return TextButton(
statesController: statesController,
style: ButtonStyle(
padding: const MaterialStatePropertyAll(
EdgeInsets.symmetric(horizontal: 12, vertical: 7),
),
maximumSize: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
return const Size(150, 48);
}
return const Size(120, 48);
}),
minimumSize: const MaterialStatePropertyAll(Size(48, 0)),
shape: const MaterialStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
backgroundColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
return const Color(0x0F212729);
}
return Colors.transparent;
}),
overlayColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
return Colors.transparent;
}
return Theme.of(context).colorScheme.secondary;
}),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildViewIconButton(context, widget.tabBar.view),
const HSpace(6),
Flexible(
child: FlowyText(
widget.tabBar.view.name,
fontSize: 14,
fontWeight: widget.isSelected ? FontWeight.w500 : FontWeight.w400,
overflow: TextOverflow.ellipsis,
),
),
],
),
onPressed: () {
if (!widget.isSelected) {
context
.read<DatabaseTabBarBloc>()
.add(DatabaseTabBarEvent.selectView(widget.tabBar.viewId));
}
},
);
}
Widget _buildViewIconButton(BuildContext context, ViewPB view) {
return view.icon.value.isNotEmpty
? EmojiText(
emoji: view.icon.value,
fontSize: 16.0,
)
: SizedBox.square(
dimension: 16.0,
child: view.defaultIcon(),
);
}
}

View File

@ -1,7 +1,6 @@
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/tab_bar_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/tab_bar/mobile/mobile_tab_bar_header.dart';
import 'package:appflowy/plugins/database_view/widgets/share_button.dart';
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
@ -15,6 +14,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'desktop/tab_bar_header.dart';
import 'mobile/mobile_tab_bar_header.dart';
abstract class DatabaseTabBarItemBuilder {
const DatabaseTabBarItemBuilder();
@ -102,14 +102,22 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
return const SizedBox.shrink();
}
return Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.leadingHeaderPadding,
),
child: PlatformExtension.isMobile
? const MobileTabBarHeader()
: const TabBarHeader(),
);
if (PlatformExtension.isDesktop) {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.leadingHeaderPadding,
),
child: const TabBarHeader(),
);
} else {
return Padding(
padding: EdgeInsets.only(
left: GridSize.leadingHeaderPadding,
right: 8,
),
child: const MobileTabBarHeader(),
);
}
},
);
},

View File

@ -1,17 +1,20 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/view/edit_database_view_screen.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_paginated_bottom_sheet.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/database_settings_list.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileDatabaseSettingsButton extends StatelessWidget {
const MobileDatabaseSettingsButton({
class MobileDatabaseControls extends StatelessWidget {
const MobileDatabaseControls({
super.key,
required this.controller,
required this.toggleExtension,
@ -55,17 +58,26 @@ class MobileDatabaseSettingsButton extends StatelessWidget {
return const SizedBox.shrink();
}
return SizedBox(
height: 24,
width: 24,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () => _showMobileSettings(context, controller),
icon: const FlowySvg(
FlowySvgs.m_setting_m,
size: Size.square(24),
return Row(
children: [
_DatabaseControlButton(
icon: FlowySvgs.settings_s,
onTap: () {
showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
builder: (_) => MobileEditDatabaseViewScreen(
databaseController: controller,
viewPB: context.read<ViewBloc>().state.view,
),
);
},
),
),
_DatabaseControlButton(
icon: FlowySvgs.align_left_s,
onTap: () => _showMobileSettings(context, controller),
),
],
);
},
),
@ -87,3 +99,30 @@ class MobileDatabaseSettingsButton extends StatelessWidget {
),
);
}
class _DatabaseControlButton extends StatelessWidget {
const _DatabaseControlButton({
required this.onTap,
required this.icon,
});
final VoidCallback onTap;
final FlowySvgData icon;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 36,
width: 36,
child: IconButton(
splashRadius: 18,
padding: EdgeInsets.zero,
onPressed: onTap,
icon: FlowySvg(
icon,
size: const Size.square(20),
),
),
);
}
}

View File

@ -1,7 +1,6 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/database/field/bottom_sheet_create_field.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
@ -9,10 +8,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.da
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/field_visibility_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra/theme_extension.dart';
@ -63,20 +59,10 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
fieldInfo: field,
popoverMutex: _popoverMutex,
index: index,
bloc: _bloc,
),
)
.toList();
if (PlatformExtension.isMobile) {
return ListView.separated(
shrinkWrap: true,
itemCount: cells.length,
itemBuilder: (_, index) => cells[index],
separatorBuilder: (_, __) => const VSpace(8),
);
}
return ReorderableListView(
proxyDecorator: (child, index, _) => Material(
color: Colors.transparent,
@ -110,7 +96,7 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
}
@visibleForTesting
class DatabasePropertyCell extends StatelessWidget {
class DatabasePropertyCell extends StatefulWidget {
const DatabasePropertyCell({
super.key,
required this.fieldInfo,
@ -118,7 +104,6 @@ class DatabasePropertyCell extends StatelessWidget {
required this.popoverMutex,
required this.index,
required this.fieldController,
required this.bloc,
});
final FieldInfo fieldInfo;
@ -126,126 +111,12 @@ class DatabasePropertyCell extends StatelessWidget {
final PopoverMutex popoverMutex;
final int index;
final FieldController fieldController;
final DatabasePropertyBloc bloc;
@override
Widget build(BuildContext context) {
return PlatformExtension.isMobile
? MobileDatabasePropertyCell(
fieldInfo: fieldInfo,
viewId: viewId,
fieldController: fieldController,
bloc: bloc,
)
: DesktopDatabasePropertyCell(
fieldInfo: fieldInfo,
viewId: viewId,
popoverMutex: popoverMutex,
index: index,
fieldController: fieldController,
);
}
State<DatabasePropertyCell> createState() => _DatabasePropertyCellState();
}
class MobileDatabasePropertyCell extends StatefulWidget {
const MobileDatabasePropertyCell({
super.key,
required this.fieldInfo,
required this.viewId,
required this.fieldController,
required this.bloc,
});
final FieldInfo fieldInfo;
final String viewId;
final FieldController fieldController;
final DatabasePropertyBloc bloc;
@override
State<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: () =>
showEditFieldScreen(context, widget.viewId, widget.fieldInfo),
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
children: [
FlowySvg(
widget.fieldInfo.fieldType.icon(),
color: Theme.of(context).iconTheme.color,
size: const Size.square(24),
),
const HSpace(8),
FlowyText.medium(
widget.fieldInfo.name,
color: AFThemeExtension.of(context).textColor,
),
const Spacer(),
// Toggle Visibility
Toggle(
padding: EdgeInsets.zero,
value: isVisible,
style: ToggleStyle.mobile,
onChanged: (newValue) {
final newVisibility = widget.fieldInfo.visibility!.toggle();
context.read<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> {
class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
final PopoverController _popoverController = PopoverController();
@override

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.46667 8.08256H4.4159V8.13333V11.0667V11.1174H4.46667H14.7333H14.7841V11.0667V8.13333V8.08256H14.7333H4.46667ZM3.05077 3.73333C3.05077 3.35636 3.35636 3.05077 3.73333 3.05077H15.4667C15.8436 3.05077 16.1492 3.35636 16.1492 3.73333C16.1492 4.1103 15.8436 4.4159 15.4667 4.4159H3.73333C3.35636 4.4159 3.05077 4.1103 3.05077 3.73333ZM3.05077 15.4667C3.05077 15.0897 3.35636 14.7841 3.73333 14.7841H15.4667C15.8436 14.7841 16.1492 15.0897 16.1492 15.4667C16.1492 15.8436 15.8436 16.1492 15.4667 16.1492H3.73333C3.35636 16.1492 3.05077 15.8436 3.05077 15.4667ZM3.05077 8.13333C3.05077 7.35135 3.68469 6.71744 4.46667 6.71744H14.7333C15.5153 6.71744 16.1492 7.35135 16.1492 8.13333V11.0667C16.1492 11.8486 15.5153 12.4826 14.7333 12.4826H4.46667C3.68469 12.4826 3.05077 11.8486 3.05077 11.0667V8.13333Z" fill="#2B2F36" stroke="white" stroke-width="0.101538"/>
</svg>

After

Width:  |  Height:  |  Size: 968 B

View File

@ -0,0 +1,8 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.97143 5.94286C4.50793 5.94286 4.94286 5.50793 4.94286 4.97143C4.94286 4.43492 4.50793 4 3.97143 4C3.43492 4 3 4.43492 3 4.97143C3 5.50793 3.43492 5.94286 3.97143 5.94286Z" fill="#2B2F36"/>
<path d="M7.53333 4.32381C7.17566 4.32381 6.88571 4.61376 6.88571 4.97143C6.88571 5.3291 7.17566 5.61905 7.53333 5.61905H15.9524C16.3101 5.61905 16.6 5.3291 16.6 4.97143C16.6 4.61376 16.3101 4.32381 15.9524 4.32381H7.53333Z" fill="#2B2F36"/>
<path d="M7.53333 9.50476C7.17566 9.50476 6.88571 9.79471 6.88571 10.1524C6.88571 10.5101 7.17566 10.8 7.53333 10.8H15.9524C16.3101 10.8 16.6 10.5101 16.6 10.1524C16.6 9.79471 16.3101 9.50476 15.9524 9.50476H7.53333Z" fill="#2B2F36"/>
<path d="M6.88571 15.3333C6.88571 14.9757 7.17566 14.6857 7.53333 14.6857H15.9524C16.3101 14.6857 16.6 14.9757 16.6 15.3333C16.6 15.691 16.3101 15.981 15.9524 15.981H7.53333C7.17566 15.981 6.88571 15.691 6.88571 15.3333Z" fill="#2B2F36"/>
<path d="M4.94286 10.1524C4.94286 10.6889 4.50793 11.1238 3.97143 11.1238C3.43492 11.1238 3 10.6889 3 10.1524C3 9.61588 3.43492 9.18095 3.97143 9.18095C4.50793 9.18095 4.94286 9.61588 4.94286 10.1524Z" fill="#2B2F36"/>
<path d="M3.97143 16.3048C4.50793 16.3048 4.94286 15.8698 4.94286 15.3333C4.94286 14.7968 4.50793 14.3619 3.97143 14.3619C3.43492 14.3619 3 14.7968 3 15.3333C3 15.8698 3.43492 16.3048 3.97143 16.3048Z" fill="#2B2F36"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -452,7 +452,14 @@
"filterBy": "Filter by...",
"typeAValue": "Type a value...",
"layout": "Layout",
"databaseLayout": "Layout"
"databaseLayout": "Layout",
"editView": "Edit View",
"boardSettings": "Board settings",
"calendarSettings": "Calendar settings",
"createView": "New view",
"duplicateView": "Duplicate view",
"deleteView": "Delete view",
"numberOfVisibleFields": "{} shown"
},
"textFilter": {
"contains": "Contains",
@ -1162,4 +1169,4 @@
"addField": "Add field",
"userIcon": "User icon"
}
}
}