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"
},
{
"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",
"request": "launch",
@ -55,6 +66,17 @@
},
"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",
"request": "launch",
@ -66,6 +88,17 @@
},
"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",
"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/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:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -16,7 +16,7 @@ void main() {
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
// create a field
await tester.scrollToRight(find.byType(GridPage));
@ -34,7 +34,7 @@ void main() {
await tester.findFieldWithName('New field 1');
// 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');
// 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-folder2/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
@ -18,15 +17,15 @@ void main() {
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// Create grid view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid);
// Create calendar view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.calendar);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Calendar);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar);
await tester.pumpAndSettle();
@ -39,7 +38,7 @@ void main() {
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// rename board view
@ -64,7 +63,7 @@ void main() {
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.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/grid_layout.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/tab_bar_add_button.dart';
import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_add_button.dart';
import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_header.dart';
import 'package:appflowy/plugins/database_view/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/cells/cells.dart';
@ -1381,13 +1381,15 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(unscheduledEvent);
}
Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async {
Future<void> tapCreateLinkedDatabaseViewButton(
DatabaseLayoutPB layoutType,
) async {
final findAddButton = find.byType(AddDatabaseViewButton);
await tapButton(findAddButton);
final findCreateButton = find.byWidgetPredicate(
(widget) =>
widget is TarBarAddButtonActionCell && widget.action == action,
widget is TabBarAddButtonActionCell && widget.action == layoutType,
);
await tapButton(findCreateButton);
}
@ -1554,7 +1556,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
final findLayoutCell = find.byType(DatabaseViewLayoutCell);
final findText = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == layout.layoutName(),
(widget) => widget is FlowyText && widget.text == layout.layoutName,
);
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/widgets/database_layout_ext.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.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_editor/appflowy_editor.dart' hide Log;
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -48,8 +50,8 @@ class DatabaseTabBarBloc
);
}
},
createView: (action) {
_createLinkedView(action.name, action.layoutType);
createView: (layout) {
_createLinkedView(layout.layoutType, layout.layoutName);
},
deleteView: (String viewId) async {
final result = await ViewBackendService.delete(viewId: viewId);
@ -89,10 +91,10 @@ class DatabaseTabBarBloc
(element) => element.viewId == viewId,
);
if (index != -1) {
final tarBar = allTabBars.removeAt(index);
final tabBar = allTabBars.removeAt(index);
// Dispose the controller when the tab is removed.
final controller =
tabBarControllerByViewId.remove(tarBar.viewId);
tabBarControllerByViewId.remove(tabBar.viewId);
controller?.dispose();
}
@ -163,7 +165,7 @@ class DatabaseTabBarBloc
return tabBarControllerByViewId;
}
Future<void> _createLinkedView(String name, ViewLayoutPB layoutType) async {
Future<void> _createLinkedView(ViewLayoutPB layoutType, String name) async {
final viewId = state.parentView.id;
final databaseIdOrError =
await DatabaseViewBackendService(viewId: viewId).getDatabaseId();
@ -207,7 +209,7 @@ class DatabaseTabBarEvent with _$DatabaseTabBarEvent {
List<ViewPB> childViews,
) = _DidLoadChildViews;
const factory DatabaseTabBarEvent.selectView(String viewId) = _DidSelectView;
const factory DatabaseTabBarEvent.createView(AddButtonAction action) =
const factory DatabaseTabBarEvent.createView(DatabaseLayoutPB layout) =
_CreateView;
const factory DatabaseTabBarEvent.renameView(String viewId, String newName) =
_RenameView;
@ -252,7 +254,9 @@ class DatabaseTabBar extends Equatable {
DatabaseTabBar({
required this.view,
}) : _builder = view.tarBarItem();
}) : _builder = PlatformExtension.isMobile
? view.mobileTabBarItem()
: view.tabBarItem();
@override
List<Object?> get props => [view.hashCode];

View File

@ -1,9 +1,10 @@
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/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_backend/log.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_web.dart';
@ -40,7 +41,7 @@ class ToggleExtensionNotifier extends ChangeNotifier {
}
}
class GridPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
class DesktopGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
final _toggleExtension = ToggleExtensionNotifier();
@override
@ -255,6 +256,15 @@ class _GridRows extends StatelessWidget {
GridState state,
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(
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
/// This is a workaround related to
@ -274,18 +284,7 @@ class _GridRows extends StatelessWidget {
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
},
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, 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'));
},
itemBuilder: (context, index) => children[index],
);
}

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/plugins/database_view/grid/application/grid_bloc.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:flowy_infra/theme_extension.dart';
@ -15,16 +16,18 @@ class GridAddRowButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color =
PlatformExtension.isMobile ? null : Theme.of(context).hintColor;
return FlowyButton(
text: FlowyText.medium(
LocaleKeys.grid_row_newRow.tr(),
color: Theme.of(context).colorScheme.tertiary,
color: color,
),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onTap: () => context.read<GridBloc>().add(const GridEvent.createRow()),
leftIcon: FlowySvg(
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,
child: Stack(
alignment: Alignment.centerRight,
fit: StackFit.expand,
children: [button, line],
),
);
@ -114,7 +113,6 @@ class _GridHeaderCellContainer extends StatelessWidget {
);
final decoration = BoxDecoration(
border: Border(
top: borderSide,
right: borderSide,
bottom: borderSide,
),
@ -123,10 +121,7 @@ class _GridHeaderCellContainer extends StatelessWidget {
return Container(
width: width,
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/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/presentation/widgets/header/mobile_field_cell.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:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme_extension.dart';
@ -52,10 +54,7 @@ class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
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) {
final cells = state.fields
.map(
(fieldInfo) => GridFieldCell(
key: _getKeyById(fieldInfo.id),
viewId: widget.viewId,
fieldInfo: fieldInfo,
),
(fieldInfo) => PlatformExtension.isDesktop
? GridFieldCell(
key: _getKeyById(fieldInfo.id),
viewId: widget.viewId,
fieldInfo: fieldInfo,
)
: MobileFieldButton(
key: _getKeyById(fieldInfo.id),
viewId: widget.viewId,
field: fieldInfo,
),
)
.toList();
return Container(
return ColoredBox(
color: Theme.of(context).colorScheme.surface,
child: RepaintBoundary(
child: ReorderableRow(
crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.center,
scrollController: ScrollController(),
header: const _CellLeading(),
needsLongPressDraggable: false,
needsLongPressDraggable: PlatformExtension.isMobile,
footer: _CellTrailing(viewId: widget.viewId),
onReorder: (int oldIndex, int newIndex) {
_onReorder(cells, oldIndex, context, newIndex);
_onReorder(
cells,
oldIndex,
context,
newIndex,
);
},
children: cells,
),
@ -123,13 +133,13 @@ class _GridHeaderState extends State<_GridHeader> {
}
void _onReorder(
List<GridFieldCell> cells,
List<Widget> cells,
int oldIndex,
BuildContext context,
int newIndex,
) {
if (cells.length > oldIndex) {
final field = cells[oldIndex].fieldInfo.field;
final field = (cells[oldIndex] as GridFieldCell).fieldInfo.field;
context
.read<GridHeaderBloc>()
.add(GridHeaderEvent.moveField(field, oldIndex, newIndex));
@ -159,7 +169,7 @@ class _CellTrailing extends StatelessWidget {
return Container(
width: GridSize.trailHeaderPadding,
decoration: BoxDecoration(
border: Border(top: borderSide, bottom: borderSide),
border: Border(bottom: borderSide),
),
padding: GridSize.headerContentInsets,
child: CreateFieldButton(viewId: viewId),
@ -189,8 +199,14 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
constraints: BoxConstraints.loose(const Size(240, 600)),
triggerActions: PopoverTriggerFlags.none,
child: FlowyButton(
margin: PlatformExtension.isDesktop
? GridSize.cellContentInsets
: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
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,
onTap: () async {
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,33 +54,30 @@ class _GridRowState extends State<GridRow> {
rowId: widget.rowId,
dataController: widget.dataController,
viewId: widget.viewId,
);
_rowBloc.add(const RowEvent.initial());
)..add(const RowEvent.initial());
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _rowBloc,
child: _RowEnterRegion(
child: BlocBuilder<RowBloc, RowState>(
// The row need to rebuild when the cell count changes.
buildWhen: (p, c) =>
p.cellByFieldId.length != c.cellByFieldId.length ||
p.rowSource != c.rowSource,
builder: (context, state) {
final content = Expanded(
child: RowContent(
builder: widget.cellBuilder,
onExpand: () => widget.openDetailPage(
context,
widget.cellBuilder,
),
child: BlocBuilder<RowBloc, RowState>(
// The row need to rebuild when the cell count changes.
buildWhen: (p, c) => p.rowSource != c.rowSource,
builder: (context, state) {
final content = Expanded(
child: RowContent(
builder: widget.cellBuilder,
onExpand: () => widget.openDetailPage(
context,
widget.cellBuilder,
),
);
),
);
return Row(
key: ValueKey(state.rowSource),
return _RowEnterRegion(
key: ValueKey(state.rowSource),
child: Row(
children: [
_RowLeading(
index: widget.index,
@ -88,9 +85,9 @@ class _GridRowState extends State<GridRow> {
),
content,
],
);
},
),
),
);
},
),
);
}
@ -155,19 +152,18 @@ class _RowLeadingState extends State<_RowLeading> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
const InsertRowButton(),
if (isDraggable) ...[
if (isDraggable)
ReorderableDragStartListener(
index: widget.index!,
child: RowMenuButton(
isDragEnabled: isDraggable,
openMenu: popoverController.show,
),
),
] else ...[
)
else
RowMenuButton(
openMenu: popoverController.show,
),
],
],
);
}
@ -253,8 +249,6 @@ class RowContent extends StatelessWidget {
builder: (context, state) {
return IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
..._makeCells(context, state.cellByFieldId),
@ -277,7 +271,6 @@ class RowContent extends StatelessWidget {
return CellContainer(
width: cellId.fieldInfo.fieldSettings?.width.toDouble() ?? 140,
isPrimary: cellId.fieldInfo.field.isPrimary,
cellContainerNotifier: CellContainerNotifier(child),
accessoryBuilder: (buildContext) {
final builder = child.accessoryBuilder;
final List<GridCellAccessoryBuilder> accessories = [];
@ -317,7 +310,6 @@ class RowContent extends StatelessWidget {
bottom: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: const SizedBox.shrink(),
),
);
}

View File

@ -90,7 +90,7 @@ class DatabaseViewLayoutCell extends StatelessWidget {
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
databaseLayout.layoutName(),
databaseLayout.layoutName,
color: AFThemeExtension.of(context).textColor,
),
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/field/field_controller.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/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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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 {
final String viewId;
final DatabaseController databaseController;
@ -57,26 +56,32 @@ class _DatabaseViewSettingContent extends StatelessWidget {
return BlocBuilder<DatabaseViewSettingExtensionBloc,
DatabaseViewSettingExtensionState>(
builder: (context, state) {
return _wrapPadding(
Row(
children: [
SortMenu(fieldController: fieldController),
const HSpace(6),
FilterMenu(fieldController: fieldController),
],
return Padding(
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: [
SortMenu(fieldController: fieldController),
const HSpace(6),
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/locale_keys.g.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:easy_localization/easy_localization.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';
class AddDatabaseViewButton extends StatefulWidget {
final Function(AddButtonAction) onTap;
final Function(DatabaseLayoutPB) onTap;
const AddDatabaseViewButton({
required this.onTap,
super.key,
@ -57,7 +58,7 @@ class _AddDatabaseViewButtonState extends State<AddDatabaseViewButton> {
),
),
popupBuilder: (BuildContext context) {
return TarBarAddButtonAction(
return TabBarAddButtonAction(
onTap: (action) {
popoverController.close();
widget.onTap(action);
@ -68,38 +69,37 @@ class _AddDatabaseViewButtonState extends State<AddDatabaseViewButton> {
}
}
class TarBarAddButtonAction extends StatelessWidget {
final Function(AddButtonAction) onTap;
const TarBarAddButtonAction({
class TabBarAddButtonAction extends StatelessWidget {
final Function(DatabaseLayoutPB) onTap;
const TabBarAddButtonAction({
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = AddButtonAction.values.map((layout) {
return TarBarAddButtonActionCell(
final cells = DatabaseLayoutPB.values.map((layout) {
return TabBarAddButtonActionCell(
action: layout,
onTap: onTap,
);
}).toList();
return ListView.separated(
controller: ScrollController(),
shrinkWrap: true,
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) => cells[index],
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
padding: const EdgeInsets.symmetric(vertical: 6.0),
padding: const EdgeInsets.symmetric(vertical: 4.0),
);
}
}
class TarBarAddButtonActionCell extends StatelessWidget {
final AddButtonAction action;
final void Function(AddButtonAction) onTap;
const TarBarAddButtonActionCell({
class TabBarAddButtonActionCell extends StatelessWidget {
final DatabaseLayoutPB action;
final void Function(DatabaseLayoutPB) onTap;
const TabBarAddButtonActionCell({
required this.action,
required this.onTap,
super.key,
@ -112,7 +112,7 @@ class TarBarAddButtonActionCell extends StatelessWidget {
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
'${LocaleKeys.grid_createView.tr()} ${action.title}',
'${LocaleKeys.grid_createView.tr()} ${action.layoutName}',
color: AFThemeExtension.of(context).textColor,
),
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/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/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
@ -12,71 +13,68 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../application/tab_bar_bloc.dart';
import 'tab_bar_add_button.dart';
class TabBarHeader extends StatefulWidget {
class TabBarHeader extends StatelessWidget {
const TabBarHeader({super.key});
@override
State<TabBarHeader> createState() => _TabBarHeaderState();
}
class _TabBarHeaderState extends State<TabBarHeader> {
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Divider(
color: Theme.of(context).dividerColor,
height: 1,
thickness: 1,
return SizedBox(
height: 30,
child: Stack(
children: [
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Divider(
color: Theme.of(context).dividerColor,
height: 1,
thickness: 1,
),
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {
return const Flexible(
child: DatabaseTabBar(),
);
},
),
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {
return SizedBox(
width: 200,
child: Column(
children: [
const VSpace(3),
pageSettingBarFromState(state),
],
),
);
},
),
],
),
],
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {
return const Flexible(
child: DatabaseTabBar(),
);
},
),
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {
return SizedBox(
width: 200,
child: Column(
children: [
const VSpace(3),
pageSettingBarFromState(context, state),
],
),
);
},
),
],
),
],
),
);
}
Widget pageSettingBarFromState(DatabaseTabBarState state) {
Widget pageSettingBarFromState(
BuildContext context,
DatabaseTabBarState state,
) {
if (state.tabBars.length < state.selectedIndex) {
return const SizedBox.shrink();
}
final tarBar = state.tabBars[state.selectedIndex];
final tabBar = state.tabBars[state.selectedIndex];
final controller =
state.tabBarControllerByViewId[tarBar.viewId]!.controller;
return tarBar.builder.settingBar(
context,
controller,
);
state.tabBarControllerByViewId[tabBar.viewId]!.controller;
return tabBar.builder.settingBar(context, controller);
}
}
@ -121,9 +119,9 @@ class _DatabaseTabBarState extends State<DatabaseTabBar> {
),
),
AddDatabaseViewButton(
onTap: (action) async {
onTap: (layoutType) async {
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/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';
@ -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/view_title_bar.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_bloc/flutter_bloc.dart';
import '../application/database_controller.dart';
import 'tab_bar_header.dart';
import 'desktop/tab_bar_header.dart';
abstract class DatabaseTabBarItemBuilder {
const DatabaseTabBarItemBuilder();
@ -94,12 +96,11 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
if (value) {
return const SizedBox.shrink();
}
return const SizedBox(
height: 30,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 40),
child: TabBarHeader(),
),
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: PlatformExtension.isDesktop
? const TabBarHeader()
: const MobileTabBarHeader(),
);
},
);
@ -145,10 +146,10 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
if (state.tabBars.length < state.selectedIndex) {
return const SizedBox.shrink();
}
final tarBar = state.tabBars[state.selectedIndex];
final tabBar = state.tabBars[state.selectedIndex];
final controller =
state.tabBarControllerByViewId[tarBar.viewId]!.controller;
return tarBar.builder.settingBarExtension(
state.tabBarControllerByViewId[tabBar.viewId]!.controller;
return tabBar.builder.settingBarExtension(
context,
controller,
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -110,7 +110,6 @@ class GridURLCell extends GridCellWidget {
}
class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
final _popoverController = PopoverController();
late final URLCellBloc _cellBloc;
late final TextEditingController _controller;
@ -167,10 +166,15 @@ class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
bottom: GridSize.cellContentInsets.bottom,
),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: widget.cellStyle?.placeholder,
hintStyle: style.copyWith(color: Theme.of(context).hintColor),
isDense: true,
),
onTapOutside: (_) => focusNode.unfocus(),
),
);
},
@ -184,12 +188,6 @@ class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
return super.focusChanged();
}
@override
void requestBeginFocus() {
widget.onCellFocus.value = true;
_popoverController.show();
}
@override
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/calendar/presentation/calendar_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/document/document.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
@ -90,18 +91,30 @@ extension ViewExtension on ViewPB {
throw UnimplementedError;
}
DatabaseTabBarItemBuilder tarBarItem() {
DatabaseTabBarItemBuilder tabBarItem() {
switch (layout) {
case ViewLayoutPB.Board:
return BoardPageTabBarBuilderImpl();
case ViewLayoutPB.Calendar:
return CalendarPageTabBarBuilderImpl();
case ViewLayoutPB.Grid:
return GridPageTabBarBuilderImpl();
case ViewLayoutPB.Document:
return DesktopGridTabBarBuilderImpl();
default:
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;

View File

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

View File

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