feat: open a mobile grid row as a page (#4000)

* chore: restore text cursor color

* chore: open row as a card in mobile

* refactor: clean up code

* chore: code review

* chore: restore c++ shared library
This commit is contained in:
Richard Shiue 2023-11-27 12:37:38 +08:00 committed by GitHub
parent 771dd9979f
commit cac3acd553
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 353 additions and 92 deletions

View File

@ -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<GridBloc>().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 {

View File

@ -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<GridBloc>().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<GridBloc>().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 {

View File

@ -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<MobileGridRow> createState() => _MobileGridRowState();
}
class _MobileGridRowState extends State<MobileGridRow> {
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<RowBloc, RowState>(
// 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<void> 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<RowBloc>().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<RowBloc, RowState>(
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<Widget> _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<GridCellAccessoryBuilder> 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),
),
),
),
);
}
}

View File

@ -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<CellContainerNotifier, bool>(
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<GridCellAccessoryBuilder> accessories;
final bool isPrimary;
@override
Widget build(BuildContext context) {
return Selector<CellContainerNotifier, bool>(
selector: (context, cellNotifier) =>
!cellNotifier.isFocus && (cellNotifier.onEnter || isPrimary),
builder: (context, showAccessory, _) {
final List<Widget> 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,
),
);
},
);
}
}

View File

@ -239,9 +239,6 @@ class MobileAppearance extends BaseAppearance {
),
colorScheme: colorTheme,
indicatorColor: Colors.blue,
textSelectionTheme: TextSelectionThemeData(
cursorColor: colorTheme.onBackground,
),
extensions: [
AFThemeExtension(
warning: theme.yellow,