mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
771dd9979f
commit
cac3acd553
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -239,9 +239,6 @@ class MobileAppearance extends BaseAppearance {
|
||||
),
|
||||
colorScheme: colorTheme,
|
||||
indicatorColor: Colors.blue,
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
cursorColor: colorTheme.onBackground,
|
||||
),
|
||||
extensions: [
|
||||
AFThemeExtension(
|
||||
warning: theme.yellow,
|
||||
|
Loading…
Reference in New Issue
Block a user