card.dart 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
  2. import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
  3. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart';
  4. import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
  5. import 'package:appflowy_popover/appflowy_popover.dart';
  6. import 'package:flowy_infra/image.dart';
  7. import 'package:flowy_infra_ui/flowy_infra_ui.dart';
  8. import 'package:flowy_infra_ui/style_widget/hover.dart';
  9. import 'package:flutter/foundation.dart';
  10. import 'package:flutter/material.dart';
  11. import 'package:flutter_bloc/flutter_bloc.dart';
  12. import 'card_bloc.dart';
  13. import 'cells/card_cell.dart';
  14. import 'card_cell_builder.dart';
  15. import 'container/accessory.dart';
  16. import 'container/card_container.dart';
  17. /// Edit a database row with card style widget
  18. class RowCard<CustomCardData> extends StatefulWidget {
  19. final RowMetaPB rowMeta;
  20. final String viewId;
  21. final String? groupingFieldId;
  22. final String? groupId;
  23. /// Allows passing a custom card data object to the card. The card will be
  24. /// returned in the [CardCellBuilder] and can be used to build the card.
  25. final CustomCardData? cardData;
  26. final bool isEditing;
  27. final RowCache rowCache;
  28. /// The [CardCellBuilder] is used to build the card cells.
  29. final CardCellBuilder<CustomCardData> cellBuilder;
  30. /// Called when the user taps on the card.
  31. final void Function(BuildContext) openCard;
  32. /// Called when the user starts editing the card.
  33. final VoidCallback onStartEditing;
  34. /// Called when the user ends editing the card.
  35. final VoidCallback onEndEditing;
  36. /// The [RowCardRenderHook] is used to render the card's cell. Other than
  37. /// using the default cell builder. For example the [SelectOptionCardCell]
  38. final RowCardRenderHook<CustomCardData>? renderHook;
  39. final RowCardStyleConfiguration styleConfiguration;
  40. const RowCard({
  41. required this.rowMeta,
  42. required this.viewId,
  43. this.groupingFieldId,
  44. this.groupId,
  45. required this.isEditing,
  46. required this.rowCache,
  47. required this.cellBuilder,
  48. required this.openCard,
  49. required this.onStartEditing,
  50. required this.onEndEditing,
  51. this.cardData,
  52. this.styleConfiguration = const RowCardStyleConfiguration(
  53. showAccessory: true,
  54. ),
  55. this.renderHook,
  56. Key? key,
  57. }) : super(key: key);
  58. @override
  59. State<RowCard<CustomCardData>> createState() =>
  60. _RowCardState<CustomCardData>();
  61. }
  62. class _RowCardState<T> extends State<RowCard<T>> {
  63. late final CardBloc _cardBloc;
  64. late final EditableRowNotifier rowNotifier;
  65. late final PopoverController popoverController;
  66. AccessoryType? accessoryType;
  67. @override
  68. void initState() {
  69. rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
  70. _cardBloc = CardBloc(
  71. viewId: widget.viewId,
  72. groupFieldId: widget.groupingFieldId,
  73. isEditing: widget.isEditing,
  74. rowMeta: widget.rowMeta,
  75. rowCache: widget.rowCache,
  76. )..add(const RowCardEvent.initial());
  77. rowNotifier.isEditing.addListener(() {
  78. if (!mounted) return;
  79. _cardBloc.add(RowCardEvent.setIsEditing(rowNotifier.isEditing.value));
  80. if (rowNotifier.isEditing.value) {
  81. widget.onStartEditing();
  82. } else {
  83. widget.onEndEditing();
  84. }
  85. });
  86. popoverController = PopoverController();
  87. super.initState();
  88. }
  89. @override
  90. Widget build(BuildContext context) {
  91. return BlocProvider.value(
  92. value: _cardBloc,
  93. child: BlocBuilder<CardBloc, RowCardState>(
  94. buildWhen: (previous, current) {
  95. // Rebuild when:
  96. // 1.If the length of the cells is not the same
  97. // 2.isEditing changed
  98. if (previous.cells.length != current.cells.length ||
  99. previous.isEditing != current.isEditing) {
  100. return true;
  101. }
  102. // 3.Compare the content of the cells. The cells consists of
  103. // list of [BoardCellEquatable] that extends the [Equatable].
  104. return !listEquals(previous.cells, current.cells);
  105. },
  106. builder: (context, state) {
  107. return AppFlowyPopover(
  108. controller: popoverController,
  109. triggerActions: PopoverTriggerFlags.none,
  110. constraints: BoxConstraints.loose(const Size(140, 200)),
  111. margin: const EdgeInsets.all(6),
  112. direction: PopoverDirection.rightWithCenterAligned,
  113. popupBuilder: (popoverContext) => _handlePopoverBuilder(
  114. context,
  115. popoverContext,
  116. ),
  117. child: RowCardContainer(
  118. buildAccessoryWhen: () => state.isEditing == false,
  119. accessoryBuilder: (context) {
  120. if (widget.styleConfiguration.showAccessory == false) {
  121. return [];
  122. } else {
  123. return [
  124. _CardEditOption(rowNotifier: rowNotifier),
  125. CardMoreOption(),
  126. ];
  127. }
  128. },
  129. openAccessory: _handleOpenAccessory,
  130. openCard: (context) => widget.openCard(context),
  131. child: _CardContent<T>(
  132. rowNotifier: rowNotifier,
  133. cellBuilder: widget.cellBuilder,
  134. styleConfiguration: widget.styleConfiguration,
  135. cells: state.cells,
  136. renderHook: widget.renderHook,
  137. cardData: widget.cardData,
  138. ),
  139. ),
  140. );
  141. },
  142. ),
  143. );
  144. }
  145. void _handleOpenAccessory(AccessoryType newAccessoryType) {
  146. accessoryType = newAccessoryType;
  147. switch (newAccessoryType) {
  148. case AccessoryType.edit:
  149. break;
  150. case AccessoryType.more:
  151. popoverController.show();
  152. break;
  153. }
  154. }
  155. Widget _handlePopoverBuilder(
  156. BuildContext context,
  157. BuildContext popoverContext,
  158. ) {
  159. switch (accessoryType!) {
  160. case AccessoryType.edit:
  161. throw UnimplementedError();
  162. case AccessoryType.more:
  163. return RowActions(
  164. viewId: context.read<CardBloc>().viewId,
  165. rowId: context.read<CardBloc>().rowMeta.id,
  166. groupId: widget.groupId,
  167. );
  168. }
  169. }
  170. @override
  171. Future<void> dispose() async {
  172. rowNotifier.dispose();
  173. _cardBloc.close();
  174. super.dispose();
  175. }
  176. }
  177. class _CardContent<CustomCardData> extends StatelessWidget {
  178. final CardCellBuilder<CustomCardData> cellBuilder;
  179. final EditableRowNotifier rowNotifier;
  180. final List<DatabaseCellContext> cells;
  181. final RowCardRenderHook<CustomCardData>? renderHook;
  182. final CustomCardData? cardData;
  183. final RowCardStyleConfiguration styleConfiguration;
  184. const _CardContent({
  185. required this.rowNotifier,
  186. required this.cellBuilder,
  187. required this.cells,
  188. required this.cardData,
  189. required this.styleConfiguration,
  190. this.renderHook,
  191. Key? key,
  192. }) : super(key: key);
  193. @override
  194. Widget build(BuildContext context) {
  195. if (styleConfiguration.hoverStyle != null) {
  196. return FlowyHover(
  197. style: styleConfiguration.hoverStyle,
  198. child: Padding(
  199. padding: styleConfiguration.cardPadding,
  200. child: Column(
  201. mainAxisSize: MainAxisSize.min,
  202. children: _makeCells(context, cells),
  203. ),
  204. ),
  205. );
  206. }
  207. return Padding(
  208. padding: styleConfiguration.cardPadding,
  209. child: Column(
  210. mainAxisSize: MainAxisSize.min,
  211. children: _makeCells(context, cells),
  212. ),
  213. );
  214. }
  215. List<Widget> _makeCells(
  216. BuildContext context,
  217. List<DatabaseCellContext> cells,
  218. ) {
  219. final List<Widget> children = [];
  220. // Remove all the cell listeners.
  221. rowNotifier.unbind();
  222. cells.asMap().forEach(
  223. (int index, DatabaseCellContext cellContext) {
  224. final isEditing = index == 0 ? rowNotifier.isEditing.value : false;
  225. final cellNotifier = EditableCardNotifier(isEditing: isEditing);
  226. if (index == 0) {
  227. // Only use the first cell to receive user's input when click the edit
  228. // button
  229. rowNotifier.bindCell(cellContext, cellNotifier);
  230. }
  231. final child = Padding(
  232. key: cellContext.key(),
  233. padding: styleConfiguration.cellPadding,
  234. child: cellBuilder.buildCell(
  235. cellContext: cellContext,
  236. cellNotifier: cellNotifier,
  237. renderHook: renderHook,
  238. cardData: cardData,
  239. ),
  240. );
  241. children.add(child);
  242. },
  243. );
  244. return children;
  245. }
  246. }
  247. class CardMoreOption extends StatelessWidget with CardAccessory {
  248. CardMoreOption({Key? key}) : super(key: key);
  249. @override
  250. Widget build(BuildContext context) {
  251. return Padding(
  252. padding: const EdgeInsets.all(3.0),
  253. child: svgWidget(
  254. 'grid/details',
  255. color: Theme.of(context).iconTheme.color,
  256. ),
  257. );
  258. }
  259. @override
  260. AccessoryType get type => AccessoryType.more;
  261. }
  262. class _CardEditOption extends StatelessWidget with CardAccessory {
  263. final EditableRowNotifier rowNotifier;
  264. const _CardEditOption({
  265. required this.rowNotifier,
  266. Key? key,
  267. }) : super(key: key);
  268. @override
  269. Widget build(BuildContext context) {
  270. return Padding(
  271. padding: const EdgeInsets.all(3.0),
  272. child: svgWidget(
  273. 'editor/edit',
  274. color: Theme.of(context).iconTheme.color,
  275. ),
  276. );
  277. }
  278. @override
  279. void onTap(BuildContext context) => rowNotifier.becomeFirstResponder();
  280. @override
  281. AccessoryType get type => AccessoryType.edit;
  282. }
  283. class RowCardStyleConfiguration {
  284. final bool showAccessory;
  285. final EdgeInsets cellPadding;
  286. final EdgeInsets cardPadding;
  287. final HoverStyle? hoverStyle;
  288. const RowCardStyleConfiguration({
  289. this.showAccessory = true,
  290. this.cellPadding = const EdgeInsets.only(left: 4, right: 4),
  291. this.cardPadding = const EdgeInsets.all(8),
  292. this.hoverStyle,
  293. });
  294. }