diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart index b091d0df8a..84327daf1e 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart @@ -15,7 +15,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/material.dart'; 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_controller.dart'; import '../application/grid_bloc.dart'; @@ -297,11 +296,14 @@ class _GridRows extends StatelessWidget { final rowMeta = rowCache.getRow(rowId)?.rowMeta; /// Return placeholder widget if the rowMeta is null. - if (rowMeta == null) return const SizedBox.shrink(); + if (rowMeta == null) { + Log.warn('RowMeta is null for rowId: $rowId'); + return const SizedBox.shrink(); + } final fieldController = context.read().databaseController.fieldController; - final dataController = RowController( + final rowController = RowController( viewId: viewId, rowMeta: rowMeta, rowCache: rowCache, @@ -313,15 +315,18 @@ class _GridRows extends StatelessWidget { viewId: viewId, index: index, isDraggable: isDraggable, - dataController: dataController, - cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), + dataController: rowController, + cellBuilder: GridCellBuilder(cellCache: rowController.cellCache), openDetailPage: (context, cellBuilder) { - _openRowDetailPage( - context, - rowId, - fieldController, - rowCache, - cellBuilder, + FlowyOverlay.show( + context: context, + builder: (BuildContext context) { + return RowDetailPage( + cellBuilder: cellBuilder, + rowController: rowController, + fieldController: fieldController, + ); + }, ); }, ); @@ -335,37 +340,6 @@ class _GridRows extends StatelessWidget { 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, - fieldController: fieldController, - ); - }, - ); - } else { - Log.warn('RowMeta is null for rowId: $rowId'); - } - } } class _WrapScrollView extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/mobile_grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/mobile_grid_page.dart index ff3e49cb2d..811d2efaae 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/mobile_grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/mobile_grid_page.dart @@ -1,13 +1,12 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; -import 'package:appflowy/plugins/database_view/application/field/field_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'; @@ -18,6 +17,7 @@ import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; import 'grid_page.dart'; @@ -26,7 +26,7 @@ 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/row/mobile_row.dart'; import 'widgets/shortcuts.dart'; import '../../widgets/setting/mobile_database_settings_button.dart'; @@ -299,32 +299,33 @@ class _GridRows extends StatelessWidget { final rowCache = context.read().getRowCache(rowId); final rowMeta = rowCache.getRow(rowId)?.rowMeta; - /// Return placeholder widget if the rowMeta is null. - if (rowMeta == null) return const SizedBox.shrink(); + if (rowMeta == null) { + Log.warn('RowMeta is null for rowId: $rowId'); + return const SizedBox.shrink(); + } final fieldController = context.read().databaseController.fieldController; - final dataController = RowController( + final rowController = RowController( viewId: viewId, rowMeta: rowMeta, rowCache: rowCache, ); - final child = GridRow( + final child = MobileGridRow( key: ValueKey(rowMeta.id), rowId: rowId, viewId: viewId, - index: index, isDraggable: isDraggable, - dataController: dataController, - cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), + dataController: rowController, + cellBuilder: GridCellBuilder(cellCache: rowController.cellCache), openDetailPage: (context, cellBuilder) { - _openRowDetailPage( - context, - rowId, - fieldController, - rowCache, - cellBuilder, + context.push( + MobileCardDetailScreen.routeName, + extra: { + MobileCardDetailScreen.argRowController: rowController, + MobileCardDetailScreen.argFieldController: fieldController, + }, ); }, ); @@ -338,37 +339,6 @@ class _GridRows extends StatelessWidget { 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, - fieldController: fieldController, - ); - }, - ); - } else { - Log.warn('RowMeta is null for rowId: $rowId'); - } - } } class _WrapScrollView extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/mobile_row.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/mobile_row.dart new file mode 100755 index 0000000000..58b0d2eca5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/mobile_row.dart @@ -0,0 +1,190 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_service.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/row/row_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/mobile_cell_container.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../layout/sizes.dart'; +import "package:appflowy/generated/locale_keys.g.dart"; +import 'package:easy_localization/easy_localization.dart'; + +class MobileGridRow extends StatefulWidget { + final RowId viewId; + final RowId rowId; + final RowController dataController; + final GridCellBuilder cellBuilder; + final void Function(BuildContext, GridCellBuilder) openDetailPage; + + final bool isDraggable; + + const MobileGridRow({ + super.key, + required this.viewId, + required this.rowId, + required this.dataController, + required this.cellBuilder, + required this.openDetailPage, + this.isDraggable = false, + }); + + @override + State createState() => _MobileGridRowState(); +} + +class _MobileGridRowState extends State { + late final RowBloc _rowBloc; + + @override + void initState() { + super.initState(); + _rowBloc = RowBloc( + rowId: widget.rowId, + dataController: widget.dataController, + viewId: widget.viewId, + )..add(const RowEvent.initial()); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _rowBloc, + child: BlocBuilder( + // The row need to rebuild when the cell count changes. + buildWhen: (p, c) => p.rowSource != c.rowSource, + builder: (context, state) { + return Row( + children: [ + SizedBox(width: GridSize.leadingHeaderPadding), + Expanded( + child: RowContent( + builder: widget.cellBuilder, + onExpand: () => widget.openDetailPage( + context, + widget.cellBuilder, + ), + ), + ), + ], + ); + }, + ), + ); + } + + @override + Future dispose() async { + _rowBloc.close(); + super.dispose(); + } +} + +class InsertRowButton extends StatelessWidget { + const InsertRowButton({super.key}); + + @override + Widget build(BuildContext context) { + return FlowyIconButton( + tooltipText: LocaleKeys.tooltip_addNewRow.tr(), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + width: 20, + height: 30, + onPressed: () => context.read().add(const RowEvent.createRow()), + iconPadding: const EdgeInsets.all(3), + icon: FlowySvg( + FlowySvgs.add_s, + color: Theme.of(context).colorScheme.tertiary, + ), + ); + } +} + +class RowContent extends StatelessWidget { + final VoidCallback onExpand; + final GridCellBuilder builder; + const RowContent({ + super.key, + required this.builder, + required this.onExpand, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + !listEquals(previous.cells, current.cells), + builder: (context, state) { + return IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ..._makeCells(context, state.cellByFieldId), + _finalCellDecoration(context), + ], + ), + ); + }, + ); + } + + List _makeCells( + BuildContext context, + CellContextByFieldId cellByFieldId, + ) { + return cellByFieldId.values.map( + (cellId) { + final GridCellWidget child = builder.build(cellId); + + return MobileCellContainer( + width: cellId.fieldInfo.fieldSettings?.width.toDouble() ?? 140, + isPrimary: cellId.fieldInfo.field.isPrimary, + accessoryBuilder: (buildContext) { + final builder = child.accessoryBuilder; + final List accessories = []; + if (cellId.fieldInfo.field.isPrimary) { + accessories.add( + GridCellAccessoryBuilder( + builder: (key) => PrimaryCellAccessory( + key: key, + onTapCallback: onExpand, + isCellEditing: buildContext.isCellEditing, + ), + ), + ); + } + + if (builder != null) { + accessories.addAll(builder(buildContext)); + } + + return accessories; + }, + child: child, + ); + }, + ).toList(); + } + + Widget _finalCellDecoration(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.basic, + child: Container( + width: GridSize.trailHeaderPadding, + padding: GridSize.headerContentInsets, + constraints: const BoxConstraints(minHeight: 46), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Theme.of(context).dividerColor), + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/mobile_cell_container.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/mobile_cell_container.dart new file mode 100644 index 0000000000..37e6597e8e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/mobile_cell_container.dart @@ -0,0 +1,130 @@ +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; + +import '../accessory/cell_accessory.dart'; +import '../accessory/cell_shortcuts.dart'; +import '../cell_builder.dart'; +import 'cell_container.dart'; + +class MobileCellContainer extends StatelessWidget { + final GridCellWidget child; + final AccessoryBuilder? accessoryBuilder; + final double width; + final bool isPrimary; + + const MobileCellContainer({ + super.key, + required this.child, + required this.width, + required this.isPrimary, + this.accessoryBuilder, + }); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: child.cellContainerNotifier, + child: Selector( + selector: (context, notifier) => notifier.isFocus, + builder: (providerContext, isFocus, _) { + Widget container = Center(child: GridCellShortcuts(child: child)); + + if (accessoryBuilder != null) { + final accessories = accessoryBuilder!.call( + GridCellAccessoryBuildContext( + anchorContext: context, + isCellEditing: isFocus, + ), + ); + + if (accessories.isNotEmpty) { + container = _GridCellEnterRegion( + accessories: accessories, + isPrimary: isPrimary, + child: container, + ); + } + } + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (!isFocus) { + child.requestFocus.notify(); + } + }, + child: Container( + constraints: BoxConstraints(maxWidth: width, minHeight: 46), + decoration: _makeBoxDecoration(context, isFocus), + child: container, + ), + ); + }, + ), + ); + } + + BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) { + if (isFocus) { + return BoxDecoration( + border: Border.fromBorderSide( + BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + ); + } + + final borderSide = BorderSide(color: Theme.of(context).dividerColor); + return BoxDecoration( + border: Border(right: borderSide, bottom: borderSide), + ); + } +} + +class _GridCellEnterRegion extends StatelessWidget { + const _GridCellEnterRegion({ + required this.child, + required this.accessories, + required this.isPrimary, + Key? key, + }) : super(key: key); + + final Widget child; + final List accessories; + final bool isPrimary; + + @override + Widget build(BuildContext context) { + return Selector( + selector: (context, cellNotifier) => + !cellNotifier.isFocus && (cellNotifier.onEnter || isPrimary), + builder: (context, showAccessory, _) { + final List children = [child]; + + if (showAccessory) { + children.add( + CellAccessoryContainer(accessories: accessories).positioned( + right: GridSize.cellContentInsets.right, + ), + ); + } + + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (p) => + CellContainerNotifier.of(context, listen: false).onEnter = true, + onExit: (p) => + CellContainerNotifier.of(context, listen: false).onEnter = false, + child: Stack( + alignment: AlignmentDirectional.center, + fit: StackFit.expand, + children: children, + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 722db49ea7..8446512456 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -239,9 +239,6 @@ class MobileAppearance extends BaseAppearance { ), colorScheme: colorTheme, indicatorColor: Colors.blue, - textSelectionTheme: TextSelectionThemeData( - cursorColor: colorTheme.onBackground, - ), extensions: [ AFThemeExtension( warning: theme.yellow,