2023-08-14 13:34:01 -07:00
|
|
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
2023-11-29 19:01:29 -07:00
|
|
|
import 'package:appflowy/mobile/presentation/database/card/card.dart';
|
2024-01-05 17:30:54 +08:00
|
|
|
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
|
|
|
|
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
|
|
|
|
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/action.dart';
|
2023-04-28 14:08:53 +08:00
|
|
|
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
2023-12-15 18:20:44 +08:00
|
|
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
2022-09-19 18:12:41 +08:00
|
|
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
2022-09-19 15:36:59 +08:00
|
|
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
2023-05-31 16:52:37 +08:00
|
|
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
2022-09-06 20:43:49 +08:00
|
|
|
import 'package:flutter/foundation.dart';
|
2022-08-10 17:59:28 +08:00
|
|
|
import 'package:flutter/material.dart';
|
2022-08-12 20:10:56 +08:00
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
2023-11-26 15:10:48 +08:00
|
|
|
|
2023-03-08 21:19:44 +08:00
|
|
|
import 'card_bloc.dart';
|
2022-08-12 20:10:56 +08:00
|
|
|
import 'card_cell_builder.dart';
|
2023-11-26 15:10:48 +08:00
|
|
|
import 'cells/card_cell.dart';
|
2022-09-19 15:36:59 +08:00
|
|
|
import 'container/accessory.dart';
|
|
|
|
import 'container/card_container.dart';
|
2022-08-10 17:59:28 +08:00
|
|
|
|
2023-04-30 20:50:24 +08:00
|
|
|
/// Edit a database row with card style widget
|
|
|
|
class RowCard<CustomCardData> extends StatefulWidget {
|
2023-06-14 22:16:33 +08:00
|
|
|
final RowMetaPB rowMeta;
|
2023-01-31 08:28:31 +08:00
|
|
|
final String viewId;
|
2023-04-30 20:50:24 +08:00
|
|
|
final String? groupingFieldId;
|
2023-08-01 08:49:48 +05:30
|
|
|
final String? groupId;
|
2023-04-30 20:50:24 +08:00
|
|
|
|
|
|
|
/// Allows passing a custom card data object to the card. The card will be
|
|
|
|
/// returned in the [CardCellBuilder] and can be used to build the card.
|
2023-03-08 21:19:44 +08:00
|
|
|
final CustomCardData? cardData;
|
2022-08-13 16:23:44 +08:00
|
|
|
final bool isEditing;
|
2023-03-08 21:19:44 +08:00
|
|
|
final RowCache rowCache;
|
2023-04-30 20:50:24 +08:00
|
|
|
|
|
|
|
/// The [CardCellBuilder] is used to build the card cells.
|
2023-03-08 21:19:44 +08:00
|
|
|
final CardCellBuilder<CustomCardData> cellBuilder;
|
2023-04-30 20:50:24 +08:00
|
|
|
|
|
|
|
/// Called when the user taps on the card.
|
2022-08-16 18:02:39 +08:00
|
|
|
final void Function(BuildContext) openCard;
|
2023-04-30 20:50:24 +08:00
|
|
|
|
|
|
|
/// Called when the user starts editing the card.
|
2022-10-06 22:26:18 +08:00
|
|
|
final VoidCallback onStartEditing;
|
2023-04-30 20:50:24 +08:00
|
|
|
|
|
|
|
/// Called when the user ends editing the card.
|
2022-10-06 22:26:18 +08:00
|
|
|
final VoidCallback onEndEditing;
|
2022-08-12 20:10:56 +08:00
|
|
|
|
2023-04-30 20:50:24 +08:00
|
|
|
/// The [RowCardRenderHook] is used to render the card's cell. Other than
|
|
|
|
/// using the default cell builder. For example the [SelectOptionCardCell]
|
|
|
|
final RowCardRenderHook<CustomCardData>? renderHook;
|
|
|
|
|
|
|
|
final RowCardStyleConfiguration styleConfiguration;
|
|
|
|
|
|
|
|
const RowCard({
|
2023-11-09 00:30:50 +01:00
|
|
|
super.key,
|
2023-06-14 22:16:33 +08:00
|
|
|
required this.rowMeta,
|
2023-01-31 08:28:31 +08:00
|
|
|
required this.viewId,
|
2022-08-13 16:23:44 +08:00
|
|
|
required this.isEditing,
|
2023-03-08 21:19:44 +08:00
|
|
|
required this.rowCache,
|
2022-08-12 20:10:56 +08:00
|
|
|
required this.cellBuilder,
|
2022-08-16 18:02:39 +08:00
|
|
|
required this.openCard,
|
2022-10-06 22:26:18 +08:00
|
|
|
required this.onStartEditing,
|
|
|
|
required this.onEndEditing,
|
2023-11-09 00:30:50 +01:00
|
|
|
this.groupingFieldId,
|
|
|
|
this.groupId,
|
2023-03-08 21:19:44 +08:00
|
|
|
this.cardData,
|
2023-04-30 20:50:24 +08:00
|
|
|
this.styleConfiguration = const RowCardStyleConfiguration(
|
|
|
|
showAccessory: true,
|
|
|
|
),
|
|
|
|
this.renderHook,
|
2023-11-09 00:30:50 +01:00
|
|
|
});
|
2022-08-12 20:10:56 +08:00
|
|
|
|
|
|
|
@override
|
2023-04-30 20:50:24 +08:00
|
|
|
State<RowCard<CustomCardData>> createState() =>
|
|
|
|
_RowCardState<CustomCardData>();
|
2022-08-12 20:10:56 +08:00
|
|
|
}
|
|
|
|
|
2023-04-30 20:50:24 +08:00
|
|
|
class _RowCardState<T> extends State<RowCard<T>> {
|
2023-11-24 00:25:12 +08:00
|
|
|
final popoverController = PopoverController();
|
2023-05-31 16:52:37 +08:00
|
|
|
late final CardBloc _cardBloc;
|
|
|
|
late final EditableRowNotifier rowNotifier;
|
2022-09-19 15:36:59 +08:00
|
|
|
AccessoryType? accessoryType;
|
2022-08-12 20:10:56 +08:00
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
2023-11-09 00:30:50 +01:00
|
|
|
super.initState();
|
2022-09-07 11:33:42 +08:00
|
|
|
rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
|
2023-03-08 21:19:44 +08:00
|
|
|
_cardBloc = CardBloc(
|
2023-01-31 08:28:31 +08:00
|
|
|
viewId: widget.viewId,
|
2023-04-30 20:50:24 +08:00
|
|
|
groupFieldId: widget.groupingFieldId,
|
2022-09-07 11:33:42 +08:00
|
|
|
isEditing: widget.isEditing,
|
2023-06-14 22:16:33 +08:00
|
|
|
rowMeta: widget.rowMeta,
|
2023-03-08 21:19:44 +08:00
|
|
|
rowCache: widget.rowCache,
|
2023-04-30 20:50:24 +08:00
|
|
|
)..add(const RowCardEvent.initial());
|
2022-09-07 11:33:42 +08:00
|
|
|
|
|
|
|
rowNotifier.isEditing.addListener(() {
|
|
|
|
if (!mounted) return;
|
2023-04-30 20:50:24 +08:00
|
|
|
_cardBloc.add(RowCardEvent.setIsEditing(rowNotifier.isEditing.value));
|
2022-10-06 22:26:18 +08:00
|
|
|
|
|
|
|
if (rowNotifier.isEditing.value) {
|
|
|
|
widget.onStartEditing();
|
|
|
|
} else {
|
|
|
|
widget.onEndEditing();
|
|
|
|
}
|
2022-09-07 11:33:42 +08:00
|
|
|
});
|
2022-08-12 20:10:56 +08:00
|
|
|
}
|
2022-08-10 17:59:28 +08:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2022-08-12 20:10:56 +08:00
|
|
|
return BlocProvider.value(
|
|
|
|
value: _cardBloc,
|
2023-04-30 20:50:24 +08:00
|
|
|
child: BlocBuilder<CardBloc, RowCardState>(
|
2022-08-26 16:07:51 +08:00
|
|
|
buildWhen: (previous, current) {
|
2022-09-18 11:06:04 +08:00
|
|
|
// Rebuild when:
|
2023-11-24 00:25:12 +08:00
|
|
|
// 1. If the length of the cells is not the same or isEditing changed
|
2022-09-07 11:33:42 +08:00
|
|
|
if (previous.cells.length != current.cells.length ||
|
|
|
|
previous.isEditing != current.isEditing) {
|
2022-09-06 20:43:49 +08:00
|
|
|
return true;
|
|
|
|
}
|
2022-09-18 11:06:04 +08:00
|
|
|
|
2023-11-24 00:25:12 +08:00
|
|
|
// 2. the content of the cells changed
|
2022-09-06 20:43:49 +08:00
|
|
|
return !listEquals(previous.cells, current.cells);
|
2022-08-26 16:07:51 +08:00
|
|
|
},
|
2022-08-12 20:10:56 +08:00
|
|
|
builder: (context, state) {
|
2023-11-20 21:56:21 -08:00
|
|
|
if (PlatformExtension.isMobile) {
|
2023-12-10 06:25:11 -07:00
|
|
|
return GestureDetector(
|
2023-11-29 19:01:29 -07:00
|
|
|
child: MobileCardContent<T>(
|
2023-11-20 21:56:21 -08:00
|
|
|
cellBuilder: widget.cellBuilder,
|
|
|
|
styleConfiguration: widget.styleConfiguration,
|
|
|
|
cells: state.cells,
|
|
|
|
renderHook: widget.renderHook,
|
|
|
|
cardData: widget.cardData,
|
|
|
|
),
|
2023-12-10 06:25:11 -07:00
|
|
|
onTap: () => widget.openCard(context),
|
2023-11-20 21:56:21 -08:00
|
|
|
);
|
|
|
|
}
|
2023-11-24 00:25:12 +08:00
|
|
|
|
2022-09-19 15:36:59 +08:00
|
|
|
return AppFlowyPopover(
|
|
|
|
controller: popoverController,
|
2022-09-22 21:02:10 +08:00
|
|
|
triggerActions: PopoverTriggerFlags.none,
|
2022-09-19 15:36:59 +08:00
|
|
|
constraints: BoxConstraints.loose(const Size(140, 200)),
|
|
|
|
direction: PopoverDirection.rightWithCenterAligned,
|
2023-12-11 11:19:20 +08:00
|
|
|
popupBuilder: (_) {
|
|
|
|
return RowActionMenu.board(
|
|
|
|
viewId: _cardBloc.viewId,
|
|
|
|
rowId: _cardBloc.rowMeta.id,
|
|
|
|
groupId: widget.groupId,
|
|
|
|
);
|
|
|
|
},
|
2023-04-30 20:50:24 +08:00
|
|
|
child: RowCardContainer(
|
2022-09-19 15:36:59 +08:00
|
|
|
buildAccessoryWhen: () => state.isEditing == false,
|
2023-11-24 00:25:12 +08:00
|
|
|
accessories: [
|
|
|
|
if (widget.styleConfiguration.showAccessory) ...[
|
|
|
|
_CardEditOption(rowNotifier: rowNotifier),
|
|
|
|
const CardMoreOption(),
|
|
|
|
],
|
|
|
|
],
|
2022-09-19 15:36:59 +08:00
|
|
|
openAccessory: _handleOpenAccessory,
|
|
|
|
openCard: (context) => widget.openCard(context),
|
2023-03-08 21:19:44 +08:00
|
|
|
child: _CardContent<T>(
|
2022-09-19 15:36:59 +08:00
|
|
|
rowNotifier: rowNotifier,
|
|
|
|
cellBuilder: widget.cellBuilder,
|
2023-04-30 20:50:24 +08:00
|
|
|
styleConfiguration: widget.styleConfiguration,
|
2022-09-19 15:36:59 +08:00
|
|
|
cells: state.cells,
|
2023-04-30 20:50:24 +08:00
|
|
|
renderHook: widget.renderHook,
|
2023-03-08 21:19:44 +08:00
|
|
|
cardData: widget.cardData,
|
2022-09-19 15:36:59 +08:00
|
|
|
),
|
2022-08-13 10:01:40 +08:00
|
|
|
),
|
2022-08-12 20:10:56 +08:00
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-09-19 15:36:59 +08:00
|
|
|
void _handleOpenAccessory(AccessoryType newAccessoryType) {
|
|
|
|
accessoryType = newAccessoryType;
|
|
|
|
switch (newAccessoryType) {
|
|
|
|
case AccessoryType.edit:
|
|
|
|
break;
|
|
|
|
case AccessoryType.more:
|
|
|
|
popoverController.show();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-18 11:06:04 +08:00
|
|
|
@override
|
|
|
|
Future<void> dispose() async {
|
|
|
|
rowNotifier.dispose();
|
|
|
|
_cardBloc.close();
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-26 15:10:48 +08:00
|
|
|
class _CardContent<CustomCardData> extends StatefulWidget {
|
2023-03-08 21:19:44 +08:00
|
|
|
const _CardContent({
|
2023-11-09 00:30:50 +01:00
|
|
|
super.key,
|
2022-09-18 11:06:04 +08:00
|
|
|
required this.rowNotifier,
|
|
|
|
required this.cellBuilder,
|
|
|
|
required this.cells,
|
2023-03-08 21:19:44 +08:00
|
|
|
required this.cardData,
|
2023-04-30 20:50:24 +08:00
|
|
|
required this.styleConfiguration,
|
|
|
|
this.renderHook,
|
2023-11-09 00:30:50 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
final EditableRowNotifier rowNotifier;
|
2023-12-10 13:44:37 +02:00
|
|
|
final CardCellBuilder<CustomCardData> cellBuilder;
|
2023-11-09 00:30:50 +01:00
|
|
|
final List<DatabaseCellContext> cells;
|
|
|
|
final CustomCardData? cardData;
|
|
|
|
final RowCardStyleConfiguration styleConfiguration;
|
2023-12-10 13:44:37 +02:00
|
|
|
final RowCardRenderHook<CustomCardData>? renderHook;
|
2022-09-18 11:06:04 +08:00
|
|
|
|
2023-11-26 15:10:48 +08:00
|
|
|
@override
|
|
|
|
State<_CardContent<CustomCardData>> createState() =>
|
|
|
|
_CardContentState<CustomCardData>();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _CardContentState<CustomCardData>
|
|
|
|
extends State<_CardContent<CustomCardData>> {
|
|
|
|
final List<EditableCardNotifier> _notifiers = [];
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
for (final element in _notifiers) {
|
|
|
|
element.dispose();
|
|
|
|
}
|
|
|
|
_notifiers.clear();
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
2022-09-18 11:06:04 +08:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2023-11-26 15:10:48 +08:00
|
|
|
if (widget.styleConfiguration.hoverStyle != null) {
|
2023-05-31 16:52:37 +08:00
|
|
|
return FlowyHover(
|
2023-11-26 15:10:48 +08:00
|
|
|
style: widget.styleConfiguration.hoverStyle,
|
|
|
|
buildWhenOnHover: () => !widget.rowNotifier.isEditing.value,
|
2023-05-31 16:52:37 +08:00
|
|
|
child: Padding(
|
2023-11-26 15:10:48 +08:00
|
|
|
padding: widget.styleConfiguration.cardPadding,
|
2023-05-31 16:52:37 +08:00
|
|
|
child: Column(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
2023-11-26 15:10:48 +08:00
|
|
|
children: _makeCells(context, widget.cells),
|
2023-05-31 16:52:37 +08:00
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return Padding(
|
2023-11-26 15:10:48 +08:00
|
|
|
padding: widget.styleConfiguration.cardPadding,
|
2023-05-31 16:52:37 +08:00
|
|
|
child: Column(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
2023-11-26 15:10:48 +08:00
|
|
|
children: _makeCells(context, widget.cells),
|
2023-05-31 16:52:37 +08:00
|
|
|
),
|
2022-09-18 11:06:04 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-08-26 16:07:51 +08:00
|
|
|
List<Widget> _makeCells(
|
|
|
|
BuildContext context,
|
2023-06-04 09:28:13 +08:00
|
|
|
List<DatabaseCellContext> cells,
|
2022-08-26 16:07:51 +08:00
|
|
|
) {
|
2022-08-29 15:42:08 +08:00
|
|
|
final List<Widget> children = [];
|
2022-09-18 11:06:04 +08:00
|
|
|
// Remove all the cell listeners.
|
2023-11-26 15:10:48 +08:00
|
|
|
widget.rowNotifier.unbind();
|
2022-09-18 11:06:04 +08:00
|
|
|
|
2023-11-09 00:30:50 +01:00
|
|
|
cells.asMap().forEach((int index, DatabaseCellContext cellContext) {
|
2023-11-26 15:10:48 +08:00
|
|
|
final isEditing = index == 0 ? widget.rowNotifier.isEditing.value : false;
|
2023-11-09 00:30:50 +01:00
|
|
|
final cellNotifier = EditableCardNotifier(isEditing: isEditing);
|
2022-09-18 11:06:04 +08:00
|
|
|
|
2023-11-09 00:30:50 +01:00
|
|
|
if (index == 0) {
|
|
|
|
// Only use the first cell to receive user's input when click the edit
|
|
|
|
// button
|
2023-11-26 15:10:48 +08:00
|
|
|
widget.rowNotifier.bindCell(cellContext, cellNotifier);
|
|
|
|
} else {
|
|
|
|
_notifiers.add(cellNotifier);
|
2023-11-09 00:30:50 +01:00
|
|
|
}
|
2022-09-06 21:10:41 +08:00
|
|
|
|
2023-11-09 00:30:50 +01:00
|
|
|
final child = Padding(
|
|
|
|
key: cellContext.key(),
|
2023-11-26 15:10:48 +08:00
|
|
|
padding: widget.styleConfiguration.cellPadding,
|
|
|
|
child: widget.cellBuilder.buildCell(
|
2023-11-09 00:30:50 +01:00
|
|
|
cellContext: cellContext,
|
|
|
|
cellNotifier: cellNotifier,
|
2023-11-26 15:10:48 +08:00
|
|
|
renderHook: widget.renderHook,
|
|
|
|
cardData: widget.cardData,
|
2023-11-09 00:30:50 +01:00
|
|
|
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
|
|
|
|
),
|
|
|
|
);
|
2022-08-31 20:57:34 +08:00
|
|
|
|
2023-11-09 00:30:50 +01:00
|
|
|
children.add(child);
|
|
|
|
});
|
2022-08-29 15:42:08 +08:00
|
|
|
return children;
|
2022-08-10 17:59:28 +08:00
|
|
|
}
|
|
|
|
}
|
2022-08-13 11:51:26 +08:00
|
|
|
|
2023-08-01 08:49:48 +05:30
|
|
|
class CardMoreOption extends StatelessWidget with CardAccessory {
|
2023-11-24 00:25:12 +08:00
|
|
|
const CardMoreOption({super.key});
|
|
|
|
|
|
|
|
@override
|
|
|
|
AccessoryType get type => AccessoryType.more;
|
2022-08-13 11:51:26 +08:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2022-08-30 20:54:11 +08:00
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.all(3.0),
|
2023-08-14 13:34:01 -07:00
|
|
|
child: FlowySvg(
|
2023-11-24 00:25:12 +08:00
|
|
|
FlowySvgs.three_dots_s,
|
|
|
|
color: Theme.of(context).hintColor,
|
2022-11-10 14:22:18 +08:00
|
|
|
),
|
2022-08-30 20:54:11 +08:00
|
|
|
);
|
2022-08-13 11:51:26 +08:00
|
|
|
}
|
|
|
|
}
|
2022-08-30 20:54:11 +08:00
|
|
|
|
|
|
|
class _CardEditOption extends StatelessWidget with CardAccessory {
|
2022-09-18 11:06:04 +08:00
|
|
|
final EditableRowNotifier rowNotifier;
|
2022-08-30 20:54:11 +08:00
|
|
|
const _CardEditOption({
|
2022-09-18 11:06:04 +08:00
|
|
|
required this.rowNotifier,
|
2023-12-08 20:01:54 +07:00
|
|
|
});
|
2022-08-30 20:54:11 +08:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.all(3.0),
|
2023-08-14 13:34:01 -07:00
|
|
|
child: FlowySvg(
|
|
|
|
FlowySvgs.edit_s,
|
2023-11-24 00:25:12 +08:00
|
|
|
color: Theme.of(context).hintColor,
|
2022-08-30 20:54:11 +08:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2022-09-19 15:36:59 +08:00
|
|
|
void onTap(BuildContext context) => rowNotifier.becomeFirstResponder();
|
|
|
|
|
|
|
|
@override
|
|
|
|
AccessoryType get type => AccessoryType.edit;
|
2022-08-30 20:54:11 +08:00
|
|
|
}
|
2023-04-30 20:50:24 +08:00
|
|
|
|
|
|
|
class RowCardStyleConfiguration {
|
|
|
|
final bool showAccessory;
|
|
|
|
final EdgeInsets cellPadding;
|
2023-05-31 16:52:37 +08:00
|
|
|
final EdgeInsets cardPadding;
|
|
|
|
final HoverStyle? hoverStyle;
|
2023-04-30 20:50:24 +08:00
|
|
|
|
|
|
|
const RowCardStyleConfiguration({
|
|
|
|
this.showAccessory = true,
|
2023-11-24 00:25:12 +08:00
|
|
|
this.cellPadding = EdgeInsets.zero,
|
2023-05-31 16:52:37 +08:00
|
|
|
this.cardPadding = const EdgeInsets.all(8),
|
|
|
|
this.hoverStyle,
|
2023-04-30 20:50:24 +08:00
|
|
|
});
|
|
|
|
}
|