mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
b00d29d0cd
commit
16467e9c13
33
frontend/.vscode/launch.json
vendored
33
frontend/.vscode/launch.json
vendored
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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];
|
||||
|
@ -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],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -94,25 +93,36 @@ class _GridHeaderState extends State<_GridHeader> {
|
||||
builder: (context, state) {
|
||||
final cells = state.fields
|
||||
.map(
|
||||
(fieldInfo) => GridFieldCell(
|
||||
(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(
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -54,20 +54,16 @@ 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,
|
||||
buildWhen: (p, c) => p.rowSource != c.rowSource,
|
||||
builder: (context, state) {
|
||||
final content = Expanded(
|
||||
child: RowContent(
|
||||
@ -79,8 +75,9 @@ class _GridRowState extends State<GridRow> {
|
||||
),
|
||||
);
|
||||
|
||||
return Row(
|
||||
return _RowEnterRegion(
|
||||
key: ValueKey(state.rowSource),
|
||||
child: Row(
|
||||
children: [
|
||||
_RowLeading(
|
||||
index: widget.index,
|
||||
@ -88,10 +85,10 @@ class _GridRowState extends State<GridRow> {
|
||||
),
|
||||
content,
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -155,20 +152,19 @@ 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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,20 +13,16 @@ 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(
|
||||
return SizedBox(
|
||||
height: 30,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
@ -54,7 +51,7 @@ class _TabBarHeaderState extends State<TabBarHeader> {
|
||||
child: Column(
|
||||
children: [
|
||||
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) {
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
@ -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),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,19 +91,31 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
FlowySvgData get iconData => layout.icon;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user