refactor: database cell controller and builder (#4398)

* refactor: get row/field data from row cache and field controller in cell controller

* refactor: reorganize cell controller tasks and builder

* refactor: rename cell_builder.dart

* refactor: database editable cell builder

* refactor: database card cell builder

* fix: make it work

* fix: start cell listener and adjust cell style on desktop

* fix: build card cell

* fix: remove unnecessary await in tests

* fix: cell cache validation

* fix: row detail banner bugs

* fix: row detail field doesn't update

* fix: calendar event card

* test: fix integration tests

* fix: adjust cell builders to fix cell controller getting disposed

* chore: code review

* fix: bugs on mobile

* test: add grid header integration tests

* test: suppress warnings, reduce flaky test and group tests
This commit is contained in:
Richard Shiue
2024-01-24 23:59:45 +08:00
committed by GitHub
parent 18a355601a
commit a1abcd7626
185 changed files with 6524 additions and 7673 deletions

View File

@ -1,11 +1,13 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
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/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.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';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/foundation.dart';
@ -13,26 +15,24 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_bloc.dart';
import 'card_cell_builder.dart';
import 'cells/card_cell.dart';
import '../cell/card_cell_builder.dart';
import '../cell/card_cell_skeleton/card_cell.dart';
import 'container/accessory.dart';
import 'container/card_container.dart';
/// Edit a database row with card style widget
class RowCard<CustomCardData> extends StatefulWidget {
class RowCard extends StatefulWidget {
final FieldController fieldController;
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;
final CardCellBuilder cellBuilder;
/// Called when the user taps on the card.
final void Function(BuildContext) openCard;
@ -43,14 +43,11 @@ class RowCard<CustomCardData> extends StatefulWidget {
/// Called when the user ends editing the card.
final VoidCallback onEndEditing;
/// 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.fieldController,
required this.rowMeta,
required this.viewId,
required this.isEditing,
@ -59,41 +56,36 @@ class RowCard<CustomCardData> extends StatefulWidget {
required this.openCard,
required this.onStartEditing,
required this.onEndEditing,
required this.styleConfiguration,
this.groupingFieldId,
this.groupId,
this.cardData,
this.styleConfiguration = const RowCardStyleConfiguration(
showAccessory: true,
),
this.renderHook,
});
@override
State<RowCard<CustomCardData>> createState() =>
_RowCardState<CustomCardData>();
State<RowCard> createState() => _RowCardState();
}
class _RowCardState<T> extends State<RowCard<T>> {
class _RowCardState extends State<RowCard> {
final popoverController = PopoverController();
late final CardBloc _cardBloc;
late final EditableRowNotifier rowNotifier;
AccessoryType? accessoryType;
@override
void initState() {
super.initState();
rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
_cardBloc = CardBloc(
fieldController: widget.fieldController,
viewId: widget.viewId,
groupFieldId: widget.groupingFieldId,
isEditing: widget.isEditing,
rowMeta: widget.rowMeta,
rowCache: widget.rowCache,
)..add(const RowCardEvent.initial());
)..add(const CardEvent.initial());
rowNotifier.isEditing.addListener(() {
if (!mounted) return;
_cardBloc.add(RowCardEvent.setIsEditing(rowNotifier.isEditing.value));
_cardBloc.add(CardEvent.setIsEditing(rowNotifier.isEditing.value));
if (rowNotifier.isEditing.value) {
widget.onStartEditing();
@ -103,11 +95,18 @@ class _RowCardState<T> extends State<RowCard<T>> {
});
}
@override
Future<void> dispose() async {
rowNotifier.dispose();
_cardBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cardBloc,
child: BlocBuilder<CardBloc, RowCardState>(
child: BlocBuilder<CardBloc, CardState>(
buildWhen: (previous, current) {
// Rebuild when:
// 1. If the length of the cells is not the same or isEditing changed
@ -119,59 +118,61 @@ class _RowCardState<T> extends State<RowCard<T>> {
// 2. the content of the cells changed
return !listEquals(previous.cells, current.cells);
},
builder: (context, state) {
if (PlatformExtension.isMobile) {
return GestureDetector(
child: MobileCardContent<T>(
cellBuilder: widget.cellBuilder,
styleConfiguration: widget.styleConfiguration,
cells: state.cells,
renderHook: widget.renderHook,
cardData: widget.cardData,
),
onTap: () => widget.openCard(context),
);
}
builder: (context, state) =>
PlatformExtension.isMobile ? _mobile(state) : _desktop(state),
),
);
}
return AppFlowyPopover(
controller: popoverController,
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,
),
),
);
},
Widget _mobile(CardState state) {
return GestureDetector(
onTap: () => widget.openCard(context),
behavior: HitTestBehavior.opaque,
child: MobileCardContent(
rowMeta: state.rowMeta,
cellBuilder: widget.cellBuilder,
styleConfiguration: widget.styleConfiguration,
cells: state.cells,
),
);
}
Widget _desktop(CardState state) {
final accessories = widget.styleConfiguration.showAccessory
? <CardAccessory>[
EditCardAccessory(rowNotifier: rowNotifier),
const MoreCardOptionsAccessory(),
]
: null;
return AppFlowyPopover(
controller: popoverController,
triggerActions: PopoverTriggerFlags.none,
constraints: BoxConstraints.loose(const Size(140, 200)),
direction: PopoverDirection.rightWithCenterAligned,
popupBuilder: (_) {
return RowActionMenu.board(
viewId: _cardBloc.viewId,
rowId: _cardBloc.rowId,
groupId: widget.groupId,
);
},
child: RowCardContainer(
buildAccessoryWhen: () => state.isEditing == false,
accessories: accessories ?? [],
openAccessory: _handleOpenAccessory,
openCard: widget.openCard,
child: _CardContent(
rowMeta: state.rowMeta,
rowNotifier: rowNotifier,
cellBuilder: widget.cellBuilder,
styleConfiguration: widget.styleConfiguration,
cells: state.cells,
),
),
);
}
void _handleOpenAccessory(AccessoryType newAccessoryType) {
accessoryType = newAccessoryType;
switch (newAccessoryType) {
case AccessoryType.edit:
break;
@ -180,118 +181,70 @@ class _RowCardState<T> extends State<RowCard<T>> {
break;
}
}
@override
Future<void> dispose() async {
rowNotifier.dispose();
_cardBloc.close();
super.dispose();
}
}
class _CardContent<CustomCardData> extends StatefulWidget {
class _CardContent extends StatelessWidget {
const _CardContent({
super.key,
required this.rowMeta,
required this.rowNotifier,
required this.cellBuilder,
required this.cells,
required this.cardData,
required this.styleConfiguration,
this.renderHook,
});
final RowMetaPB rowMeta;
final EditableRowNotifier rowNotifier;
final CardCellBuilder<CustomCardData> cellBuilder;
final List<DatabaseCellContext> cells;
final CustomCardData? cardData;
final CardCellBuilder cellBuilder;
final List<CellContext> cells;
final RowCardStyleConfiguration styleConfiguration;
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,
final child = Padding(
padding: styleConfiguration.cardPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: _makeCells(context, widget.cells),
children: _makeCells(context, rowMeta, cells),
),
);
return styleConfiguration.hoverStyle == null
? child
: FlowyHover(
style: styleConfiguration.hoverStyle,
buildWhenOnHover: () => !rowNotifier.isEditing.value,
child: child,
);
}
List<Widget> _makeCells(
BuildContext context,
List<DatabaseCellContext> cells,
RowMetaPB rowMeta,
List<CellContext> cells,
) {
final List<Widget> children = [];
// Remove all the cell listeners.
widget.rowNotifier.unbind();
rowNotifier.unbind();
cells.asMap().forEach((int index, DatabaseCellContext cellContext) {
final isEditing = index == 0 ? widget.rowNotifier.isEditing.value : false;
final cellNotifier = EditableCardNotifier(isEditing: isEditing);
return cells.mapIndexed((int index, CellContext cellContext) {
EditableCardNotifier? cellNotifier;
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);
cellNotifier =
EditableCardNotifier(isEditing: rowNotifier.isEditing.value);
rowNotifier.bindCell(cellContext, cellNotifier);
}
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,
),
return cellBuilder.build(
cellContext: cellContext,
cellNotifier: cellNotifier,
styleMap: styleConfiguration.cellStyleMap,
hasNotes: !rowMeta.isDocumentEmpty,
);
children.add(child);
});
return children;
}).toList();
}
}
class CardMoreOption extends StatelessWidget with CardAccessory {
const CardMoreOption({super.key});
@override
AccessoryType get type => AccessoryType.more;
class MoreCardOptionsAccessory extends StatelessWidget with CardAccessory {
const MoreCardOptionsAccessory({super.key});
@override
Widget build(BuildContext context) {
@ -303,11 +256,15 @@ class CardMoreOption extends StatelessWidget with CardAccessory {
),
);
}
@override
AccessoryType get type => AccessoryType.more;
}
class _CardEditOption extends StatelessWidget with CardAccessory {
class EditCardAccessory extends StatelessWidget with CardAccessory {
final EditableRowNotifier rowNotifier;
const _CardEditOption({
const EditCardAccessory({
super.key,
required this.rowNotifier,
});
@ -330,14 +287,14 @@ class _CardEditOption extends StatelessWidget with CardAccessory {
}
class RowCardStyleConfiguration {
final CardCellStyleMap cellStyleMap;
final bool showAccessory;
final EdgeInsets cellPadding;
final EdgeInsets cardPadding;
final HoverStyle? hoverStyle;
const RowCardStyleConfiguration({
required this.cellStyleMap,
this.showAccessory = true,
this.cellPadding = EdgeInsets.zero,
this.cardPadding = const EdgeInsets.all(8),
this.hoverStyle,
});

View File

@ -1,21 +1,22 @@
import 'dart:collection';
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/defines.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/row/row_listener.dart';
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../../application/cell/cell_service.dart';
import '../../application/row/row_cache.dart';
import '../../application/row/row_service.dart';
part 'card_bloc.freezed.dart';
class CardBloc extends Bloc<RowCardEvent, RowCardState> {
final RowMetaPB rowMeta;
class CardBloc extends Bloc<CardEvent, CardState> {
final FieldController fieldController;
final String rowId;
final String? groupFieldId;
final RowBackendService _rowBackendSvc;
final RowCache _rowCache;
final String viewId;
final RowListener _rowListener;
@ -23,21 +24,27 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> {
VoidCallback? _rowCallback;
CardBloc({
required this.rowMeta,
required this.fieldController,
required this.groupFieldId,
required this.viewId,
required RowMetaPB rowMeta,
required RowCache rowCache,
required bool isEditing,
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
}) : rowId = rowMeta.id,
_rowListener = RowListener(rowMeta.id),
_rowCache = rowCache,
super(
RowCardState.initial(
_makeCells(groupFieldId, rowCache.loadCells(rowMeta)),
CardState.initial(
rowMeta,
_makeCells(
fieldController,
groupFieldId,
rowCache.loadCells(rowMeta),
),
isEditing,
),
) {
on<RowCardEvent>(
on<CardEvent>(
(event, emit) async {
await event.when(
initial: () async {
@ -54,15 +61,8 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> {
setIsEditing: (bool isEditing) {
emit(state.copyWith(isEditing: isEditing));
},
didReceiveRowMeta: (rowMeta) {
final cells = state.cells
.map(
(cell) => cell.rowMeta.id == rowMeta.id
? cell.copyWith(rowMeta: rowMeta)
: cell,
)
.toList();
emit(state.copyWith(cells: cells));
didUpdateRowMeta: (rowMeta) {
emit(state.copyWith(rowMeta: rowMeta));
},
);
},
@ -75,88 +75,75 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> {
_rowCache.removeRowListener(_rowCallback!);
_rowCallback = null;
}
await _rowListener.stop();
return super.close();
}
RowInfo rowInfo() {
return RowInfo(
viewId: _rowBackendSvc.viewId,
fields: UnmodifiableListView(
state.cells.map((cell) => cell.fieldInfo).toList(),
),
rowId: rowMeta.id,
rowMeta: rowMeta,
);
}
Future<void> _startListening() async {
_rowCallback = _rowCache.addListener(
rowId: rowMeta.id,
rowId: rowId,
onRowChanged: (cellMap, reason) {
if (!isClosed) {
final cells = _makeCells(groupFieldId, cellMap);
add(RowCardEvent.didReceiveCells(cells, reason));
final cells = _makeCells(fieldController, groupFieldId, cellMap);
add(CardEvent.didReceiveCells(cells, reason));
}
},
);
_rowListener.start(
onMetaChanged: (meta) {
onMetaChanged: (rowMeta) {
if (!isClosed) {
add(RowCardEvent.didReceiveRowMeta(meta));
add(CardEvent.didUpdateRowMeta(rowMeta));
}
},
);
}
}
List<DatabaseCellContext> _makeCells(
List<CellContext> _makeCells(
FieldController fieldController,
String? groupFieldId,
CellContextByFieldId originalCellMap,
CellContextByFieldId cellMap,
) {
final List<DatabaseCellContext> cells = [];
originalCellMap
.removeWhere((fieldId, cellContext) => !cellContext.isVisible());
for (final entry in originalCellMap.entries) {
// Filter out the cell if it's fieldId equal to the groupFieldId
if (groupFieldId != null) {
if (entry.value.fieldId == groupFieldId) {
continue;
}
}
cells.add(entry.value);
}
return cells;
// Only show the non-hidden cells and cells that aren't of the grouping field
cellMap.removeWhere((_, cellContext) {
final fieldInfo = fieldController.getField(cellContext.fieldId);
return fieldInfo == null ||
!fieldInfo.fieldSettings!.visibility.isVisibleState() ||
(groupFieldId != null && cellContext.fieldId == groupFieldId);
});
return cellMap.values.toList();
}
@freezed
class RowCardEvent with _$RowCardEvent {
const factory RowCardEvent.initial() = _InitialRow;
const factory RowCardEvent.setIsEditing(bool isEditing) = _IsEditing;
const factory RowCardEvent.didReceiveCells(
List<DatabaseCellContext> cells,
class CardEvent with _$CardEvent {
const factory CardEvent.initial() = _InitialRow;
const factory CardEvent.setIsEditing(bool isEditing) = _IsEditing;
const factory CardEvent.didReceiveCells(
List<CellContext> cells,
ChangedReason reason,
) = _DidReceiveCells;
const factory RowCardEvent.didReceiveRowMeta(
RowMetaPB meta,
) = _DidReceiveRowMeta;
const factory CardEvent.didUpdateRowMeta(RowMetaPB rowMeta) =
_DidUpdateRowMeta;
}
@freezed
class RowCardState with _$RowCardState {
const factory RowCardState({
required List<DatabaseCellContext> cells,
class CardState with _$CardState {
const factory CardState({
required List<CellContext> cells,
required RowMetaPB rowMeta,
required bool isEditing,
ChangedReason? changeReason,
}) = _RowCardState;
factory RowCardState.initial(
List<DatabaseCellContext> cells,
factory CardState.initial(
RowMetaPB rowMeta,
List<CellContext> cells,
bool isEditing,
) =>
RowCardState(
CardState(
cells: cells,
rowMeta: rowMeta,
isEditing: isEditing,
);
}

View File

@ -1,218 +0,0 @@
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/card_cells.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/timestamp_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import '../../application/cell/cell_service.dart';
import 'cells/card_cell.dart';
import 'cells/checkbox_card_cell.dart';
import 'cells/checklist_card_cell.dart';
import 'cells/date_card_cell.dart';
import 'cells/number_card_cell.dart';
import 'cells/select_option_card_cell.dart';
import 'cells/text_card_cell.dart';
import 'cells/url_card_cell.dart';
// T represents as the Generic card data
class CardCellBuilder<CustomCardData> {
final CellMemCache cellCache;
final Map<FieldType, CardCellStyle>? styles;
CardCellBuilder(this.cellCache, {this.styles});
Widget buildCell({
CustomCardData? cardData,
required DatabaseCellContext cellContext,
EditableCardNotifier? cellNotifier,
RowCardRenderHook<CustomCardData>? renderHook,
required bool hasNotes,
}) {
final cellControllerBuilder = CellControllerBuilder(
cellContext: cellContext,
cellCache: cellCache,
);
final key = cellContext.key();
final style = styles?[cellContext.fieldType];
return PlatformExtension.isMobile
? _getMobileCardCellWidget(
key: key,
cellContext: cellContext,
cellControllerBuilder: cellControllerBuilder,
style: style,
cardData: cardData,
cellNotifier: cellNotifier,
renderHook: renderHook,
hasNotes: hasNotes,
)
: _getDesktopCardCellWidget(
key: key,
cellContext: cellContext,
cellControllerBuilder: cellControllerBuilder,
style: style,
cardData: cardData,
cellNotifier: cellNotifier,
renderHook: renderHook,
hasNotes: hasNotes,
);
}
Widget _getDesktopCardCellWidget({
required Key key,
required DatabaseCellContext cellContext,
required CellControllerBuilder cellControllerBuilder,
CardCellStyle? style,
CustomCardData? cardData,
EditableCardNotifier? cellNotifier,
RowCardRenderHook<CustomCardData>? renderHook,
required bool hasNotes,
}) {
switch (cellContext.fieldType) {
case FieldType.Checkbox:
return CheckboxCardCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.DateTime:
return DateCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.DateTime],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.LastEditedTime:
return TimestampCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.LastEditedTime],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.CreatedTime:
return TimestampCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.CreatedTime],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.SingleSelect:
return SelectOptionCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.SingleSelect],
cellControllerBuilder: cellControllerBuilder,
cardData: cardData,
key: key,
);
case FieldType.MultiSelect:
return SelectOptionCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.MultiSelect],
cellControllerBuilder: cellControllerBuilder,
cardData: cardData,
editableNotifier: cellNotifier,
key: key,
);
case FieldType.Checklist:
return ChecklistCardCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.Number:
return NumberCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.Number],
style: isStyleOrNull<NumberCardCellStyle>(style),
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.RichText:
return TextCardCell<CustomCardData>(
key: key,
style: isStyleOrNull<TextCardCellStyle>(style),
cardData: cardData,
renderHook: renderHook?.renderHook[FieldType.RichText],
cellControllerBuilder: cellControllerBuilder,
editableNotifier: cellNotifier,
showNotes: cellContext.fieldInfo.isPrimary && hasNotes,
);
case FieldType.URL:
return URLCardCell<CustomCardData>(
style: isStyleOrNull<URLCardCellStyle>(style),
cellControllerBuilder: cellControllerBuilder,
key: key,
);
}
throw UnimplementedError;
}
Widget _getMobileCardCellWidget({
required Key key,
required DatabaseCellContext cellContext,
required CellControllerBuilder cellControllerBuilder,
CardCellStyle? style,
CustomCardData? cardData,
EditableCardNotifier? cellNotifier,
RowCardRenderHook<CustomCardData>? renderHook,
required bool hasNotes,
}) {
switch (cellContext.fieldType) {
case FieldType.Checkbox:
return MobileCheckboxCardCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.DateTime:
return MobileDateCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.DateTime],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.LastEditedTime:
return MobileTimestampCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.LastEditedTime],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.CreatedTime:
return MobileTimestampCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.CreatedTime],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.SingleSelect:
return MobileSelectOptionCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.SingleSelect],
cellControllerBuilder: cellControllerBuilder,
cardData: cardData,
key: key,
);
case FieldType.MultiSelect:
return MobileSelectOptionCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.MultiSelect],
cellControllerBuilder: cellControllerBuilder,
cardData: cardData,
key: key,
);
case FieldType.Checklist:
return MobileChecklistCardCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.Number:
return MobileNumberCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.Number],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.RichText:
return MobileTextCardCell<CustomCardData>(
key: key,
cardData: cardData,
renderHook: renderHook?.renderHook[FieldType.RichText],
cellControllerBuilder: cellControllerBuilder,
);
case FieldType.URL:
return MobileURLCardCell<CustomCardData>(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
}
throw UnimplementedError;
}
}

View File

@ -1,187 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
import 'package:flutter/material.dart';
typedef CellRenderHook<C, CustomCardData> = Widget? Function(
C cellData,
CustomCardData cardData,
BuildContext buildContext,
);
typedef RenderHookByFieldType<C> = Map<FieldType, CellRenderHook<dynamic, C>>;
/// The [RowCardRenderHook] is used to customize the rendering of the
/// card cell. Each cell has its own field type. So the [renderHook]
/// is a map of [FieldType] to [CellRenderHook].
class RowCardRenderHook<CustomCardData> {
final RenderHookByFieldType<CustomCardData> renderHook = {};
RowCardRenderHook();
/// Add render hook for the FieldType.SingleSelect and FieldType.MultiSelect
void addSelectOptionHook(
CellRenderHook<List<SelectOptionPB>, CustomCardData?> hook,
) {
final hookFn = _typeSafeHook<List<SelectOptionPB>>(hook);
renderHook[FieldType.SingleSelect] = hookFn;
renderHook[FieldType.MultiSelect] = hookFn;
}
/// Add a render hook for the [FieldType.RichText]
void addTextCellHook(
CellRenderHook<String, CustomCardData?> hook,
) {
renderHook[FieldType.RichText] = _typeSafeHook<String>(hook);
}
/// Add a render hook for the [FieldType.Number]
void addNumberCellHook(
CellRenderHook<String, CustomCardData?> hook,
) {
renderHook[FieldType.Number] = _typeSafeHook<String>(hook);
}
/// Add a render hook for the [FieldType.Date]
void addDateCellHook(
CellRenderHook<DateCellDataPB, CustomCardData?> hook,
) {
renderHook[FieldType.DateTime] = _typeSafeHook<DateCellDataPB>(hook);
}
/// Add a render hook for [FieldType.LastEditedTime] and [FieldType.CreatedTime]
void addTimestampCellHook(
CellRenderHook<TimestampCellDataPB, CustomCardData?> hook,
) {
renderHook[FieldType.LastEditedTime] =
_typeSafeHook<TimestampCellDataPB>(hook);
renderHook[FieldType.CreatedTime] =
_typeSafeHook<TimestampCellDataPB>(hook);
}
CellRenderHook<dynamic, CustomCardData> _typeSafeHook<C>(
CellRenderHook<C, CustomCardData?> hook,
) {
Widget? hookFn(cellData, cardData, buildContext) {
if (cellData == null) {
return null;
}
if (cellData is C) {
return hook(cellData, cardData, buildContext);
} else {
Log.debug("Unexpected cellData type: ${cellData.runtimeType}");
return null;
}
}
return hookFn;
}
}
abstract class CardCellStyle {}
S? isStyleOrNull<S>(CardCellStyle? style) {
if (style is S) {
return style as S;
} else {
return null;
}
}
abstract class CardCell<T, S extends CardCellStyle> extends StatefulWidget {
final T? cardData;
final S? style;
const CardCell({super.key, this.cardData, this.style});
}
class EditableCardNotifier {
final ValueNotifier<bool> isCellEditing;
EditableCardNotifier({bool isEditing = false})
: isCellEditing = ValueNotifier(isEditing);
void dispose() {
isCellEditing.dispose();
}
}
class EditableRowNotifier {
final Map<EditableCellId, EditableCardNotifier> _cells = {};
final ValueNotifier<bool> isEditing;
EditableRowNotifier({required bool isEditing})
: isEditing = ValueNotifier(isEditing);
void bindCell(
DatabaseCellContext cellIdentifier,
EditableCardNotifier notifier,
) {
assert(
_cells.values.isEmpty,
'Only one cell can receive the notification',
);
final id = EditableCellId.from(cellIdentifier);
_cells[id]?.dispose();
notifier.isCellEditing.addListener(() {
isEditing.value = notifier.isCellEditing.value;
});
_cells[EditableCellId.from(cellIdentifier)] = notifier;
}
void becomeFirstResponder() {
if (_cells.values.isEmpty) return;
assert(
_cells.values.length == 1,
'Only one cell can receive the notification',
);
_cells.values.first.isCellEditing.value = true;
}
void resignFirstResponder() {
if (_cells.values.isEmpty) return;
assert(
_cells.values.length == 1,
'Only one cell can receive the notification',
);
_cells.values.first.isCellEditing.value = false;
}
void unbind() {
for (final notifier in _cells.values) {
notifier.dispose();
}
_cells.clear();
}
void dispose() {
unbind();
isEditing.dispose();
}
}
abstract mixin class EditableCell {
// Each cell notifier will be bind to the [EditableRowNotifier], which enable
// the row notifier receive its cells event. For example: begin editing the
// cell or end editing the cell.
//
EditableCardNotifier? get editableNotifier;
}
class EditableCellId {
String fieldId;
RowId rowId;
EditableCellId(this.rowId, this.fieldId);
factory EditableCellId.from(DatabaseCellContext cellIdentifier) =>
EditableCellId(
cellIdentifier.rowId,
cellIdentifier.fieldId,
);
}

View File

@ -1,72 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class CheckboxCardCell extends CardCell {
final CellControllerBuilder cellControllerBuilder;
const CheckboxCardCell({
required this.cellControllerBuilder,
super.key,
});
@override
State<CheckboxCardCell> createState() => _CheckboxCellState();
}
class _CheckboxCellState extends State<CheckboxCardCell> {
late CheckboxCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as CheckboxCellController;
_cellBloc = CheckboxCellBloc(cellController: cellController)
..add(const CheckboxCellEvent.initial());
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
buildWhen: (previous, current) =>
previous.isSelected != current.isSelected,
builder: (context, state) {
final icon = FlowySvg(
state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
);
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: CardSizes.cardCellPadding,
child: FlowyIconButton(
iconPadding: EdgeInsets.zero,
icon: icon,
width: 20,
onPressed: () => context
.read<CheckboxCellBloc>()
.add(const CheckboxCellEvent.select()),
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -1,50 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../row/cells/checklist_cell/checklist_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class ChecklistCardCell extends CardCell {
final CellControllerBuilder cellControllerBuilder;
const ChecklistCardCell({required this.cellControllerBuilder, super.key});
@override
State<ChecklistCardCell> createState() => _ChecklistCellState();
}
class _ChecklistCellState extends State<ChecklistCardCell> {
late ChecklistCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as ChecklistCellController;
_cellBloc = ChecklistCellBloc(cellController: cellController);
_cellBloc.add(const ChecklistCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
if (state.tasks.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: CardSizes.cardCellPadding,
child: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
),
);
},
),
);
}
}

View File

@ -1,78 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class DateCardCell<CustomCardData> extends CardCell {
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<dynamic, CustomCardData>? renderHook;
const DateCardCell({
required this.cellControllerBuilder,
this.renderHook,
super.key,
});
@override
State<DateCardCell> createState() => _DateCellState();
}
class _DateCellState extends State<DateCardCell> {
late DateCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as DateCellController;
_cellBloc = DateCellBloc(cellController: cellController)
..add(const DateCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<DateCellBloc, DateCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
return const SizedBox.shrink();
}
final Widget? custom = widget.renderHook?.call(
state.data,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: CardSizes.cardCellPadding,
child: FlowyText.regular(
state.dateStr,
fontSize: 11,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -1,87 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class NumberCardCellStyle extends CardCellStyle {
final double fontSize;
NumberCardCellStyle(this.fontSize);
}
class NumberCardCell<CustomCardData>
extends CardCell<CustomCardData, NumberCardCellStyle> {
final CellRenderHook<String, CustomCardData>? renderHook;
final CellControllerBuilder cellControllerBuilder;
const NumberCardCell({
required this.cellControllerBuilder,
super.cardData,
super.style,
this.renderHook,
super.key,
});
@override
State<NumberCardCell> createState() => _NumberCellState();
}
class _NumberCellState extends State<NumberCardCell> {
late NumberCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as NumberCellController;
_cellBloc = NumberCellBloc(cellController: cellController)
..add(const NumberCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<NumberCellBloc, NumberCellState>(
buildWhen: (previous, current) =>
previous.cellContent != current.cellContent,
builder: (context, state) {
if (state.cellContent.isEmpty) {
return const SizedBox.shrink();
}
final Widget? custom = widget.renderHook?.call(
state.cellContent,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: CardSizes.cardCellPadding,
child: FlowyText.regular(
state.cellContent,
fontSize: widget.style?.fontSize ?? 11,
color: Theme.of(context).hintColor,
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -1,92 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class SelectOptionCardCellStyle extends CardCellStyle {}
class SelectOptionCardCell<CustomCardData>
extends CardCell<CustomCardData, SelectOptionCardCellStyle>
with EditableCell {
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<List<SelectOptionPB>, CustomCardData>? renderHook;
@override
final EditableCardNotifier? editableNotifier;
SelectOptionCardCell({
required this.cellControllerBuilder,
required super.cardData,
this.renderHook,
this.editableNotifier,
super.key,
});
@override
State<SelectOptionCardCell> createState() => _SelectOptionCellState();
}
class _SelectOptionCellState extends State<SelectOptionCardCell> {
late SelectOptionCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as SelectOptionCellController;
_cellBloc = SelectOptionCellBloc(cellController: cellController)
..add(const SelectOptionCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
buildWhen: (previous, current) {
return previous.selectedOptions != current.selectedOptions;
},
builder: (context, state) {
final Widget? custom = widget.renderHook?.call(
state.selectedOptions,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
final children = state.selectedOptions
.map(
(option) => SelectOptionTag(
option: option,
fontSize: 11,
padding:
const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
),
)
.toList();
return Align(
alignment: AlignmentDirectional.topStart,
child: Padding(
padding: CardSizes.cardCellPadding,
child: Wrap(spacing: 4, runSpacing: 2, children: children),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -1,221 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../row/cell_builder.dart';
import '../define.dart';
import 'card_cell.dart';
class TextCardCellStyle extends CardCellStyle {
final double fontSize;
TextCardCellStyle(this.fontSize);
}
class TextCardCell<CustomCardData>
extends CardCell<CustomCardData, TextCardCellStyle> with EditableCell {
const TextCardCell({
super.key,
super.cardData,
super.style,
required this.cellControllerBuilder,
this.editableNotifier,
this.renderHook,
this.showNotes = false,
});
@override
final EditableCardNotifier? editableNotifier;
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<String, CustomCardData>? renderHook;
final bool showNotes;
@override
State<TextCardCell> createState() => _TextCellState();
}
class _TextCellState extends State<TextCardCell> {
late TextCellBloc _cellBloc;
late TextEditingController _controller;
bool focusWhenInit = false;
final focusNode = SingleListenerFocusNode();
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as TextCellController;
_cellBloc = TextCellBloc(cellController: cellController)
..add(const TextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
focusWhenInit = widget.editableNotifier?.isCellEditing.value ?? false;
if (focusWhenInit) {
focusNode.requestFocus();
}
// If the focusNode lost its focus, the widget's editableNotifier will
// set to false, which will cause the [EditableRowNotifier] to receive
// end edit event.
focusNode.addListener(() {
if (!focusNode.hasFocus) {
focusWhenInit = false;
widget.editableNotifier?.isCellEditing.value = false;
_cellBloc.add(const TextCellEvent.enableEdit(false));
}
});
_bindEditableNotifier();
super.initState();
}
void _bindEditableNotifier() {
widget.editableNotifier?.isCellEditing.addListener(() {
if (!mounted) return;
final isEditing = widget.editableNotifier?.isCellEditing.value ?? false;
if (isEditing) {
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
});
}
_cellBloc.add(TextCellEvent.enableEdit(isEditing));
});
}
@override
void didUpdateWidget(covariant oldWidget) {
_bindEditableNotifier();
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocListener<TextCellBloc, TextCellState>(
listener: (context, state) {
if (_controller.text != state.content) {
_controller.text = state.content;
}
},
child: BlocBuilder<TextCellBloc, TextCellState>(
buildWhen: (previous, current) {
if (previous.content != current.content &&
_controller.text == current.content &&
current.enableEdit) {
return false;
}
return previous != current;
},
builder: (context, state) {
// Returns a custom render widget
final Widget? custom = widget.renderHook?.call(
state.content,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
final isTitle =
context.read<TextCellBloc>().cellController.fieldInfo.isPrimary;
if (state.content.isEmpty &&
state.enableEdit == false &&
focusWhenInit == false &&
!isTitle) {
return const SizedBox.shrink();
}
final child = state.enableEdit || focusWhenInit
? _buildTextField()
: _buildText(state, isTitle);
return Padding(
padding: CardSizes.cardCellPadding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.showNotes) ...[
FlowyTooltip(
message: LocaleKeys.board_notesTooltip.tr(),
child: FlowySvg(
FlowySvgs.notes_s,
color: Theme.of(context).hintColor,
),
),
const HSpace(4),
],
Expanded(child: child),
],
),
);
},
),
),
);
}
Future<void> focusChanged() async {
_cellBloc.add(TextCellEvent.updateText(_controller.text));
}
@override
Future<void> dispose() async {
_cellBloc.close();
_controller.dispose();
focusNode.dispose();
super.dispose();
}
Widget _buildText(TextCellState state, bool isTitle) {
final text = state.content.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: state.content;
final color = state.content.isEmpty ? Theme.of(context).hintColor : null;
return FlowyText(
text,
fontSize: _fontSize(isTitle),
fontWeight: _fontWeight(isTitle),
color: color,
maxLines: null, // Enable multiple lines
);
}
double _fontSize(bool isTitle) {
return widget.style?.fontSize ?? (isTitle ? 12 : 11);
}
FontWeight _fontWeight(bool isTitle) {
return isTitle ? FontWeight.w500 : FontWeight.w400;
}
Widget _buildTextField() {
return TextField(
controller: _controller,
focusNode: focusNode,
onChanged: (value) => focusChanged(),
onEditingComplete: () => focusNode.unfocus(),
maxLines: null,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontSize: _fontSize(true)),
decoration: InputDecoration(
contentPadding:
EdgeInsets.symmetric(vertical: CardSizes.cardCellPadding.top),
border: InputBorder.none,
isDense: true,
isCollapsed: true,
hintText: LocaleKeys.grid_row_titlePlaceholder.tr(),
),
);
}
}

View File

@ -1,77 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class TimestampCardCell<CustomCardData> extends CardCell {
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<dynamic, CustomCardData>? renderHook;
const TimestampCardCell({
required this.cellControllerBuilder,
this.renderHook,
super.key,
});
@override
State<TimestampCardCell> createState() => _TimestampCellState();
}
class _TimestampCellState extends State<TimestampCardCell> {
late TimestampCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as TimestampCellController;
_cellBloc = TimestampCellBloc(cellController: cellController)
..add(const TimestampCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
return const SizedBox.shrink();
}
final Widget? custom = widget.renderHook?.call(
state.data,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: CardSizes.cardCellPadding,
child: FlowyText.regular(
state.dateStr,
fontSize: 11,
color: Theme.of(context).hintColor,
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -1,78 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class URLCardCellStyle extends CardCellStyle {
final double fontSize;
URLCardCellStyle(this.fontSize);
}
class URLCardCell<CustomCardData>
extends CardCell<CustomCardData, URLCardCellStyle> {
final CellControllerBuilder cellControllerBuilder;
const URLCardCell({
required this.cellControllerBuilder,
super.style,
super.key,
});
@override
State<URLCardCell> createState() => _URLCellState();
}
class _URLCellState extends State<URLCardCell> {
late URLCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as URLCellController;
_cellBloc = URLCellBloc(cellController: cellController);
_cellBloc.add(const URLCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<URLCellBloc, URLCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox();
}
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: CardSizes.cardCellPadding,
child: RichText(
textAlign: TextAlign.left,
text: TextSpan(
text: state.content,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
fontSize: widget.style?.fontSize ?? 11,
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
),
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -1,5 +0,0 @@
import 'package:flutter/widgets.dart';
class CardSizes {
static EdgeInsets get cardCellPadding => const EdgeInsets.all(4);
}

View File

@ -0,0 +1,90 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/widgets.dart';
import 'card_cell_skeleton/card_cell.dart';
import 'card_cell_skeleton/checkbox_card_cell.dart';
import 'card_cell_skeleton/checklist_card_cell.dart';
import 'card_cell_skeleton/date_card_cell.dart';
import 'card_cell_skeleton/number_card_cell.dart';
import 'card_cell_skeleton/select_option_card_cell.dart';
import 'card_cell_skeleton/text_card_cell.dart';
import 'card_cell_skeleton/url_card_cell.dart';
typedef CardCellStyleMap = Map<FieldType, CardCellStyle>;
class CardCellBuilder {
final DatabaseController databaseController;
CardCellBuilder({required this.databaseController});
Widget build({
required CellContext cellContext,
required CardCellStyleMap styleMap,
EditableCardNotifier? cellNotifier,
required bool hasNotes,
}) {
final fieldType = databaseController.fieldController
.getField(cellContext.fieldId)!
.fieldType;
final key = ValueKey(
"${databaseController.viewId}${cellContext.fieldId}${cellContext.rowId}",
);
final style = styleMap[fieldType];
return switch (fieldType) {
FieldType.Checkbox => CheckboxCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.Checklist => ChecklistCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.DateTime => DateCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.LastEditedTime || FieldType.CreatedTime => TimestampCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.SingleSelect || FieldType.MultiSelect => SelectOptionCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.Number => NumberCardCell(
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
key: key,
),
FieldType.RichText => TextCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
editableNotifier: cellNotifier,
showNotes: hasNotes,
),
FieldType.URL => URLCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
_ => throw UnimplementedError,
};
}
}

View File

@ -0,0 +1,96 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:flutter/material.dart';
abstract class CardCell<T extends CardCellStyle> extends StatefulWidget {
final T style;
const CardCell({super.key, required this.style});
}
abstract class CardCellStyle {
final EdgeInsetsGeometry padding;
const CardCellStyle({required this.padding});
}
S? isStyleOrNull<S>(CardCellStyle? style) {
if (style is S) {
return style as S;
} else {
return null;
}
}
class EditableCardNotifier {
final ValueNotifier<bool> isCellEditing;
EditableCardNotifier({bool isEditing = false})
: isCellEditing = ValueNotifier(isEditing);
void dispose() {
isCellEditing.dispose();
}
}
class EditableRowNotifier {
final Map<CellContext, EditableCardNotifier> _cells = {};
final ValueNotifier<bool> isEditing;
EditableRowNotifier({required bool isEditing})
: isEditing = ValueNotifier(isEditing);
void bindCell(
CellContext cellIdentifier,
EditableCardNotifier notifier,
) {
assert(
_cells.values.isEmpty,
'Only one cell can receive the notification',
);
_cells[cellIdentifier]?.dispose();
notifier.isCellEditing.addListener(() {
isEditing.value = notifier.isCellEditing.value;
});
_cells[cellIdentifier] = notifier;
}
void becomeFirstResponder() {
if (_cells.values.isEmpty) return;
assert(
_cells.values.length == 1,
'Only one cell can receive the notification',
);
_cells.values.first.isCellEditing.value = true;
}
void resignFirstResponder() {
if (_cells.values.isEmpty) return;
assert(
_cells.values.length == 1,
'Only one cell can receive the notification',
);
_cells.values.first.isCellEditing.value = false;
}
void unbind() {
for (final notifier in _cells.values) {
notifier.dispose();
}
_cells.clear();
}
void dispose() {
unbind();
isEditing.dispose();
}
}
abstract mixin class EditableCell {
// Each cell notifier will be bind to the [EditableRowNotifier], which enable
// the row notifier receive its cells event. For example: begin editing the
// cell or end editing the cell.
//
EditableCardNotifier? get editableNotifier;
}

View File

@ -0,0 +1,88 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class CheckboxCardCellStyle extends CardCellStyle {
final Size iconSize;
final bool showFieldName;
final TextStyle? textStyle;
CheckboxCardCellStyle({
required super.padding,
required this.iconSize,
required this.showFieldName,
this.textStyle,
}) : assert(!showFieldName || showFieldName && textStyle != null);
}
class CheckboxCardCell extends CardCell<CheckboxCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const CheckboxCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<CheckboxCardCell> createState() => _CheckboxCellState();
}
class _CheckboxCellState extends State<CheckboxCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) {
return CheckboxCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const CheckboxCellEvent.initial());
},
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
builder: (context, state) {
return Container(
alignment: AlignmentDirectional.centerStart,
padding: widget.style.padding,
child: Row(
children: [
FlowyIconButton(
iconPadding: EdgeInsets.zero,
icon: FlowySvg(
state.isSelected
? FlowySvgs.check_filled_s
: FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
size: widget.style.iconSize,
),
width: 20,
onPressed: () => context
.read<CheckboxCellBloc>()
.add(const CheckboxCellEvent.select()),
),
if (widget.style.showFieldName) ...[
const HSpace(6.0),
Text(
state.fieldName,
style: widget.style.textStyle,
),
],
],
),
);
},
),
);
}
}

View File

@ -0,0 +1,64 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class ChecklistCardCellStyle extends CardCellStyle {
final TextStyle textStyle;
ChecklistCardCellStyle({
required super.padding,
required this.textStyle,
});
}
class ChecklistCardCell extends CardCell<ChecklistCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const ChecklistCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<ChecklistCardCell> createState() => _ChecklistCellState();
}
class _ChecklistCellState extends State<ChecklistCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) {
return ChecklistCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const ChecklistCellEvent.initial());
},
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
if (state.tasks.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: widget.style.padding,
child: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
textStyle: widget.style.textStyle,
),
);
},
),
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class DateCardCellStyle extends CardCellStyle {
final TextStyle textStyle;
DateCardCellStyle({
required super.padding,
required this.textStyle,
});
}
class DateCardCell extends CardCell<DateCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const DateCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<DateCardCell> createState() => _DateCellState();
}
class _DateCellState extends State<DateCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return DateCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const DateCellEvent.initial());
},
child: BlocBuilder<DateCellBloc, DateCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
return const SizedBox.shrink();
}
return Container(
alignment: Alignment.centerLeft,
padding: widget.style.padding,
child: Text(
state.dateStr,
style: widget.style.textStyle,
),
);
},
),
);
}
}

View File

@ -0,0 +1,62 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class NumberCardCellStyle extends CardCellStyle {
final TextStyle textStyle;
const NumberCardCellStyle({
required super.padding,
required this.textStyle,
});
}
class NumberCardCell extends CardCell<NumberCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const NumberCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<NumberCardCell> createState() => _NumberCellState();
}
class _NumberCellState extends State<NumberCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return NumberCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const NumberCellEvent.initial());
},
child: BlocBuilder<NumberCellBloc, NumberCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox.shrink();
}
return Container(
alignment: AlignmentDirectional.centerStart,
padding: widget.style.padding,
child: Text(state.content, style: widget.style.textStyle),
);
},
),
);
}
}

View File

@ -0,0 +1,87 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class SelectOptionCardCellStyle extends CardCellStyle {
final double tagFontSize;
final bool wrap;
final EdgeInsets tagPadding;
SelectOptionCardCellStyle({
required super.padding,
required this.tagFontSize,
required this.wrap,
required this.tagPadding,
});
}
class SelectOptionCardCell extends CardCell<SelectOptionCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const SelectOptionCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<SelectOptionCardCell> createState() => _SelectOptionCellState();
}
class _SelectOptionCellState extends State<SelectOptionCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) {
return SelectOptionCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const SelectOptionCellEvent.initial());
},
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
buildWhen: (previous, current) {
return previous.selectedOptions != current.selectedOptions;
},
builder: (context, state) {
if (state.selectedOptions.isEmpty) {
return const SizedBox.shrink();
}
final children = state.selectedOptions
.map(
(option) => SelectOptionTag(
option: option,
fontSize: widget.style.tagFontSize,
padding: widget.style.tagPadding,
),
)
.toList();
return Container(
alignment: AlignmentDirectional.topStart,
padding: widget.style.padding,
child: widget.style.wrap
? Wrap(spacing: 4, runSpacing: 4, children: children)
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: children,
),
),
);
},
),
);
}
}

View File

@ -0,0 +1,203 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_builder.dart';
import 'card_cell.dart';
class TextCardCellStyle extends CardCellStyle {
final TextStyle textStyle;
final TextStyle titleTextStyle;
final int? maxLines;
TextCardCellStyle({
required super.padding,
required this.textStyle,
required this.titleTextStyle,
this.maxLines = 1,
});
}
class TextCardCell extends CardCell<TextCardCellStyle> with EditableCell {
final DatabaseController databaseController;
final CellContext cellContext;
final bool showNotes;
const TextCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
this.editableNotifier,
this.showNotes = false,
});
@override
final EditableCardNotifier? editableNotifier;
@override
State<TextCardCell> createState() => _TextCellState();
}
class _TextCellState extends State<TextCardCell> {
late final cellBloc = TextCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const TextCellEvent.initial());
late final TextEditingController _textEditingController =
TextEditingController(text: cellBloc.state.content);
final focusNode = SingleListenerFocusNode();
bool focusWhenInit = false;
@override
void initState() {
super.initState();
focusWhenInit = widget.editableNotifier?.isCellEditing.value ?? false;
if (focusWhenInit) {
focusNode.requestFocus();
}
// If the focusNode lost its focus, the widget's editableNotifier will
// set to false, which will cause the [EditableRowNotifier] to receive
// end edit event.
focusNode.addListener(() {
if (!focusNode.hasFocus) {
focusWhenInit = false;
widget.editableNotifier?.isCellEditing.value = false;
cellBloc.add(const TextCellEvent.enableEdit(false));
}
});
_bindEditableNotifier();
}
void _bindEditableNotifier() {
widget.editableNotifier?.isCellEditing.addListener(() {
if (!mounted) return;
final isEditing = widget.editableNotifier?.isCellEditing.value ?? false;
if (isEditing) {
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
});
}
cellBloc.add(TextCellEvent.enableEdit(isEditing));
});
}
@override
void didUpdateWidget(covariant oldWidget) {
_bindEditableNotifier();
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocConsumer<TextCellBloc, TextCellState>(
listener: (context, state) {
if (_textEditingController.text != state.content) {
_textEditingController.text = state.content;
}
},
buildWhen: (previous, current) {
if (previous.content != current.content &&
_textEditingController.text == current.content &&
current.enableEdit) {
return false;
}
return previous != current;
},
builder: (context, state) {
final isTitle = cellBloc.cellController.fieldInfo.isPrimary;
if (state.content.isEmpty &&
state.enableEdit == false &&
focusWhenInit == false &&
!isTitle) {
return const SizedBox.shrink();
}
final child = state.enableEdit || focusWhenInit
? _buildTextField()
: _buildText(state, isTitle);
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (isTitle && widget.showNotes)
FlowyTooltip(
message: LocaleKeys.board_notesTooltip.tr(),
child: FlowySvg(
FlowySvgs.notes_s,
color: Theme.of(context).hintColor,
),
),
Expanded(child: child),
],
);
},
),
);
}
@override
void dispose() {
_textEditingController.dispose();
focusNode.dispose();
cellBloc.close();
super.dispose();
}
Widget _buildText(TextCellState state, bool isTitle) {
final text = state.content.isEmpty
? isTitle
? LocaleKeys.grid_row_titlePlaceholder.tr()
: LocaleKeys.grid_row_textPlaceholder.tr()
: state.content;
final color = state.content.isEmpty ? Theme.of(context).hintColor : null;
final textStyle =
isTitle ? widget.style.titleTextStyle : widget.style.textStyle;
return Padding(
padding: widget.style.padding,
child: Text(
text,
style: textStyle.copyWith(color: color),
maxLines: widget.style.maxLines,
),
);
}
Widget _buildTextField() {
final padding =
widget.style.padding.add(const EdgeInsets.symmetric(vertical: 4.0));
return TextField(
controller: _textEditingController,
focusNode: focusNode,
onChanged: (_) =>
cellBloc.add(TextCellEvent.updateText(_textEditingController.text)),
onEditingComplete: () => focusNode.unfocus(),
maxLines: null,
style: widget.style.titleTextStyle,
decoration: InputDecoration(
contentPadding: padding,
border: InputBorder.none,
isDense: true,
isCollapsed: true,
hintText: LocaleKeys.grid_row_titlePlaceholder.tr(),
),
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class TimestampCardCellStyle extends CardCellStyle {
final TextStyle textStyle;
TimestampCardCellStyle({
required super.padding,
required this.textStyle,
});
}
class TimestampCardCell extends CardCell<TimestampCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const TimestampCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<TimestampCardCell> createState() => _TimestampCellState();
}
class _TimestampCellState extends State<TimestampCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) {
return TimestampCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const TimestampCellEvent.initial());
},
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
return const SizedBox.shrink();
}
return Container(
alignment: AlignmentDirectional.centerStart,
padding: widget.style.padding,
child: Text(
state.dateStr,
style: widget.style.textStyle,
),
);
},
),
);
}
}

View File

@ -0,0 +1,64 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class URLCardCellStyle extends CardCellStyle {
final TextStyle textStyle;
URLCardCellStyle({
required super.padding,
required this.textStyle,
});
}
class URLCardCell extends CardCell<URLCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const URLCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<URLCardCell> createState() => _URLCellState();
}
class _URLCellState extends State<URLCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return URLCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const URLCellEvent.initial());
},
child: BlocBuilder<URLCellBloc, URLCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox.shrink();
}
return Container(
alignment: AlignmentDirectional.centerStart,
padding: widget.style.padding,
child: Text(
state.content,
style: widget.style.textStyle,
),
);
},
),
);
}
}

View File

@ -0,0 +1,77 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
import '../card_cell_builder.dart';
import '../card_cell_skeleton/checkbox_card_cell.dart';
import '../card_cell_skeleton/checklist_card_cell.dart';
import '../card_cell_skeleton/date_card_cell.dart';
import '../card_cell_skeleton/number_card_cell.dart';
import '../card_cell_skeleton/select_option_card_cell.dart';
import '../card_cell_skeleton/text_card_cell.dart';
import '../card_cell_skeleton/timestamp_card_cell.dart';
import '../card_cell_skeleton/url_card_cell.dart';
CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) {
const EdgeInsetsGeometry padding = EdgeInsets.symmetric(vertical: 2);
final TextStyle textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 10,
overflow: TextOverflow.ellipsis,
fontWeight: FontWeight.w400,
);
return {
FieldType.Checkbox: CheckboxCardCellStyle(
padding: padding,
iconSize: const Size.square(16),
showFieldName: true,
textStyle: textStyle,
),
FieldType.Checklist: ChecklistCardCellStyle(
padding: padding,
textStyle: textStyle.copyWith(color: Theme.of(context).hintColor),
),
FieldType.CreatedTime: TimestampCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.DateTime: DateCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.LastEditedTime: TimestampCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.MultiSelect: SelectOptionCardCellStyle(
padding: padding,
tagFontSize: 9,
wrap: true,
tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
),
FieldType.Number: NumberCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.RichText: TextCardCellStyle(
padding: padding,
textStyle: textStyle,
titleTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 11,
overflow: TextOverflow.ellipsis,
),
),
FieldType.SingleSelect: SelectOptionCardCellStyle(
padding: padding,
tagFontSize: 9,
wrap: true,
tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
),
FieldType.URL: URLCardCellStyle(
padding: padding,
textStyle: textStyle.copyWith(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
),
};
}

View File

@ -0,0 +1,77 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
import '../card_cell_builder.dart';
import '../card_cell_skeleton/checkbox_card_cell.dart';
import '../card_cell_skeleton/checklist_card_cell.dart';
import '../card_cell_skeleton/date_card_cell.dart';
import '../card_cell_skeleton/number_card_cell.dart';
import '../card_cell_skeleton/select_option_card_cell.dart';
import '../card_cell_skeleton/text_card_cell.dart';
import '../card_cell_skeleton/timestamp_card_cell.dart';
import '../card_cell_skeleton/url_card_cell.dart';
CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {
const EdgeInsetsGeometry padding = EdgeInsets.all(4);
final TextStyle textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 11,
overflow: TextOverflow.ellipsis,
fontWeight: FontWeight.w400,
);
return {
FieldType.Checkbox: CheckboxCardCellStyle(
padding: padding,
iconSize: const Size.square(16),
showFieldName: true,
textStyle: textStyle,
),
FieldType.Checklist: ChecklistCardCellStyle(
padding: padding,
textStyle: textStyle.copyWith(color: Theme.of(context).hintColor),
),
FieldType.CreatedTime: TimestampCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.DateTime: DateCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.LastEditedTime: TimestampCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.MultiSelect: SelectOptionCardCellStyle(
padding: padding,
tagFontSize: 11,
wrap: true,
tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
),
FieldType.Number: NumberCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.RichText: TextCardCellStyle(
padding: padding,
textStyle: textStyle,
maxLines: null,
titleTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
overflow: TextOverflow.ellipsis,
),
),
FieldType.SingleSelect: SelectOptionCardCellStyle(
padding: padding,
tagFontSize: 11,
wrap: true,
tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
),
FieldType.URL: URLCardCellStyle(
padding: padding,
textStyle: textStyle.copyWith(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
),
};
}

View File

@ -0,0 +1,76 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
import '../card_cell_builder.dart';
import '../card_cell_skeleton/checkbox_card_cell.dart';
import '../card_cell_skeleton/checklist_card_cell.dart';
import '../card_cell_skeleton/date_card_cell.dart';
import '../card_cell_skeleton/number_card_cell.dart';
import '../card_cell_skeleton/select_option_card_cell.dart';
import '../card_cell_skeleton/text_card_cell.dart';
import '../card_cell_skeleton/timestamp_card_cell.dart';
import '../card_cell_skeleton/url_card_cell.dart';
CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) {
const EdgeInsetsGeometry padding = EdgeInsets.all(4);
final TextStyle textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 14,
overflow: TextOverflow.ellipsis,
fontWeight: FontWeight.w400,
);
return {
FieldType.Checkbox: CheckboxCardCellStyle(
padding: padding,
iconSize: const Size.square(24),
showFieldName: true,
textStyle: textStyle,
),
FieldType.Checklist: ChecklistCardCellStyle(
padding: padding,
textStyle: textStyle.copyWith(color: Theme.of(context).hintColor),
),
FieldType.CreatedTime: TimestampCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.DateTime: DateCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.LastEditedTime: TimestampCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.MultiSelect: SelectOptionCardCellStyle(
padding: padding,
tagFontSize: 12,
wrap: true,
tagPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
),
FieldType.Number: NumberCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.RichText: TextCardCellStyle(
padding: padding,
textStyle: textStyle,
titleTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
overflow: TextOverflow.ellipsis,
),
),
FieldType.SingleSelect: SelectOptionCardCellStyle(
padding: padding,
tagFontSize: 12,
wrap: true,
tagPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
),
FieldType.URL: URLCardCellStyle(
padding: padding,
textStyle: textStyle.copyWith(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
),
};
}

View File

@ -0,0 +1,33 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/checkbox.dart';
class DesktopGridCheckboxCellSkin extends IEditableCheckboxCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
CheckboxCellBloc bloc,
CheckboxCellState state,
) {
return Container(
alignment: AlignmentDirectional.centerStart,
padding: GridSize.cellContentInsets,
child: FlowyIconButton(
hoverColor: Colors.transparent,
onPressed: () => bloc.add(const CheckboxCellEvent.select()),
icon: FlowySvg(
state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
size: const Size.square(20),
),
width: 20,
),
);
}
}

View File

@ -0,0 +1,52 @@
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_skeleton/checklist.dart';
class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
ChecklistCellBloc bloc,
ChecklistCellState state,
PopoverController popoverController,
) {
return AppFlowyPopover(
margin: EdgeInsets.zero,
controller: popoverController,
constraints: BoxConstraints.loose(const Size(360, 400)),
direction: PopoverDirection.bottomWithLeftAligned,
triggerActions: PopoverTriggerFlags.none,
popupBuilder: (BuildContext popoverContext) {
WidgetsBinding.instance.addPostFrameCallback((_) {
cellContainerNotifier.isFocus = true;
});
return BlocProvider.value(
value: bloc,
child: ChecklistCellEditor(
cellController: bloc.cellController,
),
);
},
onClose: () => cellContainerNotifier.isFocus = false,
child: Container(
alignment: AlignmentDirectional.centerStart,
padding: GridSize.cellContentInsets,
child: state.tasks.isEmpty
? const SizedBox.shrink()
: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
),
),
);
}
}

View File

@ -0,0 +1,63 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/widgets.dart';
import '../editable_cell_skeleton/date.dart';
class DesktopGridDateCellSkin extends IEditableDateCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
DateCellBloc bloc,
DateCellState state,
PopoverController popoverController,
) {
return AppFlowyPopover(
controller: popoverController,
triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(260, 620)),
margin: EdgeInsets.zero,
child: Container(
alignment: AlignmentDirectional.centerStart,
padding: GridSize.cellContentInsets,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FlowyText.medium(
state.dateStr,
overflow: TextOverflow.ellipsis,
),
),
if (state.data?.reminderId.isNotEmpty ?? false) ...[
const HSpace(4),
FlowyTooltip(
message: LocaleKeys.grid_field_reminderOnDateTooltip.tr(),
child: const FlowySvg(FlowySvgs.clock_alarm_s),
),
],
],
),
),
popupBuilder: (BuildContext popoverContent) {
return DateCellEditor(
cellController: bloc.cellController,
onDismissed: () => cellContainerNotifier.isFocus = false,
);
},
onClose: () {
cellContainerNotifier.isFocus = false;
},
);
}
}

View File

@ -0,0 +1,36 @@
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/number.dart';
class DesktopGridNumberCellSkin extends IEditableNumberCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
NumberCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/select_option_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/widgets.dart';
import '../editable_cell_skeleton/select_option.dart';
class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
SelectOptionCellBloc bloc,
SelectOptionCellState state,
PopoverController popoverController,
) {
return AppFlowyPopover(
controller: popoverController,
constraints: BoxConstraints.loose(const Size.square(300)),
margin: EdgeInsets.zero,
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (BuildContext popoverContext) {
WidgetsBinding.instance.addPostFrameCallback((_) {
cellContainerNotifier.isFocus = true;
});
return SelectOptionCellEditor(
cellController: bloc.cellController,
);
},
onClose: () => cellContainerNotifier.isFocus = false,
child: Container(
alignment: AlignmentDirectional.centerStart,
padding: GridSize.cellContentInsets,
child: state.selectedOptions.isEmpty
? const SizedBox.shrink()
: _buildOptions(context, state.selectedOptions),
),
);
}
Widget _buildOptions(context, List<SelectOptionPB> options) {
return Wrap(
runSpacing: 4,
children: options.map(
(option) {
return Padding(
padding: const EdgeInsets.only(right: 4),
child: SelectOptionTag(
option: option,
padding: const EdgeInsets.symmetric(
vertical: 1,
horizontal: 8,
),
),
);
},
).toList(),
);
}
}

View File

@ -0,0 +1,64 @@
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_skeleton/text.dart';
class DesktopGridTextCellSkin extends IEditableTextCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TextCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return Padding(
padding: GridSize.cellContentInsets,
child: Row(
children: [
BlocBuilder<TextCellBloc, TextCellState>(
buildWhen: (p, c) => p.emoji != c.emoji,
builder: (context, state) {
if (state.emoji.isEmpty) {
return const SizedBox.shrink();
}
return Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
state.emoji,
fontSize: 16,
),
const HSpace(6),
],
),
);
},
),
Expanded(
child: TextField(
controller: textEditingController,
focusNode: focusNode,
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
decoration: const InputDecoration(
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
isCollapsed: true,
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,26 @@
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/widgets.dart';
import '../editable_cell_skeleton/timestamp.dart';
class DesktopGridTimestampCellSkin extends IEditableTimestampCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TimestampCellBloc bloc,
TimestampCellState state,
) {
return Container(
alignment: AlignmentDirectional.centerStart,
padding: GridSize.cellContentInsets,
child: FlowyText.medium(
state.dateStr,
maxLines: null,
),
);
}
}

View File

@ -0,0 +1,213 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../editable_cell_skeleton/url.dart';
class DesktopGridURLSkin extends IEditableURLCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
URLCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
URLCellDataNotifier cellDataNotifier,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
autofocus: false,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintStyle: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Theme.of(context).hintColor),
isDense: true,
),
onTapOutside: (_) => focusNode.unfocus(),
);
}
@override
List<GridCellAccessoryBuilder> accessoryBuilder(
GridCellAccessoryBuildContext context,
URLCellDataNotifier cellDataNotifier,
) {
return [
accessoryFromType(
GridURLCellAccessoryType.visitURL,
cellDataNotifier,
),
accessoryFromType(
GridURLCellAccessoryType.copyURL,
cellDataNotifier,
),
];
}
}
GridCellAccessoryBuilder accessoryFromType(
GridURLCellAccessoryType ty,
URLCellDataNotifier cellDataNotifier,
) {
switch (ty) {
case GridURLCellAccessoryType.visitURL:
return VisitURLCellAccessoryBuilder(
builder: (Key key) => _VisitURLAccessory(
key: key,
cellDataNotifier: cellDataNotifier,
),
);
case GridURLCellAccessoryType.copyURL:
return CopyURLCellAccessoryBuilder(
builder: (Key key) => _CopyURLAccessory(
key: key,
cellDataNotifier: cellDataNotifier,
),
);
}
}
enum GridURLCellAccessoryType {
copyURL,
visitURL,
}
typedef CopyURLCellAccessoryBuilder
= GridCellAccessoryBuilder<State<_CopyURLAccessory>>;
class _CopyURLAccessory extends StatefulWidget {
const _CopyURLAccessory({
super.key,
required this.cellDataNotifier,
});
final URLCellDataNotifier cellDataNotifier;
@override
State<_CopyURLAccessory> createState() => _CopyURLAccessoryState();
}
class _CopyURLAccessoryState extends State<_CopyURLAccessory>
with GridCellAccessoryState {
@override
Widget build(BuildContext context) {
if (widget.cellDataNotifier.value.isNotEmpty) {
return FlowyTooltip(
message: LocaleKeys.tooltip_urlCopyAccessory.tr(),
preferBelow: false,
child: _URLAccessoryIconContainer(
child: FlowySvg(
FlowySvgs.copy_s,
color: AFThemeExtension.of(context).textColor,
),
),
);
} else {
return const SizedBox.shrink();
}
}
@override
void onTap() {
final content = widget.cellDataNotifier.value;
if (content.isEmpty) {
return;
}
Clipboard.setData(ClipboardData(text: content));
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
}
}
typedef VisitURLCellAccessoryBuilder
= GridCellAccessoryBuilder<State<_VisitURLAccessory>>;
class _VisitURLAccessory extends StatefulWidget {
const _VisitURLAccessory({
super.key,
required this.cellDataNotifier,
});
final URLCellDataNotifier cellDataNotifier;
@override
State<_VisitURLAccessory> createState() => _VisitURLAccessoryState();
}
class _VisitURLAccessoryState extends State<_VisitURLAccessory>
with GridCellAccessoryState {
@override
Widget build(BuildContext context) {
if (widget.cellDataNotifier.value.isNotEmpty) {
return FlowyTooltip(
message: LocaleKeys.tooltip_urlLaunchAccessory.tr(),
preferBelow: false,
child: _URLAccessoryIconContainer(
child: FlowySvg(
FlowySvgs.attach_s,
color: AFThemeExtension.of(context).textColor,
),
),
);
} else {
return const SizedBox.shrink();
}
}
@override
bool enable() {
return widget.cellDataNotifier.value.isNotEmpty;
}
@override
void onTap() {
final content = widget.cellDataNotifier.value;
if (content.isEmpty) {
return;
}
final shouldAddScheme =
!['http', 'https'].any((pattern) => content.startsWith(pattern));
final url = shouldAddScheme ? 'http://$content' : content;
canLaunchUrlString(url).then((value) => launchUrlString(url));
}
}
class _URLAccessoryIconContainer extends StatelessWidget {
const _URLAccessoryIconContainer({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 26,
height: 26,
child: Padding(
padding: const EdgeInsets.all(3.0),
child: child,
),
);
}
}

View File

@ -0,0 +1,32 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/checkbox.dart';
class DesktopRowDetailCheckboxCellSkin extends IEditableCheckboxCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
CheckboxCellBloc bloc,
CheckboxCellState state,
) {
return Container(
alignment: AlignmentDirectional.centerStart,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
child: FlowyIconButton(
hoverColor: Colors.transparent,
onPressed: () => bloc.add(const CheckboxCellEvent.select()),
icon: FlowySvg(
state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
size: const Size.square(20),
),
width: 20,
),
);
}
}

View File

@ -0,0 +1,184 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_skeleton/checklist.dart';
class DesktopRowDetailChecklistCellSkin extends IEditableChecklistCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
ChecklistCellBloc bloc,
ChecklistCellState state,
PopoverController popoverController,
) {
return ChecklistItems(
context: context,
cellContainerNotifier: cellContainerNotifier,
bloc: bloc,
state: state,
popoverController: popoverController,
);
}
}
class ChecklistItems extends StatefulWidget {
const ChecklistItems({
super.key,
required this.context,
required this.cellContainerNotifier,
required this.bloc,
required this.state,
required this.popoverController,
});
final BuildContext context;
final CellContainerNotifier cellContainerNotifier;
final ChecklistCellBloc bloc;
final ChecklistCellState state;
final PopoverController popoverController;
@override
State<ChecklistItems> createState() => _ChecklistItemsState();
}
class _ChecklistItemsState extends State<ChecklistItems> {
bool showIncompleteOnly = false;
@override
Widget build(BuildContext context) {
final tasks = [...widget.state.tasks];
if (showIncompleteOnly) {
tasks.removeWhere((task) => task.isSelected);
}
final children = tasks
.mapIndexed(
(index, task) => ChecklistItem(
task: task,
autofocus: widget.state.newTask && index == tasks.length - 1,
onSubmitted: () {
if (index == tasks.length - 1) {
widget.bloc.add(const ChecklistCellEvent.createNewTask(""));
}
},
),
)
.toList();
return Align(
alignment: AlignmentDirectional.centerStart,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: ChecklistProgressBar(
tasks: widget.state.tasks,
percent: widget.state.percent,
),
),
const HSpace(6.0),
FlowyIconButton(
tooltipText: showIncompleteOnly
? LocaleKeys.grid_checklist_showComplete.tr()
: LocaleKeys.grid_checklist_hideComplete.tr(),
width: 32,
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
icon: FlowySvg(
showIncompleteOnly ? FlowySvgs.show_m : FlowySvgs.hide_m,
size: const Size.square(16),
),
onPressed: () {
setState(
() => showIncompleteOnly = !showIncompleteOnly,
);
},
),
],
),
),
const VSpace(4),
...children,
const ChecklistItemControl(),
],
),
);
}
}
class ChecklistItemControl extends StatefulWidget {
const ChecklistItemControl({super.key});
@override
State<ChecklistItemControl> createState() => _ChecklistItemControlState();
}
class _ChecklistItemControlState extends State<ChecklistItemControl> {
bool _isHover = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onHover: (_) => setState(() => _isHover = true),
onExit: (_) => setState(() => _isHover = false),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => context
.read<ChecklistCellBloc>()
.add(const ChecklistCellEvent.createNewTask("")),
child: Padding(
padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0),
child: SizedBox(
height: 12,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
child: _isHover
? FlowyTooltip(
message: LocaleKeys.grid_checklist_addNew.tr(),
child: Row(
children: [
const Flexible(child: Center(child: Divider())),
const HSpace(12.0),
FilledButton(
style: FilledButton.styleFrom(
minimumSize: const Size.square(12),
maximumSize: const Size.square(12),
padding: EdgeInsets.zero,
),
onPressed: () => context
.read<ChecklistCellBloc>()
.add(
const ChecklistCellEvent.createNewTask(""),
),
child: FlowySvg(
FlowySvgs.add_s,
color: Theme.of(context).colorScheme.onPrimary,
),
),
const HSpace(12.0),
const Flexible(child: Center(child: Divider())),
],
),
)
: const SizedBox.expand(),
),
),
),
),
);
}
}

View File

@ -0,0 +1,67 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
class DesktopRowDetailDateCellSkin extends IEditableDateCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
DateCellBloc bloc,
DateCellState state,
PopoverController popoverController,
) {
final text = state.dateStr.isEmpty
? LocaleKeys.grid_row_textPlaceholder.tr()
: state.dateStr;
final color = state.dateStr.isEmpty ? Theme.of(context).hintColor : null;
return AppFlowyPopover(
controller: popoverController,
triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(260, 620)),
margin: EdgeInsets.zero,
child: Container(
alignment: AlignmentDirectional.centerStart,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FlowyText.medium(
text,
color: color,
overflow: TextOverflow.ellipsis,
),
),
if (state.data?.reminderId.isNotEmpty ?? false) ...[
const HSpace(4),
FlowyTooltip(
message: LocaleKeys.grid_field_reminderOnDateTooltip.tr(),
child: const FlowySvg(FlowySvgs.clock_alarm_s),
),
],
],
),
),
popupBuilder: (BuildContext popoverContent) {
return DateCellEditor(
cellController: bloc.cellController,
onDismissed: () => cellContainerNotifier.isFocus = false,
);
},
onClose: () {
cellContainerNotifier.isFocus = false;
},
);
}
}

View File

@ -0,0 +1,40 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/number.dart';
class DesktopRowDetailNumberCellSkin extends IEditableNumberCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
NumberCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: LocaleKeys.grid_row_textPlaceholder.tr(),
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).hintColor,
),
isDense: true,
),
);
}
}

View File

@ -0,0 +1,74 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/select_option_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/select_option.dart';
class DesktopRowDetailSelectOptionCellSkin
extends IEditableSelectOptionCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
SelectOptionCellBloc bloc,
SelectOptionCellState state,
PopoverController popoverController,
) {
return AppFlowyPopover(
controller: popoverController,
constraints: BoxConstraints.loose(const Size.square(300)),
margin: EdgeInsets.zero,
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (BuildContext popoverContext) {
WidgetsBinding.instance.addPostFrameCallback((_) {
cellContainerNotifier.isFocus = true;
});
return SelectOptionCellEditor(
cellController: bloc.cellController,
);
},
onClose: () => cellContainerNotifier.isFocus = false,
child: Container(
alignment: AlignmentDirectional.centerStart,
padding: state.selectedOptions.isEmpty
? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0)
: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 5.0),
child: state.selectedOptions.isEmpty
? _buildPlaceholder(context)
: _buildOptions(context, state.selectedOptions),
),
);
}
Widget _buildPlaceholder(BuildContext context) {
return FlowyText(
LocaleKeys.grid_row_textPlaceholder.tr(),
color: Theme.of(context).hintColor,
);
}
Widget _buildOptions(BuildContext context, List<SelectOptionPB> options) {
return Wrap(
runSpacing: 4,
spacing: 4,
children: options.map(
(option) {
return SelectOptionTag(
option: option,
padding: const EdgeInsets.symmetric(
vertical: 1,
horizontal: 8,
),
);
},
).toList(),
);
}
}

View File

@ -0,0 +1,38 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/text.dart';
class DesktopRowDetailTextCellSkin extends IEditableTextCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TextCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: LocaleKeys.grid_row_textPlaceholder.tr(),
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).hintColor,
),
isDense: true,
),
);
}
}

View File

@ -0,0 +1,25 @@
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/widgets.dart';
import '../editable_cell_skeleton/timestamp.dart';
class DesktopRowDetailTimestampCellSkin extends IEditableTimestampCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TimestampCellBloc bloc,
TimestampCellState state,
) {
return Container(
alignment: AlignmentDirectional.centerStart,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6.0),
child: FlowyText.medium(
state.dateStr,
maxLines: null,
),
);
}
}

View File

@ -0,0 +1,57 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/url.dart';
class DesktopRowDetailURLSkin extends IEditableURLCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
URLCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
URLCellDataNotifier cellDataNotifier,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
autofocus: false,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: LocaleKeys.grid_row_textPlaceholder.tr(),
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).hintColor,
),
isDense: true,
),
);
}
@override
List<GridCellAccessoryBuilder> accessoryBuilder(
GridCellAccessoryBuildContext context,
URLCellDataNotifier cellDataNotifier,
) {
return [
accessoryFromType(
GridURLCellAccessoryType.visitURL,
cellDataNotifier,
),
];
}
}

View File

@ -0,0 +1,390 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'editable_cell_skeleton/checkbox.dart';
import 'editable_cell_skeleton/checklist.dart';
import 'editable_cell_skeleton/date.dart';
import 'editable_cell_skeleton/number.dart';
import 'editable_cell_skeleton/select_option.dart';
import 'editable_cell_skeleton/text.dart';
import 'editable_cell_skeleton/timestamp.dart';
import 'editable_cell_skeleton/url.dart';
import '../row/accessory/cell_accessory.dart';
import '../row/accessory/cell_shortcuts.dart';
import '../row/cells/cell_container.dart';
enum EditableCellStyle {
desktopGrid,
desktopRowDetail,
mobileGrid,
mobileRowDetail,
}
/// Build an editable cell widget
class EditableCellBuilder {
final DatabaseController databaseController;
EditableCellBuilder({
required this.databaseController,
});
EditableCellWidget buildStyled(
CellContext cellContext,
EditableCellStyle style,
) {
final fieldType = databaseController.fieldController
.getField(cellContext.fieldId)!
.fieldType;
final key = ValueKey(
"${databaseController.viewId}${cellContext.fieldId}${cellContext.rowId}",
);
return switch (fieldType) {
FieldType.Checkbox => EditableCheckboxCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableCheckboxCellSkin.fromStyle(style),
key: key,
),
FieldType.Checklist => EditableChecklistCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableChecklistCellSkin.fromStyle(style),
key: key,
),
FieldType.CreatedTime => EditableTimestampCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableTimestampCellSkin.fromStyle(style),
key: key,
fieldType: FieldType.CreatedTime,
),
FieldType.DateTime => EditableDateCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableDateCellSkin.fromStyle(style),
key: key,
),
FieldType.LastEditedTime => EditableTimestampCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableTimestampCellSkin.fromStyle(style),
key: key,
fieldType: FieldType.LastEditedTime,
),
FieldType.MultiSelect => EditableSelectOptionCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableSelectOptionCellSkin.fromStyle(style),
key: key,
fieldType: FieldType.MultiSelect,
),
FieldType.Number => EditableNumberCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableNumberCellSkin.fromStyle(style),
key: key,
),
FieldType.RichText => EditableTextCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableTextCellSkin.fromStyle(style),
key: key,
),
FieldType.SingleSelect => EditableSelectOptionCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableSelectOptionCellSkin.fromStyle(style),
key: key,
fieldType: FieldType.SingleSelect,
),
FieldType.URL => EditableURLCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableURLCellSkin.fromStyle(style),
key: key,
),
_ => throw UnimplementedError(),
};
}
EditableCellWidget buildCustom(
CellContext cellContext, {
required EditableCellSkinMap skinMap,
}) {
final cellController = makeCellController(databaseController, cellContext);
final key = ValueKey(
"${databaseController.viewId}${cellContext.fieldId}${cellContext.rowId}",
);
final fieldType = cellController.fieldType;
assert(skinMap.has(fieldType));
return switch (fieldType) {
FieldType.Checkbox => EditableCheckboxCell(
databaseController: databaseController,
cellContext: cellContext,
skin: skinMap.checkboxSkin!,
key: key,
),
FieldType.Checklist => EditableChecklistCell(
databaseController: databaseController,
cellContext: cellContext,
skin: skinMap.checklistSkin!,
key: key,
),
FieldType.CreatedTime => EditableTimestampCell(
databaseController: databaseController,
cellContext: cellContext,
skin: skinMap.timestampSkin!,
key: key,
fieldType: FieldType.CreatedTime,
),
FieldType.DateTime => EditableDateCell(
databaseController: databaseController,
cellContext: cellContext,
skin: skinMap.dateSkin!,
key: key,
),
FieldType.LastEditedTime => EditableTimestampCell(
databaseController: databaseController,
cellContext: cellContext,
skin: skinMap.timestampSkin!,
key: key,
fieldType: FieldType.LastEditedTime,
),
FieldType.MultiSelect => EditableSelectOptionCell(
databaseController: databaseController,
cellContext: cellContext,
skin: skinMap.selectOptionSkin!,
key: key,
fieldType: FieldType.MultiSelect,
),
FieldType.Number => EditableNumberCell(
databaseController: databaseController,
cellContext: cellContext,
skin: skinMap.numberSkin!,
key: key,
),
FieldType.RichText => EditableTextCell(
databaseController: databaseController,
cellContext: cellContext,
skin: skinMap.textSkin!,
key: key,
),
FieldType.SingleSelect => EditableSelectOptionCell(
databaseController: databaseController,
cellContext: cellContext,
skin: skinMap.selectOptionSkin!,
key: key,
fieldType: FieldType.SingleSelect,
),
FieldType.URL => EditableURLCell(
databaseController: databaseController,
cellContext: cellContext,
skin: skinMap.urlSkin!,
key: key,
),
_ => throw UnimplementedError(),
};
}
}
abstract class CellEditable {
RequestFocusListener get requestFocus;
CellContainerNotifier get cellContainerNotifier;
// ValueNotifier<bool> get onCellEditing;
}
typedef AccessoryBuilder = List<GridCellAccessoryBuilder> Function(
GridCellAccessoryBuildContext buildContext,
);
abstract class CellAccessory extends Widget {
const CellAccessory({super.key});
// The hover will show if the isHover's value is true
ValueNotifier<bool>? get onAccessoryHover;
AccessoryBuilder? get accessoryBuilder;
}
abstract class EditableCellWidget extends StatefulWidget
implements CellAccessory, CellEditable, CellShortcuts {
EditableCellWidget({super.key});
@override
final CellContainerNotifier cellContainerNotifier = CellContainerNotifier();
// When the cell is focused, we assume that the accessory also be hovered.
@override
ValueNotifier<bool> get onAccessoryHover => ValueNotifier(false);
// @override
// final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
@override
List<GridCellAccessoryBuilder> Function(
GridCellAccessoryBuildContext buildContext,
)? get accessoryBuilder => null;
@override
final RequestFocusListener requestFocus = RequestFocusListener();
@override
final Map<CellKeyboardKey, CellKeyboardAction> shortcutHandlers = {};
}
abstract class GridCellState<T extends EditableCellWidget> extends State<T> {
@override
void initState() {
super.initState();
widget.requestFocus.setListener(requestBeginFocus);
}
@override
void didUpdateWidget(covariant T oldWidget) {
if (oldWidget != this) {
widget.requestFocus.setListener(requestBeginFocus);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
widget.onAccessoryHover.dispose();
widget.requestFocus.removeAllListener();
widget.requestFocus.dispose();
super.dispose();
}
/// Subclass can override this method to request focus.
void requestBeginFocus();
String? onCopy() => null;
}
abstract class GridEditableTextCell<T extends EditableCellWidget>
extends GridCellState<T> {
SingleListenerFocusNode get focusNode;
@override
void initState() {
super.initState();
widget.shortcutHandlers[CellKeyboardKey.onEnter] =
() => focusNode.unfocus();
_listenOnFocusNodeChanged();
}
@override
void dispose() {
widget.shortcutHandlers.clear();
focusNode.removeAllListener();
focusNode.dispose();
super.dispose();
}
@override
void requestBeginFocus() {
if (!focusNode.hasFocus && focusNode.canRequestFocus) {
FocusScope.of(context).requestFocus(focusNode);
}
}
void _listenOnFocusNodeChanged() {
widget.cellContainerNotifier.isFocus = focusNode.hasFocus;
focusNode.setListener(() {
widget.cellContainerNotifier.isFocus = focusNode.hasFocus;
focusChanged();
});
}
Future<void> focusChanged() async {}
}
class RequestFocusListener extends ChangeNotifier {
VoidCallback? _listener;
void setListener(VoidCallback listener) {
if (_listener != null) {
removeListener(_listener!);
}
_listener = listener;
addListener(listener);
}
void removeAllListener() {
if (_listener != null) {
removeListener(_listener!);
_listener = null;
}
}
void notify() {
notifyListeners();
}
}
class SingleListenerFocusNode extends FocusNode {
VoidCallback? _listener;
void setListener(VoidCallback listener) {
if (_listener != null) {
removeListener(_listener!);
}
_listener = listener;
super.addListener(listener);
}
void removeAllListener() {
if (_listener != null) {
removeListener(_listener!);
}
}
}
class EditableCellSkinMap {
EditableCellSkinMap({
this.checkboxSkin,
this.checklistSkin,
this.timestampSkin,
this.dateSkin,
this.selectOptionSkin,
this.numberSkin,
this.textSkin,
this.urlSkin,
});
final IEditableCheckboxCellSkin? checkboxSkin;
final IEditableChecklistCellSkin? checklistSkin;
final IEditableTimestampCellSkin? timestampSkin;
final IEditableDateCellSkin? dateSkin;
final IEditableSelectOptionCellSkin? selectOptionSkin;
final IEditableNumberCellSkin? numberSkin;
final IEditableTextCellSkin? textSkin;
final IEditableURLCellSkin? urlSkin;
bool has(FieldType fieldType) {
return switch (fieldType) {
FieldType.Checkbox => checkboxSkin != null,
FieldType.Checklist => checklistSkin != null,
FieldType.CreatedTime ||
FieldType.LastEditedTime =>
throw timestampSkin != null,
FieldType.DateTime => dateSkin != null,
FieldType.MultiSelect ||
FieldType.SingleSelect =>
selectOptionSkin != null,
FieldType.Number => numberSkin != null,
FieldType.RichText => textSkin != null,
FieldType.URL => urlSkin != null,
_ => throw UnimplementedError(),
};
}
}

View File

@ -0,0 +1,95 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../desktop_grid/desktop_grid_checkbox_cell.dart';
import '../desktop_row_detail/desktop_row_detail_checkbox_cell.dart';
import '../mobile_grid/mobile_grid_checkbox_cell.dart';
import '../mobile_row_detail/mobile_row_detail_checkbox_cell.dart';
abstract class IEditableCheckboxCellSkin {
const IEditableCheckboxCellSkin();
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
CheckboxCellBloc bloc,
CheckboxCellState state,
);
factory IEditableCheckboxCellSkin.fromStyle(EditableCellStyle style) {
return switch (style) {
EditableCellStyle.desktopGrid => DesktopGridCheckboxCellSkin(),
EditableCellStyle.desktopRowDetail => DesktopRowDetailCheckboxCellSkin(),
EditableCellStyle.mobileGrid => MobileGridCheckboxCellSkin(),
EditableCellStyle.mobileRowDetail => MobileRowDetailCheckboxCellSkin(),
};
}
}
class EditableCheckboxCell extends EditableCellWidget {
final DatabaseController databaseController;
final CellContext cellContext;
final IEditableCheckboxCellSkin skin;
EditableCheckboxCell({
super.key,
required this.databaseController,
required this.cellContext,
required this.skin,
});
@override
GridCellState<EditableCheckboxCell> createState() => _CheckboxCellState();
}
class _CheckboxCellState extends GridCellState<EditableCheckboxCell> {
late final cellBloc = CheckboxCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const CheckboxCellEvent.initial());
@override
void dispose() {
cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
builder: (context, state) {
return widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
state,
);
},
),
);
}
@override
void requestBeginFocus() {
cellBloc.add(const CheckboxCellEvent.select());
}
@override
String? onCopy() {
if (cellBloc.state.isSelected) {
return "Yes";
} else {
return "No";
}
}
}

View File

@ -0,0 +1,93 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../desktop_grid/desktop_grid_checklist_cell.dart';
import '../desktop_row_detail/desktop_row_detail_checklist_cell.dart';
import '../mobile_grid/mobile_grid_checklist_cell.dart';
import '../mobile_row_detail/mobile_row_detail_checklist_cell.dart';
abstract class IEditableChecklistCellSkin {
const IEditableChecklistCellSkin();
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
ChecklistCellBloc bloc,
ChecklistCellState state,
PopoverController popoverController,
);
factory IEditableChecklistCellSkin.fromStyle(EditableCellStyle style) {
return switch (style) {
EditableCellStyle.desktopGrid => DesktopGridChecklistCellSkin(),
EditableCellStyle.desktopRowDetail => DesktopRowDetailChecklistCellSkin(),
EditableCellStyle.mobileGrid => MobileGridChecklistCellSkin(),
EditableCellStyle.mobileRowDetail => MobileRowDetailChecklistCellSkin(),
};
}
}
class EditableChecklistCell extends EditableCellWidget {
final DatabaseController databaseController;
final CellContext cellContext;
final IEditableChecklistCellSkin skin;
EditableChecklistCell({
super.key,
required this.databaseController,
required this.cellContext,
required this.skin,
});
@override
GridCellState<EditableChecklistCell> createState() =>
GridChecklistCellState();
}
class GridChecklistCellState extends GridCellState<EditableChecklistCell> {
final PopoverController _popover = PopoverController();
late final cellBloc = ChecklistCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const ChecklistCellEvent.initial());
@override
void dispose() {
cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
return widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
state,
_popover,
);
},
),
);
}
@override
void requestBeginFocus() {
if (widget.skin is DesktopGridChecklistCellSkin) {
_popover.show();
}
}
}

View File

@ -0,0 +1,94 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../desktop_grid/desktop_grid_date_cell.dart';
import '../desktop_row_detail/desktop_row_detail_date_cell.dart';
import '../mobile_grid/mobile_grid_date_cell.dart';
import '../mobile_row_detail/mobile_row_detail_date_cell.dart';
abstract class IEditableDateCellSkin {
const IEditableDateCellSkin();
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
DateCellBloc bloc,
DateCellState state,
PopoverController popoverController,
);
factory IEditableDateCellSkin.fromStyle(EditableCellStyle style) {
return switch (style) {
EditableCellStyle.desktopGrid => DesktopGridDateCellSkin(),
EditableCellStyle.desktopRowDetail => DesktopRowDetailDateCellSkin(),
EditableCellStyle.mobileGrid => MobileGridDateCellSkin(),
EditableCellStyle.mobileRowDetail => MobileRowDetailDateCellSkin(),
};
}
}
class EditableDateCell extends EditableCellWidget {
final DatabaseController databaseController;
final CellContext cellContext;
final IEditableDateCellSkin skin;
EditableDateCell({
super.key,
required this.databaseController,
required this.cellContext,
required this.skin,
});
@override
GridCellState<EditableDateCell> createState() => _DateCellState();
}
class _DateCellState extends GridCellState<EditableDateCell> {
final PopoverController _popover = PopoverController();
late final cellBloc = DateCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const DateCellEvent.initial());
@override
void dispose() {
cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocBuilder<DateCellBloc, DateCellState>(
builder: (context, state) {
return widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
state,
_popover,
);
},
),
);
}
@override
void requestBeginFocus() {
_popover.show();
widget.cellContainerNotifier.isFocus = true;
}
@override
String? onCopy() => cellBloc.state.dateStr;
}

View File

@ -0,0 +1,116 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../desktop_grid/desktop_grid_number_cell.dart';
import '../desktop_row_detail/desktop_row_detail_number_cell.dart';
import '../mobile_grid/mobile_grid_number_cell.dart';
import '../mobile_row_detail/mobile_row_detail_number_cell.dart';
abstract class IEditableNumberCellSkin {
const IEditableNumberCellSkin();
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
NumberCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
);
factory IEditableNumberCellSkin.fromStyle(EditableCellStyle style) {
return switch (style) {
EditableCellStyle.desktopGrid => DesktopGridNumberCellSkin(),
EditableCellStyle.desktopRowDetail => DesktopRowDetailNumberCellSkin(),
EditableCellStyle.mobileGrid => MobileGridNumberCellSkin(),
EditableCellStyle.mobileRowDetail => MobileRowDetailNumberCellSkin(),
};
}
}
class EditableNumberCell extends EditableCellWidget {
final DatabaseController databaseController;
final CellContext cellContext;
final IEditableNumberCellSkin skin;
EditableNumberCell({
super.key,
required this.databaseController,
required this.cellContext,
required this.skin,
});
@override
GridEditableTextCell<EditableNumberCell> createState() => _NumberCellState();
}
class _NumberCellState extends GridEditableTextCell<EditableNumberCell> {
late final TextEditingController _textEditingController;
late final cellBloc = NumberCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const NumberCellEvent.initial());
@override
void initState() {
super.initState();
_textEditingController =
TextEditingController(text: cellBloc.state.content);
}
@override
Future<void> dispose() async {
_textEditingController.dispose();
cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocListener<NumberCellBloc, NumberCellState>(
listener: (context, state) =>
_textEditingController.text = state.content,
child: widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
focusNode,
_textEditingController,
),
),
);
}
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void requestBeginFocus() {
focusNode.requestFocus();
}
@override
String? onCopy() => cellBloc.state.content;
@override
Future<void> focusChanged() async {
if (mounted &&
!cellBloc.isClosed &&
cellBloc.state.content != _textEditingController.text.trim()) {
cellBloc
.add(NumberCellEvent.updateCell(_textEditingController.text.trim()));
}
return super.focusChanged();
}
}

View File

@ -0,0 +1,96 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../desktop_grid/desktop_grid_select_option_cell.dart';
import '../desktop_row_detail/desktop_row_detail_select_option_cell.dart';
import '../mobile_grid/mobile_grid_select_option_cell.dart';
import '../mobile_row_detail/mobile_row_detail_select_cell_option.dart';
abstract class IEditableSelectOptionCellSkin {
const IEditableSelectOptionCellSkin();
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
SelectOptionCellBloc bloc,
SelectOptionCellState state,
PopoverController popoverController,
);
factory IEditableSelectOptionCellSkin.fromStyle(EditableCellStyle style) {
return switch (style) {
EditableCellStyle.desktopGrid => DesktopGridSelectOptionCellSkin(),
EditableCellStyle.desktopRowDetail =>
DesktopRowDetailSelectOptionCellSkin(),
EditableCellStyle.mobileGrid => MobileGridSelectOptionCellSkin(),
EditableCellStyle.mobileRowDetail =>
MobileRowDetailSelectOptionCellSkin(),
};
}
}
class EditableSelectOptionCell extends EditableCellWidget {
final DatabaseController databaseController;
final CellContext cellContext;
final IEditableSelectOptionCellSkin skin;
final FieldType fieldType;
EditableSelectOptionCell({
super.key,
required this.databaseController,
required this.cellContext,
required this.skin,
required this.fieldType,
});
@override
GridCellState<EditableSelectOptionCell> createState() =>
_SelectOptionCellState();
}
class _SelectOptionCellState extends GridCellState<EditableSelectOptionCell> {
final PopoverController _popover = PopoverController();
late final cellBloc = SelectOptionCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const SelectOptionCellEvent.initial());
@override
void dispose() {
cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
builder: (context, state) {
return widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
state,
_popover,
);
},
),
);
}
@override
void requestBeginFocus() => _popover.show();
}

View File

@ -0,0 +1,115 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../desktop_grid/desktop_grid_text_cell.dart';
import '../desktop_row_detail/desktop_row_detail_text_cell.dart';
import '../mobile_grid/mobile_grid_text_cell.dart';
import '../mobile_row_detail/mobile_row_detail_text_cell.dart';
abstract class IEditableTextCellSkin {
const IEditableTextCellSkin();
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TextCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
);
factory IEditableTextCellSkin.fromStyle(EditableCellStyle style) {
return switch (style) {
EditableCellStyle.desktopGrid => DesktopGridTextCellSkin(),
EditableCellStyle.desktopRowDetail => DesktopRowDetailTextCellSkin(),
EditableCellStyle.mobileGrid => MobileGridTextCellSkin(),
EditableCellStyle.mobileRowDetail => MobileRowDetailTextCellSkin(),
};
}
}
class EditableTextCell extends EditableCellWidget {
final DatabaseController databaseController;
final CellContext cellContext;
final IEditableTextCellSkin skin;
EditableTextCell({
super.key,
required this.databaseController,
required this.cellContext,
required this.skin,
});
@override
GridEditableTextCell<EditableTextCell> createState() => _TextCellState();
}
class _TextCellState extends GridEditableTextCell<EditableTextCell> {
late final TextEditingController _textEditingController;
late final cellBloc = TextCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const TextCellEvent.initial());
@override
void initState() {
super.initState();
_textEditingController =
TextEditingController(text: cellBloc.state.content);
}
@override
Future<void> dispose() async {
_textEditingController.dispose();
cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocListener<TextCellBloc, TextCellState>(
listener: (context, state) {
_textEditingController.text = state.content;
},
child: widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
focusNode,
_textEditingController,
),
),
);
}
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void requestBeginFocus() {
focusNode.requestFocus();
}
@override
String? onCopy() => cellBloc.state.content;
@override
Future<void> focusChanged() {
if (mounted &&
!cellBloc.isClosed &&
cellBloc.state.content != _textEditingController.text.trim()) {
cellBloc
.add(TextCellEvent.updateText(_textEditingController.text.trim()));
}
return super.focusChanged();
}
}

View File

@ -0,0 +1,93 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../desktop_grid/desktop_grid_timestamp_cell.dart';
import '../desktop_row_detail/desktop_row_detail_timestamp_cell.dart';
import '../mobile_grid/mobile_grid_timestamp_cell.dart';
import '../mobile_row_detail/mobile_row_detail_timestamp_cell.dart';
abstract class IEditableTimestampCellSkin {
const IEditableTimestampCellSkin();
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TimestampCellBloc bloc,
TimestampCellState state,
);
factory IEditableTimestampCellSkin.fromStyle(EditableCellStyle style) {
return switch (style) {
EditableCellStyle.desktopGrid => DesktopGridTimestampCellSkin(),
EditableCellStyle.desktopRowDetail => DesktopRowDetailTimestampCellSkin(),
EditableCellStyle.mobileGrid => MobileGridTimestampCellSkin(),
EditableCellStyle.mobileRowDetail => MobileRowDetailTimestampCellSkin(),
};
}
}
class EditableTimestampCell extends EditableCellWidget {
final DatabaseController databaseController;
final CellContext cellContext;
final IEditableTimestampCellSkin skin;
final FieldType fieldType;
EditableTimestampCell({
super.key,
required this.databaseController,
required this.cellContext,
required this.skin,
required this.fieldType,
});
@override
GridCellState<EditableTimestampCell> createState() => _TimestampCellState();
}
class _TimestampCellState extends GridCellState<EditableTimestampCell> {
late final cellBloc = TimestampCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const TimestampCellEvent.initial());
@override
void dispose() {
cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
builder: (context, state) {
return widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
state,
);
},
),
);
}
@override
void requestBeginFocus() {
widget.cellContainerNotifier.isFocus = true;
}
@override
String? onCopy() => cellBloc.state.dateStr;
}

View File

@ -0,0 +1,131 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../desktop_grid/desktop_grid_url_cell.dart';
import '../desktop_row_detail/desktop_row_detail_url_cell.dart';
import '../mobile_grid/mobile_grid_url_cell.dart';
import '../mobile_row_detail/mobile_row_detail_url_cell.dart';
abstract class IEditableURLCellSkin {
const IEditableURLCellSkin();
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
URLCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
URLCellDataNotifier cellDataNotifier,
);
List<GridCellAccessoryBuilder> accessoryBuilder(
GridCellAccessoryBuildContext context,
URLCellDataNotifier cellDataNotifier,
);
factory IEditableURLCellSkin.fromStyle(EditableCellStyle style) {
return switch (style) {
EditableCellStyle.desktopGrid => DesktopGridURLSkin(),
EditableCellStyle.desktopRowDetail => DesktopRowDetailURLSkin(),
EditableCellStyle.mobileGrid => MobileGridURLCellSkin(),
EditableCellStyle.mobileRowDetail => MobileRowDetailURLCellSkin(),
};
}
}
typedef URLCellDataNotifier = CellDataNotifier<String>;
class EditableURLCell extends EditableCellWidget {
final DatabaseController databaseController;
final CellContext cellContext;
final IEditableURLCellSkin skin;
final URLCellDataNotifier _cellDataNotifier;
EditableURLCell({
super.key,
required this.databaseController,
required this.cellContext,
required this.skin,
}) : _cellDataNotifier = CellDataNotifier(value: '');
@override
List<GridCellAccessoryBuilder> Function(
GridCellAccessoryBuildContext buildContext,
) get accessoryBuilder => (context) {
return skin.accessoryBuilder(context, _cellDataNotifier);
};
@override
GridCellState<EditableURLCell> createState() => _GridURLCellState();
}
class _GridURLCellState extends GridEditableTextCell<EditableURLCell> {
late final TextEditingController _textEditingController;
late final cellBloc = URLCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const URLCellEvent.initial());
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
super.initState();
_textEditingController =
TextEditingController(text: cellBloc.state.content);
}
@override
void dispose() {
_textEditingController.dispose();
cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocListener<URLCellBloc, URLCellState>(
listenWhen: (previous, current) => previous.content != current.content,
listener: (context, state) {
_textEditingController.text = state.content;
widget._cellDataNotifier.value = state.content;
},
child: widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
focusNode,
_textEditingController,
widget._cellDataNotifier,
),
),
);
}
@override
Future<void> focusChanged() async {
if (mounted &&
!cellBloc.isClosed &&
cellBloc.state.content != _textEditingController.text.trim()) {
cellBloc.add(URLCellEvent.updateURL(_textEditingController.text.trim()));
}
return super.focusChanged();
}
@override
String? onCopy() => cellBloc.state.content;
}

View File

@ -0,0 +1,28 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/checkbox.dart';
class MobileGridCheckboxCellSkin extends IEditableCheckboxCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
CheckboxCellBloc bloc,
CheckboxCellState state,
) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
child: FlowySvg(
state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
size: const Size.square(24),
),
),
);
}
}

View File

@ -0,0 +1,53 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/mobile_checklist_cell_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_skeleton/checklist.dart';
class MobileGridChecklistCellSkin extends IEditableChecklistCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
ChecklistCellBloc bloc,
ChecklistCellState state,
PopoverController popoverController,
) {
return FlowyButton(
radius: BorderRadius.zero,
hoverColor: Colors.transparent,
text: Container(
alignment: Alignment.centerLeft,
padding: GridSize.cellContentInsets,
child: state.tasks.isEmpty
? const SizedBox.shrink()
: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
textStyle: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontSize: 15),
),
),
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
backgroundColor: Theme.of(context).colorScheme.background,
builder: (context) {
return BlocProvider.value(
value: bloc,
child: const MobileChecklistCellEditScreen(),
);
},
),
);
}
}

View File

@ -0,0 +1,49 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_cell_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class MobileGridDateCellSkin extends IEditableDateCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
DateCellBloc bloc,
DateCellState state,
PopoverController popoverController,
) {
return FlowyButton(
radius: BorderRadius.zero,
hoverColor: Colors.transparent,
margin: EdgeInsets.zero,
text: Align(
alignment: AlignmentDirectional.centerStart,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: FlowyText(
state.dateStr,
fontSize: 15,
),
),
),
onTap: () {
showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
builder: (context) {
return MobileDateCellEditScreen(
controller: bloc.cellController,
showAsFullScreen: false,
);
},
);
},
);
}
}

View File

@ -0,0 +1,30 @@
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/number.dart';
class MobileGridNumberCellSkin extends IEditableNumberCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
NumberCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 15),
maxLines: 1,
decoration: const InputDecoration(
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 12),
isCollapsed: true,
),
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
);
}
}

View File

@ -0,0 +1,66 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/mobile_select_option_editor.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/select_option.dart';
class MobileGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
SelectOptionCellBloc bloc,
SelectOptionCellState state,
PopoverController popoverController,
) {
return FlowyButton(
hoverColor: Colors.transparent,
radius: BorderRadius.zero,
margin: EdgeInsets.zero,
text: Align(
alignment: AlignmentDirectional.centerStart,
child: state.selectedOptions.isEmpty
? const SizedBox.shrink()
: _buildOptions(context, state.selectedOptions),
),
onTap: () {
showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
builder: (context) {
return MobileSelectOptionEditor(
cellController: bloc.cellController,
);
},
);
},
);
}
Widget _buildOptions(BuildContext context, List<SelectOptionPB> options) {
final children = options
.mapIndexed(
(index, option) => SelectOptionTag(
option: option,
fontSize: 14,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
),
)
.toList();
return ListView.separated(
scrollDirection: Axis.horizontal,
separatorBuilder: (context, index) => const HSpace(8),
itemCount: children.length,
itemBuilder: (context, index) => children[index],
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
);
}
}

View File

@ -0,0 +1,50 @@
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_skeleton/text.dart';
class MobileGridTextCellSkin extends IEditableTextCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TextCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return Row(
children: [
BlocBuilder<TextCellBloc, TextCellState>(
buildWhen: (p, c) => p.emoji != c.emoji,
builder: (context, state) => Center(
child: FlowyText(
state.emoji,
fontSize: 16,
),
),
),
const HSpace(6),
Expanded(
child: TextField(
controller: textEditingController,
focusNode: focusNode,
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 15),
maxLines: 1,
decoration: const InputDecoration(
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding:
EdgeInsets.symmetric(horizontal: 14, vertical: 12),
isCollapsed: true,
),
onTapOutside: (event) => focusNode.unfocus(),
),
),
],
);
}
}

View File

@ -0,0 +1,28 @@
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/timestamp.dart';
class MobileGridTimestampCellSkin extends IEditableTimestampCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TimestampCellBloc bloc,
TimestampCellState state,
) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
child: FlowyText(
state.dateStr,
fontSize: 15,
maxLines: 1,
),
),
);
}
}

View File

@ -0,0 +1,101 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../editable_cell_skeleton/url.dart';
class MobileGridURLCellSkin extends IEditableURLCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
URLCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
URLCellDataNotifier cellDataNotifier,
) {
return BlocSelector<URLCellBloc, URLCellState, String>(
selector: (state) => state.content,
builder: (context, content) {
if (content.isEmpty) {
return TextField(
focusNode: focusNode,
keyboardType: TextInputType.url,
maxLines: 1,
decoration: const InputDecoration(
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
isCollapsed: true,
),
onTapOutside: (event) =>
FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (value) => bloc.add(URLCellEvent.updateURL(value)),
);
}
return GestureDetector(
onTap: () {
if (content.isEmpty) {
return;
}
final shouldAddScheme = !['http', 'https']
.any((pattern) => content.startsWith(pattern));
final url = shouldAddScheme ? 'http://$content' : content;
canLaunchUrlString(url).then((value) => launchUrlString(url));
},
onLongPress: () => showMobileBottomSheet(
context,
title: LocaleKeys.board_mobile_editURL.tr(),
showHeader: true,
showCloseButton: true,
builder: (_) {
final controller = TextEditingController(text: content);
return TextField(
controller: controller,
autofocus: true,
keyboardType: TextInputType.url,
onEditingComplete: () {
bloc.add(URLCellEvent.updateURL(controller.text));
context.pop();
},
);
},
),
child: Container(
alignment: AlignmentDirectional.centerStart,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Text(
content,
maxLines: 1,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
decoration: TextDecoration.underline,
color: Theme.of(context).colorScheme.primary,
),
),
),
),
);
},
);
}
@override
List<GridCellAccessoryBuilder<State<StatefulWidget>>> accessoryBuilder(
GridCellAccessoryBuildContext context,
URLCellDataNotifier cellDataNotifier,
) =>
const [];
}

View File

@ -0,0 +1,41 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/checkbox.dart';
class MobileRowDetailCheckboxCellSkin extends IEditableCheckboxCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
CheckboxCellBloc bloc,
CheckboxCellState state,
) {
return InkWell(
onTap: () => bloc.add(const CheckboxCellEvent.select()),
borderRadius: const BorderRadius.all(Radius.circular(14)),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
alignment: AlignmentDirectional.centerStart,
child: FlowySvg(
state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
color: Theme.of(context).colorScheme.onBackground,
blendMode: BlendMode.dst,
size: const Size.square(24),
),
),
);
}
}

View File

@ -0,0 +1,67 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/mobile_checklist_cell_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_skeleton/checklist.dart';
class MobileRowDetailChecklistCellSkin extends IEditableChecklistCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
ChecklistCellBloc bloc,
ChecklistCellState state,
PopoverController popoverController,
) {
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14)),
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
backgroundColor: Theme.of(context).colorScheme.background,
builder: (context) {
return BlocProvider.value(
value: bloc,
child: const MobileChecklistCellEditScreen(),
);
},
),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
alignment: AlignmentDirectional.centerStart,
child: state.tasks.isEmpty
? FlowyText(
LocaleKeys.grid_row_textPlaceholder.tr(),
fontSize: 15,
color: Theme.of(context).hintColor,
)
: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 15,
color: Theme.of(context).hintColor,
),
),
),
);
}
}

View File

@ -0,0 +1,59 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_cell_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class MobileRowDetailDateCellSkin extends IEditableDateCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
DateCellBloc bloc,
DateCellState state,
PopoverController popoverController,
) {
final text = state.dateStr.isEmpty
? LocaleKeys.grid_row_textPlaceholder.tr()
: state.dateStr;
final color = state.dateStr.isEmpty ? Theme.of(context).hintColor : null;
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14)),
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
builder: (context) {
return MobileDateCellEditScreen(
controller: bloc.cellController,
showAsFullScreen: false,
);
},
),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
child: FlowyText.regular(
text,
fontSize: 16,
color: color,
maxLines: null,
),
),
);
}
}

View File

@ -0,0 +1,50 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/number.dart';
class MobileRowDetailNumberCellSkin extends IEditableNumberCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
NumberCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return TextField(
controller: textEditingController,
keyboardType: const TextInputType.numberWithOptions(
signed: true,
decimal: true,
),
focusNode: focusNode,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 16),
decoration: InputDecoration(
enabledBorder:
_getInputBorder(color: Theme.of(context).colorScheme.outline),
focusedBorder:
_getInputBorder(color: Theme.of(context).colorScheme.primary),
hintText: LocaleKeys.grid_row_textPlaceholder.tr(),
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
isCollapsed: true,
isDense: true,
constraints: const BoxConstraints(),
),
// close keyboard when tapping outside of the text field
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
);
}
InputBorder _getInputBorder({Color? color}) {
return OutlineInputBorder(
borderSide: BorderSide(color: color!),
borderRadius: const BorderRadius.all(Radius.circular(14)),
gapPadding: 0,
);
}
}

View File

@ -0,0 +1,107 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/mobile_select_option_editor.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/select_option.dart';
class MobileRowDetailSelectOptionCellSkin
extends IEditableSelectOptionCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
SelectOptionCellBloc bloc,
SelectOptionCellState state,
PopoverController popoverController,
) {
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14)),
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
builder: (context) {
return MobileSelectOptionEditor(
cellController: bloc.cellController,
);
},
),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: state.selectedOptions.isEmpty ? 13 : 10,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
child: Row(
children: [
Expanded(
child: state.selectedOptions.isEmpty
? _buildPlaceholder(context)
: _buildOptions(context, state.selectedOptions),
),
const HSpace(6),
RotatedBox(
quarterTurns: 3,
child: Icon(
Icons.chevron_left,
color: Theme.of(context).hintColor,
),
),
const HSpace(2),
],
),
),
);
}
Widget _buildPlaceholder(BuildContext context) {
return Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(vertical: 1),
child: FlowyText(
LocaleKeys.grid_row_textPlaceholder.tr(),
color: Theme.of(context).hintColor,
),
);
}
Widget _buildOptions(BuildContext context, List<SelectOptionPB> options) {
final children = options.mapIndexed(
(index, option) {
return Padding(
padding: EdgeInsets.only(left: index == 0 ? 0 : 4),
child: SelectOptionTag(
option: option,
fontSize: 14,
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 5),
),
);
},
).toList();
return Align(
alignment: AlignmentDirectional.centerStart,
child: Wrap(
runSpacing: 4,
children: children,
),
);
}
}

View File

@ -0,0 +1,49 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/text.dart';
class MobileRowDetailTextCellSkin extends IEditableTextCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TextCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
maxLines: null,
decoration: InputDecoration(
enabledBorder:
_getInputBorder(color: Theme.of(context).colorScheme.outline),
focusedBorder:
_getInputBorder(color: Theme.of(context).colorScheme.primary),
hintText: LocaleKeys.grid_row_textPlaceholder.tr(),
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
isCollapsed: true,
isDense: true,
constraints: const BoxConstraints(minHeight: 48),
hintStyle: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Theme.of(context).hintColor),
),
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
);
}
InputBorder _getInputBorder({Color? color}) {
return OutlineInputBorder(
borderSide: BorderSide(color: color!),
borderRadius: const BorderRadius.all(Radius.circular(14)),
gapPadding: 0,
);
}
}

View File

@ -0,0 +1,40 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/timestamp.dart';
class MobileRowDetailTimestampCellSkin extends IEditableTimestampCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TimestampCellBloc bloc,
TimestampCellState state,
) {
return Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
child: FlowyText.medium(
state.dateStr.isEmpty
? LocaleKeys.grid_row_textPlaceholder.tr()
: state.dateStr,
fontSize: 16,
color: state.dateStr.isEmpty ? Theme.of(context).hintColor : null,
maxLines: null,
),
);
}
}

View File

@ -0,0 +1,100 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../editable_cell_skeleton/url.dart';
class MobileRowDetailURLCellSkin extends IEditableURLCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
URLCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
URLCellDataNotifier cellDataNotifier,
) {
return BlocSelector<URLCellBloc, URLCellState, String>(
selector: (state) => state.content,
builder: (context, content) {
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14)),
onTap: () {
if (content.isEmpty) {
_showURLEditor(context, bloc, content);
return;
}
final shouldAddScheme = !['http', 'https']
.any((pattern) => content.startsWith(pattern));
final url = shouldAddScheme ? 'http://$content' : content;
canLaunchUrlString(url).then((value) => launchUrlString(url));
},
onLongPress: () => _showURLEditor(context, bloc, content),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
child: Text(
content.isEmpty
? LocaleKeys.grid_row_textPlaceholder.tr()
: content,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16,
decoration:
content.isEmpty ? null : TextDecoration.underline,
color: content.isEmpty
? Theme.of(context).hintColor
: Theme.of(context).colorScheme.primary,
),
),
),
),
);
},
);
}
@override
List<GridCellAccessoryBuilder<State<StatefulWidget>>> accessoryBuilder(
GridCellAccessoryBuildContext context,
URLCellDataNotifier cellDataNotifier,
) =>
const [];
void _showURLEditor(BuildContext context, URLCellBloc bloc, String content) {
showMobileBottomSheet(
context,
title: LocaleKeys.board_mobile_editURL.tr(),
showHeader: true,
showCloseButton: true,
builder: (_) {
final controller = TextEditingController(text: content);
return TextField(
controller: controller,
autofocus: true,
keyboardType: TextInputType.url,
onEditingComplete: () {
bloc.add(URLCellEvent.updateURL(controller.text));
context.pop();
},
);
},
);
}
}

View File

@ -10,7 +10,7 @@ import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:styled_widget/styled_widget.dart';
import '../cell_builder.dart';
import '../../cell/editable_cell_builder.dart';
class GridCellAccessoryBuildContext {
final BuildContext anchorContext;
@ -54,11 +54,11 @@ abstract mixin class GridCellAccessoryState {
class PrimaryCellAccessory extends StatefulWidget {
const PrimaryCellAccessory({
super.key,
required this.onTapCallback,
required this.onTap,
required this.isCellEditing,
});
final VoidCallback onTapCallback;
final VoidCallback onTap;
final bool isCellEditing;
@override
@ -86,7 +86,7 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
}
@override
void onTap() => widget.onTapCallback();
void onTap() => widget.onTap();
@override
bool enable() => !widget.isCellEditing;

View File

@ -1,444 +0,0 @@
import 'package:appflowy/mobile/presentation/database/card/card_detail/cells/checkbox_cell.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/cells/number_cell.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/cells/text_cell.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/cells/url_cell.dart';
import 'package:appflowy/mobile/presentation/database/card/row/cells/cells.dart';
import 'package:appflowy/mobile/presentation/database/card/row/cells/mobile_checklist_cell.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import '../../application/cell/cell_service.dart';
import 'accessory/cell_accessory.dart';
import 'accessory/cell_shortcuts.dart';
import 'cells/cell_container.dart';
import 'cells/checkbox_cell/checkbox_cell.dart';
import 'cells/checklist_cell/checklist_cell.dart';
import 'cells/date_cell/date_cell.dart';
import 'cells/number_cell/number_cell.dart';
import 'cells/select_option_cell/select_option_cell.dart';
import 'cells/text_cell/text_cell.dart';
import 'cells/timestamp_cell/timestamp_cell.dart';
import 'cells/url_cell/url_cell.dart';
/// Build the cell widget in Grid style.
class GridCellBuilder {
final CellMemCache cellCache;
GridCellBuilder({
required this.cellCache,
});
GridCellWidget build(
DatabaseCellContext cellContext, {
GridCellStyle? style,
}) {
final cellControllerBuilder = CellControllerBuilder(
cellContext: cellContext,
cellCache: cellCache,
);
final key = cellContext.key();
if (PlatformExtension.isMobile) {
return _getMobileCardCellWidget(
key,
cellContext,
cellControllerBuilder,
style,
);
}
return _getDesktopGridCellWidget(
key,
cellContext,
cellControllerBuilder,
style,
);
}
GridCellWidget _getDesktopGridCellWidget(
ValueKey key,
DatabaseCellContext cellContext,
CellControllerBuilder cellControllerBuilder,
GridCellStyle? style,
) {
switch (cellContext.fieldType) {
case FieldType.Checkbox:
return GridCheckboxCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.DateTime:
return GridDateCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
style: style,
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return GridTimestampCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
style: style,
fieldType: cellContext.fieldType,
);
case FieldType.SingleSelect:
return GridSingleSelectCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.MultiSelect:
return GridMultiSelectCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.Checklist:
return GridChecklistCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.Number:
return GridNumberCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.RichText:
return GridTextCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.URL:
return GridURLCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
}
throw UnimplementedError;
}
// editable cell/(card's propery value) widget
GridCellWidget _getMobileCardCellWidget(
ValueKey key,
DatabaseCellContext cellContext,
CellControllerBuilder cellControllerBuilder,
GridCellStyle? style,
) {
switch (cellContext.fieldType) {
case FieldType.RichText:
style as GridTextCellStyle?;
return MobileTextCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
);
case FieldType.Number:
style as GridNumberCellStyle?;
return MobileNumberCell(
cellControllerBuilder: cellControllerBuilder,
hintText: style?.placeholder,
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return MobileTimestampCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.Checkbox:
return MobileCheckboxCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.DateTime:
style as DateCellStyle?;
return GridDateCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
);
case FieldType.URL:
style as GridURLCellStyle?;
return MobileURLCell(
cellControllerBuilder: cellControllerBuilder,
hintText: style?.placeholder,
key: key,
);
case FieldType.SingleSelect:
return GridSingleSelectCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.MultiSelect:
return GridMultiSelectCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.Checklist:
return MobileChecklistCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
}
throw UnimplementedError;
}
}
class MobileRowDetailPageCellBuilder {
final CellMemCache cellCache;
MobileRowDetailPageCellBuilder({
required this.cellCache,
});
GridCellWidget build(
DatabaseCellContext cellContext, {
GridCellStyle? style,
}) {
final cellControllerBuilder = CellControllerBuilder(
cellContext: cellContext,
cellCache: cellCache,
);
final key = cellContext.key();
switch (cellContext.fieldType) {
case FieldType.RichText:
style as GridTextCellStyle?;
return RowDetailTextCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
);
case FieldType.Number:
style as GridNumberCellStyle?;
return RowDetailNumberCell(
cellControllerBuilder: cellControllerBuilder,
hintText: style?.placeholder,
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return GridTimestampCell(
cellControllerBuilder: cellControllerBuilder,
fieldType: cellContext.fieldType,
style: style,
key: key,
);
case FieldType.Checkbox:
return RowDetailCheckboxCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.DateTime:
style as DateCellStyle?;
return GridDateCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
);
case FieldType.URL:
style as GridURLCellStyle?;
return RowDetailURLCell(
cellControllerBuilder: cellControllerBuilder,
hintText: style?.placeholder,
key: key,
);
case FieldType.SingleSelect:
return GridSingleSelectCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.MultiSelect:
return GridMultiSelectCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.Checklist:
return MobileChecklistCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
}
throw UnimplementedError;
}
}
class BlankCell extends StatelessWidget {
const BlankCell({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox.shrink();
}
}
abstract class CellEditable {
RequestFocusListener get requestFocus;
CellContainerNotifier get cellContainerNotifier;
// ValueNotifier<bool> get onCellEditing;
}
typedef AccessoryBuilder = List<GridCellAccessoryBuilder> Function(
GridCellAccessoryBuildContext buildContext,
);
abstract class CellAccessory extends Widget {
const CellAccessory({super.key});
// The hover will show if the isHover's value is true
ValueNotifier<bool>? get onAccessoryHover;
AccessoryBuilder? get accessoryBuilder;
}
abstract class GridCellWidget extends StatefulWidget
implements CellAccessory, CellEditable, CellShortcuts {
GridCellWidget({super.key});
@override
final CellContainerNotifier cellContainerNotifier = CellContainerNotifier();
// When the cell is focused, we assume that the accessory also be hovered.
@override
ValueNotifier<bool> get onAccessoryHover => ValueNotifier(false);
// @override
// final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
@override
List<GridCellAccessoryBuilder> Function(
GridCellAccessoryBuildContext buildContext,
)? get accessoryBuilder => null;
@override
final RequestFocusListener requestFocus = RequestFocusListener();
@override
final Map<CellKeyboardKey, CellKeyboardAction> shortcutHandlers = {};
}
abstract class GridCellState<T extends GridCellWidget> extends State<T> {
@override
void initState() {
super.initState();
widget.requestFocus.setListener(requestBeginFocus);
}
@override
void didUpdateWidget(covariant T oldWidget) {
if (oldWidget != this) {
widget.requestFocus.setListener(requestBeginFocus);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
widget.onAccessoryHover.dispose();
widget.requestFocus.removeAllListener();
widget.requestFocus.dispose();
super.dispose();
}
/// Subclass can override this method to request focus.
void requestBeginFocus();
String? onCopy() => null;
}
abstract class GridEditableTextCell<T extends GridCellWidget>
extends GridCellState<T> {
SingleListenerFocusNode get focusNode;
@override
void initState() {
super.initState();
widget.shortcutHandlers[CellKeyboardKey.onEnter] =
() => focusNode.unfocus();
_listenOnFocusNodeChanged();
}
@override
void dispose() {
widget.shortcutHandlers.clear();
focusNode.removeAllListener();
focusNode.dispose();
super.dispose();
}
@override
void requestBeginFocus() {
if (!focusNode.hasFocus && focusNode.canRequestFocus) {
FocusScope.of(context).requestFocus(focusNode);
}
}
void _listenOnFocusNodeChanged() {
widget.cellContainerNotifier.isFocus = focusNode.hasFocus;
focusNode.setListener(() {
widget.cellContainerNotifier.isFocus = focusNode.hasFocus;
focusChanged();
});
}
Future<void> focusChanged() async {}
}
class RequestFocusListener extends ChangeNotifier {
VoidCallback? _listener;
void setListener(VoidCallback listener) {
if (_listener != null) {
removeListener(_listener!);
}
_listener = listener;
addListener(listener);
}
void removeAllListener() {
if (_listener != null) {
removeListener(_listener!);
_listener = null;
}
}
void notify() {
notifyListeners();
}
}
abstract class GridCellStyle {
const GridCellStyle();
}
class SingleListenerFocusNode extends FocusNode {
VoidCallback? _listener;
void setListener(VoidCallback listener) {
if (_listener != null) {
removeListener(_listener!);
}
_listener = listener;
super.addListener(listener);
}
void removeAllListener() {
if (_listener != null) {
removeListener(_listener!);
}
}
}

View File

@ -6,10 +6,10 @@ import '../../../grid/presentation/layout/sizes.dart';
import '../../../grid/presentation/widgets/row/row.dart';
import '../accessory/cell_accessory.dart';
import '../accessory/cell_shortcuts.dart';
import '../cell_builder.dart';
import '../../cell/editable_cell_builder.dart';
class CellContainer extends StatelessWidget {
final GridCellWidget child;
final EditableCellWidget child;
final AccessoryBuilder? accessoryBuilder;
final double width;
final bool isPrimary;

View File

@ -1,8 +0,0 @@
export 'checkbox_cell/checkbox_cell.dart';
export 'checklist_cell/checklist_cell.dart';
export 'date_cell/date_cell.dart';
export 'number_cell/number_cell.dart';
export 'select_option_cell/select_option_cell.dart';
export 'text_cell/text_cell.dart';
export 'timestamp_cell/timestamp_cell.dart';
export 'url_cell/url_cell.dart';

View File

@ -1,124 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'checkbox_cell_bloc.dart';
class GridCheckboxCellStyle extends GridCellStyle {
EdgeInsets? cellPadding;
GridCheckboxCellStyle({
this.cellPadding,
});
}
class GridCheckboxCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
late final GridCheckboxCellStyle cellStyle;
GridCheckboxCell({
required this.cellControllerBuilder,
GridCellStyle? style,
super.key,
}) {
if (style != null) {
cellStyle = (style as GridCheckboxCellStyle);
} else {
cellStyle = GridCheckboxCellStyle();
}
}
@override
GridCellState<GridCheckboxCell> createState() => _CheckboxCellState();
}
class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
late CheckboxCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as CheckboxCellController;
_cellBloc = CheckboxCellBloc(cellController: cellController)
..add(const CheckboxCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
builder: (context, state) {
final icon = state.isSelected
? const CheckboxCellCheck()
: const CheckboxCellUncheck();
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding:
widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
child: FlowyIconButton(
hoverColor: Colors.transparent,
onPressed: () => context
.read<CheckboxCellBloc>()
.add(const CheckboxCellEvent.select()),
iconPadding: EdgeInsets.zero,
icon: icon,
width: 20,
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
void requestBeginFocus() {
_cellBloc.add(const CheckboxCellEvent.select());
}
@override
String? onCopy() {
if (_cellBloc.state.isSelected) {
return "Yes";
} else {
return "No";
}
}
}
class CheckboxCellCheck extends StatelessWidget {
const CheckboxCellCheck({super.key});
@override
Widget build(BuildContext context) {
return const FlowySvg(
FlowySvgs.check_filled_s,
blendMode: BlendMode.dst,
);
}
}
class CheckboxCellUncheck extends StatelessWidget {
const CheckboxCellUncheck({super.key});
@override
Widget build(BuildContext context) {
return const FlowySvg(
FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
);
}
}

View File

@ -1,7 +1,8 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'checkbox_cell_bloc.freezed.dart';
@ -13,16 +14,17 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
required this.cellController,
}) : super(CheckboxCellState.initial(cellController)) {
on<CheckboxCellEvent>(
(event, emit) async {
await event.when(
initial: () {
_startListening();
(event, emit) {
event.when(
initial: () => _startListening(),
didUpdateCell: (isSelected) {
emit(state.copyWith(isSelected: isSelected));
},
didReceiveCellUpdate: (cellData) {
emit(state.copyWith(isSelected: _isSelected(cellData)));
didUpdateField: (fieldName) {
emit(state.copyWith(fieldName: fieldName));
},
select: () async {
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
select: () {
cellController.saveCellData(state.isSelected ? "No" : "Yes");
},
);
},
@ -41,12 +43,17 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellData) {
_onCellChangedFn = cellController.addListener(
onCellChanged: (cellData) {
if (!isClosed) {
add(CheckboxCellEvent.didReceiveCellUpdate(cellData));
add(CheckboxCellEvent.didUpdateCell(_isSelected(cellData)));
}
}),
},
onCellFieldChanged: (field) {
if (!isClosed) {
add(CheckboxCellEvent.didUpdateField(field.name));
}
},
);
}
}
@ -55,18 +62,24 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
class CheckboxCellEvent with _$CheckboxCellEvent {
const factory CheckboxCellEvent.initial() = _Initial;
const factory CheckboxCellEvent.select() = _Selected;
const factory CheckboxCellEvent.didReceiveCellUpdate(String? cellData) =
_DidReceiveCellUpdate;
const factory CheckboxCellEvent.didUpdateCell(bool isSelected) =
_DidUpdateCell;
const factory CheckboxCellEvent.didUpdateField(String fieldName) =
_DidUpdateField;
}
@freezed
class CheckboxCellState with _$CheckboxCellState {
const factory CheckboxCellState({
required bool isSelected,
required String fieldName,
}) = _CheckboxCellState;
factory CheckboxCellState.initial(TextCellController context) {
return CheckboxCellState(isSelected: _isSelected(context.getCellData()));
factory CheckboxCellState.initial(CheckboxCellController cellController) {
return CheckboxCellState(
isSelected: _isSelected(cellController.getCellData()),
fieldName: cellController.fieldInfo.field.name,
);
}
}

View File

@ -1,250 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../cell_builder.dart';
import 'checklist_cell_bloc.dart';
import 'checklist_cell_editor.dart';
import 'checklist_progress_bar.dart';
class ChecklistCellStyle extends GridCellStyle {
final String placeholder;
final EdgeInsets? cellPadding;
final bool showTasksInline;
final bool useRoundedBorders;
const ChecklistCellStyle({
this.placeholder = "",
this.cellPadding,
this.showTasksInline = false,
this.useRoundedBorders = false,
});
}
class GridChecklistCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
late final ChecklistCellStyle cellStyle;
GridChecklistCell({
required this.cellControllerBuilder,
GridCellStyle? style,
super.key,
}) {
if (style != null) {
cellStyle = (style as ChecklistCellStyle);
} else {
cellStyle = const ChecklistCellStyle();
}
}
@override
GridCellState<GridChecklistCell> createState() => GridChecklistCellState();
}
class GridChecklistCellState extends GridCellState<GridChecklistCell> {
late ChecklistCellBloc _cellBloc;
late final PopoverController _popover;
bool showIncompleteOnly = false;
@override
void initState() {
_popover = PopoverController();
final cellController =
widget.cellControllerBuilder.build() as ChecklistCellController;
_cellBloc = ChecklistCellBloc(cellController: cellController)
..add(const ChecklistCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
if (widget.cellStyle.showTasksInline) {
final tasks = List.from(state.tasks);
if (showIncompleteOnly) {
tasks.removeWhere((task) => task.isSelected);
}
final children = tasks
.mapIndexed(
(index, task) => ChecklistItem(
task: task,
autofocus: state.newTask && index == tasks.length - 1,
onSubmitted: () {
if (index == tasks.length - 1) {
context
.read<ChecklistCellBloc>()
.add(const ChecklistCellEvent.createNewTask(""));
}
},
),
)
.toList();
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding:
widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
),
),
const HSpace(6.0),
FlowyIconButton(
tooltipText: showIncompleteOnly
? LocaleKeys.grid_checklist_showComplete.tr()
: LocaleKeys.grid_checklist_hideComplete.tr(),
width: 32,
iconColorOnHover:
Theme.of(context).colorScheme.onSurface,
icon: FlowySvg(
showIncompleteOnly
? FlowySvgs.show_m
: FlowySvgs.hide_m,
size: const Size.square(16),
),
onPressed: () {
setState(
() => showIncompleteOnly = !showIncompleteOnly,
);
},
),
],
),
),
const VSpace(4),
...children,
const ChecklistItemControl(),
],
),
),
);
}
return AppFlowyPopover(
margin: EdgeInsets.zero,
controller: _popover,
constraints: BoxConstraints.loose(const Size(360, 400)),
direction: PopoverDirection.bottomWithLeftAligned,
triggerActions: PopoverTriggerFlags.none,
popupBuilder: (BuildContext popoverContext) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.cellContainerNotifier.isFocus = true;
});
return GridChecklistCellEditor(
cellController: widget.cellControllerBuilder.build()
as ChecklistCellController,
);
},
onClose: () => widget.cellContainerNotifier.isFocus = false,
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding:
widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
child: state.tasks.isEmpty
? FlowyText.medium(
widget.cellStyle.placeholder,
color: Theme.of(context).hintColor,
)
: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
),
),
),
);
},
),
);
}
@override
void requestBeginFocus() {
if (!widget.cellStyle.showTasksInline) {
_popover.show();
}
}
}
class ChecklistItemControl extends StatefulWidget {
const ChecklistItemControl({super.key});
@override
State<ChecklistItemControl> createState() => _ChecklistItemControlState();
}
class _ChecklistItemControlState extends State<ChecklistItemControl> {
bool _isHover = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onHover: (_) => setState(() => _isHover = true),
onExit: (_) => setState(() => _isHover = false),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => context
.read<ChecklistCellBloc>()
.add(const ChecklistCellEvent.createNewTask("")),
child: Padding(
padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0),
child: SizedBox(
height: 12,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
child: _isHover
? FlowyTooltip(
message: LocaleKeys.grid_checklist_addNew.tr(),
child: Row(
children: [
const Flexible(child: Center(child: Divider())),
const HSpace(12.0),
FilledButton(
style: FilledButton.styleFrom(
minimumSize: const Size.square(12),
maximumSize: const Size.square(12),
padding: EdgeInsets.zero,
),
onPressed: () => context
.read<ChecklistCellBloc>()
.add(
const ChecklistCellEvent.createNewTask(""),
),
child: FlowySvg(
FlowySvgs.add_s,
color: Theme.of(context).colorScheme.onPrimary,
),
),
const HSpace(12.0),
const Flexible(child: Center(child: Divider())),
],
),
)
: const SizedBox.expand(),
),
),
),
),
);
}
}

View File

@ -84,7 +84,7 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
_onCellChangedFn = cellController.addListener(
onCellChanged: (data) {
if (!isClosed) {
add(ChecklistCellEvent.didReceiveOptions(data));

View File

@ -17,17 +17,15 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'checklist_cell_bloc.dart';
import 'checklist_progress_bar.dart';
class GridChecklistCellEditor extends StatefulWidget {
class ChecklistCellEditor extends StatefulWidget {
final ChecklistCellController cellController;
const GridChecklistCellEditor({required this.cellController, super.key});
const ChecklistCellEditor({required this.cellController, super.key});
@override
State<GridChecklistCellEditor> createState() => _GridChecklistCellState();
State<ChecklistCellEditor> createState() => _GridChecklistCellState();
}
class _GridChecklistCellState extends State<GridChecklistCellEditor> {
late ChecklistCellBloc _bloc;
class _GridChecklistCellState extends State<ChecklistCellEditor> {
/// Focus node for the new task text field
late final FocusNode newTaskFocusNode;
@ -44,56 +42,50 @@ class _GridChecklistCellState extends State<GridChecklistCellEditor> {
return KeyEventResult.ignored;
},
);
_bloc = ChecklistCellBloc(cellController: widget.cellController)
..add(const ChecklistCellEvent.initial());
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _bloc,
child: BlocConsumer<ChecklistCellBloc, ChecklistCellState>(
listener: (context, state) {
if (state.tasks.isEmpty) {
newTaskFocusNode.requestFocus();
}
},
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: state.tasks.isEmpty
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
),
return BlocConsumer<ChecklistCellBloc, ChecklistCellState>(
listener: (context, state) {
if (state.tasks.isEmpty) {
newTaskFocusNode.requestFocus();
}
},
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: state.tasks.isEmpty
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
),
),
ChecklistItemList(
options: state.tasks,
onUpdateTask: () => newTaskFocusNode.requestFocus(),
),
if (state.tasks.isNotEmpty)
const TypeOptionSeparator(spacing: 0.0),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: NewTaskItem(focusNode: newTaskFocusNode),
),
],
);
},
),
),
),
ChecklistItemList(
options: state.tasks,
onUpdateTask: () => newTaskFocusNode.requestFocus(),
),
if (state.tasks.isNotEmpty) const TypeOptionSeparator(spacing: 0.0),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: NewTaskItem(focusNode: newTaskFocusNode),
),
],
);
},
);
}
@override
void dispose() {
_bloc.close();
newTaskFocusNode.dispose();
super.dispose();
}
}

View File

@ -1,6 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:percent_indicator/percent_indicator.dart';
import 'checklist_cell_bloc.dart';
@ -9,13 +8,13 @@ class ChecklistProgressBar extends StatefulWidget {
final List<ChecklistSelectOption> tasks;
final double percent;
final int segmentLimit = 5;
final double fontSize;
final TextStyle? textStyle;
const ChecklistProgressBar({
super.key,
required this.tasks,
required this.percent,
this.fontSize = 11,
this.textStyle,
});
@override
@ -72,12 +71,9 @@ class _ChecklistProgressBarState extends State<ChecklistProgressBar> {
width: PlatformExtension.isDesktop ? 36 : 45,
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: FlowyText.regular(
child: Text(
"${(widget.percent * 100).round()}%",
fontSize: widget.fontSize,
color: PlatformExtension.isDesktop
? Theme.of(context).hintColor
: null,
style: widget.textStyle,
),
),
),

View File

@ -4,7 +4,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
@ -16,11 +15,8 @@ import 'package:go_router/go_router.dart';
class MobileChecklistCellEditScreen extends StatefulWidget {
const MobileChecklistCellEditScreen({
super.key,
required this.cellController,
});
final ChecklistCellController cellController;
@override
State<MobileChecklistCellEditScreen> createState() =>
_MobileChecklistCellEditScreenState();
@ -32,26 +28,21 @@ class _MobileChecklistCellEditScreenState
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints.tightFor(height: 420),
child: BlocProvider(
create: (context) => ChecklistCellBloc(
cellController: widget.cellController,
)..add(const ChecklistCellEvent.initial()),
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const DragHandler(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _buildHeader(context),
),
const Divider(),
const Expanded(child: _TaskList()),
],
);
},
),
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const DragHandler(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _buildHeader(context),
),
const Divider(),
const Expanded(child: _TaskList()),
],
);
},
),
);
}
@ -109,10 +100,8 @@ class _TaskList extends StatelessWidget {
cells.add(const _NewTaskButton());
return ListView.separated(
shrinkWrap: true,
itemCount: cells.length,
separatorBuilder: (_, __) => const VSpace(8),
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (_, int index) => cells[index],
padding: const EdgeInsets.only(bottom: 12.0),
);

View File

@ -1,215 +0,0 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../grid/presentation/layout/sizes.dart';
import '../../cell_builder.dart';
import 'date_cell_bloc.dart';
import 'date_editor.dart';
class DateCellStyle extends GridCellStyle {
String placeholder;
Alignment alignment;
EdgeInsets? cellPadding;
final bool useRoundedBorder;
DateCellStyle({
this.placeholder = "",
this.alignment = Alignment.centerLeft,
this.cellPadding,
this.useRoundedBorder = false,
});
}
class GridDateCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
late final DateCellStyle cellStyle;
GridDateCell({
super.key,
GridCellStyle? style,
required this.cellControllerBuilder,
}) {
if (style != null) {
cellStyle = (style as DateCellStyle);
} else {
cellStyle = DateCellStyle();
}
}
@override
GridCellState<GridDateCell> createState() => _DateCellState();
}
class _DateCellState extends GridCellState<GridDateCell> {
final PopoverController _popover = PopoverController();
late final DateCellController _cellController;
late DateCellBloc _cellBloc;
@override
void initState() {
super.initState();
_cellController =
widget.cellControllerBuilder.build() as DateCellController;
_cellBloc = DateCellBloc(cellController: _cellController)
..add(const DateCellEvent.initial());
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<DateCellBloc, DateCellState>(
builder: (context, state) {
final text = state.dateStr.isEmpty
? widget.cellStyle.placeholder
: state.dateStr;
final color =
state.dateStr.isEmpty ? Theme.of(context).hintColor : null;
final padding =
widget.cellStyle.cellPadding ?? GridSize.cellContentInsets;
final alignment = widget.cellStyle.alignment;
if (PlatformExtension.isDesktopOrWeb) {
return AppFlowyPopover(
controller: _popover,
triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(260, 620)),
margin: EdgeInsets.zero,
child: Container(
alignment: alignment,
padding: padding,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FlowyText.medium(
text,
color: color,
overflow: TextOverflow.ellipsis,
),
),
if (state.data?.reminderId.isNotEmpty == true) ...[
const HSpace(5),
FlowyTooltip(
message:
LocaleKeys.grid_field_reminderOnDateTooltip.tr(),
child: const FlowySvg(FlowySvgs.clock_alarm_s),
),
],
],
),
),
popupBuilder: (_) => DateCellEditor(
cellController: _cellController,
onDismissed: () => widget.cellContainerNotifier.isFocus = false,
),
onClose: () => widget.cellContainerNotifier.isFocus = false,
);
} else if (widget.cellStyle.useRoundedBorder) {
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14)),
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
builder: (_) => MobileDateCellEditScreen(
controller: _cellController,
showAsFullScreen: false,
),
),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
padding: padding,
child: FlowyText.regular(
text,
fontSize: 16,
color: color,
maxLines: null,
),
),
);
} else {
return FlowyButton(
radius: BorderRadius.zero,
hoverColor: Colors.transparent,
margin: EdgeInsets.zero,
text: Align(
alignment: alignment,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
children: [
if (state.data?.reminderId.isNotEmpty == true) ...[
FlowyTooltip(
message:
LocaleKeys.grid_field_reminderOnDateTooltip.tr(),
child: const FlowySvg(FlowySvgs.clock_alarm_s),
),
const HSpace(5),
],
FlowyText(
text,
color: color,
fontSize: 15,
maxLines: 1,
),
],
),
),
),
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
backgroundColor:
Theme.of(context).colorScheme.secondaryContainer,
builder: (_) => MobileDateCellEditScreen(
controller: _cellController,
showAsFullScreen: false,
),
),
);
}
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
_cellController.dispose();
super.dispose();
}
@override
void requestBeginFocus() {
_popover.show();
widget.cellContainerNotifier.isFocus = true;
}
@override
String? onCopy() => _cellBloc.state.dateStr;
}

View File

@ -41,7 +41,7 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
_onCellChangedFn = cellController.addListener(
onCellChanged: ((data) {
if (!isClosed) {
add(DateCellEvent.didReceiveCellUpdate(data));

View File

@ -339,7 +339,7 @@ class DateCellEditorBloc
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
_onCellChangedFn = cellController.addListener(
onCellChanged: (cell) {
if (!isClosed) {
add(DateCellEditorEvent.didReceiveCellUpdate(cell));

View File

@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../accessory/cell_shortcuts.dart';
import '../cell_builder.dart';
import '../../cell/editable_cell_builder.dart';
import 'cell_container.dart';
class MobileCellContainer extends StatelessWidget {
final GridCellWidget child;
final EditableCellWidget child;
final bool isPrimary;
final VoidCallback? onPrimaryFieldCellTap;

View File

@ -1,115 +0,0 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'number_cell_bloc.dart';
import '../../../../grid/presentation/layout/sizes.dart';
import '../../cell_builder.dart';
class GridNumberCellStyle extends GridCellStyle {
String? placeholder;
TextStyle? textStyle;
EdgeInsets? cellPadding;
GridNumberCellStyle({
this.placeholder,
this.textStyle,
this.cellPadding,
});
}
class GridNumberCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
late final GridNumberCellStyle cellStyle;
GridNumberCell({
required this.cellControllerBuilder,
required GridCellStyle? style,
super.key,
}) {
if (style != null) {
cellStyle = (style as GridNumberCellStyle);
} else {
cellStyle = GridNumberCellStyle();
}
}
@override
GridEditableTextCell<GridNumberCell> createState() => _NumberCellState();
}
class _NumberCellState extends GridEditableTextCell<GridNumberCell> {
late NumberCellBloc _cellBloc;
late TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as NumberCellController;
_cellBloc = NumberCellBloc(cellController: cellController)
..add(const NumberCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.cellContent);
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: MultiBlocListener(
listeners: [
BlocListener<NumberCellBloc, NumberCellState>(
listenWhen: (p, c) => p.cellContent != c.cellContent,
listener: (context, state) => _controller.text = state.cellContent,
),
],
child: TextField(
controller: _controller,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding:
widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: widget.cellStyle.placeholder,
isDense: true,
),
onTapOutside: (_) => focusNode.unfocus(),
),
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Future<void> focusChanged() async {
if (mounted) {
if (_cellBloc.isClosed == false &&
_controller.text != _cellBloc.state.cellContent) {
_cellBloc.add(NumberCellEvent.updateCell(_controller.text));
}
}
}
@override
String? onCopy() {
return _cellBloc.state.cellContent;
}
}

View File

@ -5,7 +5,6 @@ import 'dart:async';
part 'number_cell_bloc.freezed.dart';
//
class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
final NumberCellController cellController;
void Function()? _onCellChangedFn;
@ -19,12 +18,12 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
initial: () {
_startListening();
},
didReceiveCellUpdate: (cellContent) {
emit(state.copyWith(cellContent: cellContent ?? ""));
didReceiveCellUpdate: (cellData) {
emit(state.copyWith(content: cellData ?? ""));
},
updateCell: (text) async {
if (state.cellContent != text) {
emit(state.copyWith(cellContent: text));
if (state.content != text) {
emit(state.copyWith(content: text));
await cellController.saveCellData(text);
// If the input content is "abc" that can't parsered as number then the data stored in the backend will be an empty string.
@ -53,7 +52,7 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
_onCellChangedFn = cellController.addListener(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(NumberCellEvent.didReceiveCellUpdate(cellContent));
@ -74,12 +73,12 @@ class NumberCellEvent with _$NumberCellEvent {
@freezed
class NumberCellState with _$NumberCellState {
const factory NumberCellState({
required String cellContent,
required String content,
}) = _NumberCellState;
factory NumberCellState.initial(TextCellController context) {
factory NumberCellState.initial(TextCellController cellController) {
return NumberCellState(
cellContent: context.getCellData() ?? "",
content: cellController.getCellData() ?? "",
);
}
}

View File

@ -1,8 +1,8 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -87,7 +87,11 @@ class SelectOptionTag extends StatelessWidget {
padding: onRemove == null ? padding : padding.copyWith(right: 2.0),
decoration: BoxDecoration(
color: optionColor,
borderRadius: Corners.s6Border,
borderRadius: BorderRadius.all(
Radius.circular(
PlatformExtension.isDesktopOrWeb ? 6 : 11,
),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
@ -145,8 +149,7 @@ class SelectOptionTagCell extends StatelessWidget {
),
child: SelectOptionTag(
option: option,
padding:
const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
),

View File

@ -1,349 +0,0 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/mobile_select_option_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../grid/presentation/layout/sizes.dart';
import '../../cell_builder.dart';
import 'extension.dart';
import 'select_option_cell_bloc.dart';
import 'select_option_editor.dart';
class SelectOptionCellStyle extends GridCellStyle {
String placeholder;
EdgeInsets? cellPadding;
bool useRoundedBorder;
SelectOptionCellStyle({
this.placeholder = "",
this.cellPadding,
this.useRoundedBorder = false,
});
}
class GridSingleSelectCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
late final SelectOptionCellStyle cellStyle;
GridSingleSelectCell({
super.key,
required this.cellControllerBuilder,
GridCellStyle? style,
}) {
if (style != null) {
cellStyle = (style as SelectOptionCellStyle);
} else {
cellStyle = SelectOptionCellStyle();
}
}
@override
GridCellState<GridSingleSelectCell> createState() => _SingleSelectCellState();
}
class _SingleSelectCellState extends GridCellState<GridSingleSelectCell> {
final PopoverController _popoverController = PopoverController();
late SelectOptionCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as SelectOptionCellController;
_cellBloc = SelectOptionCellBloc(cellController: cellController)
..add(const SelectOptionCellEvent.initial());
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
builder: (context, state) {
return SelectOptionWrap(
selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
onCellEditing: (isFocus) =>
widget.cellContainerNotifier.isFocus = isFocus,
popoverController: _popoverController,
cellControllerBuilder: widget.cellControllerBuilder,
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
void requestBeginFocus() => _popoverController.show();
}
//----------------------------------------------------------------
class GridMultiSelectCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
late final SelectOptionCellStyle cellStyle;
GridMultiSelectCell({
super.key,
required this.cellControllerBuilder,
GridCellStyle? style,
}) {
if (style != null) {
cellStyle = (style as SelectOptionCellStyle);
} else {
cellStyle = SelectOptionCellStyle();
}
}
@override
GridCellState<GridMultiSelectCell> createState() => _MultiSelectCellState();
}
class _MultiSelectCellState extends GridCellState<GridMultiSelectCell> {
final PopoverController _popoverController = PopoverController();
late SelectOptionCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as SelectOptionCellController;
_cellBloc = SelectOptionCellBloc(cellController: cellController)
..add(const SelectOptionCellEvent.initial());
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
builder: (context, state) {
return SelectOptionWrap(
selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
onCellEditing: (isFocus) =>
widget.cellContainerNotifier.isFocus = isFocus,
popoverController: _popoverController,
cellControllerBuilder: widget.cellControllerBuilder,
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
void requestBeginFocus() => _popoverController.show();
}
class SelectOptionWrap extends StatefulWidget {
final List<SelectOptionPB> selectOptions;
final SelectOptionCellStyle cellStyle;
final CellControllerBuilder cellControllerBuilder;
final PopoverController popoverController;
final void Function(bool) onCellEditing;
const SelectOptionWrap({
super.key,
required this.selectOptions,
required this.cellControllerBuilder,
required this.onCellEditing,
required this.popoverController,
required this.cellStyle,
});
@override
State<StatefulWidget> createState() => _SelectOptionWrapState();
}
class _SelectOptionWrapState extends State<SelectOptionWrap> {
@override
Widget build(BuildContext context) {
final constraints = BoxConstraints.loose(
Size(SelectOptionCellEditor.editorPanelWidth, 300),
);
final cellController =
widget.cellControllerBuilder.build() as SelectOptionCellController;
if (PlatformExtension.isDesktopOrWeb) {
return AppFlowyPopover(
controller: widget.popoverController,
constraints: constraints,
margin: EdgeInsets.zero,
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (BuildContext popoverContext) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onCellEditing(true);
});
return SelectOptionCellEditor(
cellController: cellController,
);
},
onClose: () => widget.onCellEditing(false),
child: Padding(
padding: widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
child: _buildOptions(context),
),
);
} else if (widget.cellStyle.useRoundedBorder) {
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14)),
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
builder: (context) {
return MobileSelectOptionEditor(
cellController: cellController,
);
},
),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: widget.selectOptions.isEmpty ? 13 : 10,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
child: Row(
children: [
Expanded(child: _buildMobileOptions(isInRowDetail: true)),
const HSpace(6),
RotatedBox(
quarterTurns: 3,
child: Icon(
Icons.chevron_left,
color: Theme.of(context).hintColor,
),
),
const HSpace(2),
],
),
),
);
} else {
return FlowyButton(
hoverColor: Colors.transparent,
radius: BorderRadius.zero,
margin: EdgeInsets.zero,
text: Padding(
padding: widget.cellStyle.cellPadding ?? EdgeInsets.zero,
child: _buildMobileOptions(isInRowDetail: false),
),
onTap: () {
showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
builder: (context) {
return MobileSelectOptionEditor(
cellController: cellController,
);
},
);
},
);
}
}
Widget _buildOptions(BuildContext context) {
final Widget child;
if (widget.selectOptions.isEmpty) {
child = Padding(
padding: const EdgeInsets.symmetric(vertical: 1),
child: FlowyText.medium(
widget.cellStyle.placeholder,
color: Theme.of(context).hintColor,
),
);
} else {
final children = widget.selectOptions.map(
(option) {
return Padding(
padding: const EdgeInsets.only(right: 4),
child: SelectOptionTag(
option: option,
padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 8),
),
);
},
).toList();
child = Wrap(
runSpacing: 4,
children: children,
);
}
return Align(alignment: Alignment.centerLeft, child: child);
}
Widget _buildMobileOptions({required bool isInRowDetail}) {
if (widget.selectOptions.isEmpty) {
return Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(vertical: 1),
child: FlowyText(
widget.cellStyle.placeholder,
color: Theme.of(context).hintColor,
),
);
} else {
final children = widget.selectOptions.mapIndexed(
(index, option) {
return Padding(
padding: EdgeInsets.only(left: index == 0 ? 0 : 4),
child: SelectOptionTag(
option: option,
fontSize: 14,
padding: isInRowDetail
? const EdgeInsets.symmetric(horizontal: 11, vertical: 5)
: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
),
);
},
).toList();
return isInRowDetail
? Align(
alignment: Alignment.centerLeft,
child: Wrap(
runSpacing: 4,
children: children,
),
)
: Align(
alignment: Alignment.centerLeft,
child: ListView.separated(
separatorBuilder: (context, index) => const HSpace(4),
itemCount: children.length,
itemBuilder: (context, index) => children[index],
scrollDirection: Axis.horizontal,
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
);
}
}
}

View File

@ -16,14 +16,14 @@ class SelectOptionCellBloc
}) : super(SelectOptionCellState.initial(cellController)) {
on<SelectOptionCellEvent>(
(event, emit) async {
await event.map(
initial: (_InitialCell value) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveOptions: (_DidReceiveOptions value) {
didReceiveOptions: (List<SelectOptionPB> selectedOptions) {
emit(
state.copyWith(
selectedOptions: value.selectedOptions,
selectedOptions: selectedOptions,
),
);
},
@ -43,16 +43,16 @@ class SelectOptionCellBloc
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((selectOptionContext) {
_onCellChangedFn = cellController.addListener(
onCellChanged: (selectOptionCellData) {
if (!isClosed) {
add(
SelectOptionCellEvent.didReceiveOptions(
selectOptionContext?.selectOptions ?? [],
selectOptionCellData?.selectOptions ?? [],
),
);
}
}),
},
);
}
}
@ -71,8 +71,10 @@ class SelectOptionCellState with _$SelectOptionCellState {
required List<SelectOptionPB> selectedOptions,
}) = _SelectOptionCellState;
factory SelectOptionCellState.initial(SelectOptionCellController context) {
final data = context.getCellData();
factory SelectOptionCellState.initial(
SelectOptionCellController cellController,
) {
final data = cellController.getCellData();
return SelectOptionCellState(
selectedOptions: data?.selectOptions ?? [],

View File

@ -21,11 +21,9 @@ import 'select_option_editor_bloc.dart';
import 'text_field.dart';
const double _editorPanelWidth = 300;
const double _padding = 12.0;
class SelectOptionCellEditor extends StatefulWidget {
final SelectOptionCellController cellController;
static double editorPanelWidth = 300;
const SelectOptionCellEditor({super.key, required this.cellController});
@ -112,7 +110,7 @@ class _OptionList extends StatelessWidget {
VSpace(GridSize.typeOptionSeparatorHeight),
physics: StyledScrollPhysics(),
itemBuilder: (_, int index) => cells[index],
padding: const EdgeInsets.only(top: 6.0, bottom: 12.0),
padding: const EdgeInsets.symmetric(vertical: 8.0),
);
},
);
@ -149,7 +147,7 @@ class _TextField extends StatelessWidget {
);
return Padding(
padding: const EdgeInsets.all(_padding),
padding: const EdgeInsets.all(12.0),
child: SelectOptionTextField(
options: state.options,
selectedOptionMap: optionMap,
@ -199,7 +197,7 @@ class _Title extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
height: GridSize.popoverItemHeight,
child: Row(
@ -241,7 +239,7 @@ class _CreateOptionCell extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SizedBox(
height: 28,
child: FlowyButton(
@ -342,7 +340,7 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
mutex: widget.popoverMutex,
clickHandler: PopoverClickHandler.gestureDetector,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: FlowyHover(
resetHoverOnRebuild: false,
style: HoverStyle(

View File

@ -4,6 +4,7 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -16,6 +17,8 @@ class SelectOptionCellEditorBloc
final SelectOptionCellBackendService _selectOptionService;
final SelectOptionCellController cellController;
VoidCallback? _onCellChangedFn;
SelectOptionCellEditorBloc({
required this.cellController,
}) : _selectOptionService = SelectOptionCellBackendService(
@ -104,7 +107,10 @@ class SelectOptionCellEditorBloc
@override
Future<void> close() async {
await cellController.dispose();
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
return super.close();
}
@ -241,11 +247,11 @@ class SelectOptionCellEditorBloc
}
void _startListening() {
cellController.startListening(
_onCellChangedFn = cellController.addListener(
onCellChanged: ((selectOptionContext) {
_loadOptions();
}),
onCellFieldChanged: () {
onCellFieldChanged: (field) {
_loadOptions();
},
);

View File

@ -1,154 +0,0 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../grid/presentation/layout/sizes.dart';
import '../../cell_builder.dart';
class GridTextCellStyle extends GridCellStyle {
final String? placeholder;
final TextStyle? textStyle;
final EdgeInsets? cellPadding;
final bool autofocus;
final double emojiFontSize;
final double emojiHPadding;
final bool showEmoji;
final bool useRoundedBorder;
const GridTextCellStyle({
this.placeholder,
this.textStyle,
this.cellPadding,
this.autofocus = false,
this.showEmoji = true,
this.emojiFontSize = 16,
this.emojiHPadding = 4,
this.useRoundedBorder = false,
});
}
class GridTextCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
late final GridTextCellStyle cellStyle;
GridTextCell({
required this.cellControllerBuilder,
GridCellStyle? style,
super.key,
}) {
if (style != null) {
cellStyle = (style as GridTextCellStyle);
} else {
cellStyle = const GridTextCellStyle();
}
}
@override
GridEditableTextCell<GridTextCell> createState() => _GridTextCellState();
}
class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
late TextCellBloc _cellBloc;
late TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as TextCellController;
_cellBloc = TextCellBloc(cellController: cellController)
..add(const TextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocListener<TextCellBloc, TextCellState>(
listener: (context, state) {
if (_controller.text != state.content) {
_controller.text = state.content;
}
},
child: Row(
children: [
if (widget.cellStyle.showEmoji) ...[
// Only build the emoji when it changes
BlocBuilder<TextCellBloc, TextCellState>(
buildWhen: (p, c) => p.emoji != c.emoji,
builder: (context, state) => Center(
child: FlowyText(
state.emoji,
fontSize: widget.cellStyle.emojiFontSize,
),
),
),
HSpace(widget.cellStyle.emojiHPadding),
],
Expanded(
child: widget.cellStyle.useRoundedBorder
? FlowyTextField(
controller: _controller,
textStyle: widget.cellStyle.textStyle ??
Theme.of(context).textTheme.bodyMedium,
focusNode: focusNode,
autoFocus: widget.cellStyle.autofocus,
hintText: widget.cellStyle.placeholder,
onChanged: (text) => _cellBloc.add(
TextCellEvent.updateText(text),
),
debounceDuration: const Duration(milliseconds: 300),
)
: TextField(
controller: _controller,
focusNode: focusNode,
maxLines: null,
style: widget.cellStyle.textStyle ??
Theme.of(context).textTheme.bodyMedium,
autofocus: widget.cellStyle.autofocus,
decoration: InputDecoration(
contentPadding: widget.cellStyle.cellPadding ??
GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: widget.cellStyle.placeholder,
isDense: true,
isCollapsed: true,
),
onTapOutside: (_) => focusNode.unfocus(),
),
),
],
),
),
);
}
@override
Future<void> dispose() async {
_controller.dispose();
_cellBloc.close();
super.dispose();
}
@override
String? onCopy() => _cellBloc.state.content;
@override
Future<void> focusChanged() {
_cellBloc.add(
TextCellEvent.updateText(_controller.text),
);
return super.focusChanged();
}
}

View File

@ -1,13 +1,15 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'text_cell_bloc.freezed.dart';
class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
final TextCellController cellController;
void Function()? _onCellChangedFn;
TextCellBloc({
required this.cellController,
}) : super(TextCellState.initial(cellController)) {
@ -17,18 +19,17 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
initial: () {
_startListening();
},
updateText: (text) {
if (state.content != text) {
cellController.saveCellData(text);
emit(state.copyWith(content: text));
}
},
didReceiveCellUpdate: (content) {
didReceiveCellUpdate: (String content) {
emit(state.copyWith(content: content));
},
didUpdateEmoji: (String emoji) {
emit(state.copyWith(emoji: emoji));
},
updateText: (String text) {
if (state.content != text) {
cellController.saveCellData(text, debounce: true);
}
},
enableEdit: (bool enabled) {
emit(state.copyWith(enableEdit: enabled));
},
@ -48,15 +49,15 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
_onCellChangedFn = cellController.addListener(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(TextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
}
}),
onRowMetaChanged: () {
if (!isClosed) {
add(TextCellEvent.didUpdateEmoji(cellController.emoji ?? ""));
if (!isClosed && cellController.fieldInfo.isPrimary) {
add(TextCellEvent.didUpdateEmoji(cellController.icon ?? ""));
}
},
);
@ -81,9 +82,11 @@ class TextCellState with _$TextCellState {
required bool enableEdit,
}) = _TextCellState;
factory TextCellState.initial(TextCellController context) => TextCellState(
content: context.getCellData() ?? "",
emoji: context.emoji ?? "",
factory TextCellState.initial(TextCellController cellController) =>
TextCellState(
content: cellController.getCellData() ?? "",
emoji:
cellController.fieldInfo.isPrimary ? cellController.icon ?? "" : "",
enableEdit: false,
);
}

View File

@ -1,132 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class TimestampCellStyle extends GridCellStyle {
String? placeholder;
Alignment alignment;
EdgeInsets? cellPadding;
final bool useRoundedBorder;
TimestampCellStyle({
this.placeholder,
this.alignment = Alignment.center,
this.cellPadding,
this.useRoundedBorder = false,
});
}
class GridTimestampCell extends GridCellWidget {
/// The [GridTimestampCell] is used by both [FieldType.CreatedTime]
/// and [FieldType.LastEditedTime]. So it needs to know the field type.
final FieldType fieldType;
final CellControllerBuilder cellControllerBuilder;
late final TimestampCellStyle? cellStyle;
GridTimestampCell({
super.key,
GridCellStyle? style,
required this.fieldType,
required this.cellControllerBuilder,
}) {
if (style != null) {
cellStyle = (style as TimestampCellStyle);
} else {
cellStyle = null;
}
}
@override
GridCellState<GridTimestampCell> createState() => _TimestampCellState();
}
class _TimestampCellState extends GridCellState<GridTimestampCell> {
late TimestampCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as TimestampCellController;
_cellBloc = TimestampCellBloc(cellController: cellController)
..add(const TimestampCellEvent.initial());
}
@override
Widget build(BuildContext context) {
final alignment = widget.cellStyle?.alignment ?? Alignment.centerLeft;
final placeholder = widget.cellStyle?.placeholder ?? "";
final padding = widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets;
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
builder: (context, state) {
final isEmpty = state.dateStr.isEmpty;
final text = isEmpty ? placeholder : state.dateStr;
if (PlatformExtension.isDesktopOrWeb ||
widget.cellStyle == null ||
!widget.cellStyle!.useRoundedBorder) {
return Align(
alignment: alignment,
child: Padding(
padding: padding,
child: FlowyText.medium(
text,
color: isEmpty ? Theme.of(context).hintColor : null,
maxLines: null,
),
),
);
} else {
return Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
child: Padding(
padding: padding,
child: FlowyText.medium(
text,
fontSize: 16,
color: isEmpty
? Theme.of(context).hintColor
: AFThemeExtension.of(context).textColor,
maxLines: null,
),
),
);
}
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
String? onCopy() => _cellBloc.state.dateStr;
@override
void requestBeginFocus() {
return;
}
}

View File

@ -42,7 +42,7 @@ class TimestampCellBloc extends Bloc<TimestampCellEvent, TimestampCellState> {
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
_onCellChangedFn = cellController.addListener(
onCellChanged: ((data) {
if (!isClosed) {
add(TimestampCellEvent.didReceiveCellUpdate(data));

View File

@ -1,299 +0,0 @@
import 'dart:async';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../../../../grid/presentation/layout/sizes.dart';
import '../../accessory/cell_accessory.dart';
import '../../cell_builder.dart';
import 'url_cell_bloc.dart';
class GridURLCellStyle extends GridCellStyle {
String? placeholder;
TextStyle? textStyle;
bool? autofocus;
EdgeInsets? cellPadding;
List<GridURLCellAccessoryType> accessoryTypes;
GridURLCellStyle({
this.placeholder,
this.accessoryTypes = const [],
this.cellPadding,
});
}
enum GridURLCellAccessoryType {
copyURL,
visitURL,
}
typedef URLCellDataNotifier = CellDataNotifier<String>;
class GridURLCell extends GridCellWidget {
GridURLCell({
super.key,
required this.cellControllerBuilder,
GridCellStyle? style,
}) : _cellDataNotifier = CellDataNotifier(value: '') {
if (style != null) {
cellStyle = (style as GridURLCellStyle);
} else {
cellStyle = GridURLCellStyle();
}
}
/// Use
final URLCellDataNotifier _cellDataNotifier;
final CellControllerBuilder cellControllerBuilder;
late final GridURLCellStyle cellStyle;
@override
GridCellState<GridURLCell> createState() => _GridURLCellState();
@override
List<GridCellAccessoryBuilder> Function(
GridCellAccessoryBuildContext buildContext,
) get accessoryBuilder => (buildContext) {
final List<GridCellAccessoryBuilder> accessories = [];
accessories.addAll(
cellStyle.accessoryTypes.map((ty) {
return _accessoryFromType(ty, buildContext);
}),
);
// If the accessories is empty then the default accessory will be GridURLCellAccessoryType.visitURL
if (accessories.isEmpty) {
accessories.add(
_accessoryFromType(
GridURLCellAccessoryType.visitURL,
buildContext,
),
);
}
return accessories;
};
GridCellAccessoryBuilder _accessoryFromType(
GridURLCellAccessoryType ty,
GridCellAccessoryBuildContext buildContext,
) {
switch (ty) {
case GridURLCellAccessoryType.visitURL:
return VisitURLCellAccessoryBuilder(
builder: (Key key) => _VisitURLAccessory(
key: key,
cellDataNotifier: _cellDataNotifier,
),
);
case GridURLCellAccessoryType.copyURL:
return CopyURLCellAccessoryBuilder(
builder: (Key key) => _CopyURLAccessory(
key: key,
cellDataNotifier: _cellDataNotifier,
),
);
}
}
}
class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
late final URLCellBloc _cellBloc;
late final TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as URLCellController;
_cellBloc = URLCellBloc(cellController: cellController)
..add(const URLCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocConsumer<URLCellBloc, URLCellState>(
listenWhen: (previous, current) => previous.content != current.content,
listener: (context, state) {
_controller.text = state.content;
},
builder: (context, state) {
final style = widget.cellStyle.textStyle ??
Theme.of(context).textTheme.bodyMedium!;
widget._cellDataNotifier.value = state.content;
return TextField(
controller: _controller,
focusNode: focusNode,
maxLines: null,
style: style.copyWith(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
autofocus: false,
decoration: InputDecoration(
contentPadding:
widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: widget.cellStyle.placeholder,
hintStyle: style.copyWith(color: Theme.of(context).hintColor),
isDense: true,
),
onTapOutside: (_) => focusNode.unfocus(),
);
},
),
);
}
@override
Future<void> focusChanged() async {
_cellBloc.add(URLCellEvent.updateURL(_controller.text.trim()));
return super.focusChanged();
}
@override
String? onCopy() => _cellBloc.state.content;
}
typedef CopyURLCellAccessoryBuilder
= GridCellAccessoryBuilder<State<_CopyURLAccessory>>;
class _CopyURLAccessory extends StatefulWidget {
const _CopyURLAccessory({
super.key,
required this.cellDataNotifier,
});
final URLCellDataNotifier cellDataNotifier;
@override
State<_CopyURLAccessory> createState() => _CopyURLAccessoryState();
}
class _CopyURLAccessoryState extends State<_CopyURLAccessory>
with GridCellAccessoryState {
@override
Widget build(BuildContext context) {
if (widget.cellDataNotifier.value.isNotEmpty) {
return FlowyTooltip(
message: LocaleKeys.tooltip_urlCopyAccessory.tr(),
preferBelow: false,
child: _URLAccessoryIconContainer(
child: FlowySvg(
FlowySvgs.copy_s,
color: AFThemeExtension.of(context).textColor,
),
),
);
} else {
return const SizedBox.shrink();
}
}
@override
void onTap() {
final content = widget.cellDataNotifier.value;
if (content.isEmpty) {
return;
}
Clipboard.setData(ClipboardData(text: content));
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
}
}
typedef VisitURLCellAccessoryBuilder
= GridCellAccessoryBuilder<State<_VisitURLAccessory>>;
class _VisitURLAccessory extends StatefulWidget {
const _VisitURLAccessory({
super.key,
required this.cellDataNotifier,
});
final URLCellDataNotifier cellDataNotifier;
@override
State<_VisitURLAccessory> createState() => _VisitURLAccessoryState();
}
class _VisitURLAccessoryState extends State<_VisitURLAccessory>
with GridCellAccessoryState {
@override
Widget build(BuildContext context) {
if (widget.cellDataNotifier.value.isNotEmpty) {
return FlowyTooltip(
message: LocaleKeys.tooltip_urlLaunchAccessory.tr(),
preferBelow: false,
child: _URLAccessoryIconContainer(
child: FlowySvg(
FlowySvgs.attach_s,
color: AFThemeExtension.of(context).textColor,
),
),
);
} else {
return const SizedBox.shrink();
}
}
@override
bool enable() {
return widget.cellDataNotifier.value.isNotEmpty;
}
@override
void onTap() {
final content = widget.cellDataNotifier.value;
if (content.isEmpty) {
return;
}
final shouldAddScheme =
!['http', 'https'].any((pattern) => content.startsWith(pattern));
final url = shouldAddScheme ? 'http://$content' : content;
canLaunchUrlString(url).then((value) => launchUrlString(url));
}
}
class _URLAccessoryIconContainer extends StatelessWidget {
const _URLAccessoryIconContainer({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 26,
height: 26,
child: Padding(
padding: const EdgeInsets.all(3.0),
child: child,
),
);
}
}

View File

@ -27,7 +27,7 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
);
},
updateURL: (String url) {
cellController.saveCellData(url, deduplicate: true);
cellController.saveCellData(url, debounce: true);
},
);
},
@ -45,7 +45,7 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
_onCellChangedFn = cellController.addListener(
onCellChanged: ((cellData) {
if (!isClosed) {
add(URLCellEvent.didReceiveCellUpdate(cellData));

View File

@ -46,7 +46,7 @@ class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
_onCellChangedFn = cellController.addListener(
onCellChanged: ((cellData) {
if (!isClosed) {
add(URLCellEditorEvent.didReceiveCellUpdate(cellData));

View File

@ -1,9 +1,12 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_banner_bloc.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/row_action.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@ -12,12 +15,12 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'cell_builder.dart';
import 'cells/cells.dart';
typedef OnSubmittedEmoji = void Function(String emoji);
const _kBannerActionHeight = 40.0;
class RowBanner extends StatefulWidget {
final RowController rowController;
final GridCellBuilder cellBuilder;
final EditableCellBuilder cellBuilder;
const RowBanner({
required this.rowController,
@ -33,6 +36,12 @@ class _RowBannerState extends State<RowBanner> {
final _isHovering = ValueNotifier(false);
final popoverController = PopoverController();
@override
void dispose() {
_isHovering.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider<RowBannerBloc>(
@ -57,7 +66,7 @@ class _RowBannerState extends State<RowBanner> {
popoverController: popoverController,
),
),
const HSpace(4),
const VSpace(4),
_BannerTitle(
cellBuilder: widget.cellBuilder,
popoverController: popoverController,
@ -81,6 +90,7 @@ class _RowBannerState extends State<RowBanner> {
class _BannerAction extends StatelessWidget {
final ValueNotifier<bool> isHovering;
final PopoverController popoverController;
const _BannerAction({
required this.isHovering,
required this.popoverController,
@ -88,48 +98,43 @@ class _BannerAction extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: isHovering,
builder: (BuildContext context, bool value, Widget? child) {
if (!value) {
return const SizedBox(height: _kBannerActionHeight);
}
return SizedBox(
height: _kBannerActionHeight,
child: ValueListenableBuilder(
valueListenable: isHovering,
builder: (BuildContext context, bool isHovering, Widget? child) {
if (!isHovering) {
return const SizedBox.shrink();
}
return BlocBuilder<RowBannerBloc, RowBannerState>(
builder: (context, state) {
final children = <Widget>[];
final rowMeta = state.rowMeta;
if (rowMeta.icon.isEmpty) {
children.add(
EmojiPickerButton(
showEmojiPicker: () => popoverController.show(),
),
return BlocBuilder<RowBannerBloc, RowBannerState>(
builder: (context, state) {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (state.rowMeta.icon.isEmpty)
AddEmojiButton(
onTap: () => popoverController.show(),
)
else
RemoveEmojiButton(
onTap: () => context
.read<RowBannerBloc>()
.add(const RowBannerEvent.setIcon('')),
),
],
);
} else {
children.add(
RemoveEmojiButton(
onRemoved: () {
context
.read<RowBannerBloc>()
.add(const RowBannerEvent.setIcon(''));
},
),
);
}
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
},
);
},
},
);
},
),
);
}
}
class _BannerTitle extends StatefulWidget {
final GridCellBuilder cellBuilder;
class _BannerTitle extends StatelessWidget {
final EditableCellBuilder cellBuilder;
final PopoverController popoverController;
final RowController rowController;
@ -139,57 +144,41 @@ class _BannerTitle extends StatefulWidget {
required this.rowController,
});
@override
State<_BannerTitle> createState() => _BannerTitleState();
}
class _BannerTitleState extends State<_BannerTitle> {
@override
Widget build(BuildContext context) {
return BlocBuilder<RowBannerBloc, RowBannerState>(
builder: (context, state) {
final children = <Widget>[];
if (state.rowMeta.icon.isNotEmpty) {
children.add(
final children = <Widget>[
if (state.rowMeta.icon.isNotEmpty)
EmojiButton(
emoji: state.rowMeta.icon,
showEmojiPicker: () => widget.popoverController.show(),
showEmojiPicker: () => popoverController.show(),
),
);
}
children.add(const HSpace(4));
if (state.primaryField != null) {
final style = GridTextCellStyle(
placeholder: LocaleKeys.grid_row_titlePlaceholder.tr(),
textStyle: Theme.of(context).textTheme.titleLarge,
showEmoji: false,
autofocus: true,
cellPadding: EdgeInsets.zero,
);
final cellContext = DatabaseCellContext(
viewId: widget.rowController.viewId,
rowMeta: widget.rowController.rowMeta,
fieldInfo: FieldInfo.initial(state.primaryField!),
);
children.add(
const HSpace(4),
if (state.primaryField != null)
Expanded(
child: widget.cellBuilder.build(cellContext, style: style),
child: cellBuilder.buildCustom(
CellContext(
fieldId: state.primaryField!.id,
rowId: rowController.rowId,
),
skinMap: EditableCellSkinMap(textSkin: _TitleSkin()),
),
),
);
}
];
return AppFlowyPopover(
controller: widget.popoverController,
controller: popoverController,
triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.bottomWithLeftAligned,
constraints: const BoxConstraints(maxWidth: 380, maxHeight: 300),
popupBuilder: (popoverContext) => _buildEmojiPicker((emoji) {
context.read<RowBannerBloc>().add(RowBannerEvent.setIcon(emoji));
widget.popoverController.close();
}),
popupBuilder: (popoverContext) => EmojiSelectionMenu(
onSubmitted: (emoji) {
popoverController.close();
context.read<RowBannerBloc>().add(RowBannerEvent.setIcon(emoji));
},
onExit: () {},
),
child: Row(children: children),
);
},
@ -197,9 +186,6 @@ class _BannerTitleState extends State<_BannerTitle> {
}
}
typedef OnSubmittedEmoji = void Function(String emoji);
const _kBannerActionHeight = 40.0;
class EmojiButton extends StatelessWidget {
final String emoji;
final VoidCallback showEmojiPicker;
@ -213,7 +199,6 @@ class EmojiButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
height: _kBannerActionHeight,
width: _kBannerActionHeight,
child: FlowyButton(
margin: EdgeInsets.zero,
@ -228,18 +213,13 @@ class EmojiButton extends StatelessWidget {
}
}
class EmojiPickerButton extends StatefulWidget {
final VoidCallback showEmojiPicker;
const EmojiPickerButton({
class AddEmojiButton extends StatelessWidget {
final VoidCallback onTap;
const AddEmojiButton({
super.key,
required this.showEmojiPicker,
required this.onTap,
});
@override
State<EmojiPickerButton> createState() => _EmojiPickerButtonState();
}
class _EmojiPickerButtonState extends State<EmojiPickerButton> {
@override
Widget build(BuildContext context) {
return SizedBox(
@ -250,7 +230,7 @@ class _EmojiPickerButtonState extends State<EmojiPickerButton> {
LocaleKeys.document_plugins_cover_addIcon.tr(),
),
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
onTap: widget.showEmojiPicker,
onTap: onTap,
margin: const EdgeInsets.all(4),
),
);
@ -258,14 +238,12 @@ class _EmojiPickerButtonState extends State<EmojiPickerButton> {
}
class RemoveEmojiButton extends StatelessWidget {
final VoidCallback onRemoved;
RemoveEmojiButton({
final VoidCallback onTap;
const RemoveEmojiButton({
super.key,
required this.onRemoved,
required this.onTap,
});
final popoverController = PopoverController();
@override
Widget build(BuildContext context) {
return SizedBox(
@ -276,20 +254,13 @@ class RemoveEmojiButton extends StatelessWidget {
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
onTap: onRemoved,
onTap: onTap,
margin: const EdgeInsets.all(4),
),
);
}
}
Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) {
return EmojiSelectionMenu(
onSubmitted: onSubmitted,
onExit: () {},
);
}
class RowActionButton extends StatelessWidget {
final RowController rowController;
const RowActionButton({super.key, required this.rowController});
@ -308,3 +279,34 @@ class RowActionButton extends StatelessWidget {
);
}
}
class _TitleSkin extends IEditableTextCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TextCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
maxLines: null,
autofocus: true,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 28),
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: LocaleKeys.grid_row_titlePlaceholder.tr(),
isDense: true,
isCollapsed: true,
),
onChanged: (text) => bloc.add(TextCellEvent.updateText(text.trim())),
);
}
}

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/row_document.dart';
@ -8,32 +8,29 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'cell_builder.dart';
import '../cell/editable_cell_builder.dart';
import 'row_banner.dart';
import 'row_property.dart';
class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
final FieldController fieldController;
final DatabaseController databaseController;
final RowController rowController;
final GridCellBuilder cellBuilder;
const RowDetailPage({
super.key,
required this.fieldController,
required this.rowController,
required this.cellBuilder,
required this.databaseController,
});
@override
State<RowDetailPage> createState() => _RowDetailPageState();
static String identifier() {
return (RowDetailPage).toString();
}
}
class _RowDetailPageState extends State<RowDetailPage> {
final scrollController = ScrollController();
late final cellBuilder = EditableCellBuilder(
databaseController: widget.databaseController,
);
@override
void dispose() {
@ -47,9 +44,10 @@ class _RowDetailPageState extends State<RowDetailPage> {
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
RowDetailBloc(rowController: widget.rowController)
..add(const RowDetailEvent.initial()),
create: (context) => RowDetailBloc(
fieldController: widget.databaseController.fieldController,
rowController: widget.rowController,
),
),
BlocProvider.value(
value: getIt<ReminderBloc>(),
@ -60,15 +58,15 @@ class _RowDetailPageState extends State<RowDetailPage> {
children: [
RowBanner(
rowController: widget.rowController,
cellBuilder: widget.cellBuilder,
cellBuilder: cellBuilder,
),
const VSpace(16),
Padding(
padding: const EdgeInsets.only(left: 40, right: 60),
child: RowPropertyList(
cellBuilder: widget.cellBuilder,
viewId: widget.rowController.viewId,
fieldController: widget.fieldController,
cellBuilder: cellBuilder,
viewId: widget.databaseController.viewId,
fieldController: widget.databaseController.fieldController,
),
),
const VSpace(20),
@ -80,7 +78,6 @@ class _RowDetailPageState extends State<RowDetailPage> {
RowDocument(
viewId: widget.rowController.viewId,
rowId: widget.rowController.rowId,
scrollController: scrollController,
),
],
),

View File

@ -15,12 +15,10 @@ class RowDocument extends StatelessWidget {
super.key,
required this.viewId,
required this.rowId,
required this.scrollController,
});
final String viewId;
final String rowId;
final ScrollController scrollController;
@override
Widget build(BuildContext context) {
@ -39,7 +37,6 @@ class RowDocument extends StatelessWidget {
),
finish: () => RowEditor(
viewPB: state.viewPB!,
scrollController: scrollController,
onIsEmptyChanged: (isEmpty) => context
.read<RowDocumentBloc>()
.add(RowDocumentEvent.updateIsEmpty(isEmpty)),
@ -55,12 +52,10 @@ class RowEditor extends StatefulWidget {
const RowEditor({
super.key,
required this.viewPB,
required this.scrollController,
this.onIsEmptyChanged,
});
final ViewPB viewPB;
final ScrollController scrollController;
final void Function(bool)? onIsEmptyChanged;
@override
@ -119,7 +114,7 @@ class _RowEditorState extends State<RowEditor> {
shrinkWrap: true,
autoFocus: false,
editorState: editorState,
scrollController: widget.scrollController,
// scrollController: widget.scrollController,
styleCustomizer: EditorStyleCustomizer(
context: context,
padding: const EdgeInsets.only(left: 16, right: 54),

Some files were not shown because too many files have changed in this diff Show More