From 28e77ae68c0f6593c78d9acb2bd0160f9e1c2bf6 Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 13 Aug 2022 11:51:26 +0800 Subject: [PATCH] chore: add card container --- .../plugins/board/application/board_bloc.dart | 6 +- .../card/board_checkbox_cell_bloc.dart | 71 ++++++++++ .../application/card/board_url_cell_bloc.dart | 78 ++++++++++ .../plugins/board/presentation/card/card.dart | 24 +++- .../presentation/card/card_container.dart | 133 ++++++++++++++++++ .../widgets/cell/cell_accessory.dart | 14 +- .../widgets/cell/cell_builder.dart | 12 ++ .../widgets/cell/cell_container.dart | 36 ++--- .../presentation/widgets/row/grid_row.dart | 31 ++-- .../example/lib/multi_board_list_example.dart | 15 +- 10 files changed, 364 insertions(+), 56 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart create mode 100644 frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 37487a4eec..08b519c1a3 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -108,8 +108,8 @@ class BoardBloc extends Bloc { ); } - List _buildRows(List rows) { - return rows.map((row) { + List _buildRows(List rows) { + final items = rows.map((row) { // final rowInfo = RowInfo( // gridId: _dataController.gridId, // blockId: row.blockId, @@ -120,6 +120,8 @@ class BoardBloc extends Bloc { // ); return BoardColumnItem(row: row); }).toList(); + + return [...items]; } Future _loadGrid(Emitter emit) async { diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart new file mode 100644 index 0000000000..3834db112c --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart @@ -0,0 +1,71 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'board_checkbox_cell_bloc.freezed.dart'; + +class BoardCheckboxCellBloc + extends Bloc { + final GridCheckboxCellController cellController; + void Function()? _onCellChangedFn; + BoardCheckboxCellBloc({ + required this.cellController, + }) : super(BoardCheckboxCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + didReceiveCellUpdate: (cellData) { + emit(state.copyWith(isSelected: _isSelected(cellData))); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellContent) { + if (!isClosed) { + add(BoardCheckboxCellEvent.didReceiveCellUpdate(cellContent ?? "")); + } + }), + ); + } +} + +@freezed +class BoardCheckboxCellEvent with _$BoardCheckboxCellEvent { + const factory BoardCheckboxCellEvent.initial() = _InitialCell; + const factory BoardCheckboxCellEvent.didReceiveCellUpdate( + String cellContent) = _DidReceiveCellUpdate; +} + +@freezed +class BoardCheckboxCellState with _$BoardCheckboxCellState { + const factory BoardCheckboxCellState({ + required bool isSelected, + }) = _CheckboxCellState; + + factory BoardCheckboxCellState.initial(GridCellController context) { + return BoardCheckboxCellState( + isSelected: _isSelected(context.getCellData())); + } +} + +bool _isSelected(String? cellData) { + return cellData == "Yes"; +} diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart new file mode 100644 index 0000000000..045a1633fa --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart @@ -0,0 +1,78 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'board_url_cell_bloc.freezed.dart'; + +class BoardURLCellBloc extends Bloc { + final GridURLCellController cellController; + void Function()? _onCellChangedFn; + BoardURLCellBloc({ + required this.cellController, + }) : super(BoardURLCellState.initial(cellController)) { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + didReceiveCellUpdate: (cellData) { + emit(state.copyWith( + content: cellData?.content ?? "", + url: cellData?.url ?? "", + )); + }, + updateURL: (String url) { + cellController.saveCellData(url, deduplicate: true); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellData) { + if (!isClosed) { + add(BoardURLCellEvent.didReceiveCellUpdate(cellData)); + } + }), + ); + } +} + +@freezed +class BoardURLCellEvent with _$BoardURLCellEvent { + const factory BoardURLCellEvent.initial() = _InitialCell; + const factory BoardURLCellEvent.updateURL(String url) = _UpdateURL; + const factory BoardURLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) = + _DidReceiveCellUpdate; +} + +@freezed +class BoardURLCellState with _$BoardURLCellState { + const factory BoardURLCellState({ + required String content, + required String url, + }) = _BoardURLCellState; + + factory BoardURLCellState.initial(GridURLCellController context) { + final cellData = context.getCellData(); + return BoardURLCellState( + content: cellData?.content ?? "", + url: cellData?.url ?? "", + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 344c72a767..2c17d5e3c8 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -1,10 +1,12 @@ import 'package:app_flowy/plugins/board/application/card/card_bloc.dart'; import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - import 'card_cell_builder.dart'; +import 'card_container.dart'; class BoardCard extends StatefulWidget { final String gridId; @@ -40,8 +42,10 @@ class _BoardCardState extends State { value: _cardBloc, child: BlocBuilder( builder: (context, state) { - return Padding( - padding: const EdgeInsets.all(8.0), + return BoardCardContainer( + accessoryBuilder: (context) { + return [const _CardMoreOption()]; + }, child: Column( children: _makeCells(context, state.gridCellMap), ), @@ -64,3 +68,17 @@ class _BoardCardState extends State { ).toList(); } } + +class _CardMoreOption extends StatelessWidget with CardAccessory { + const _CardMoreOption({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return svgWidget('home/details', color: context.read().iconColor); + } + + @override + void onTap(BuildContext context) { + print('show options'); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart new file mode 100644 index 0000000000..13f3af2195 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart @@ -0,0 +1,133 @@ +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; + +class BoardCardContainer extends StatelessWidget { + final Widget child; + final CardAccessoryBuilder? accessoryBuilder; + const BoardCardContainer({ + required this.child, + this.accessoryBuilder, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => _CardContainerNotifier(), + child: Consumer<_CardContainerNotifier>( + builder: (context, notifier, _) { + Widget container = Center(child: child); + if (accessoryBuilder != null) { + final accessories = accessoryBuilder!(context); + if (accessories.isNotEmpty) { + container = _CardEnterRegion( + child: container, + accessories: accessories, + ); + } + } + return Padding( + padding: const EdgeInsets.all(8), + child: container, + ); + }, + ), + ); + } +} + +abstract class CardAccessory implements Widget { + void onTap(BuildContext context); +} + +typedef CardAccessoryBuilder = List Function( + BuildContext buildContext, +); + +class CardAccessoryContainer extends StatelessWidget { + final List accessories; + const CardAccessoryContainer({required this.accessories, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.read(); + final children = accessories.map((accessory) { + final hover = FlowyHover( + style: HoverStyle( + hoverColor: theme.hover, + backgroundColor: theme.surface, + ), + builder: (_, onHover) => Container( + width: 26, + height: 26, + padding: const EdgeInsets.all(3), + child: accessory, + ), + ); + return GestureDetector( + child: hover, + behavior: HitTestBehavior.opaque, + onTap: () => accessory.onTap(context), + ); + }).toList(); + + return Wrap(children: children, spacing: 6); + } +} + +class _CardEnterRegion extends StatelessWidget { + final Widget child; + final List accessories; + const _CardEnterRegion( + {required this.child, required this.accessories, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Selector<_CardContainerNotifier, bool>( + selector: (context, notifier) => notifier.onEnter, + builder: (context, onEnter, _) { + List children = [child]; + if (onEnter) { + children.add(CardAccessoryContainer(accessories: accessories) + .positioned(right: 0)); + } + + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (p) => + Provider.of<_CardContainerNotifier>(context, listen: false) + .onEnter = true, + onExit: (p) => + Provider.of<_CardContainerNotifier>(context, listen: false) + .onEnter = false, + child: IntrinsicHeight( + child: Stack( + alignment: AlignmentDirectional.center, + fit: StackFit.expand, + children: children, + )), + ); + }, + ); + } +} + +class _CardContainerNotifier extends ChangeNotifier { + bool _onEnter = false; + + _CardContainerNotifier(); + + set onEnter(bool value) { + if (_onEnter != value) { + _onEnter = value; + notifyListeners(); + } + } + + bool get onEnter => _onEnter; +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart index e41f9bc9ac..9b3f281130 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart @@ -8,6 +8,8 @@ import 'package:styled_widget/styled_widget.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'cell_builder.dart'; + class GridCellAccessoryBuildContext { final BuildContext anchorContext; final bool isCellEditing; @@ -57,18 +59,6 @@ class PrimaryCellAccessory extends StatelessWidget with GridCellAccessory { bool enable() => !isCellEditing; } -typedef AccessoryBuilder = List Function( - GridCellAccessoryBuildContext buildContext); - -abstract class CellAccessory extends Widget { - const CellAccessory({Key? key}) : super(key: key); - - // The hover will show if the isHover's value is true - ValueNotifier? get onAccessoryHover; - - AccessoryBuilder? get accessoryBuilder; -} - class AccessoryHover extends StatefulWidget { final CellAccessory child; final EdgeInsets contentPadding; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart index 6c3fa38bc1..0a7c3a48a7 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart @@ -94,6 +94,18 @@ abstract class CellEditable { ValueNotifier get onCellEditing; } +typedef AccessoryBuilder = List Function( + GridCellAccessoryBuildContext buildContext); + +abstract class CellAccessory extends Widget { + const CellAccessory({Key? key}) : super(key: key); + + // The hover will show if the isHover's value is true + ValueNotifier? get onAccessoryHover; + + AccessoryBuilder? get accessoryBuilder; +} + abstract class GridCellWidget extends StatefulWidget implements CellAccessory, CellEditable, CellShortcuts { GridCellWidget({Key? key}) : super(key: key) { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart index b2d174e3e2..ed09ec3f36 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart @@ -25,24 +25,28 @@ class CellContainer extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProxyProvider( - create: (_) => CellContainerNotifier(child), + _CellContainerNotifier>( + create: (_) => _CellContainerNotifier(child), update: (_, rowStateNotifier, cellStateNotifier) => cellStateNotifier!..onEnter = rowStateNotifier.onEnter, - child: Selector( + child: Selector<_CellContainerNotifier, bool>( selector: (context, notifier) => notifier.isFocus, builder: (context, isFocus, _) { Widget container = Center(child: GridCellShortcuts(child: child)); if (accessoryBuilder != null) { - final accessories = accessoryBuilder!(GridCellAccessoryBuildContext( - anchorContext: context, - isCellEditing: isFocus, - )); + final accessories = accessoryBuilder!( + GridCellAccessoryBuildContext( + anchorContext: context, + isCellEditing: isFocus, + ), + ); if (accessories.isNotEmpty) { - container = - CellEnterRegion(child: container, accessories: accessories); + container = _GridCellEnterRegion( + child: container, + accessories: accessories, + ); } } @@ -74,16 +78,16 @@ class CellContainer extends StatelessWidget { } } -class CellEnterRegion extends StatelessWidget { +class _GridCellEnterRegion extends StatelessWidget { final Widget child; final List accessories; - const CellEnterRegion( + const _GridCellEnterRegion( {required this.child, required this.accessories, Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Selector( + return Selector<_CellContainerNotifier, bool>( selector: (context, notifier) => notifier.onEnter, builder: (context, onEnter, _) { List children = [child]; @@ -95,10 +99,10 @@ class CellEnterRegion extends StatelessWidget { return MouseRegion( cursor: SystemMouseCursors.click, onEnter: (p) => - Provider.of(context, listen: false) + Provider.of<_CellContainerNotifier>(context, listen: false) .onEnter = true, onExit: (p) => - Provider.of(context, listen: false) + Provider.of<_CellContainerNotifier>(context, listen: false) .onEnter = false, child: Stack( alignment: AlignmentDirectional.center, @@ -111,13 +115,13 @@ class CellEnterRegion extends StatelessWidget { } } -class CellContainerNotifier extends ChangeNotifier { +class _CellContainerNotifier extends ChangeNotifier { final CellEditable cellEditable; VoidCallback? _onCellFocusListener; bool _isFocus = false; bool _onEnter = false; - CellContainerNotifier(this.cellEditable) { + _CellContainerNotifier(this.cellEditable) { _onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value; cellEditable.onCellFocus.addListener(_onCellFocusListener!); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart index 3864c1a6a0..c96b5e1526 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart @@ -181,28 +181,27 @@ class RowContent extends StatelessWidget { return gridCellMap.values.map( (cellId) { final GridCellWidget child = builder.build(cellId); - accessoryBuilder(GridCellAccessoryBuildContext buildContext) { - final builder = child.accessoryBuilder; - List accessories = []; - if (cellId.field.isPrimary) { - accessories.add(PrimaryCellAccessory( - onTapCallback: onExpand, - isCellEditing: buildContext.isCellEditing, - )); - } - - if (builder != null) { - accessories.addAll(builder(buildContext)); - } - return accessories; - } return CellContainer( width: cellId.field.width.toDouble(), child: child, rowStateNotifier: Provider.of(context, listen: false), - accessoryBuilder: accessoryBuilder, + accessoryBuilder: (buildContext) { + final builder = child.accessoryBuilder; + List accessories = []; + if (cellId.field.isPrimary) { + accessories.add(PrimaryCellAccessory( + onTapCallback: onExpand, + isCellEditing: buildContext.isCellEditing, + )); + } + + if (builder != null) { + accessories.addAll(builder(buildContext)); + } + return accessories; + }, ); }, ).toList(); diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 282e6028c5..83f75d2a0e 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -23,18 +23,19 @@ class _MultiBoardListExampleState extends State { @override void initState() { - final column1 = AFBoardColumnData(id: "To Do", items: [ + List a = [ TextItem("Card 1"), TextItem("Card 2"), - RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), + // RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), TextItem("Card 4"), - ]); - final column2 = AFBoardColumnData(id: "In Progress", items: [ - RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), - TextItem("Card 6"), + ]; + final column1 = AFBoardColumnData(id: "To Do", items: a); + final column2 = AFBoardColumnData(id: "In Progress", items: [ + // RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), + // TextItem("Card 6"), ]); - final column3 = AFBoardColumnData(id: "Done", items: []); + final column3 = AFBoardColumnData(id: "Done", items: []); boardDataController.addColumn(column1); boardDataController.addColumn(column2);