345 lines
10 KiB
Dart
Raw Normal View History

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';
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';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
2022-09-19 18:12:41 +08:00
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
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';
import 'card_bloc.dart';
2022-08-12 20:10:56 +08:00
import 'card_cell_builder.dart';
import 'cells/card_cell.dart';
import 'container/accessory.dart';
import 'container/card_container.dart';
2022-08-10 17:59:28 +08:00
/// Edit a database row with card style widget
class RowCard<CustomCardData> extends StatefulWidget {
final RowMetaPB rowMeta;
final String viewId;
final String? groupingFieldId;
final String? groupId;
/// 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.
final CustomCardData? cardData;
final bool isEditing;
final RowCache rowCache;
/// The [CardCellBuilder] is used to build the card cells.
final CardCellBuilder<CustomCardData> cellBuilder;
/// Called when the user taps on the card.
2022-08-16 18:02:39 +08:00
final void Function(BuildContext) openCard;
/// Called when the user starts editing the card.
2022-10-06 22:26:18 +08:00
final VoidCallback onStartEditing;
/// 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
/// 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({
super.key,
required this.rowMeta,
required this.viewId,
required this.isEditing,
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,
this.groupingFieldId,
this.groupId,
this.cardData,
this.styleConfiguration = const RowCardStyleConfiguration(
showAccessory: true,
),
this.renderHook,
});
2022-08-12 20:10:56 +08:00
@override
State<RowCard<CustomCardData>> createState() =>
_RowCardState<CustomCardData>();
2022-08-12 20:10:56 +08:00
}
class _RowCardState<T> extends State<RowCard<T>> {
final popoverController = PopoverController();
late final CardBloc _cardBloc;
late final EditableRowNotifier rowNotifier;
AccessoryType? accessoryType;
2022-08-12 20:10:56 +08:00
@override
void initState() {
super.initState();
rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
_cardBloc = CardBloc(
viewId: widget.viewId,
groupFieldId: widget.groupingFieldId,
isEditing: widget.isEditing,
rowMeta: widget.rowMeta,
rowCache: widget.rowCache,
)..add(const RowCardEvent.initial());
rowNotifier.isEditing.addListener(() {
if (!mounted) return;
_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-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,
child: BlocBuilder<CardBloc, RowCardState>(
buildWhen: (previous, current) {
// Rebuild when:
// 1. If the length of the cells is not the same or isEditing changed
if (previous.cells.length != current.cells.length ||
previous.isEditing != current.isEditing) {
return true;
}
// 2. the content of the cells changed
return !listEquals(previous.cells, current.cells);
},
2022-08-12 20:10:56 +08:00
builder: (context, state) {
feat: mobile card detail screen (#3935) * feat: add CardDetailScreen and CardPropertyEditScreen - add basic UI layout for these two screens - add MobileTextCell as the GridCellWidget adapts to mobile * feat: add MobileNumberCell and MobileTimestampCell * feat: Add MobileDateCell and MobileCheckboxCell - Add MobileDateCellEditScreen - Add dateStr and endDateStr in DateCellCalendarState * feat: add MobileFieldTypeOptionEditor - Add placeholder for different TypeOptionMobileWidgetBuilders - Add _MobileSwitchFieldButton * feat: add property delete feature in CardPropertyEditScreen * fix: fix VisibilitySwitch didn't update * feat: add MobileCreateRowFieldScreen - Refactor MobileFieldEditor to used in CardPropertyEditScreen and MobileCreateRowFieldScreen - Add MobileCreateRowFieldScreen * chore: localization and improve spacing * feat: add TimestampTypeOptionMobileWidget - Refactor TimeFormatListTile to be used in TimestampTypeOptionMobileWidget and _DateCellEditBody - Add IncludeTimeSwitch to be used in TimestampTypeOptionMobileWidget and _DateCellEditBody * feat: add checkbox and DateTypeOptionMobileWidget * chore: improve UI * chore: improve spacing * fix: fix end time shown issue * fix: minor issues * fix: flutter analyze * chore: delete unused TextEditingController * fix: prevent group field from deleting * feat: add NumberTypeOptionMobileWidget * chore: rename and clean code * chore: clean code * chore: extract class * chore: refactor reorder cells * chore: improve super.key * chore: refactor MobileFieldTypeList * chore: reorginize code * chore: remove unused import file * chore: clean code * chore: add commas due to flutter upgrade * feat: add MobileURLCell * fix: close keyboard when user tap outside of textfield * chore: update go_router version * fix: add missing GridCellStyle --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-11-20 21:56:21 -08:00
if (PlatformExtension.isMobile) {
return GestureDetector(
2023-11-29 19:01:29 -07:00
child: MobileCardContent<T>(
feat: mobile card detail screen (#3935) * feat: add CardDetailScreen and CardPropertyEditScreen - add basic UI layout for these two screens - add MobileTextCell as the GridCellWidget adapts to mobile * feat: add MobileNumberCell and MobileTimestampCell * feat: Add MobileDateCell and MobileCheckboxCell - Add MobileDateCellEditScreen - Add dateStr and endDateStr in DateCellCalendarState * feat: add MobileFieldTypeOptionEditor - Add placeholder for different TypeOptionMobileWidgetBuilders - Add _MobileSwitchFieldButton * feat: add property delete feature in CardPropertyEditScreen * fix: fix VisibilitySwitch didn't update * feat: add MobileCreateRowFieldScreen - Refactor MobileFieldEditor to used in CardPropertyEditScreen and MobileCreateRowFieldScreen - Add MobileCreateRowFieldScreen * chore: localization and improve spacing * feat: add TimestampTypeOptionMobileWidget - Refactor TimeFormatListTile to be used in TimestampTypeOptionMobileWidget and _DateCellEditBody - Add IncludeTimeSwitch to be used in TimestampTypeOptionMobileWidget and _DateCellEditBody * feat: add checkbox and DateTypeOptionMobileWidget * chore: improve UI * chore: improve spacing * fix: fix end time shown issue * fix: minor issues * fix: flutter analyze * chore: delete unused TextEditingController * fix: prevent group field from deleting * feat: add NumberTypeOptionMobileWidget * chore: rename and clean code * chore: clean code * chore: extract class * chore: refactor reorder cells * chore: improve super.key * chore: refactor MobileFieldTypeList * chore: reorginize code * chore: remove unused import file * chore: clean code * chore: add commas due to flutter upgrade * feat: add MobileURLCell * fix: close keyboard when user tap outside of textfield * chore: update go_router version * fix: add missing GridCellStyle --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-11-20 21:56:21 -08:00
cellBuilder: widget.cellBuilder,
styleConfiguration: widget.styleConfiguration,
cells: state.cells,
renderHook: widget.renderHook,
cardData: widget.cardData,
),
onTap: () => widget.openCard(context),
feat: mobile card detail screen (#3935) * feat: add CardDetailScreen and CardPropertyEditScreen - add basic UI layout for these two screens - add MobileTextCell as the GridCellWidget adapts to mobile * feat: add MobileNumberCell and MobileTimestampCell * feat: Add MobileDateCell and MobileCheckboxCell - Add MobileDateCellEditScreen - Add dateStr and endDateStr in DateCellCalendarState * feat: add MobileFieldTypeOptionEditor - Add placeholder for different TypeOptionMobileWidgetBuilders - Add _MobileSwitchFieldButton * feat: add property delete feature in CardPropertyEditScreen * fix: fix VisibilitySwitch didn't update * feat: add MobileCreateRowFieldScreen - Refactor MobileFieldEditor to used in CardPropertyEditScreen and MobileCreateRowFieldScreen - Add MobileCreateRowFieldScreen * chore: localization and improve spacing * feat: add TimestampTypeOptionMobileWidget - Refactor TimeFormatListTile to be used in TimestampTypeOptionMobileWidget and _DateCellEditBody - Add IncludeTimeSwitch to be used in TimestampTypeOptionMobileWidget and _DateCellEditBody * feat: add checkbox and DateTypeOptionMobileWidget * chore: improve UI * chore: improve spacing * fix: fix end time shown issue * fix: minor issues * fix: flutter analyze * chore: delete unused TextEditingController * fix: prevent group field from deleting * feat: add NumberTypeOptionMobileWidget * chore: rename and clean code * chore: clean code * chore: extract class * chore: refactor reorder cells * chore: improve super.key * chore: refactor MobileFieldTypeList * chore: reorginize code * chore: remove unused import file * chore: clean code * chore: add commas due to flutter upgrade * feat: add MobileURLCell * fix: close keyboard when user tap outside of textfield * chore: update go_router version * fix: add missing GridCellStyle --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-11-20 21:56:21 -08:00
);
}
return AppFlowyPopover(
controller: popoverController,
2022-09-22 21:02:10 +08:00
triggerActions: PopoverTriggerFlags.none,
constraints: BoxConstraints.loose(const Size(140, 200)),
direction: PopoverDirection.rightWithCenterAligned,
popupBuilder: (_) {
return RowActionMenu.board(
viewId: _cardBloc.viewId,
rowId: _cardBloc.rowMeta.id,
groupId: widget.groupId,
);
},
child: RowCardContainer(
buildAccessoryWhen: () => state.isEditing == false,
accessories: [
if (widget.styleConfiguration.showAccessory) ...[
_CardEditOption(rowNotifier: rowNotifier),
const CardMoreOption(),
],
],
openAccessory: _handleOpenAccessory,
openCard: (context) => widget.openCard(context),
child: _CardContent<T>(
rowNotifier: rowNotifier,
cellBuilder: widget.cellBuilder,
styleConfiguration: widget.styleConfiguration,
cells: state.cells,
renderHook: widget.renderHook,
cardData: widget.cardData,
),
),
2022-08-12 20:10:56 +08:00
);
},
),
);
}
void _handleOpenAccessory(AccessoryType newAccessoryType) {
accessoryType = newAccessoryType;
switch (newAccessoryType) {
case AccessoryType.edit:
break;
case AccessoryType.more:
popoverController.show();
break;
}
}
@override
Future<void> dispose() async {
rowNotifier.dispose();
_cardBloc.close();
super.dispose();
}
}
class _CardContent<CustomCardData> extends StatefulWidget {
const _CardContent({
super.key,
required this.rowNotifier,
required this.cellBuilder,
required this.cells,
required this.cardData,
required this.styleConfiguration,
this.renderHook,
});
final EditableRowNotifier rowNotifier;
2023-12-10 13:44:37 +02:00
final CardCellBuilder<CustomCardData> cellBuilder;
final List<DatabaseCellContext> cells;
final CustomCardData? cardData;
final RowCardStyleConfiguration styleConfiguration;
2023-12-10 13:44:37 +02:00
final RowCardRenderHook<CustomCardData>? renderHook;
@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();
}
@override
Widget build(BuildContext context) {
if (widget.styleConfiguration.hoverStyle != null) {
return FlowyHover(
style: widget.styleConfiguration.hoverStyle,
buildWhenOnHover: () => !widget.rowNotifier.isEditing.value,
child: Padding(
padding: widget.styleConfiguration.cardPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: _makeCells(context, widget.cells),
),
),
);
}
return Padding(
padding: widget.styleConfiguration.cardPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: _makeCells(context, widget.cells),
),
);
}
List<Widget> _makeCells(
BuildContext context,
List<DatabaseCellContext> cells,
) {
2022-08-29 15:42:08 +08:00
final List<Widget> children = [];
// Remove all the cell listeners.
widget.rowNotifier.unbind();
cells.asMap().forEach((int index, DatabaseCellContext cellContext) {
final isEditing = index == 0 ? widget.rowNotifier.isEditing.value : false;
final cellNotifier = EditableCardNotifier(isEditing: isEditing);
if (index == 0) {
// Only use the first cell to receive user's input when click the edit
// button
widget.rowNotifier.bindCell(cellContext, cellNotifier);
} else {
_notifiers.add(cellNotifier);
}
2022-09-06 21:10:41 +08:00
final child = Padding(
key: cellContext.key(),
padding: widget.styleConfiguration.cellPadding,
child: widget.cellBuilder.buildCell(
cellContext: cellContext,
cellNotifier: cellNotifier,
renderHook: widget.renderHook,
cardData: widget.cardData,
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
),
);
2022-08-31 20:57:34 +08: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
class CardMoreOption extends StatelessWidget with CardAccessory {
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),
child: FlowySvg(
FlowySvgs.three_dots_s,
color: Theme.of(context).hintColor,
),
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 {
final EditableRowNotifier rowNotifier;
2022-08-30 20:54:11 +08:00
const _CardEditOption({
required this.rowNotifier,
});
2022-08-30 20:54:11 +08:00
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(3.0),
child: FlowySvg(
FlowySvgs.edit_s,
color: Theme.of(context).hintColor,
2022-08-30 20:54:11 +08:00
),
);
}
@override
void onTap(BuildContext context) => rowNotifier.becomeFirstResponder();
@override
AccessoryType get type => AccessoryType.edit;
2022-08-30 20:54:11 +08:00
}
class RowCardStyleConfiguration {
final bool showAccessory;
final EdgeInsets cellPadding;
final EdgeInsets cardPadding;
final HoverStyle? hoverStyle;
const RowCardStyleConfiguration({
this.showAccessory = true,
this.cellPadding = EdgeInsets.zero,
this.cardPadding = const EdgeInsets.all(8),
this.hoverStyle,
});
}