chore: improve mobile grid page (#3939)

* chore: more typo

* chore: improve appearance of mobile grid page

* fix: focus problems with editable grid cells

* chore: apply suggestions from Mathias

* revert: the dragged header looks ugly

* chore: more suggestions from Mathias

* chore: more tarbars

* fix: scrollbar padding is a bit off

* chore: add launch tasks and fix android debug

* chore: more mobile grid improvement

* fix: initial attempt to fix cell focus bug

* chore: fix grid cell lazy loading

* chore: fix cell focus problems

* chore: update same changes to desktop

* fix: revert accessory changes due to regression

* chore: new database view name i18n

* chore: add mobile tab bar header

* fix: fiz zuh eentuhgrashun tastes

* chore: rudimentary grid header

* style: code style stuffz

* chore: restore android standard lib

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Richard Shiue 2023-11-21 22:54:09 +08:00 committed by GitHub
parent b00d29d0cd
commit 16467e9c13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 958 additions and 325 deletions

View File

@ -44,6 +44,17 @@
}, },
"cwd": "${workspaceRoot}/appflowy_flutter" "cwd": "${workspaceRoot}/appflowy_flutter"
}, },
{
"name": "AF-iOS: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For iOS",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{ {
"name": "AF-iOS: Clean + Rebuild All", "name": "AF-iOS: Clean + Rebuild All",
"request": "launch", "request": "launch",
@ -55,6 +66,17 @@
}, },
"cwd": "${workspaceRoot}/appflowy_flutter" "cwd": "${workspaceRoot}/appflowy_flutter"
}, },
{
"name": "AF-iOS-Simulator: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{ {
"name": "AF-iOS-Simulator: Clean + Rebuild All", "name": "AF-iOS-Simulator: Clean + Rebuild All",
"request": "launch", "request": "launch",
@ -66,6 +88,17 @@
}, },
"cwd": "${workspaceRoot}/appflowy_flutter" "cwd": "${workspaceRoot}/appflowy_flutter"
}, },
{
"name": "AF-Android: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For Android",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{ {
"name": "AF-Android: Clean + Rebuild All", "name": "AF-Android: Clean + Rebuild All",
"request": "launch", "request": "launch",

View File

@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_add_button.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
@ -16,7 +16,7 @@ void main() {
await tester.tapGoButton(); await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid); await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
// create a field // create a field
await tester.scrollToRight(find.byType(GridPage)); await tester.scrollToRight(find.byType(GridPage));
@ -34,7 +34,7 @@ void main() {
await tester.findFieldWithName('New field 1'); await tester.findFieldWithName('New field 1');
// go back to linked database view, expect field to be hidden // go back to linked database view, expect field to be hidden
await tester.tapTabBarLinkedViewByViewName('grid'); await tester.tapTabBarLinkedViewByViewName('Grid');
await tester.noFieldWithName('New field 1'); await tester.noFieldWithName('New field 1');
// use the settings button to show the field // use the settings button to show the field

View File

@ -1,4 +1,3 @@
import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_add_button.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -18,15 +17,15 @@ void main() {
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Create board view // Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board); await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board); tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// Create grid view // Create grid view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid); await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid); tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid);
// Create calendar view // Create calendar view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.calendar); await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Calendar);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar); tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@ -39,7 +38,7 @@ void main() {
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Create board view // Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board); await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board); tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// rename board view // rename board view
@ -64,7 +63,7 @@ void main() {
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Create board view // Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board); await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board); tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// delete the board // delete the board

View File

@ -36,8 +36,8 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/so
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart';
import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_header.dart'; import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_add_button.dart';
import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_add_button.dart'; import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_header.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
@ -1381,13 +1381,15 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(unscheduledEvent); await tapButton(unscheduledEvent);
} }
Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async { Future<void> tapCreateLinkedDatabaseViewButton(
DatabaseLayoutPB layoutType,
) async {
final findAddButton = find.byType(AddDatabaseViewButton); final findAddButton = find.byType(AddDatabaseViewButton);
await tapButton(findAddButton); await tapButton(findAddButton);
final findCreateButton = find.byWidgetPredicate( final findCreateButton = find.byWidgetPredicate(
(widget) => (widget) =>
widget is TarBarAddButtonActionCell && widget.action == action, widget is TabBarAddButtonActionCell && widget.action == layoutType,
); );
await tapButton(findCreateButton); await tapButton(findCreateButton);
} }
@ -1554,7 +1556,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async { Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
final findLayoutCell = find.byType(DatabaseViewLayoutCell); final findLayoutCell = find.byType(DatabaseViewLayoutCell);
final findText = find.byWidgetPredicate( final findText = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == layout.layoutName(), (widget) => widget is FlowyText && widget.text == layout.layoutName,
); );
final button = find.descendant( final button = find.descendant(

View File

@ -1,9 +1,11 @@
import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_add_button.dart';
import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@ -48,8 +50,8 @@ class DatabaseTabBarBloc
); );
} }
}, },
createView: (action) { createView: (layout) {
_createLinkedView(action.name, action.layoutType); _createLinkedView(layout.layoutType, layout.layoutName);
}, },
deleteView: (String viewId) async { deleteView: (String viewId) async {
final result = await ViewBackendService.delete(viewId: viewId); final result = await ViewBackendService.delete(viewId: viewId);
@ -89,10 +91,10 @@ class DatabaseTabBarBloc
(element) => element.viewId == viewId, (element) => element.viewId == viewId,
); );
if (index != -1) { if (index != -1) {
final tarBar = allTabBars.removeAt(index); final tabBar = allTabBars.removeAt(index);
// Dispose the controller when the tab is removed. // Dispose the controller when the tab is removed.
final controller = final controller =
tabBarControllerByViewId.remove(tarBar.viewId); tabBarControllerByViewId.remove(tabBar.viewId);
controller?.dispose(); controller?.dispose();
} }
@ -163,7 +165,7 @@ class DatabaseTabBarBloc
return tabBarControllerByViewId; return tabBarControllerByViewId;
} }
Future<void> _createLinkedView(String name, ViewLayoutPB layoutType) async { Future<void> _createLinkedView(ViewLayoutPB layoutType, String name) async {
final viewId = state.parentView.id; final viewId = state.parentView.id;
final databaseIdOrError = final databaseIdOrError =
await DatabaseViewBackendService(viewId: viewId).getDatabaseId(); await DatabaseViewBackendService(viewId: viewId).getDatabaseId();
@ -207,7 +209,7 @@ class DatabaseTabBarEvent with _$DatabaseTabBarEvent {
List<ViewPB> childViews, List<ViewPB> childViews,
) = _DidLoadChildViews; ) = _DidLoadChildViews;
const factory DatabaseTabBarEvent.selectView(String viewId) = _DidSelectView; const factory DatabaseTabBarEvent.selectView(String viewId) = _DidSelectView;
const factory DatabaseTabBarEvent.createView(AddButtonAction action) = const factory DatabaseTabBarEvent.createView(DatabaseLayoutPB layout) =
_CreateView; _CreateView;
const factory DatabaseTabBarEvent.renameView(String viewId, String newName) = const factory DatabaseTabBarEvent.renameView(String viewId, String newName) =
_RenameView; _RenameView;
@ -252,7 +254,9 @@ class DatabaseTabBar extends Equatable {
DatabaseTabBar({ DatabaseTabBar({
required this.view, required this.view,
}) : _builder = view.tarBarItem(); }) : _builder = PlatformExtension.isMobile
? view.mobileTabBarItem()
: view.tabBarItem();
@override @override
List<Object?> get props => [view.hashCode]; List<Object?> get props => [view.hashCode];

View File

@ -1,9 +1,10 @@
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
import 'package:appflowy/plugins/database_view/tab_bar/setting_menu.dart'; import 'package:appflowy/plugins/database_view/tab_bar/desktop/setting_menu.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
@ -40,7 +41,7 @@ class ToggleExtensionNotifier extends ChangeNotifier {
} }
} }
class GridPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder { class DesktopGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
final _toggleExtension = ToggleExtensionNotifier(); final _toggleExtension = ToggleExtensionNotifier();
@override @override
@ -255,6 +256,15 @@ class _GridRows extends StatelessWidget {
GridState state, GridState state,
List<RowInfo> rowInfos, List<RowInfo> rowInfos,
) { ) {
final children = rowInfos.mapIndexed((index, rowInfo) {
return _renderRow(
context,
rowInfo.rowId,
isDraggable: state.reorderable,
index: index,
);
}).toList()
..add(const GridRowBottomBar(key: Key('gridFooter')));
return ReorderableListView.builder( return ReorderableListView.builder(
/// TODO(Xazin): Resolve inconsistent scrollbar behavior /// TODO(Xazin): Resolve inconsistent scrollbar behavior
/// This is a workaround related to /// This is a workaround related to
@ -274,18 +284,7 @@ class _GridRows extends StatelessWidget {
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex)); context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
}, },
itemCount: rowInfos.length + 1, // the extra item is the footer itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) { itemBuilder: (context, index) => children[index],
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
isDraggable: state.reorderable,
index: index,
);
}
return const GridRowBottomBar(key: Key('gridFooter'));
},
); );
} }

View File

@ -0,0 +1,436 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.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/tab_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/log.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:linked_scroll_controller/linked_scroll_controller.dart';
import 'grid_page.dart';
import 'grid_scroll.dart';
import 'layout/layout.dart';
import 'layout/sizes.dart';
import 'widgets/footer/grid_footer.dart';
import 'widgets/header/grid_header.dart';
import 'widgets/row/row.dart';
import 'widgets/shortcuts.dart';
import 'widgets/toolbar/mobile_grid_setting.dart';
class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
final _toggleExtension = ToggleExtensionNotifier();
@override
Widget content(
BuildContext context,
ViewPB view,
DatabaseController controller,
bool shrinkWrap,
) {
return MobileGridPage(
key: _makeValueKey(controller),
view: view,
databaseController: controller,
);
}
@override
Widget settingBar(BuildContext context, DatabaseController controller) {
return MobileGridSettingButton(
key: _makeValueKey(controller),
controller: controller,
toggleExtension: _toggleExtension,
);
}
@override
Widget settingBarExtension(
BuildContext context,
DatabaseController controller,
) {
return const SizedBox.shrink();
}
ValueKey _makeValueKey(DatabaseController controller) {
return ValueKey(controller.viewId);
}
}
class MobileGridPage extends StatefulWidget {
final DatabaseController databaseController;
const MobileGridPage({
required this.view,
required this.databaseController,
this.onDeleted,
super.key,
});
final ViewPB view;
final VoidCallback? onDeleted;
@override
State<MobileGridPage> createState() => _MobileGridPageState();
}
class _MobileGridPageState extends State<MobileGridPage> {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<GridBloc>(
create: (context) => GridBloc(
view: widget.view,
databaseController: widget.databaseController,
)..add(const GridEvent.initial()),
),
],
child: BlocBuilder<GridBloc, GridState>(
builder: (context, state) {
return state.loadingState.map(
loading: (_) =>
const Center(child: CircularProgressIndicator.adaptive()),
finish: (result) => result.successOrFail.fold(
(_) => GridShortcuts(
child: GridPageContent(view: widget.view),
),
(err) => FlowyErrorPage.message(
err.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
),
),
);
},
),
);
}
}
class GridPageContent extends StatefulWidget {
final ViewPB view;
const GridPageContent({
required this.view,
super.key,
});
@override
State<GridPageContent> createState() => _GridPageContentState();
}
class _GridPageContentState extends State<GridPageContent> {
final _scrollController = GridScrollController(
scrollGroupController: LinkedScrollControllerGroup(),
);
late final ScrollController headerScrollController;
@override
void initState() {
super.initState();
headerScrollController = _scrollController.linkHorizontalController();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<GridBloc, GridState>(
buildWhen: (previous, current) => previous.fields != current.fields,
builder: (context, state) {
final contentWidth = GridLayout.headerWidth(state.fields.fields);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(right: 14),
child:
_GridHeader(headerScrollController: headerScrollController),
),
_GridRows(
viewId: state.viewId,
contentWidth: contentWidth,
scrollController: _scrollController,
),
],
);
},
);
}
}
class _GridHeader extends StatelessWidget {
final ScrollController headerScrollController;
const _GridHeader({required this.headerScrollController});
@override
Widget build(BuildContext context) {
return BlocBuilder<GridBloc, GridState>(
builder: (context, state) {
return GridHeaderSliverAdaptor(
viewId: state.viewId,
fieldController:
context.read<GridBloc>().databaseController.fieldController,
anchorScrollController: headerScrollController,
);
},
);
}
}
class _GridRows extends StatelessWidget {
final String viewId;
final double contentWidth;
final GridScrollController scrollController;
const _GridRows({
required this.viewId,
required this.contentWidth,
required this.scrollController,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: _WrapScrollView(
scrollController: scrollController,
contentWidth: contentWidth,
child: BlocBuilder<GridBloc, GridState>(
buildWhen: (previous, current) => current.reason.maybeWhen(
reorderRows: () => true,
reorderSingleRow: (reorderRow, rowInfo) => true,
delete: (item) => true,
insert: (item) => true,
orElse: () => false,
),
builder: (context, state) {
final rowInfos = state.rowInfos;
final behavior = ScrollConfiguration.of(context).copyWith(
scrollbars: false,
physics: const ClampingScrollPhysics(),
);
return ScrollConfiguration(
behavior: behavior,
child: _renderList(context, state, rowInfos),
);
},
),
),
);
}
Widget _renderList(
BuildContext context,
GridState state,
List<RowInfo> rowInfos,
) {
final children = rowInfos.mapIndexed((index, rowInfo) {
return ReorderableDelayedDragStartListener(
key: ValueKey(rowInfo.rowMeta.id),
index: index,
child: _renderRow(
context,
rowInfo.rowId,
isDraggable: state.reorderable,
index: index,
),
);
}).toList();
return ReorderableListView.builder(
scrollController: scrollController.verticalController,
buildDefaultDragHandles: false,
proxyDecorator: (child, index, animation) => Material(
color: Colors.transparent,
child: child,
),
onReorder: (fromIndex, newIndex) {
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex == toIndex) {
return;
}
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
},
itemCount: rowInfos.length,
itemBuilder: (context, index) => children[index],
footer: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: GridSize.footerContentInsets,
child: const SizedBox(
height: 42,
child: GridAddRowButton(
key: Key('gridFooter'),
),
),
),
Container(
height: 30,
alignment: AlignmentDirectional.centerStart,
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: const _GridFooter(),
),
],
),
);
}
Widget _renderRow(
BuildContext context,
RowId rowId, {
int? index,
required bool isDraggable,
Animation<double>? animation,
}) {
final rowCache = context.read<GridBloc>().getRowCache(rowId);
final rowMeta = rowCache.getRow(rowId)?.rowMeta;
/// Return placeholder widget if the rowMeta is null.
if (rowMeta == null) return const SizedBox.shrink();
final fieldController =
context.read<GridBloc>().databaseController.fieldController;
final dataController = RowController(
viewId: viewId,
rowMeta: rowMeta,
rowCache: rowCache,
);
final child = GridRow(
key: ValueKey(rowMeta.id),
rowId: rowId,
viewId: viewId,
index: index,
isDraggable: isDraggable,
dataController: dataController,
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
openDetailPage: (context, cellBuilder) {
_openRowDetailPage(
context,
rowId,
fieldController,
rowCache,
cellBuilder,
);
},
);
if (animation != null) {
return SizeTransition(
sizeFactor: animation,
child: child,
);
}
return child;
}
void _openRowDetailPage(
BuildContext context,
RowId rowId,
FieldController fieldController,
RowCache rowCache,
GridCellBuilder cellBuilder,
) {
final rowMeta = rowCache.getRow(rowId)?.rowMeta;
// Most of the cases, the rowMeta should not be null.
if (rowMeta != null) {
final dataController = RowController(
viewId: viewId,
rowMeta: rowMeta,
rowCache: rowCache,
);
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: cellBuilder,
rowController: dataController,
);
},
);
} else {
Log.warn('RowMeta is null for rowId: $rowId');
}
}
}
class _WrapScrollView extends StatelessWidget {
const _WrapScrollView({
required this.contentWidth,
required this.scrollController,
required this.child,
});
final GridScrollController scrollController;
final double contentWidth;
final Widget child;
@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,
),
),
);
}
}
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,
),
),
],
),
),
);
},
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
@ -15,16 +16,18 @@ class GridAddRowButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final color =
PlatformExtension.isMobile ? null : Theme.of(context).hintColor;
return FlowyButton( return FlowyButton(
text: FlowyText.medium( text: FlowyText.medium(
LocaleKeys.grid_row_newRow.tr(), LocaleKeys.grid_row_newRow.tr(),
color: Theme.of(context).colorScheme.tertiary, color: color,
), ),
hoverColor: AFThemeExtension.of(context).lightGreyHover, hoverColor: AFThemeExtension.of(context).lightGreyHover,
onTap: () => context.read<GridBloc>().add(const GridEvent.createRow()), onTap: () => context.read<GridBloc>().add(const GridEvent.createRow()),
leftIcon: FlowySvg( leftIcon: FlowySvg(
FlowySvgs.add_s, FlowySvgs.add_s,
color: Theme.of(context).colorScheme.tertiary, color: color,
), ),
); );
} }

View File

@ -81,7 +81,6 @@ class _GridFieldCellState extends State<GridFieldCell> {
width: state.width, width: state.width,
child: Stack( child: Stack(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
fit: StackFit.expand,
children: [button, line], children: [button, line],
), ),
); );
@ -114,7 +113,6 @@ class _GridHeaderCellContainer extends StatelessWidget {
); );
final decoration = BoxDecoration( final decoration = BoxDecoration(
border: Border( border: Border(
top: borderSide,
right: borderSide, right: borderSide,
bottom: borderSide, bottom: borderSide,
), ),
@ -123,10 +121,7 @@ class _GridHeaderCellContainer extends StatelessWidget {
return Container( return Container(
width: width, width: width,
decoration: decoration, decoration: decoration,
child: ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: child, child: child,
),
); );
} }
} }

View File

@ -3,7 +3,9 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/mobile_field_cell.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
@ -52,10 +54,7 @@ class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
return SingleChildScrollView( return SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
controller: widget.anchorScrollController, controller: widget.anchorScrollController,
child: SizedBox(
height: GridSize.headerHeight,
child: _GridHeader(viewId: widget.viewId), child: _GridHeader(viewId: widget.viewId),
),
); );
}, },
), ),
@ -94,25 +93,36 @@ class _GridHeaderState extends State<_GridHeader> {
builder: (context, state) { builder: (context, state) {
final cells = state.fields final cells = state.fields
.map( .map(
(fieldInfo) => GridFieldCell( (fieldInfo) => PlatformExtension.isDesktop
? GridFieldCell(
key: _getKeyById(fieldInfo.id), key: _getKeyById(fieldInfo.id),
viewId: widget.viewId, viewId: widget.viewId,
fieldInfo: fieldInfo, fieldInfo: fieldInfo,
)
: MobileFieldButton(
key: _getKeyById(fieldInfo.id),
viewId: widget.viewId,
field: fieldInfo,
), ),
) )
.toList(); .toList();
return Container( return ColoredBox(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: RepaintBoundary( child: RepaintBoundary(
child: ReorderableRow( child: ReorderableRow(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.center,
scrollController: ScrollController(), scrollController: ScrollController(),
header: const _CellLeading(), header: const _CellLeading(),
needsLongPressDraggable: false, needsLongPressDraggable: PlatformExtension.isMobile,
footer: _CellTrailing(viewId: widget.viewId), footer: _CellTrailing(viewId: widget.viewId),
onReorder: (int oldIndex, int newIndex) { onReorder: (int oldIndex, int newIndex) {
_onReorder(cells, oldIndex, context, newIndex); _onReorder(
cells,
oldIndex,
context,
newIndex,
);
}, },
children: cells, children: cells,
), ),
@ -123,13 +133,13 @@ class _GridHeaderState extends State<_GridHeader> {
} }
void _onReorder( void _onReorder(
List<GridFieldCell> cells, List<Widget> cells,
int oldIndex, int oldIndex,
BuildContext context, BuildContext context,
int newIndex, int newIndex,
) { ) {
if (cells.length > oldIndex) { if (cells.length > oldIndex) {
final field = cells[oldIndex].fieldInfo.field; final field = (cells[oldIndex] as GridFieldCell).fieldInfo.field;
context context
.read<GridHeaderBloc>() .read<GridHeaderBloc>()
.add(GridHeaderEvent.moveField(field, oldIndex, newIndex)); .add(GridHeaderEvent.moveField(field, oldIndex, newIndex));
@ -159,7 +169,7 @@ class _CellTrailing extends StatelessWidget {
return Container( return Container(
width: GridSize.trailHeaderPadding, width: GridSize.trailHeaderPadding,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border(top: borderSide, bottom: borderSide), border: Border(bottom: borderSide),
), ),
padding: GridSize.headerContentInsets, padding: GridSize.headerContentInsets,
child: CreateFieldButton(viewId: viewId), child: CreateFieldButton(viewId: viewId),
@ -189,8 +199,14 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
constraints: BoxConstraints.loose(const Size(240, 600)), constraints: BoxConstraints.loose(const Size(240, 600)),
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
child: FlowyButton( child: FlowyButton(
margin: PlatformExtension.isDesktop
? GridSize.cellContentInsets
: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
radius: BorderRadius.zero, radius: BorderRadius.zero,
text: FlowyText.medium(LocaleKeys.grid_field_newProperty.tr()), text: FlowyText.medium(
LocaleKeys.grid_field_newProperty.tr(),
overflow: TextOverflow.ellipsis,
),
hoverColor: AFThemeExtension.of(context).greyHover, hoverColor: AFThemeExtension.of(context).greyHover,
onTap: () async { onTap: () async {
final result = await TypeOptionBackendService.createFieldTypeOption( final result = await TypeOptionBackendService.createFieldTypeOption(

View File

@ -0,0 +1,65 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'field_type_extension.dart';
class MobileFieldButton extends StatelessWidget {
final String viewId;
final FieldInfo field;
final int? maxLines;
final BorderRadius? radius;
final EdgeInsets? margin;
const MobileFieldButton({
required this.viewId,
required this.field,
this.maxLines = 1,
this.radius = BorderRadius.zero,
this.margin,
super.key,
});
@override
Widget build(BuildContext context) {
final border = BorderSide(
color: Theme.of(context).dividerColor,
width: 1.0,
);
return Container(
width: field.fieldSettings!.width.toDouble(),
decoration: BoxDecoration(
border: Border(right: border, bottom: border),
),
child: TextButton(
onLongPress: () {
debugPrint("gimme the bottom drawer");
},
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
),
onPressed: () {},
child: Row(
children: [
FlowySvg(
field.fieldType.icon(),
color: Theme.of(context).iconTheme.color,
),
const HSpace(6),
Expanded(
child: FlowyText.medium(
field.name,
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
color: AFThemeExtension.of(context).textColor,
),
),
],
),
),
);
}
}

View File

@ -54,20 +54,16 @@ class _GridRowState extends State<GridRow> {
rowId: widget.rowId, rowId: widget.rowId,
dataController: widget.dataController, dataController: widget.dataController,
viewId: widget.viewId, viewId: widget.viewId,
); )..add(const RowEvent.initial());
_rowBloc.add(const RowEvent.initial());
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider.value( return BlocProvider.value(
value: _rowBloc, value: _rowBloc,
child: _RowEnterRegion(
child: BlocBuilder<RowBloc, RowState>( child: BlocBuilder<RowBloc, RowState>(
// The row need to rebuild when the cell count changes. // The row need to rebuild when the cell count changes.
buildWhen: (p, c) => buildWhen: (p, c) => p.rowSource != c.rowSource,
p.cellByFieldId.length != c.cellByFieldId.length ||
p.rowSource != c.rowSource,
builder: (context, state) { builder: (context, state) {
final content = Expanded( final content = Expanded(
child: RowContent( child: RowContent(
@ -79,8 +75,9 @@ class _GridRowState extends State<GridRow> {
), ),
); );
return Row( return _RowEnterRegion(
key: ValueKey(state.rowSource), key: ValueKey(state.rowSource),
child: Row(
children: [ children: [
_RowLeading( _RowLeading(
index: widget.index, index: widget.index,
@ -88,10 +85,10 @@ class _GridRowState extends State<GridRow> {
), ),
content, content,
], ],
),
); );
}, },
), ),
),
); );
} }
@ -155,20 +152,19 @@ class _RowLeadingState extends State<_RowLeading> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const InsertRowButton(), const InsertRowButton(),
if (isDraggable) ...[ if (isDraggable)
ReorderableDragStartListener( ReorderableDragStartListener(
index: widget.index!, index: widget.index!,
child: RowMenuButton( child: RowMenuButton(
isDragEnabled: isDraggable, isDragEnabled: isDraggable,
openMenu: popoverController.show, openMenu: popoverController.show,
), ),
), )
] else ...[ else
RowMenuButton( RowMenuButton(
openMenu: popoverController.show, openMenu: popoverController.show,
), ),
], ],
],
); );
} }
@ -253,8 +249,6 @@ class RowContent extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
return IntrinsicHeight( return IntrinsicHeight(
child: Row( child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
..._makeCells(context, state.cellByFieldId), ..._makeCells(context, state.cellByFieldId),
@ -277,7 +271,6 @@ class RowContent extends StatelessWidget {
return CellContainer( return CellContainer(
width: cellId.fieldInfo.fieldSettings?.width.toDouble() ?? 140, width: cellId.fieldInfo.fieldSettings?.width.toDouble() ?? 140,
isPrimary: cellId.fieldInfo.field.isPrimary, isPrimary: cellId.fieldInfo.field.isPrimary,
cellContainerNotifier: CellContainerNotifier(child),
accessoryBuilder: (buildContext) { accessoryBuilder: (buildContext) {
final builder = child.accessoryBuilder; final builder = child.accessoryBuilder;
final List<GridCellAccessoryBuilder> accessories = []; final List<GridCellAccessoryBuilder> accessories = [];
@ -317,7 +310,6 @@ class RowContent extends StatelessWidget {
bottom: BorderSide(color: Theme.of(context).dividerColor), bottom: BorderSide(color: Theme.of(context).dividerColor),
), ),
), ),
child: const SizedBox.shrink(),
), ),
); );
} }

View File

@ -90,7 +90,7 @@ class DatabaseViewLayoutCell extends StatelessWidget {
child: FlowyButton( child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover, hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium( text: FlowyText.medium(
databaseLayout.layoutName(), databaseLayout.layoutName,
color: AFThemeExtension.of(context).textColor, color: AFThemeExtension.of(context).textColor,
), ),
leftIcon: FlowySvg( leftIcon: FlowySvg(

View File

@ -0,0 +1,63 @@
import 'package:appflowy/generated/flowy_svgs.g.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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileGridSettingButton extends StatelessWidget {
final DatabaseController controller;
final ToggleExtensionNotifier toggleExtension;
const MobileGridSettingButton({
required this.controller,
required this.toggleExtension,
super.key,
});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<GridFilterMenuBloc>(
create: (context) => GridFilterMenuBloc(
viewId: controller.viewId,
fieldController: controller.fieldController,
)..add(const GridFilterMenuEvent.initial()),
),
BlocProvider<SortMenuBloc>(
create: (context) => SortMenuBloc(
viewId: controller.viewId,
fieldController: controller.fieldController,
)..add(const SortMenuEvent.initial()),
),
],
child: MultiBlocListener(
listeners: [
BlocListener<GridFilterMenuBloc, GridFilterMenuState>(
listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => toggleExtension.toggle(),
),
BlocListener<SortMenuBloc, SortMenuState>(
listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => toggleExtension.toggle(),
),
],
child: ValueListenableBuilder<bool>(
valueListenable: controller.isLoading,
builder: (context, isLoading, child) {
if (isLoading) {
return const SizedBox.shrink();
}
return IconButton(
onPressed: () {},
icon: const FlowySvg(
FlowySvgs.m_setting_m,
),
);
},
),
),
);
}
}

View File

@ -1,16 +1,15 @@
import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_menu.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../application/field/field_controller.dart';
import '../grid/presentation/layout/sizes.dart';
import '../grid/presentation/widgets/filter/filter_menu.dart';
import '../grid/presentation/widgets/sort/sort_menu.dart';
class DatabaseViewSettingExtension extends StatelessWidget { class DatabaseViewSettingExtension extends StatelessWidget {
final String viewId; final String viewId;
final DatabaseController databaseController; final DatabaseController databaseController;
@ -57,26 +56,32 @@ class _DatabaseViewSettingContent extends StatelessWidget {
return BlocBuilder<DatabaseViewSettingExtensionBloc, return BlocBuilder<DatabaseViewSettingExtensionBloc,
DatabaseViewSettingExtensionState>( DatabaseViewSettingExtensionState>(
builder: (context, state) { builder: (context, state) {
return _wrapPadding( return Padding(
Row( padding: EdgeInsets.symmetric(
horizontal: GridSize.leadingHeaderPadding,
),
child: DecoratedBox(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1.0,
),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [ children: [
SortMenu(fieldController: fieldController), SortMenu(fieldController: fieldController),
const HSpace(6), const HSpace(6),
FilterMenu(fieldController: fieldController), FilterMenu(fieldController: fieldController),
], ],
), ),
),
),
); );
}, },
); );
} }
Widget _wrapPadding(Widget child) {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.leadingHeaderPadding,
vertical: 8,
),
child: child,
);
}
} }

View File

@ -1,7 +1,8 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
@ -11,7 +12,7 @@ import 'package:flowy_infra_ui/style_widget/extension.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AddDatabaseViewButton extends StatefulWidget { class AddDatabaseViewButton extends StatefulWidget {
final Function(AddButtonAction) onTap; final Function(DatabaseLayoutPB) onTap;
const AddDatabaseViewButton({ const AddDatabaseViewButton({
required this.onTap, required this.onTap,
super.key, super.key,
@ -57,7 +58,7 @@ class _AddDatabaseViewButtonState extends State<AddDatabaseViewButton> {
), ),
), ),
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {
return TarBarAddButtonAction( return TabBarAddButtonAction(
onTap: (action) { onTap: (action) {
popoverController.close(); popoverController.close();
widget.onTap(action); widget.onTap(action);
@ -68,38 +69,37 @@ class _AddDatabaseViewButtonState extends State<AddDatabaseViewButton> {
} }
} }
class TarBarAddButtonAction extends StatelessWidget { class TabBarAddButtonAction extends StatelessWidget {
final Function(AddButtonAction) onTap; final Function(DatabaseLayoutPB) onTap;
const TarBarAddButtonAction({ const TabBarAddButtonAction({
required this.onTap, required this.onTap,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cells = AddButtonAction.values.map((layout) { final cells = DatabaseLayoutPB.values.map((layout) {
return TarBarAddButtonActionCell( return TabBarAddButtonActionCell(
action: layout, action: layout,
onTap: onTap, onTap: onTap,
); );
}).toList(); }).toList();
return ListView.separated( return ListView.separated(
controller: ScrollController(),
shrinkWrap: true, shrinkWrap: true,
itemCount: cells.length, itemCount: cells.length,
itemBuilder: (BuildContext context, int index) => cells[index], itemBuilder: (BuildContext context, int index) => cells[index],
separatorBuilder: (BuildContext context, int index) => separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight), VSpace(GridSize.typeOptionSeparatorHeight),
padding: const EdgeInsets.symmetric(vertical: 6.0), padding: const EdgeInsets.symmetric(vertical: 4.0),
); );
} }
} }
class TarBarAddButtonActionCell extends StatelessWidget { class TabBarAddButtonActionCell extends StatelessWidget {
final AddButtonAction action; final DatabaseLayoutPB action;
final void Function(AddButtonAction) onTap; final void Function(DatabaseLayoutPB) onTap;
const TarBarAddButtonActionCell({ const TabBarAddButtonActionCell({
required this.action, required this.action,
required this.onTap, required this.onTap,
super.key, super.key,
@ -112,7 +112,7 @@ class TarBarAddButtonActionCell extends StatelessWidget {
child: FlowyButton( child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover, hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium( text: FlowyText.medium(
'${LocaleKeys.grid_createView.tr()} ${action.title}', '${LocaleKeys.grid_createView.tr()} ${action.layoutName}',
color: AFThemeExtension.of(context).textColor, color: AFThemeExtension.of(context).textColor,
), ),
leftIcon: FlowySvg( leftIcon: FlowySvg(
@ -124,46 +124,3 @@ class TarBarAddButtonActionCell extends StatelessWidget {
); );
} }
} }
enum AddButtonAction {
grid,
calendar,
board;
String get title {
switch (this) {
case AddButtonAction.board:
return LocaleKeys.board_menuName.tr();
case AddButtonAction.calendar:
return LocaleKeys.calendar_menuName.tr();
case AddButtonAction.grid:
return LocaleKeys.grid_menuName.tr();
default:
return "";
}
}
ViewLayoutPB get layoutType {
switch (this) {
case AddButtonAction.board:
return ViewLayoutPB.Board;
case AddButtonAction.calendar:
return ViewLayoutPB.Calendar;
case AddButtonAction.grid:
return ViewLayoutPB.Grid;
default:
return ViewLayoutPB.Grid;
}
}
FlowySvgData get icon {
switch (this) {
case AddButtonAction.board:
return FlowySvgs.board_s;
case AddButtonAction.calendar:
return FlowySvgs.date_s;
case AddButtonAction.grid:
return FlowySvgs.grid_s;
}
}
}

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/plugins/database_view/application/tab_bar_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
@ -12,20 +13,16 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../application/tab_bar_bloc.dart';
import 'tab_bar_add_button.dart'; import 'tab_bar_add_button.dart';
class TabBarHeader extends StatefulWidget { class TabBarHeader extends StatelessWidget {
const TabBarHeader({super.key}); const TabBarHeader({super.key});
@override
State<TabBarHeader> createState() => _TabBarHeaderState();
}
class _TabBarHeaderState extends State<TabBarHeader> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return SizedBox(
height: 30,
child: Stack(
children: [ children: [
Positioned( Positioned(
bottom: 0, bottom: 0,
@ -54,7 +51,7 @@ class _TabBarHeaderState extends State<TabBarHeader> {
child: Column( child: Column(
children: [ children: [
const VSpace(3), const VSpace(3),
pageSettingBarFromState(state), pageSettingBarFromState(context, state),
], ],
), ),
); );
@ -63,20 +60,21 @@ class _TabBarHeaderState extends State<TabBarHeader> {
], ],
), ),
], ],
),
); );
} }
Widget pageSettingBarFromState(DatabaseTabBarState state) { Widget pageSettingBarFromState(
BuildContext context,
DatabaseTabBarState state,
) {
if (state.tabBars.length < state.selectedIndex) { if (state.tabBars.length < state.selectedIndex) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final tarBar = state.tabBars[state.selectedIndex]; final tabBar = state.tabBars[state.selectedIndex];
final controller = final controller =
state.tabBarControllerByViewId[tarBar.viewId]!.controller; state.tabBarControllerByViewId[tabBar.viewId]!.controller;
return tarBar.builder.settingBar( return tabBar.builder.settingBar(context, controller);
context,
controller,
);
} }
} }
@ -121,9 +119,9 @@ class _DatabaseTabBarState extends State<DatabaseTabBar> {
), ),
), ),
AddDatabaseViewButton( AddDatabaseViewButton(
onTap: (action) async { onTap: (layoutType) async {
context.read<DatabaseTabBarBloc>().add( context.read<DatabaseTabBarBloc>().add(
DatabaseTabBarEvent.createView(action), DatabaseTabBarEvent.createView(layoutType),
); );
}, },
), ),

View File

@ -0,0 +1,52 @@
import 'package:appflowy/plugins/database_view/application/tab_bar_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/mobile_grid_setting.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileTabBarHeader extends StatelessWidget {
const MobileTabBarHeader({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {
final currentView = state.tabBars.firstWhereIndexedOrNull(
(index, tabBar) => index == state.selectedIndex,
);
if (currentView == null) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
children: [
Expanded(
child: Text(
currentView.view.name,
style: Theme.of(context).textTheme.titleLarge,
overflow: TextOverflow.ellipsis,
),
),
MobileGridSettingButton(
controller: state
.tabBarControllerByViewId[currentView.viewId]!
.controller,
toggleExtension: ToggleExtensionNotifier(),
),
],
),
),
const Divider(height: 1, thickness: 1),
],
);
},
);
}
}

View File

@ -1,4 +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/application/tab_bar_bloc.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/database_view/widgets/share_button.dart';
import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/plugin/plugin.dart';
@ -6,11 +8,11 @@ import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart'; import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../application/database_controller.dart'; import 'desktop/tab_bar_header.dart';
import 'tab_bar_header.dart';
abstract class DatabaseTabBarItemBuilder { abstract class DatabaseTabBarItemBuilder {
const DatabaseTabBarItemBuilder(); const DatabaseTabBarItemBuilder();
@ -94,12 +96,11 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
if (value) { if (value) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return const SizedBox( return Padding(
height: 30, padding: const EdgeInsets.symmetric(horizontal: 40),
child: Padding( child: PlatformExtension.isDesktop
padding: EdgeInsets.symmetric(horizontal: 40), ? const TabBarHeader()
child: TabBarHeader(), : const MobileTabBarHeader(),
),
); );
}, },
); );
@ -145,10 +146,10 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
if (state.tabBars.length < state.selectedIndex) { if (state.tabBars.length < state.selectedIndex) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final tarBar = state.tabBars[state.selectedIndex]; final tabBar = state.tabBars[state.selectedIndex];
final controller = final controller =
state.tabBarControllerByViewId[tarBar.viewId]!.controller; state.tabBarControllerByViewId[tabBar.viewId]!.controller;
return tarBar.builder.settingBarExtension( return tabBar.builder.settingBarExtension(
context, context,
controller, controller,
); );

View File

@ -1,30 +1,34 @@
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_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
extension DatabaseLayoutExtension on DatabaseLayoutPB { extension DatabaseLayoutExtension on DatabaseLayoutPB {
String layoutName() { String get layoutName {
switch (this) { return switch (this) {
case DatabaseLayoutPB.Board: DatabaseLayoutPB.Board => LocaleKeys.board_menuName.tr(),
return LocaleKeys.board_menuName.tr(); DatabaseLayoutPB.Calendar => LocaleKeys.calendar_menuName.tr(),
case DatabaseLayoutPB.Calendar: DatabaseLayoutPB.Grid => LocaleKeys.grid_menuName.tr(),
return LocaleKeys.calendar_menuName.tr(); _ => "",
case DatabaseLayoutPB.Grid: };
return LocaleKeys.grid_menuName.tr();
default:
return "";
} }
ViewLayoutPB get layoutType {
return switch (this) {
DatabaseLayoutPB.Board => ViewLayoutPB.Board,
DatabaseLayoutPB.Calendar => ViewLayoutPB.Calendar,
DatabaseLayoutPB.Grid => ViewLayoutPB.Grid,
_ => throw UnimplementedError(),
};
} }
FlowySvgData get icon { FlowySvgData get icon {
switch (this) { return switch (this) {
case DatabaseLayoutPB.Board: DatabaseLayoutPB.Board => FlowySvgs.board_s,
return FlowySvgs.board_s; DatabaseLayoutPB.Calendar => FlowySvgs.date_s,
case DatabaseLayoutPB.Calendar: DatabaseLayoutPB.Grid => FlowySvgs.grid_s,
case DatabaseLayoutPB.Grid: _ => throw UnimplementedError(),
return FlowySvgs.grid_s; };
}
throw UnimplementedError();
} }
} }

View File

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import '../../application/cell/cell_service.dart'; import '../../application/cell/cell_service.dart';
import 'accessory/cell_accessory.dart'; import 'accessory/cell_accessory.dart';
import 'accessory/cell_shortcuts.dart'; import 'accessory/cell_shortcuts.dart';
import 'cells/cell_container.dart';
import 'cells/checkbox_cell/checkbox_cell.dart'; import 'cells/checkbox_cell/checkbox_cell.dart';
import 'cells/checklist_cell/checklist_cell.dart'; import 'cells/checklist_cell/checklist_cell.dart';
import 'cells/date_cell/date_cell.dart'; import 'cells/date_cell/date_cell.dart';
@ -200,7 +201,7 @@ class BlankCell extends StatelessWidget {
abstract class CellEditable { abstract class CellEditable {
RequestFocusListener get requestFocus; RequestFocusListener get requestFocus;
ValueNotifier<bool> get onCellFocus; CellContainerNotifier get cellContainerNotifier;
// ValueNotifier<bool> get onCellEditing; // ValueNotifier<bool> get onCellEditing;
} }
@ -223,11 +224,11 @@ abstract class GridCellWidget extends StatefulWidget
GridCellWidget({super.key}); GridCellWidget({super.key});
@override @override
final ValueNotifier<bool> onCellFocus = ValueNotifier<bool>(false); final CellContainerNotifier cellContainerNotifier = CellContainerNotifier();
// When the cell is focused, we assume that the accessory also be hovered. // When the cell is focused, we assume that the accessory also be hovered.
@override @override
ValueNotifier<bool> get onAccessoryHover => onCellFocus; ValueNotifier<bool> get onAccessoryHover => ValueNotifier(false);
// @override // @override
// final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false); // final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
@ -278,17 +279,19 @@ abstract class GridEditableTextCell<T extends GridCellWidget>
@override @override
void initState() { void initState() {
super.initState();
widget.shortcutHandlers[CellKeyboardKey.onEnter] = widget.shortcutHandlers[CellKeyboardKey.onEnter] =
() => focusNode.unfocus(); () => focusNode.unfocus();
_listenOnFocusNodeChanged(); _listenOnFocusNodeChanged();
super.initState();
} }
@override @override
void didUpdateWidget(covariant T oldWidget) { void didUpdateWidget(covariant T oldWidget) {
if (oldWidget != this) { // if (!focusNode.hasFocus && widget.cellContainerNotifier.isFocus) {
_listenOnFocusNodeChanged(); // focusNode.requestFocus();
} // } else if (focusNode.hasFocus && !widget.cellContainerNotifier.isFocus) {
// focusNode.unfocus();
// }
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
} }
@ -302,15 +305,15 @@ abstract class GridEditableTextCell<T extends GridCellWidget>
@override @override
void requestBeginFocus() { void requestBeginFocus() {
if (focusNode.hasFocus == false && focusNode.canRequestFocus) { if (!focusNode.hasFocus && focusNode.canRequestFocus) {
FocusScope.of(context).requestFocus(focusNode); FocusScope.of(context).requestFocus(focusNode);
} }
} }
void _listenOnFocusNodeChanged() { void _listenOnFocusNodeChanged() {
widget.onCellFocus.value = focusNode.hasFocus; widget.cellContainerNotifier.isFocus = focusNode.hasFocus;
focusNode.setListener(() { focusNode.setListener(() {
widget.onCellFocus.value = focusNode.hasFocus; widget.cellContainerNotifier.isFocus = focusNode.hasFocus;
focusChanged(); focusChanged();
}); });
} }

View File

@ -13,28 +13,26 @@ class CellContainer extends StatelessWidget {
final AccessoryBuilder? accessoryBuilder; final AccessoryBuilder? accessoryBuilder;
final double width; final double width;
final bool isPrimary; final bool isPrimary;
final CellContainerNotifier cellContainerNotifier;
const CellContainer({ const CellContainer({
Key? key, Key? key,
required this.child, required this.child,
required this.width, required this.width,
required this.isPrimary, required this.isPrimary,
required this.cellContainerNotifier,
this.accessoryBuilder, this.accessoryBuilder,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: cellContainerNotifier, value: child.cellContainerNotifier,
child: Selector<CellContainerNotifier, bool>( child: Selector<CellContainerNotifier, bool>(
selector: (context, notifier) => notifier.isFocus, selector: (context, notifier) => notifier.isFocus,
builder: (privderContext, isFocus, _) { builder: (providerContext, isFocus, _) {
Widget container = Center(child: GridCellShortcuts(child: child)); Widget container = Center(child: GridCellShortcuts(child: child));
if (accessoryBuilder != null) { if (accessoryBuilder != null) {
final accessories = accessoryBuilder!( final accessories = accessoryBuilder!.call(
GridCellAccessoryBuildContext( GridCellAccessoryBuildContext(
anchorContext: context, anchorContext: context,
isCellEditing: isFocus, isCellEditing: isFocus,
@ -52,7 +50,11 @@ class CellContainer extends StatelessWidget {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () => child.requestFocus.notify(), onTap: () {
if (!isFocus) {
child.requestFocus.notify();
}
},
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: width, minHeight: 46), constraints: BoxConstraints(maxWidth: width, minHeight: 46),
decoration: _makeBoxDecoration(context, isFocus), decoration: _makeBoxDecoration(context, isFocus),
@ -81,9 +83,6 @@ class CellContainer extends StatelessWidget {
} }
class _GridCellEnterRegion extends StatelessWidget { class _GridCellEnterRegion extends StatelessWidget {
final Widget child;
final List<GridCellAccessoryBuilder> accessories;
final bool isPrimary;
const _GridCellEnterRegion({ const _GridCellEnterRegion({
required this.child, required this.child,
required this.accessories, required this.accessories,
@ -91,6 +90,10 @@ class _GridCellEnterRegion extends StatelessWidget {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final Widget child;
final List<GridCellAccessoryBuilder> accessories;
final bool isPrimary;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector2<RegionStateNotifier, CellContainerNotifier, bool>( return Selector2<RegionStateNotifier, CellContainerNotifier, bool>(
@ -99,6 +102,7 @@ class _GridCellEnterRegion extends StatelessWidget {
(cellNotifier.onEnter || regionNotifier.onEnter && isPrimary), (cellNotifier.onEnter || regionNotifier.onEnter && isPrimary),
builder: (context, showAccessory, _) { builder: (context, showAccessory, _) {
final List<Widget> children = [child]; final List<Widget> children = [child];
if (showAccessory) { if (showAccessory) {
children.add( children.add(
CellAccessoryContainer(accessories: accessories).positioned( CellAccessoryContainer(accessories: accessories).positioned(
@ -110,11 +114,9 @@ class _GridCellEnterRegion extends StatelessWidget {
return MouseRegion( return MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
onEnter: (p) => onEnter: (p) =>
Provider.of<CellContainerNotifier>(context, listen: false) CellContainerNotifier.of(context, listen: false).onEnter = true,
.onEnter = true,
onExit: (p) => onExit: (p) =>
Provider.of<CellContainerNotifier>(context, listen: false) CellContainerNotifier.of(context, listen: false).onEnter = false,
.onEnter = false,
child: Stack( child: Stack(
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
fit: StackFit.expand, fit: StackFit.expand,
@ -127,24 +129,9 @@ class _GridCellEnterRegion extends StatelessWidget {
} }
class CellContainerNotifier extends ChangeNotifier { class CellContainerNotifier extends ChangeNotifier {
final CellEditable cellEditable;
VoidCallback? _onCellFocusListener;
bool _isFocus = false; bool _isFocus = false;
bool _onEnter = false; bool _onEnter = false;
CellContainerNotifier(this.cellEditable) {
_onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value;
cellEditable.onCellFocus.addListener(_onCellFocusListener!);
}
@override
void dispose() {
if (_onCellFocusListener != null) {
cellEditable.onCellFocus.removeListener(_onCellFocusListener!);
}
super.dispose();
}
set isFocus(bool value) { set isFocus(bool value) {
if (_isFocus != value) { if (_isFocus != value) {
_isFocus = value; _isFocus = value;
@ -162,4 +149,8 @@ class CellContainerNotifier extends ChangeNotifier {
bool get isFocus => _isFocus; bool get isFocus => _isFocus;
bool get onEnter => _onEnter; bool get onEnter => _onEnter;
static CellContainerNotifier of(BuildContext context, {bool listen = true}) {
return Provider.of<CellContainerNotifier>(context, listen: listen);
}
} }

View File

@ -144,16 +144,16 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
constraints: BoxConstraints.loose(const Size(360, 400)), constraints: BoxConstraints.loose(const Size(360, 400)),
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext popoverContext) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onCellFocus.value = true; widget.cellContainerNotifier.isFocus = true;
}); });
return GridChecklistCellEditor( return GridChecklistCellEditor(
cellController: widget.cellControllerBuilder.build() cellController: widget.cellControllerBuilder.build()
as ChecklistCellController, as ChecklistCellController,
); );
}, },
onClose: () => widget.onCellFocus.value = false, onClose: () => widget.cellContainerNotifier.isFocus = false,
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Padding( child: Padding(

View File

@ -87,11 +87,11 @@ class _DateCellState extends GridCellState<GridDateCell> {
return DateCellEditor( return DateCellEditor(
cellController: cellController:
widget.cellControllerBuilder.build() as DateCellController, widget.cellControllerBuilder.build() as DateCellController,
onDismissed: () => widget.onCellFocus.value = false, onDismissed: () => widget.cellContainerNotifier.isFocus = false,
); );
}, },
onClose: () { onClose: () {
widget.onCellFocus.value = false; widget.cellContainerNotifier.isFocus = false;
}, },
); );
}, },
@ -108,7 +108,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
@override @override
void requestBeginFocus() { void requestBeginFocus() {
_popover.show(); _popover.show();
widget.onCellFocus.value = true; widget.cellContainerNotifier.isFocus = true;
} }
@override @override

View File

@ -80,9 +80,14 @@ class _NumberCellState extends GridEditableTextCell<GridNumberCell> {
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
border: InputBorder.none, border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: widget.cellStyle.placeholder, hintText: widget.cellStyle.placeholder,
isDense: true, isDense: true,
), ),
onTapOutside: (_) => focusNode.unfocus(),
), ),
), ),
), ),

View File

@ -64,7 +64,8 @@ class _SingleSelectCellState extends GridCellState<GridSingleSelectCell> {
return SelectOptionWrap( return SelectOptionWrap(
selectOptions: state.selectedOptions, selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle, cellStyle: widget.cellStyle,
onCellEditing: widget.onCellFocus, onCellEditing: (isFocus) =>
widget.cellContainerNotifier.isFocus = isFocus,
popoverController: _popover, popoverController: _popover,
cellControllerBuilder: widget.cellControllerBuilder, cellControllerBuilder: widget.cellControllerBuilder,
); );
@ -127,7 +128,8 @@ class _MultiSelectCellState extends GridCellState<GridMultiSelectCell> {
return SelectOptionWrap( return SelectOptionWrap(
selectOptions: state.selectedOptions, selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle, cellStyle: widget.cellStyle,
onCellEditing: widget.onCellFocus, onCellEditing: (isFocus) =>
widget.cellContainerNotifier.isFocus = isFocus,
popoverController: _popover, popoverController: _popover,
cellControllerBuilder: widget.cellControllerBuilder, cellControllerBuilder: widget.cellControllerBuilder,
); );
@ -151,7 +153,7 @@ class SelectOptionWrap extends StatefulWidget {
final SelectOptionCellStyle? cellStyle; final SelectOptionCellStyle? cellStyle;
final CellControllerBuilder cellControllerBuilder; final CellControllerBuilder cellControllerBuilder;
final PopoverController popoverController; final PopoverController popoverController;
final ValueNotifier onCellEditing; final void Function(bool) onCellEditing;
const SelectOptionWrap({ const SelectOptionWrap({
required this.selectOptions, required this.selectOptions,
@ -179,16 +181,16 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
constraints: constraints, constraints: constraints,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext popoverContext) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onCellEditing.value = true; widget.onCellEditing(true);
}); });
return SelectOptionCellEditor( return SelectOptionCellEditor(
cellController: widget.cellControllerBuilder.build() cellController: widget.cellControllerBuilder.build()
as SelectOptionCellController, as SelectOptionCellController,
); );
}, },
onClose: () => widget.onCellEditing.value = false, onClose: () => widget.onCellEditing(false),
child: Padding( child: Padding(
padding: widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets, padding: widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
child: child, child: child,

View File

@ -122,9 +122,14 @@ class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
bottom: GridSize.cellContentInsets.bottom, bottom: GridSize.cellContentInsets.bottom,
), ),
border: InputBorder.none, border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: widget.cellStyle.placeholder, hintText: widget.cellStyle.placeholder,
isDense: true, isDense: true,
), ),
onTapOutside: (_) => focusNode.unfocus(),
), ),
), ),
], ],

View File

@ -110,7 +110,6 @@ class GridURLCell extends GridCellWidget {
} }
class _GridURLCellState extends GridEditableTextCell<GridURLCell> { class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
final _popoverController = PopoverController();
late final URLCellBloc _cellBloc; late final URLCellBloc _cellBloc;
late final TextEditingController _controller; late final TextEditingController _controller;
@ -167,10 +166,15 @@ class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
bottom: GridSize.cellContentInsets.bottom, bottom: GridSize.cellContentInsets.bottom,
), ),
border: InputBorder.none, border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: widget.cellStyle?.placeholder, hintText: widget.cellStyle?.placeholder,
hintStyle: style.copyWith(color: Theme.of(context).hintColor), hintStyle: style.copyWith(color: Theme.of(context).hintColor),
isDense: true, isDense: true,
), ),
onTapOutside: (_) => focusNode.unfocus(),
), ),
); );
}, },
@ -184,12 +188,6 @@ class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
return super.focusChanged(); return super.focusChanged();
} }
@override
void requestBeginFocus() {
widget.onCellFocus.value = true;
_popoverController.show();
}
@override @override
String? onCopy() => _cellBloc.state.content; String? onCopy() => _cellBloc.state.content;
} }

View File

@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart'; import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart'; import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/mobile_grid_page.dart';
import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/plugins/document/document.dart';
import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/plugin/plugin.dart';
@ -90,19 +91,31 @@ extension ViewExtension on ViewPB {
throw UnimplementedError; throw UnimplementedError;
} }
DatabaseTabBarItemBuilder tarBarItem() { DatabaseTabBarItemBuilder tabBarItem() {
switch (layout) { switch (layout) {
case ViewLayoutPB.Board: case ViewLayoutPB.Board:
return BoardPageTabBarBuilderImpl(); return BoardPageTabBarBuilderImpl();
case ViewLayoutPB.Calendar: case ViewLayoutPB.Calendar:
return CalendarPageTabBarBuilderImpl(); return CalendarPageTabBarBuilderImpl();
case ViewLayoutPB.Grid: case ViewLayoutPB.Grid:
return GridPageTabBarBuilderImpl(); return DesktopGridTabBarBuilderImpl();
case ViewLayoutPB.Document: default:
throw UnimplementedError; throw UnimplementedError;
} }
}
DatabaseTabBarItemBuilder mobileTabBarItem() {
switch (layout) {
case ViewLayoutPB.Board:
return BoardPageTabBarBuilderImpl();
case ViewLayoutPB.Calendar:
return CalendarPageTabBarBuilderImpl();
case ViewLayoutPB.Grid:
return MobileGridTabBarBuilderImpl();
default:
throw UnimplementedError; throw UnimplementedError;
} }
}
FlowySvgData get iconData => layout.icon; FlowySvgData get iconData => layout.icon;

View File

@ -17,7 +17,7 @@ class StyledSingleChildScrollView extends StatefulWidget {
const StyledSingleChildScrollView({ const StyledSingleChildScrollView({
Key? key, Key? key,
@required this.child, required this.child,
this.contentSize, this.contentSize,
this.axis = Axis.vertical, this.axis = Axis.vertical,
this.trackColor, this.trackColor,

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class FlowyText extends StatelessWidget { class FlowyText extends StatelessWidget {
@ -91,12 +89,6 @@ class FlowyText extends StatelessWidget {
maxLines: maxLines, maxLines: maxLines,
textAlign: textAlign, textAlign: textAlign,
overflow: overflow ?? TextOverflow.clip, overflow: overflow ?? TextOverflow.clip,
textHeightBehavior: Platform.isAndroid || Platform.isIOS
? const TextHeightBehavior(
applyHeightToFirstAscent: false,
applyHeightToLastDescent: false,
)
: null,
style: Theme.of(context).textTheme.bodyMedium!.copyWith( style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: fontSize, fontSize: fontSize,
fontWeight: fontWeight, fontWeight: fontWeight,