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) {
return rows.map((row) {
List<AFColumnItem> _buildRows(List<RowPB> rows) {
final items = rows.map((row) {
// final rowInfo = RowInfo(
// gridId: _dataController.gridId,
// blockId: row.blockId,
@ -120,6 +120,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
// );
return BoardColumnItem(row: row);
}).toList();
return <AFColumnItem>[...items];
}
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_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<BoardCard> {
value: _cardBloc,
child: BlocBuilder<BoardCardBloc, BoardCardState>(
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<BoardCard> {
).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: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<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 {
final CellAccessory child;
final EdgeInsets contentPadding;

View File

@ -94,6 +94,18 @@ abstract class CellEditable {
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
implements CellAccessory, CellEditable, CellShortcuts {
GridCellWidget({Key? key}) : super(key: key) {

View File

@ -25,24 +25,28 @@ class CellContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<RegionStateNotifier,
CellContainerNotifier>(
create: (_) => CellContainerNotifier(child),
_CellContainerNotifier>(
create: (_) => _CellContainerNotifier(child),
update: (_, rowStateNotifier, cellStateNotifier) =>
cellStateNotifier!..onEnter = rowStateNotifier.onEnter,
child: Selector<CellContainerNotifier, bool>(
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<GridCellAccessory> accessories;
const CellEnterRegion(
const _GridCellEnterRegion(
{required this.child, required this.accessories, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return Selector<CellContainerNotifier, bool>(
return Selector<_CellContainerNotifier, bool>(
selector: (context, notifier) => notifier.onEnter,
builder: (context, onEnter, _) {
List<Widget> children = [child];
@ -95,10 +99,10 @@ class CellEnterRegion extends StatelessWidget {
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (p) =>
Provider.of<CellContainerNotifier>(context, listen: false)
Provider.of<_CellContainerNotifier>(context, listen: false)
.onEnter = true,
onExit: (p) =>
Provider.of<CellContainerNotifier>(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!);
}

View File

@ -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<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(
width: cellId.field.width.toDouble(),
child: child,
rowStateNotifier:
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();

View File

@ -23,18 +23,19 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
@override
void initState() {
final column1 = AFBoardColumnData(id: "To Do", items: [
List<AFColumnItem> 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: <AFColumnItem>[
// 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(column2);