diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index 97b0d162ef..a051401446 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -177,7 +177,8 @@ }, "row": { "duplicate": "Duplicate", - "delete": "Delete" + "delete": "Delete", + "textPlaceholder": "Empty" }, "selectOption": { "purpleColor": "Purple", diff --git a/frontend/app_flowy/lib/startup/deps_resolver.dart b/frontend/app_flowy/lib/startup/deps_resolver.dart index 2fd03d639f..8f239f1ece 100644 --- a/frontend/app_flowy/lib/startup/deps_resolver.dart +++ b/frontend/app_flowy/lib/startup/deps_resolver.dart @@ -170,7 +170,6 @@ void _resolveGridDeps(GetIt getIt) { getIt.registerFactoryParam( (cellData, _) => TextCellBloc( - service: CellService(), cellData: cellData, ), ); @@ -189,7 +188,7 @@ void _resolveGridDeps(GetIt getIt) { getIt.registerFactoryParam( (cellData, _) => DateCellBloc( - cellIdentifier: cellData, + cellData: cellData, ), ); diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart index 6d2935458a..880267f6c9 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart @@ -11,13 +11,13 @@ part 'checkbox_cell_bloc.freezed.dart'; class CheckboxCellBloc extends Bloc { final CellService _service; - final CellListener _listener; + final CellListener _cellListener; CheckboxCellBloc({ required CellService service, required GridCell cellData, }) : _service = service, - _listener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id), + _cellListener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id), super(CheckboxCellState.initial(cellData)) { on( (event, emit) async { @@ -38,18 +38,18 @@ class CheckboxCellBloc extends Bloc { @override Future close() async { - await _listener.stop(); + await _cellListener.stop(); return super.close(); } void _startListening() { - _listener.updateCellNotifier?.addPublishListener((result) { + _cellListener.updateCellNotifier?.addPublishListener((result) { result.fold( (notificationData) async => await _loadCellData(), (err) => Log.error(err), ); }); - _listener.start(); + _cellListener.start(); } Future _loadCellData() async { diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart index 532b67ae3a..cbabe6c5fe 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart @@ -15,11 +15,11 @@ class DateCellBloc extends Bloc { final CellListener _cellListener; final SingleFieldListener _fieldListener; - DateCellBloc({required GridCell cellIdentifier}) + DateCellBloc({required GridCell cellData}) : _service = CellService(), - _cellListener = CellListener(rowId: cellIdentifier.rowId, fieldId: cellIdentifier.field.id), - _fieldListener = SingleFieldListener(fieldId: cellIdentifier.field.id), - super(DateCellState.initial(cellIdentifier)) { + _cellListener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id), + _fieldListener = SingleFieldListener(fieldId: cellData.field.id), + super(DateCellState.initial(cellData)) { on( (event, emit) async { event.map( diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart index ec78136dd3..f32c066c5e 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart @@ -1,22 +1,29 @@ import 'package:app_flowy/workspace/application/grid/row/row_service.dart'; +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Cell; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; +import 'cell_listener.dart'; import 'cell_service.dart'; part 'text_cell_bloc.freezed.dart'; class TextCellBloc extends Bloc { - final CellService service; + final CellService _service; + final CellListener _cellListener; TextCellBloc({ - required this.service, required GridCell cellData, - }) : super(TextCellState.initial(cellData)) { + }) : _service = CellService(), + _cellListener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id), + super(TextCellState.initial(cellData)) { on( (event, emit) async { await event.map( - initial: (_InitialCell value) async {}, + initial: (_InitialCell value) async { + _startListening(); + }, updateText: (_UpdateText value) { updateCellContent(value.text); emit(state.copyWith(content: value.text)); @@ -27,16 +34,28 @@ class TextCellBloc extends Bloc { content: value.cellData.cell?.content ?? "", )); }, + didReceiveCellUpdate: (_DidReceiveCellUpdate value) { + emit(state.copyWith( + cellData: state.cellData.copyWith(cell: value.cell), + content: value.cell.content, + )); + }, ); }, ); } + @override + Future close() async { + await _cellListener.stop(); + return super.close(); + } + void updateCellContent(String content) { final fieldId = state.cellData.field.id; final gridId = state.cellData.gridId; final rowId = state.cellData.rowId; - service.updateCell( + _service.updateCell( data: content, fieldId: fieldId, gridId: gridId, @@ -44,9 +63,29 @@ class TextCellBloc extends Bloc { ); } - @override - Future close() async { - return super.close(); + void _startListening() { + _cellListener.updateCellNotifier?.addPublishListener((result) { + result.fold( + (notificationData) async => await _loadCellData(), + (err) => Log.error(err), + ); + }); + _cellListener.start(); + } + + Future _loadCellData() async { + final result = await _service.getCell( + gridId: state.cellData.gridId, + fieldId: state.cellData.field.id, + rowId: state.cellData.rowId, + ); + if (isClosed) { + return; + } + result.fold( + (cell) => add(TextCellEvent.didReceiveCellUpdate(cell)), + (err) => Log.error(err), + ); } } @@ -54,6 +93,7 @@ class TextCellBloc extends Bloc { class TextCellEvent with _$TextCellEvent { const factory TextCellEvent.initial() = _InitialCell; const factory TextCellEvent.didReceiveCellData(GridCell cellData) = _DidReceiveCellData; + const factory TextCellEvent.didReceiveCellUpdate(Cell cell) = _DidReceiveCellUpdate; const factory TextCellEvent.updateText(String text) = _UpdateText; } diff --git a/frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart index e93f0be89b..1ba10f7614 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart @@ -22,8 +22,11 @@ class RowDetailBloc extends Bloc { await event.map( initial: (_Initial value) async { await _startListening(); + _loadCellData(); + }, + didReceiveCellDatas: (_DidReceiveCellDatas value) { + emit(state.copyWith(cellDatas: value.cellDatas)); }, - didReceiveCellDatas: (_DidReceiveCellDatas value) {}, ); }, ); @@ -40,16 +43,16 @@ class RowDetailBloc extends Bloc { Future _startListening() async { _rowListenFn = _rowCache.addRowListener( rowId: rowData.rowId, - onUpdated: (cellDatas) => add(RowDetailEvent.didReceiveCellDatas(cellDatas)), + onUpdated: (cellDatas) => add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())), listenWhen: () => !isClosed, ); } - Future _loadRow(Emitter emit) async { + Future _loadCellData() async { final data = _rowCache.loadCellData(rowData.rowId); - data.foldRight(null, (cellDatas, _) { + data.foldRight(null, (cellDataMap, _) { if (!isClosed) { - add(RowDetailEvent.didReceiveCellDatas(cellDatas)); + add(RowDetailEvent.didReceiveCellDatas(cellDataMap.values.toList())); } }); } @@ -58,16 +61,16 @@ class RowDetailBloc extends Bloc { @freezed class RowDetailEvent with _$RowDetailEvent { const factory RowDetailEvent.initial() = _Initial; - const factory RowDetailEvent.didReceiveCellDatas(CellDataMap cellData) = _DidReceiveCellDatas; + const factory RowDetailEvent.didReceiveCellDatas(List cellDatas) = _DidReceiveCellDatas; } @freezed class RowDetailState with _$RowDetailState { const factory RowDetailState({ - required Option cellDataMap, + required List cellDatas, }) = _RowDetailState; factory RowDetailState.initial() => RowDetailState( - cellDataMap: none(), + cellDatas: List.empty(), ); } diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart index 214dc7a198..a7de233006 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart @@ -82,16 +82,14 @@ class CreateItem extends StatelessWidget { return FlowyHover( style: config, - builder: (context, onHover) { - return GestureDetector( - onTap: () => onSelected(pluginBuilder), - child: FlowyText.medium( - pluginBuilder.menuName, - color: theme.textColor, - fontSize: 12, - ).padding(horizontal: 10, vertical: 6), - ); - }, + child: GestureDetector( + onTap: () => onSelected(pluginBuilder), + child: FlowyText.medium( + pluginBuilder.menuName, + color: theme.textColor, + fontSize: 12, + ).padding(horizontal: 10, vertical: 6), + ), ); } } diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart index 41ba6ab4d1..d028355ed1 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart @@ -95,9 +95,11 @@ class _MenuAppState extends State { Widget _renderViewSection(AppDataNotifier notifier) { return MultiProvider( providers: [ChangeNotifierProvider.value(value: notifier)], - child: Consumer(builder: (context, AppDataNotifier notifier, child) { - return ViewSection(appData: notifier); - }), + child: Consumer( + builder: (context, AppDataNotifier notifier, child) { + return ViewSection(appData: notifier); + }, + ), ); } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart index b3d35977b8..0fc69efc42 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart @@ -1,6 +1,5 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/workspace/application/grid/grid_bloc.dart'; -import 'package:app_flowy/workspace/application/grid/row/row_bloc.dart'; import 'package:app_flowy/workspace/application/grid/row/row_service.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; @@ -214,8 +213,7 @@ class _GridRowsState extends State<_GridRows> { key: _key, initialItemCount: context.read().state.rows.length, itemBuilder: (BuildContext context, int index, Animation animation) { - final rowData = context.read().state.rows[index]; - return _renderRow(context, rowData, animation); + return _renderRow(context, state.rows[index], animation); }, ); }, diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart index 94f2e0da70..3304de6e51 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart @@ -1,13 +1,17 @@ import 'package:app_flowy/workspace/application/grid/row/row_service.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType; import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; import 'checkbox_cell.dart'; import 'date_cell.dart'; import 'number_cell.dart'; import 'selection_cell/selection_cell.dart'; import 'text_cell.dart'; -Widget buildGridCell(GridCell cellData) { +GridCellWidget buildGridCell(GridCell cellData, {GridCellStyle? style}) { final key = ValueKey(cellData.field.id + cellData.rowId); switch (cellData.field.fieldType) { case FieldType.Checkbox: @@ -19,7 +23,7 @@ Widget buildGridCell(GridCell cellData) { case FieldType.Number: return NumberCell(cellData: cellData, key: key); case FieldType.RichText: - return GridTextCell(cellData: cellData, key: key); + return GridTextCell(cellData: cellData, key: key, style: style); case FieldType.SingleSelect: return SingleSelectCell(cellData: cellData, key: key); default: @@ -35,3 +39,116 @@ class BlankCell extends StatelessWidget { return Container(); } } + +abstract class GridCellWidget extends HoverWidget { + @override + final ValueNotifier onFocus = ValueNotifier(false); + GridCellWidget({Key? key}) : super(key: key); +} + +abstract class B { + ValueNotifier get onFocus; +} + +abstract class GridCellStyle {} + +// +abstract class HoverWidget extends StatefulWidget { + const HoverWidget({Key? key}) : super(key: key); + + ValueNotifier get onFocus; +} + +class FlowyHover2 extends StatefulWidget { + final HoverWidget child; + const FlowyHover2({required this.child, Key? key}) : super(key: key); + + @override + State createState() => _FlowyHover2State(); +} + +class _FlowyHover2State extends State { + late FlowyHoverState _hoverState; + + @override + void initState() { + _hoverState = FlowyHoverState(); + widget.child.onFocus.addListener(() { + _hoverState.onFocus = widget.child.onFocus.value; + }); + super.initState(); + } + + @override + void dispose() { + _hoverState.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: _hoverState, + child: MouseRegion( + cursor: SystemMouseCursors.click, + opaque: false, + onEnter: (p) => setState(() => _hoverState.onHover = true), + onExit: (p) => setState(() => _hoverState.onHover = false), + child: Stack( + fit: StackFit.expand, + alignment: AlignmentDirectional.center, + children: [ + const _HoverBackground(), + widget.child, + ], + ), + ), + ); + } +} + +class _HoverBackground extends StatelessWidget { + const _HoverBackground({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + return Consumer( + builder: (context, state, child) { + if (state.onHover || state.onFocus) { + return FlowyHoverContainer( + style: HoverStyle( + borderRadius: Corners.s6Border, + hoverColor: theme.shader6, + ), + ); + } else { + return const SizedBox(); + } + }, + ); + } +} + +class FlowyHoverState extends ChangeNotifier { + bool _onHover = false; + bool _onFocus = false; + + set onHover(bool value) { + if (_onHover != value) { + _onHover = value; + notifyListeners(); + } + } + + bool get onHover => _onHover; + + set onFocus(bool value) { + if (_onFocus != value) { + _onFocus = value; + notifyListeners(); + } + } + + bool get onFocus => _onFocus; +} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart index 243edae3a9..17c1e87710 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart @@ -1,8 +1,8 @@ import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; - import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; +import 'cell_builder.dart'; class CellStateNotifier extends ChangeNotifier { bool _isFocus = false; @@ -28,7 +28,7 @@ class CellStateNotifier extends ChangeNotifier { } class CellContainer extends StatelessWidget { - final Widget child; + final GridCellWidget child; final Widget? expander; final double width; const CellContainer({ @@ -46,6 +46,9 @@ class CellContainer extends StatelessWidget { selector: (context, notifier) => notifier.isFocus, builder: (context, isFocus, _) { Widget container = Center(child: child); + child.onFocus.addListener(() { + Provider.of(context, listen: false).isFocus = child.onFocus.value; + }); if (expander != null) { container = _CellEnterRegion(child: container, expander: expander!); @@ -75,16 +78,16 @@ class CellContainer extends StatelessWidget { } class _CellEnterRegion extends StatelessWidget { - final Widget expander; final Widget child; - const _CellEnterRegion({required this.expander, required this.child, Key? key}) : super(key: key); + final Widget expander; + const _CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Selector( selector: (context, notifier) => notifier.onEnter, builder: (context, onEnter, _) { - List children = [child]; + List children = [Expanded(child: child)]; if (onEnter) { children.add(expander); } @@ -93,8 +96,8 @@ class _CellEnterRegion extends StatelessWidget { cursor: SystemMouseCursors.click, onEnter: (p) => Provider.of(context, listen: false).onEnter = true, onExit: (p) => Provider.of(context, listen: false).onEnter = false, - child: Stack( - alignment: AlignmentDirectional.centerEnd, + child: Row( + // alignment: AlignmentDirectional.centerEnd, children: children, ), ); @@ -102,35 +105,3 @@ class _CellEnterRegion extends StatelessWidget { ); } } - -abstract class GridCellWidget extends StatefulWidget { - const GridCellWidget({Key? key}) : super(key: key); - - void setFocus(BuildContext context, bool value) { - Provider.of(context, listen: false).isFocus = value; - } -} - -class CellFocusNode extends FocusNode { - VoidCallback? focusCallback; - - void addCallback(BuildContext context, VoidCallback callback) { - if (focusCallback != null) { - removeListener(focusCallback!); - } - focusCallback = () { - Provider.of(context, listen: false).isFocus = hasFocus; - callback(); - }; - - addListener(focusCallback!); - } - - @override - void dispose() { - if (focusCallback != null) { - removeListener(focusCallback!); - } - super.dispose(); - } -} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart index cbf8d0fadb..608913d41d 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart @@ -4,11 +4,12 @@ import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'cell_builder.dart'; -class CheckboxCell extends StatefulWidget { +class CheckboxCell extends GridCellWidget { final GridCell cellData; - const CheckboxCell({ + CheckboxCell({ required this.cellData, Key? key, }) : super(key: key); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell.dart index dcd2b2f5c2..84d60c797a 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell.dart @@ -1,17 +1,22 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/workspace/application/grid/prelude.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:table_calendar/table_calendar.dart'; +import 'cell_builder.dart'; + +abstract class GridCellDelegate { + void onFocus(bool isFocus); + GridCellDelegate get delegate; +} class DateCell extends GridCellWidget { final GridCell cellData; - const DateCell({ + DateCell({ required this.cellData, Key? key, }) : super(key: key); @@ -39,13 +44,13 @@ class _DateCellState extends State { child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - widget.setFocus(context, true); + widget.onFocus.value = true; _CellCalendar.show( context, onSelected: (day) { context.read().add(DateCellEvent.selectDay(day)); }, - onDismissed: () => widget.setFocus(context, false), + onDismissed: () => widget.onFocus.value = false, ); }, child: MouseRegion( diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart index 09331e49b5..2d98aa0bd6 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart @@ -2,14 +2,15 @@ import 'dart:async'; import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/workspace/application/grid/prelude.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'cell_builder.dart'; + class NumberCell extends GridCellWidget { final GridCell cellData; - const NumberCell({ + NumberCell({ required this.cellData, Key? key, }) : super(key: key); @@ -21,21 +22,23 @@ class NumberCell extends GridCellWidget { class _NumberCellState extends State { late NumberCellBloc _cellBloc; late TextEditingController _controller; - late CellFocusNode _focusNode; + late FocusNode _focusNode; Timer? _delayOperation; @override void initState() { _cellBloc = getIt(param1: widget.cellData)..add(const NumberCellEvent.initial()); _controller = TextEditingController(text: _cellBloc.state.content); - _focusNode = CellFocusNode(); + _focusNode = FocusNode(); + _focusNode.addListener(() { + widget.onFocus.value = _focusNode.hasFocus; + focusChanged(); + }); super.initState(); } @override Widget build(BuildContext context) { - _focusNode.addCallback(context, focusChanged); - return BlocProvider.value( value: _cellBloc, child: BlocConsumer( diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_cell.dart index 1261a4e5f4..05dd66e224 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_cell.dart @@ -1,6 +1,6 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/workspace/application/grid/prelude.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -10,7 +10,7 @@ import 'selection_editor.dart'; class SingleSelectCell extends GridCellWidget { final GridCell cellData; - const SingleSelectCell({ + SingleSelectCell({ required this.cellData, Key? key, }) : super(key: key); @@ -38,13 +38,13 @@ class _SingleSelectCellState extends State { return SizedBox.expand( child: InkWell( onTap: () { - widget.setFocus(context, true); + widget.onFocus.value = true; SelectOptionCellEditor.show( context, state.cellData, state.options, state.selectedOptions, - () => widget.setFocus(context, false), + () => widget.onFocus.value = false, ); }, child: ClipRRect(child: Row(children: children)), @@ -66,7 +66,7 @@ class _SingleSelectCellState extends State { class MultiSelectCell extends GridCellWidget { final GridCell cellData; - const MultiSelectCell({ + MultiSelectCell({ required this.cellData, Key? key, }) : super(key: key); @@ -94,13 +94,13 @@ class _MultiSelectCellState extends State { return SizedBox.expand( child: InkWell( onTap: () { - widget.setFocus(context, true); + widget.onFocus.value = true; SelectOptionCellEditor.show( context, state.cellData, state.options, state.selectedOptions, - () => widget.setFocus(context, false), + () => widget.onFocus.value = false, ); }, child: ClipRRect(child: Row(children: children)), diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart index 93fb4b15e7..688f8344f7 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart @@ -1,16 +1,39 @@ import 'dart:async'; -import 'package:app_flowy/startup/startup.dart'; -import 'package:app_flowy/workspace/application/grid/prelude.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'cell_container.dart'; +import 'package:app_flowy/startup/startup.dart'; +import 'package:app_flowy/workspace/application/grid/prelude.dart'; +import 'cell_builder.dart'; + +class GridTextCellStyle extends GridCellStyle { + String? placeholder; + Color? hoverColor; + bool filled; + InputBorder? inputBorder; + EdgeInsets? contentPadding; + GridTextCellStyle({ + this.placeholder, + this.hoverColor, + this.filled = false, + this.inputBorder, + this.contentPadding, + }); +} class GridTextCell extends GridCellWidget { final GridCell cellData; - const GridTextCell({ + late final GridTextCellStyle? cellStyle; + GridTextCell({ required this.cellData, + GridCellStyle? style, Key? key, - }) : super(key: key); + }) : super(key: key) { + if (style != null) { + cellStyle = (style as GridTextCellStyle); + } else { + cellStyle = null; + } + } @override State createState() => _GridTextCellState(); @@ -19,21 +42,25 @@ class GridTextCell extends GridCellWidget { class _GridTextCellState extends State { late TextCellBloc _cellBloc; late TextEditingController _controller; - late CellFocusNode _focusNode; + late FocusNode _focusNode; + Timer? _delayOperation; @override void initState() { _cellBloc = getIt(param1: widget.cellData); + _cellBloc.add(const TextCellEvent.initial()); _controller = TextEditingController(text: _cellBloc.state.content); - _focusNode = CellFocusNode(); + _focusNode = FocusNode(); + _focusNode.addListener(() { + widget.onFocus.value = _focusNode.hasFocus; + focusChanged(); + }); super.initState(); } @override Widget build(BuildContext context) { - _focusNode.addCallback(context, focusChanged); - return BlocProvider.value( value: _cellBloc, child: BlocConsumer( @@ -42,6 +69,7 @@ class _GridTextCellState extends State { _controller.text = state.content; } }, + buildWhen: (previous, current) => previous.content != current.content, builder: (context, state) { return TextField( controller: _controller, @@ -50,9 +78,13 @@ class _GridTextCellState extends State { onEditingComplete: () => _focusNode.unfocus(), maxLines: 1, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), - decoration: const InputDecoration( - contentPadding: EdgeInsets.zero, - border: InputBorder.none, + decoration: InputDecoration( + contentPadding: widget.cellStyle?.contentPadding ?? EdgeInsets.zero, + border: widget.cellStyle?.inputBorder ?? InputBorder.none, + hintText: widget.cellStyle?.placeholder, + hoverColor: widget.cellStyle?.hoverColor ?? Colors.transparent, + filled: widget.cellStyle?.filled ?? false, + fillColor: Colors.transparent, isDense: true, ), ); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart index c6009b64af..aa45487fe2 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart @@ -7,6 +7,7 @@ import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Field; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'field_type_extension.dart'; @@ -20,21 +21,21 @@ class GridFieldCell extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = context.watch(); - return BlocProvider( create: (context) => FieldCellBloc(cellContext: cellContext)..add(const FieldCellEvent.initial()), child: BlocBuilder( builder: (context, state) { - final button = FlowyButton( - hoverColor: theme.shader6, + final button = FieldCellButton( + field: state.field, onTap: () => _showActionSheet(context), - leftIcon: svgWidget(state.field.fieldType.iconName(), color: theme.iconColor), - text: FlowyText.medium(state.field.name, fontSize: 12), - padding: GridSize.cellContentInsets, ); - const line = Positioned(top: 0, bottom: 0, right: 0, child: _DragToExpandLine()); + const line = Positioned( + top: 0, + bottom: 0, + right: 0, + child: _DragToExpandLine(), + ); return _CellContainer( width: state.field.width.toDouble(), @@ -125,9 +126,31 @@ class _DragToExpandLine extends StatelessWidget { borderRadius: BorderRadius.zero, contentMargin: const EdgeInsets.only(left: 5), ), - builder: (_, onHover) => const SizedBox(width: 2), + child: const SizedBox(width: 2), ), ), ); } } + +class FieldCellButton extends StatelessWidget { + final VoidCallback onTap; + final Field field; + const FieldCellButton({ + required this.field, + required this.onTap, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + return FlowyButton( + hoverColor: theme.shader6, + onTap: onTap, + leftIcon: svgWidget(field.fieldType.iconName(), color: theme.iconColor), + text: FlowyText.medium(field.name, fontSize: 12), + padding: GridSize.cellContentInsets, + ); + } +} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor.dart index f503877a4a..6efe439399 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor.dart @@ -2,12 +2,14 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/workspace/application/grid/field/field_editor_bloc.dart'; import 'package:app_flowy/workspace/application/grid/field/field_service.dart'; import 'package:app_flowy/workspace/application/grid/field/field_switch_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Field; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:app_flowy/generated/locale_keys.g.dart'; import 'field_name_input.dart'; import 'field_switcher.dart'; @@ -70,7 +72,7 @@ class _FieldEditorWidget extends StatelessWidget { (field) => ListView( shrinkWrap: true, children: [ - const FlowyText.medium("Edit property", fontSize: 12), + FlowyText.medium(LocaleKeys.grid_field_editProperty.tr(), fontSize: 12), const VSpace(10), const _FieldNameTextField(), const VSpace(10), diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart index e41a4b484b..99e9f108cd 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart @@ -1,11 +1,19 @@ import 'package:app_flowy/workspace/application/grid/row/row_detail_bloc.dart'; import 'package:app_flowy/workspace/application/grid/row/row_service.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart'; +import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType; +import 'package:easy_localization/easy_localization.dart'; +import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:window_size/window_size.dart'; -class RowDetailPage extends StatelessWidget with FlowyOverlayDelegate { +class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { final GridRow rowData; final GridRowCache rowCache; @@ -16,24 +24,17 @@ class RowDetailPage extends StatelessWidget with FlowyOverlayDelegate { }) : super(key: key); @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => RowDetailBloc(rowData: rowData, rowCache: rowCache), - child: Container(), - ); - } + State createState() => _RowDetailPageState(); void show(BuildContext context) async { - FlowyOverlay.of(context).remove(identifier()); - - const size = Size(460, 400); final window = await getWindowInfo(); + final size = Size(window.frame.size.width * 0.7, window.frame.size.height * 0.7); FlowyOverlay.of(context).insertWithRect( widget: OverlayContainer( child: this, - constraints: BoxConstraints.tight(const Size(460, 400)), + constraints: BoxConstraints.tight(size), ), - identifier: identifier(), + identifier: RowDetailPage.identifier(), anchorPosition: Offset(-size.width / 2.0, -size.height / 2.0), anchorSize: window.frame.size, anchorDirection: AnchorDirection.center, @@ -46,3 +47,97 @@ class RowDetailPage extends StatelessWidget with FlowyOverlayDelegate { return (RowDetailPage).toString(); } } + +class _RowDetailPageState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + final bloc = RowDetailBloc(rowData: widget.rowData, rowCache: widget.rowCache); + bloc.add(const RowDetailEvent.initial()); + return bloc; + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 80, vertical: 40), + child: _PropertyList(), + ), + ); + } +} + +class _PropertyList extends StatelessWidget { + const _PropertyList({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous.cellDatas != current.cellDatas, + builder: (context, state) { + return ListView.separated( + itemCount: state.cellDatas.length, + itemBuilder: (BuildContext context, int index) { + return _RowDetailCell(cellData: state.cellDatas[index]); + }, + separatorBuilder: (BuildContext context, int index) { + return const VSpace(2); + }, + ); + }, + ); + } +} + +class _RowDetailCell extends StatelessWidget { + final GridCell cellData; + const _RowDetailCell({required this.cellData, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + final cell = buildGridCell( + cellData, + style: _buildCellStyle(theme, cellData.field.fieldType), + ); + return SizedBox( + height: 36, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 150, + child: FieldCellButton(field: cellData.field, onTap: () {}), + ), + const HSpace(10), + Expanded(child: FlowyHover2(child: cell)), + ], + ), + ); + } +} + +GridCellStyle? _buildCellStyle(AppTheme theme, FieldType fieldType) { + switch (fieldType) { + case FieldType.Checkbox: + return null; + case FieldType.DateTime: + return null; + case FieldType.MultiSelect: + return null; + case FieldType.Number: + return null; + case FieldType.RichText: + return GridTextCellStyle( + hoverColor: theme.shader6, + filled: true, + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), + ); + case FieldType.SingleSelect: + return null; + default: + return null; + } +} diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart index cc822a6ef9..cbcec14fee 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart @@ -86,29 +86,27 @@ class ActionCell extends StatelessWidget { return FlowyHover( style: HoverStyle(hoverColor: theme.hover), - builder: (context, onHover) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => onSelected(action), - child: SizedBox( - height: itemHeight, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (action.icon != null) action.icon!, - HSpace(ActionListSizes.itemHPadding), - FlowyText.medium( - action.name, - fontSize: 12, - ), - ], - ), - ).padding( - horizontal: ActionListSizes.padding, - vertical: ActionListSizes.padding, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => onSelected(action), + child: SizedBox( + height: itemHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (action.icon != null) action.icon!, + HSpace(ActionListSizes.itemHPadding), + FlowyText.medium( + action.name, + fontSize: 12, + ), + ], ), - ); - }, + ).padding( + horizontal: ActionListSizes.padding, + vertical: ActionListSizes.padding, + ), + ), ); } } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart index f8caf35b84..e7c7b183cf 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart @@ -1,17 +1,22 @@ import 'package:flutter/material.dart'; // ignore: unused_import import 'package:flowy_infra/time/duration.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:provider/provider.dart'; typedef HoverBuilder = Widget Function(BuildContext context, bool onHover); class FlowyHover extends StatefulWidget { final HoverStyle style; - final HoverBuilder builder; + final HoverBuilder? builder; + final Widget? child; final bool Function()? setSelected; const FlowyHover({ Key? key, - required this.builder, + this.builder, + this.child, required this.style, this.setSelected, }) : super(key: key); @@ -27,25 +32,27 @@ class _FlowyHoverState extends State { Widget build(BuildContext context) { return MouseRegion( cursor: SystemMouseCursors.click, + opaque: false, onEnter: (p) => setState(() => _onHover = true), onExit: (p) => setState(() => _onHover = false), - child: render(), + child: renderWidget(), ); } - Widget render() { + Widget renderWidget() { var showHover = _onHover; if (!showHover && widget.setSelected != null) { showHover = widget.setSelected!(); } + final child = widget.child ?? widget.builder!(context, _onHover); if (showHover) { return FlowyHoverContainer( style: widget.style, - child: widget.builder(context, _onHover), + child: child, ); } else { - return widget.builder(context, _onHover); + return child; } } } @@ -67,11 +74,11 @@ class HoverStyle { class FlowyHoverContainer extends StatelessWidget { final HoverStyle style; - final Widget child; + final Widget? child; const FlowyHoverContainer({ Key? key, - required this.child, + this.child, required this.style, }) : super(key: key); @@ -93,3 +100,104 @@ class FlowyHoverContainer extends StatelessWidget { ); } } + +// +abstract class HoverWidget extends StatefulWidget { + const HoverWidget({Key? key}) : super(key: key); + + ValueNotifier get onFocus; +} + +class FlowyHover2 extends StatefulWidget { + final HoverWidget child; + const FlowyHover2({required this.child, Key? key}) : super(key: key); + + @override + State createState() => _FlowyHover2State(); +} + +class _FlowyHover2State extends State { + late FlowyHoverState _hoverState; + + @override + void initState() { + _hoverState = FlowyHoverState(); + widget.child.onFocus.addListener(() { + _hoverState.onFocus = widget.child.onFocus.value; + }); + super.initState(); + } + + @override + void dispose() { + _hoverState.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: _hoverState, + child: MouseRegion( + cursor: SystemMouseCursors.click, + opaque: false, + onEnter: (p) => setState(() => _hoverState.onHover = true), + onExit: (p) => setState(() => _hoverState.onHover = false), + child: Stack( + fit: StackFit.expand, + alignment: AlignmentDirectional.center, + children: [ + const _HoverBackground(), + widget.child, + ], + ), + ), + ); + } +} + +class _HoverBackground extends StatelessWidget { + const _HoverBackground({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + return Consumer( + builder: (context, state, child) { + if (state.onHover || state.onFocus) { + return FlowyHoverContainer( + style: HoverStyle( + borderRadius: Corners.s6Border, + hoverColor: theme.shader6, + ), + ); + } else { + return const SizedBox(); + } + }, + ); + } +} + +class FlowyHoverState extends ChangeNotifier { + bool _onHover = false; + bool _onFocus = false; + + set onHover(bool value) { + if (_onHover != value) { + _onHover = value; + notifyListeners(); + } + } + + bool get onHover => _onHover; + + set onFocus(bool value) { + if (_onFocus != value) { + _onFocus = value; + notifyListeners(); + } + } + + bool get onFocus => _onFocus; +}