mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
@ -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,
|
||||
});
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CardSizes {
|
||||
static EdgeInsets get cardCellPadding => const EdgeInsets.all(4);
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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 [];
|
||||
}
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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!);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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';
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
@ -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));
|
||||
|
@ -339,7 +339,7 @@ class DateCellEditorBloc
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.startListening(
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (cell) {
|
||||
if (!isClosed) {
|
||||
add(DateCellEditorEvent.didReceiveCellUpdate(cell));
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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() ?? "",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 ?? [],
|
||||
|
@ -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(
|
||||
|
@ -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();
|
||||
},
|
||||
);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
@ -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));
|
||||
|
@ -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())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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
Reference in New Issue
Block a user