chore: add card container

This commit is contained in:
appflowy 2022-08-13 11:51:26 +08:00
parent 2282aa948e
commit 28e77ae68c
10 changed files with 364 additions and 56 deletions

View File

@ -108,8 +108,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
); );
} }
List<BoardColumnItem> _buildRows(List<RowPB> rows) { List<AFColumnItem> _buildRows(List<RowPB> rows) {
return rows.map((row) { final items = rows.map((row) {
// final rowInfo = RowInfo( // final rowInfo = RowInfo(
// gridId: _dataController.gridId, // gridId: _dataController.gridId,
// blockId: row.blockId, // blockId: row.blockId,
@ -120,6 +120,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
// ); // );
return BoardColumnItem(row: row); return BoardColumnItem(row: row);
}).toList(); }).toList();
return <AFColumnItem>[...items];
} }
Future<void> _loadGrid(Emitter<BoardState> emit) async { Future<void> _loadGrid(Emitter<BoardState> emit) async {

View File

@ -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<BoardCheckboxCellEvent, BoardCheckboxCellState> {
final GridCheckboxCellController cellController;
void Function()? _onCellChangedFn;
BoardCheckboxCellBloc({
required this.cellController,
}) : super(BoardCheckboxCellState.initial(cellController)) {
on<BoardCheckboxCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveCellUpdate: (cellData) {
emit(state.copyWith(isSelected: _isSelected(cellData)));
},
);
},
);
}
@override
Future<void> 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";
}

View File

@ -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<BoardURLCellEvent, BoardURLCellState> {
final GridURLCellController cellController;
void Function()? _onCellChangedFn;
BoardURLCellBloc({
required this.cellController,
}) : super(BoardURLCellState.initial(cellController)) {
on<BoardURLCellEvent>(
(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<void> 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 ?? "",
);
}
}

View File

@ -1,10 +1,12 @@
import 'package:app_flowy/plugins/board/application/card/card_bloc.dart'; 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/board/application/card/card_data_controller.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell_builder.dart'; import 'card_cell_builder.dart';
import 'card_container.dart';
class BoardCard extends StatefulWidget { class BoardCard extends StatefulWidget {
final String gridId; final String gridId;
@ -40,8 +42,10 @@ class _BoardCardState extends State<BoardCard> {
value: _cardBloc, value: _cardBloc,
child: BlocBuilder<BoardCardBloc, BoardCardState>( child: BlocBuilder<BoardCardBloc, BoardCardState>(
builder: (context, state) { builder: (context, state) {
return Padding( return BoardCardContainer(
padding: const EdgeInsets.all(8.0), accessoryBuilder: (context) {
return [const _CardMoreOption()];
},
child: Column( child: Column(
children: _makeCells(context, state.gridCellMap), children: _makeCells(context, state.gridCellMap),
), ),
@ -64,3 +68,17 @@ class _BoardCardState extends State<BoardCard> {
).toList(); ).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<AppTheme>().iconColor);
}
@override
void onTap(BuildContext context) {
print('show options');
}
}

View File

@ -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<CardAccessory> Function(
BuildContext buildContext,
);
class CardAccessoryContainer extends StatelessWidget {
final List<CardAccessory> accessories;
const CardAccessoryContainer({required this.accessories, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.read<AppTheme>();
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<CardAccessory> 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<Widget> 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;
}

View File

@ -8,6 +8,8 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'cell_builder.dart';
class GridCellAccessoryBuildContext { class GridCellAccessoryBuildContext {
final BuildContext anchorContext; final BuildContext anchorContext;
final bool isCellEditing; final bool isCellEditing;
@ -57,18 +59,6 @@ class PrimaryCellAccessory extends StatelessWidget with GridCellAccessory {
bool enable() => !isCellEditing; bool enable() => !isCellEditing;
} }
typedef AccessoryBuilder = List<GridCellAccessory> 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<bool>? get onAccessoryHover;
AccessoryBuilder? get accessoryBuilder;
}
class AccessoryHover extends StatefulWidget { class AccessoryHover extends StatefulWidget {
final CellAccessory child; final CellAccessory child;
final EdgeInsets contentPadding; final EdgeInsets contentPadding;

View File

@ -94,6 +94,18 @@ abstract class CellEditable {
ValueNotifier<bool> get onCellEditing; ValueNotifier<bool> get onCellEditing;
} }
typedef AccessoryBuilder = List<GridCellAccessory> 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<bool>? get onAccessoryHover;
AccessoryBuilder? get accessoryBuilder;
}
abstract class GridCellWidget extends StatefulWidget abstract class GridCellWidget extends StatefulWidget
implements CellAccessory, CellEditable, CellShortcuts { implements CellAccessory, CellEditable, CellShortcuts {
GridCellWidget({Key? key}) : super(key: key) { GridCellWidget({Key? key}) : super(key: key) {

View File

@ -25,24 +25,28 @@ class CellContainer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<RegionStateNotifier, return ChangeNotifierProxyProvider<RegionStateNotifier,
CellContainerNotifier>( _CellContainerNotifier>(
create: (_) => CellContainerNotifier(child), create: (_) => _CellContainerNotifier(child),
update: (_, rowStateNotifier, cellStateNotifier) => update: (_, rowStateNotifier, cellStateNotifier) =>
cellStateNotifier!..onEnter = rowStateNotifier.onEnter, cellStateNotifier!..onEnter = rowStateNotifier.onEnter,
child: Selector<CellContainerNotifier, bool>( child: Selector<_CellContainerNotifier, bool>(
selector: (context, notifier) => notifier.isFocus, selector: (context, notifier) => notifier.isFocus,
builder: (context, isFocus, _) { builder: (context, isFocus, _) {
Widget container = Center(child: GridCellShortcuts(child: child)); Widget container = Center(child: GridCellShortcuts(child: child));
if (accessoryBuilder != null) { if (accessoryBuilder != null) {
final accessories = accessoryBuilder!(GridCellAccessoryBuildContext( final accessories = accessoryBuilder!(
anchorContext: context, GridCellAccessoryBuildContext(
isCellEditing: isFocus, anchorContext: context,
)); isCellEditing: isFocus,
),
);
if (accessories.isNotEmpty) { if (accessories.isNotEmpty) {
container = container = _GridCellEnterRegion(
CellEnterRegion(child: container, accessories: accessories); child: container,
accessories: accessories,
);
} }
} }
@ -74,16 +78,16 @@ class CellContainer extends StatelessWidget {
} }
} }
class CellEnterRegion extends StatelessWidget { class _GridCellEnterRegion extends StatelessWidget {
final Widget child; final Widget child;
final List<GridCellAccessory> accessories; final List<GridCellAccessory> accessories;
const CellEnterRegion( const _GridCellEnterRegion(
{required this.child, required this.accessories, Key? key}) {required this.child, required this.accessories, Key? key})
: super(key: key); : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<CellContainerNotifier, bool>( return Selector<_CellContainerNotifier, bool>(
selector: (context, notifier) => notifier.onEnter, selector: (context, notifier) => notifier.onEnter,
builder: (context, onEnter, _) { builder: (context, onEnter, _) {
List<Widget> children = [child]; List<Widget> children = [child];
@ -95,10 +99,10 @@ class CellEnterRegion extends StatelessWidget {
return MouseRegion( return MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
onEnter: (p) => onEnter: (p) =>
Provider.of<CellContainerNotifier>(context, listen: false) Provider.of<_CellContainerNotifier>(context, listen: false)
.onEnter = true, .onEnter = true,
onExit: (p) => onExit: (p) =>
Provider.of<CellContainerNotifier>(context, listen: false) Provider.of<_CellContainerNotifier>(context, listen: false)
.onEnter = false, .onEnter = false,
child: Stack( child: Stack(
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
@ -111,13 +115,13 @@ class CellEnterRegion extends StatelessWidget {
} }
} }
class CellContainerNotifier extends ChangeNotifier { class _CellContainerNotifier extends ChangeNotifier {
final CellEditable cellEditable; final CellEditable cellEditable;
VoidCallback? _onCellFocusListener; VoidCallback? _onCellFocusListener;
bool _isFocus = false; bool _isFocus = false;
bool _onEnter = false; bool _onEnter = false;
CellContainerNotifier(this.cellEditable) { _CellContainerNotifier(this.cellEditable) {
_onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value; _onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value;
cellEditable.onCellFocus.addListener(_onCellFocusListener!); cellEditable.onCellFocus.addListener(_onCellFocusListener!);
} }

View File

@ -181,28 +181,27 @@ class RowContent extends StatelessWidget {
return gridCellMap.values.map( return gridCellMap.values.map(
(cellId) { (cellId) {
final GridCellWidget child = builder.build(cellId); final GridCellWidget child = builder.build(cellId);
accessoryBuilder(GridCellAccessoryBuildContext buildContext) {
final builder = child.accessoryBuilder;
List<GridCellAccessory> accessories = [];
if (cellId.field.isPrimary) {
accessories.add(PrimaryCellAccessory(
onTapCallback: onExpand,
isCellEditing: buildContext.isCellEditing,
));
}
if (builder != null) {
accessories.addAll(builder(buildContext));
}
return accessories;
}
return CellContainer( return CellContainer(
width: cellId.field.width.toDouble(), width: cellId.field.width.toDouble(),
child: child, child: child,
rowStateNotifier: rowStateNotifier:
Provider.of<RegionStateNotifier>(context, listen: false), Provider.of<RegionStateNotifier>(context, listen: false),
accessoryBuilder: accessoryBuilder, accessoryBuilder: (buildContext) {
final builder = child.accessoryBuilder;
List<GridCellAccessory> accessories = [];
if (cellId.field.isPrimary) {
accessories.add(PrimaryCellAccessory(
onTapCallback: onExpand,
isCellEditing: buildContext.isCellEditing,
));
}
if (builder != null) {
accessories.addAll(builder(buildContext));
}
return accessories;
},
); );
}, },
).toList(); ).toList();

View File

@ -23,18 +23,19 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
@override @override
void initState() { void initState() {
final column1 = AFBoardColumnData(id: "To Do", items: [ List<AFColumnItem> a = [
TextItem("Card 1"), TextItem("Card 1"),
TextItem("Card 2"), 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"), TextItem("Card 4"),
]); ];
final column2 = AFBoardColumnData(id: "In Progress", items: [ final column1 = AFBoardColumnData(id: "To Do", items: a);
RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), final column2 = AFBoardColumnData(id: "In Progress", items: <AFColumnItem>[
TextItem("Card 6"), // 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: <AFColumnItem>[]);
boardDataController.addColumn(column1); boardDataController.addColumn(column1);
boardDataController.addColumn(column2); boardDataController.addColumn(column2);