|
@@ -1,5 +1,7 @@
|
|
|
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/tar_bar/setting_menu.dart';
|
|
|
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
|
|
import 'package:appflowy_backend/log.dart';
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
@@ -15,25 +17,77 @@ import 'package:linked_scroll_controller/linked_scroll_controller.dart';
|
|
|
import '../../application/field/field_controller.dart';
|
|
|
import '../../application/row/row_cache.dart';
|
|
|
import '../../application/row/row_data_controller.dart';
|
|
|
-import '../../application/setting/setting_bloc.dart';
|
|
|
-import '../application/filter/filter_menu_bloc.dart';
|
|
|
import '../application/grid_bloc.dart';
|
|
|
import '../../application/database_controller.dart';
|
|
|
-import '../application/sort/sort_menu_bloc.dart';
|
|
|
import 'grid_scroll.dart';
|
|
|
+import '../../tar_bar/tab_bar_view.dart';
|
|
|
import 'layout/layout.dart';
|
|
|
import 'layout/sizes.dart';
|
|
|
-import 'widgets/accessory_menu.dart';
|
|
|
import 'widgets/row/row.dart';
|
|
|
import 'widgets/footer/grid_footer.dart';
|
|
|
import 'widgets/header/grid_header.dart';
|
|
|
import '../../widgets/row/row_detail.dart';
|
|
|
import 'widgets/shortcuts.dart';
|
|
|
-import 'widgets/toolbar/grid_toolbar.dart';
|
|
|
+
|
|
|
+class ToggleExtensionNotifier extends ChangeNotifier {
|
|
|
+ bool _isToggled = false;
|
|
|
+
|
|
|
+ get isToggled => _isToggled;
|
|
|
+
|
|
|
+ void toggle() {
|
|
|
+ _isToggled = !_isToggled;
|
|
|
+ notifyListeners();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class GridPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
|
|
+ final _toggleExtension = ToggleExtensionNotifier();
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget content(
|
|
|
+ BuildContext context,
|
|
|
+ ViewPB view,
|
|
|
+ DatabaseController controller,
|
|
|
+ ) {
|
|
|
+ return GridPage(
|
|
|
+ key: _makeValueKey(controller),
|
|
|
+ view: view,
|
|
|
+ databaseController: controller,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget settingBar(BuildContext context, DatabaseController controller) {
|
|
|
+ return GridSettingBar(
|
|
|
+ key: _makeValueKey(controller),
|
|
|
+ controller: controller,
|
|
|
+ toggleExtension: _toggleExtension,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget settingBarExtension(
|
|
|
+ BuildContext context,
|
|
|
+ DatabaseController controller,
|
|
|
+ ) {
|
|
|
+ return DatabaseViewSettingExtension(
|
|
|
+ key: _makeValueKey(controller),
|
|
|
+ viewId: controller.viewId,
|
|
|
+ databaseController: controller,
|
|
|
+ toggleExtension: _toggleExtension,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ ValueKey _makeValueKey(DatabaseController controller) {
|
|
|
+ return ValueKey(controller.viewId);
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
class GridPage extends StatefulWidget {
|
|
|
+ final DatabaseController databaseController;
|
|
|
const GridPage({
|
|
|
required this.view,
|
|
|
+ required this.databaseController,
|
|
|
this.onDeleted,
|
|
|
Key? key,
|
|
|
}) : super(key: key);
|
|
@@ -46,12 +100,9 @@ class GridPage extends StatefulWidget {
|
|
|
}
|
|
|
|
|
|
class _GridPageState extends State<GridPage> {
|
|
|
- late DatabaseController databaseController;
|
|
|
-
|
|
|
@override
|
|
|
void initState() {
|
|
|
super.initState();
|
|
|
- databaseController = DatabaseController(view: widget.view);
|
|
|
}
|
|
|
|
|
|
@override
|
|
@@ -61,24 +112,9 @@ class _GridPageState extends State<GridPage> {
|
|
|
BlocProvider<GridBloc>(
|
|
|
create: (context) => GridBloc(
|
|
|
view: widget.view,
|
|
|
- databaseController: databaseController,
|
|
|
+ databaseController: widget.databaseController,
|
|
|
)..add(const GridEvent.initial()),
|
|
|
),
|
|
|
- BlocProvider<GridFilterMenuBloc>(
|
|
|
- create: (context) => GridFilterMenuBloc(
|
|
|
- viewId: widget.view.id,
|
|
|
- fieldController: databaseController.fieldController,
|
|
|
- )..add(const GridFilterMenuEvent.initial()),
|
|
|
- ),
|
|
|
- BlocProvider<SortMenuBloc>(
|
|
|
- create: (context) => SortMenuBloc(
|
|
|
- viewId: widget.view.id,
|
|
|
- fieldController: databaseController.fieldController,
|
|
|
- )..add(const SortMenuEvent.initial()),
|
|
|
- ),
|
|
|
- BlocProvider<DatabaseSettingBloc>(
|
|
|
- create: (context) => DatabaseSettingBloc(viewId: widget.view.id),
|
|
|
- ),
|
|
|
],
|
|
|
child: BlocBuilder<GridBloc, GridState>(
|
|
|
builder: (context, state) {
|
|
@@ -87,9 +123,7 @@ class _GridPageState extends State<GridPage> {
|
|
|
const Center(child: CircularProgressIndicator.adaptive()),
|
|
|
finish: (result) => result.successOrFail.fold(
|
|
|
(_) => GridShortcuts(
|
|
|
- child: FlowyGrid(
|
|
|
- viewId: widget.view.id,
|
|
|
- ),
|
|
|
+ child: GridPageContent(view: widget.view),
|
|
|
),
|
|
|
(err) => FlowyErrorPage(err.toString()),
|
|
|
),
|
|
@@ -100,18 +134,18 @@ class _GridPageState extends State<GridPage> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-class FlowyGrid extends StatefulWidget {
|
|
|
- final String viewId;
|
|
|
- const FlowyGrid({
|
|
|
- required this.viewId,
|
|
|
+class GridPageContent extends StatefulWidget {
|
|
|
+ final ViewPB view;
|
|
|
+ const GridPageContent({
|
|
|
+ required this.view,
|
|
|
super.key,
|
|
|
});
|
|
|
|
|
|
@override
|
|
|
- State<FlowyGrid> createState() => _FlowyGridState();
|
|
|
+ State<GridPageContent> createState() => _GridPageContentState();
|
|
|
}
|
|
|
|
|
|
-class _FlowyGridState extends State<FlowyGrid> {
|
|
|
+class _GridPageContentState extends State<GridPageContent> {
|
|
|
final _scrollController = GridScrollController(
|
|
|
scrollGroupController: LinkedScrollControllerGroup(),
|
|
|
);
|
|
@@ -135,106 +169,114 @@ class _FlowyGridState extends State<FlowyGrid> {
|
|
|
buildWhen: (previous, current) => previous.fields != current.fields,
|
|
|
builder: (context, state) {
|
|
|
final contentWidth = GridLayout.headerWidth(state.fields.value);
|
|
|
- final child = _WrapScrollView(
|
|
|
- scrollController: _scrollController,
|
|
|
- contentWidth: contentWidth,
|
|
|
- child: _GridRows(
|
|
|
- viewId: widget.viewId,
|
|
|
- verticalScrollController: _scrollController.verticalController,
|
|
|
- ),
|
|
|
- );
|
|
|
|
|
|
return Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
- const GridToolbar(),
|
|
|
- GridAccessoryMenu(viewId: state.viewId),
|
|
|
- _gridHeader(context, state.viewId),
|
|
|
- Flexible(child: child),
|
|
|
- const _RowCountBadge(),
|
|
|
+ _GridHeader(headerScrollController: headerScrollController),
|
|
|
+ _GridRows(
|
|
|
+ viewId: state.viewId,
|
|
|
+ contentWidth: contentWidth,
|
|
|
+ scrollController: _scrollController,
|
|
|
+ ),
|
|
|
+ const _GridFooter(),
|
|
|
],
|
|
|
);
|
|
|
},
|
|
|
);
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- Widget _gridHeader(BuildContext context, String viewId) {
|
|
|
- final fieldController =
|
|
|
- context.read<GridBloc>().databaseController.fieldController;
|
|
|
- return GridHeaderSliverAdaptor(
|
|
|
- viewId: viewId,
|
|
|
- fieldController: fieldController,
|
|
|
- anchorScrollController: headerScrollController,
|
|
|
+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.verticalScrollController,
|
|
|
+ required this.contentWidth,
|
|
|
+ required this.scrollController,
|
|
|
});
|
|
|
|
|
|
- final ScrollController verticalScrollController;
|
|
|
-
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
- final filterState = context.watch<GridFilterMenuBloc>().state;
|
|
|
- final sortState = context.watch<SortMenuBloc>().state;
|
|
|
-
|
|
|
- return 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,
|
|
|
- );
|
|
|
- return ScrollConfiguration(
|
|
|
- behavior: behavior,
|
|
|
- child: ReorderableListView.builder(
|
|
|
- /// TODO(Xazin): Resolve inconsistent scrollbar behavior
|
|
|
- /// This is a workaround related to
|
|
|
- /// https://github.com/flutter/flutter/issues/25652
|
|
|
- cacheExtent: 5000,
|
|
|
- scrollController: verticalScrollController,
|
|
|
- buildDefaultDragHandles: false,
|
|
|
- proxyDecorator: (child, index, animation) => Material(
|
|
|
- color: Colors.white.withOpacity(.1),
|
|
|
- child: Opacity(opacity: .5, 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 + 1, // the extra item is the footer
|
|
|
- itemBuilder: (context, index) {
|
|
|
- if (index < rowInfos.length) {
|
|
|
- final rowInfo = rowInfos[index];
|
|
|
- return _renderRow(
|
|
|
- context,
|
|
|
- rowInfo.rowId,
|
|
|
- index: index,
|
|
|
- isSortEnabled: sortState.sortInfos.isNotEmpty,
|
|
|
- isFilterEnabled: filterState.filters.isNotEmpty,
|
|
|
- );
|
|
|
- }
|
|
|
- return const _GridFooter(key: Key('gridFooter'));
|
|
|
- },
|
|
|
+ return Flexible(
|
|
|
+ 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,
|
|
|
+ );
|
|
|
+ return ScrollConfiguration(
|
|
|
+ behavior: behavior,
|
|
|
+ child: ReorderableListView.builder(
|
|
|
+ /// TODO(Xazin): Resolve inconsistent scrollbar behavior
|
|
|
+ /// This is a workaround related to
|
|
|
+ /// https://github.com/flutter/flutter/issues/25652
|
|
|
+ cacheExtent: 5000,
|
|
|
+ scrollController: scrollController.verticalController,
|
|
|
+ buildDefaultDragHandles: false,
|
|
|
+ proxyDecorator: (child, index, animation) => Material(
|
|
|
+ color: Colors.white.withOpacity(.1),
|
|
|
+ child: Opacity(opacity: .5, 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 + 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'));
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ ),
|
|
|
);
|
|
|
}
|
|
|
|
|
@@ -242,8 +284,7 @@ class _GridRows extends StatelessWidget {
|
|
|
BuildContext context,
|
|
|
RowId rowId, {
|
|
|
int? index,
|
|
|
- bool isSortEnabled = false,
|
|
|
- bool isFilterEnabled = false,
|
|
|
+ required bool isDraggable,
|
|
|
Animation<double>? animation,
|
|
|
}) {
|
|
|
final rowCache = context.read<GridBloc>().getRowCache(rowId);
|
|
@@ -265,7 +306,7 @@ class _GridRows extends StatelessWidget {
|
|
|
rowId: rowId,
|
|
|
viewId: viewId,
|
|
|
index: index,
|
|
|
- isDraggable: !isSortEnabled && !isFilterEnabled,
|
|
|
+ isDraggable: isDraggable,
|
|
|
dataController: dataController,
|
|
|
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
|
|
|
openDetailPage: (context, cellBuilder) {
|
|
@@ -320,22 +361,6 @@ class _GridRows extends StatelessWidget {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-class _GridFooter extends StatelessWidget {
|
|
|
- const _GridFooter({
|
|
|
- super.key,
|
|
|
- });
|
|
|
-
|
|
|
- @override
|
|
|
- Widget build(BuildContext context) {
|
|
|
- return Container(
|
|
|
- padding: GridSize.footerContentInsets,
|
|
|
- height: GridSize.footerHeight,
|
|
|
- margin: const EdgeInsets.only(bottom: 200),
|
|
|
- child: const GridAddRowButton(),
|
|
|
- );
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
class _WrapScrollView extends StatelessWidget {
|
|
|
const _WrapScrollView({
|
|
|
required this.contentWidth,
|
|
@@ -366,8 +391,8 @@ class _WrapScrollView extends StatelessWidget {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-class _RowCountBadge extends StatelessWidget {
|
|
|
- const _RowCountBadge();
|
|
|
+class _GridFooter extends StatelessWidget {
|
|
|
+ const _GridFooter();
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|