diff --git a/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md b/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md
index 119d25f89d..7cf059202f 100644
--- a/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md
+++ b/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md
@@ -1,3 +1,8 @@
+# 0.0.3
+* Support customize UI
+* Update example
+* Add AppFlowy style widget
+
## 0.0.2
* Update documentation
diff --git a/frontend/app_flowy/packages/appflowy_board/README.md b/frontend/app_flowy/packages/appflowy_board/README.md
index 922cdd01b2..893bc3ed64 100644
--- a/frontend/app_flowy/packages/appflowy_board/README.md
+++ b/frontend/app_flowy/packages/appflowy_board/README.md
@@ -6,30 +6,25 @@ The **appflowy_board** is a package that is used in [AppFlowy](https://github.co
**appflowy_board** will be a standard git repository when it becomes stable.
## Getting Started
+
+
+
```dart
@override
void initState() {
- final column1 = BoardColumnData(id: "1", items: [
- TextItem("a"),
- TextItem("b"),
- TextItem("c"),
- TextItem("d"),
+ final column1 = BoardColumnData(id: "To Do", items: [
+ TextItem("Card 1"),
+ TextItem("Card 2"),
+ TextItem("Card 3"),
+ TextItem("Card 4"),
]);
- final column2 = BoardColumnData(id: "2", items: [
- TextItem("1"),
- TextItem("2"),
- TextItem("3"),
- TextItem("4"),
- TextItem("5"),
+ final column2 = BoardColumnData(id: "In Progress", items: [
+ TextItem("Card 5"),
+ TextItem("Card 6"),
]);
- final column3 = BoardColumnData(id: "3", items: [
- TextItem("A"),
- TextItem("B"),
- TextItem("C"),
- TextItem("D"),
- ]);
+ final column3 = BoardColumnData(id: "Done", items: []);
boardDataController.addColumn(column1);
boardDataController.addColumn(column2);
@@ -40,25 +35,52 @@ The **appflowy_board** is a package that is used in [AppFlowy](https://github.co
@override
Widget build(BuildContext context) {
- return Board(
- dataController: boardDataController,
- background: Container(color: Colors.red),
- footBuilder: (context, columnData) {
- return Container(
- color: Colors.purple,
- height: 30,
- );
- },
- headerBuilder: (context, columnData) {
- return Container(
- color: Colors.yellow,
- height: 30,
- );
- },
- cardBuilder: (context, item) {
- return _RowWidget(item: item as TextItem, key: ObjectKey(item));
- },
- columnConstraints: const BoxConstraints.tightFor(width: 240),
+ final config = BoardConfig(
+ columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
+ );
+ return Container(
+ color: Colors.white,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
+ child: Board(
+ dataController: boardDataController,
+ footBuilder: (context, columnData) {
+ return AppFlowyColumnFooter(
+ icon: const Icon(Icons.add, size: 20),
+ title: const Text('New'),
+ height: 50,
+ margin: config.columnItemPadding,
+ );
+ },
+ headerBuilder: (context, columnData) {
+ return AppFlowyColumnHeader(
+ icon: const Icon(Icons.lightbulb_circle),
+ title: Text(columnData.id),
+ addIcon: const Icon(Icons.add, size: 20),
+ moreIcon: const Icon(Icons.more_horiz, size: 20),
+ height: 50,
+ margin: config.columnItemPadding,
+ );
+ },
+ cardBuilder: (context, item) {
+ final textItem = item as TextItem;
+ return AppFlowyColumnItemCard(
+ key: ObjectKey(item),
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Text(textItem.s),
+ ),
+ ),
+ );
+ },
+ columnConstraints: const BoxConstraints.tightFor(width: 240),
+ config: BoardConfig(
+ columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
+ ),
+ ),
+ ),
);
}
```
\ No newline at end of file
diff --git a/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_1.gif b/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_1.gif
new file mode 100644
index 0000000000..bf1345608e
Binary files /dev/null and b/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_1.gif differ
diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/main.dart
index 975cbb6ca8..c881370e03 100644
--- a/frontend/app_flowy/packages/appflowy_board/example/lib/main.dart
+++ b/frontend/app_flowy/packages/appflowy_board/example/lib/main.dart
@@ -32,7 +32,7 @@ class _MyAppState extends State {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
- title: const Text('FlowyBoard example'),
+ title: const Text('AppFlowy Board'),
),
body: _examples[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
@@ -43,10 +43,10 @@ class _MyAppState extends State {
items: [
BottomNavigationBarItem(
icon: Icon(Icons.grid_on, color: _bottomNavigationColor),
- label: "MultiBoardList"),
+ label: "MultiColumn"),
BottomNavigationBarItem(
icon: Icon(Icons.grid_on, color: _bottomNavigationColor),
- label: "SingleBoardList"),
+ label: "SingleColumn"),
],
onTap: (int index) {
setState(() {
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 8715a4450c..7fe24362d2 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,26 +23,18 @@ class _MultiBoardListExampleState extends State {
@override
void initState() {
- final column1 = BoardColumnData(id: "1", items: [
- TextItem("a"),
- TextItem("b"),
- TextItem("c"),
- TextItem("d"),
+ final column1 = BoardColumnData(id: "To Do", items: [
+ TextItem("Card 1"),
+ TextItem("Card 2"),
+ TextItem("Card 3"),
+ TextItem("Card 4"),
]);
- final column2 = BoardColumnData(id: "2", items: [
- TextItem("1"),
- TextItem("2"),
- TextItem("3"),
- TextItem("4"),
- TextItem("5"),
+ final column2 = BoardColumnData(id: "In Progress", items: [
+ TextItem("Card 5"),
+ TextItem("Card 6"),
]);
- final column3 = BoardColumnData(id: "3", items: [
- TextItem("A"),
- TextItem("B"),
- TextItem("C"),
- TextItem("D"),
- ]);
+ final column3 = BoardColumnData(id: "Done", items: []);
boardDataController.addColumn(column1);
boardDataController.addColumn(column2);
@@ -53,40 +45,52 @@ class _MultiBoardListExampleState extends State {
@override
Widget build(BuildContext context) {
- return Board(
- dataController: boardDataController,
- background: Container(color: Colors.red),
- footBuilder: (context, columnData) {
- return Container(
- color: Colors.purple,
- height: 30,
- );
- },
- headerBuilder: (context, columnData) {
- return Container(
- color: Colors.yellow,
- height: 30,
- );
- },
- cardBuilder: (context, item) {
- return _RowWidget(item: item as TextItem, key: ObjectKey(item));
- },
- columnConstraints: const BoxConstraints.tightFor(width: 240),
+ final config = BoardConfig(
+ columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
);
- }
-}
-
-class _RowWidget extends StatelessWidget {
- final TextItem item;
- const _RowWidget({Key? key, required this.item}) : super(key: key);
-
- @override
- Widget build(BuildContext context) {
return Container(
- key: ObjectKey(item),
- height: 60,
- color: Colors.green,
- child: Center(child: Text(item.s)),
+ color: Colors.white,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
+ child: Board(
+ dataController: boardDataController,
+ footBuilder: (context, columnData) {
+ return AppFlowyColumnFooter(
+ icon: const Icon(Icons.add, size: 20),
+ title: const Text('New'),
+ height: 50,
+ margin: config.columnItemPadding,
+ );
+ },
+ headerBuilder: (context, columnData) {
+ return AppFlowyColumnHeader(
+ icon: const Icon(Icons.lightbulb_circle),
+ title: Text(columnData.id),
+ addIcon: const Icon(Icons.add, size: 20),
+ moreIcon: const Icon(Icons.more_horiz, size: 20),
+ height: 50,
+ margin: config.columnItemPadding,
+ );
+ },
+ cardBuilder: (context, item) {
+ final textItem = item as TextItem;
+ return AppFlowyColumnItemCard(
+ key: ObjectKey(item),
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Text(textItem.s),
+ ),
+ ),
+ );
+ },
+ columnConstraints: const BoxConstraints.tightFor(width: 240),
+ config: BoardConfig(
+ columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
+ ),
+ ),
+ ),
);
}
}
@@ -99,3 +103,12 @@ class TextItem extends ColumnItem {
@override
String get id => s;
}
+
+extension HexColor on Color {
+ static Color fromHex(String hexString) {
+ final buffer = StringBuffer();
+ if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
+ buffer.write(hexString.replaceFirst('#', ''));
+ return Color(int.parse(buffer.toString(), radix: 16));
+ }
+}
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/appflowy_board.dart b/frontend/app_flowy/packages/appflowy_board/lib/appflowy_board.dart
index 684868a2a3..fc8f3c662f 100644
--- a/frontend/app_flowy/packages/appflowy_board/lib/appflowy_board.dart
+++ b/frontend/app_flowy/packages/appflowy_board/lib/appflowy_board.dart
@@ -2,4 +2,5 @@ library appflowy_board;
export 'src/widgets/board_column/board_column_data.dart';
export 'src/widgets/board_data.dart';
+export 'src/widgets/styled_widgets/appflowy_styled_widgets.dart';
export 'src/widgets/board.dart';
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart
index 6f923ddf16..b9f766f961 100644
--- a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart
@@ -6,7 +6,7 @@ const DART_LOG = "Dart_LOG";
class Log {
// static const enableLog = bool.hasEnvironment(DART_LOG);
// static final shared = Log();
- static const enableLog = false;
+ static const enableLog = true;
static void info(String? message) {
if (enableLog) {
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart
index bef98842c0..3cd2a331f1 100644
--- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart
@@ -3,23 +3,29 @@ import 'package:provider/provider.dart';
import 'board_column/board_column.dart';
import 'board_column/board_column_data.dart';
import 'board_data.dart';
-import 'flex/drag_target_inteceptor.dart';
-import 'flex/reorder_flex.dart';
-import 'phantom/phantom_controller.dart';
+import 'reorder_flex/drag_target_inteceptor.dart';
+import 'reorder_flex/reorder_flex.dart';
+import 'reorder_phantom/phantom_controller.dart';
import '../rendering/board_overlay.dart';
+class BoardConfig {
+ final double cornerRadius;
+ final EdgeInsets columnPadding;
+ final EdgeInsets columnItemPadding;
+ final Color columnBackgroundColor;
+
+ const BoardConfig({
+ this.cornerRadius = 6.0,
+ this.columnPadding = const EdgeInsets.symmetric(horizontal: 8),
+ this.columnItemPadding = const EdgeInsets.symmetric(horizontal: 10),
+ this.columnBackgroundColor = Colors.transparent,
+ });
+}
+
class Board extends StatelessWidget {
/// The direction to use as the main axis.
final Axis direction = Axis.vertical;
- /// How much space to place between children in a run in the main axis.
- /// Defaults to 10.0.
- final double spacing;
-
- /// How much space to place between the runs themselves in the cross axis.
- /// Defaults to 0.0.
- final double runSpacing;
-
///
final Widget? background;
@@ -40,15 +46,16 @@ class Board extends StatelessWidget {
///
final BoardPhantomController phantomController;
+ final BoardConfig config;
+
Board({
required this.dataController,
required this.cardBuilder,
- this.spacing = 10.0,
- this.runSpacing = 0.0,
this.background,
this.footBuilder,
this.headerBuilder,
this.columnConstraints = const BoxConstraints(maxWidth: 200),
+ this.config = const BoardConfig(),
Key? key,
}) : phantomController = BoardPhantomController(delegate: dataController),
super(key: key);
@@ -60,9 +67,9 @@ class Board extends StatelessWidget {
child: Consumer(
builder: (context, notifier, child) {
return BoardContent(
+ config: config,
dataController: dataController,
background: background,
- spacing: spacing,
delegate: phantomController,
columnConstraints: columnConstraints,
cardBuilder: cardBuilder,
@@ -84,8 +91,8 @@ class BoardContent extends StatefulWidget {
final OnDragEnded? onDragEnded;
final BoardDataController dataController;
final Widget? background;
- final double spacing;
- final ReorderFlexConfig config;
+ final BoardConfig config;
+ final ReorderFlexConfig reorderFlexConfig;
final BoxConstraints columnConstraints;
///
@@ -101,7 +108,8 @@ class BoardContent extends StatefulWidget {
final BoardPhantomController phantomController;
- BoardContent({
+ const BoardContent({
+ required this.config,
required this.onReorder,
required this.delegate,
required this.dataController,
@@ -109,14 +117,13 @@ class BoardContent extends StatefulWidget {
this.onDragEnded,
this.scrollController,
this.background,
- this.spacing = 10.0,
required this.columnConstraints,
required this.cardBuilder,
this.footBuilder,
this.headerBuilder,
required this.phantomController,
Key? key,
- }) : config = ReorderFlexConfig(spacing: spacing),
+ }) : reorderFlexConfig = const ReorderFlexConfig(),
super(key: key);
@override
@@ -140,7 +147,7 @@ class _BoardContentState extends State {
final reorderFlex = ReorderFlex(
key: widget.key,
- config: widget.config,
+ config: widget.reorderFlexConfig,
scrollController: widget.scrollController,
onDragStarted: widget.onDragStarted,
onReorder: widget.onReorder,
@@ -154,7 +161,15 @@ class _BoardContentState extends State {
return Stack(
alignment: AlignmentDirectional.topStart,
children: [
- if (widget.background != null) widget.background!,
+ if (widget.background != null)
+ Container(
+ clipBehavior: Clip.hardEdge,
+ decoration: BoxDecoration(
+ borderRadius:
+ BorderRadius.circular(widget.config.cornerRadius),
+ ),
+ child: widget.background,
+ ),
reorderFlex,
],
);
@@ -173,8 +188,12 @@ class _BoardContentState extends State {
}
List _buildColumns() {
- final List children = widget.dataController.columnDatas.map(
- (columnData) {
+ final List children =
+ widget.dataController.columnDatas.asMap().entries.map(
+ (item) {
+ final columnData = item.value;
+ final columnIndex = item.key;
+
final dataSource = _BoardColumnDataSourceImpl(
columnId: columnData.id,
dataController: widget.dataController,
@@ -188,6 +207,8 @@ class _BoardContentState extends State {
return ConstrainedBox(
constraints: widget.columnConstraints,
child: BoardColumnWidget(
+ margin: _marginFromIndex(columnIndex),
+ itemMargin: widget.config.columnItemPadding,
headerBuilder: widget.headerBuilder,
footBuilder: widget.footBuilder,
cardBuilder: widget.cardBuilder,
@@ -195,7 +216,8 @@ class _BoardContentState extends State {
scrollController: ScrollController(),
phantomController: widget.phantomController,
onReorder: widget.dataController.moveColumnItem,
- spacing: 10,
+ cornerRadius: widget.config.cornerRadius,
+ backgroundColor: widget.config.columnBackgroundColor,
),
);
},
@@ -206,6 +228,22 @@ class _BoardContentState extends State {
return children;
}
+
+ EdgeInsets _marginFromIndex(int index) {
+ if (widget.dataController.columnDatas.isEmpty) {
+ return widget.config.columnPadding;
+ }
+
+ if (index == 0) {
+ return EdgeInsets.only(right: widget.config.columnPadding.right);
+ }
+
+ if (index == widget.dataController.columnDatas.length - 1) {
+ return EdgeInsets.only(left: widget.config.columnPadding.left);
+ }
+
+ return widget.config.columnPadding;
+ }
}
class _BoardColumnDataSourceImpl extends BoardColumnDataDataSource {
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart
index f95f4dae5c..d8981096e3 100644
--- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart
@@ -3,9 +3,9 @@ import 'dart:collection';
import 'package:flutter/material.dart';
import '../../rendering/board_overlay.dart';
import '../../utils/log.dart';
-import '../phantom/phantom_controller.dart';
-import '../flex/reorder_flex.dart';
-import '../flex/drag_target_inteceptor.dart';
+import '../reorder_phantom/phantom_controller.dart';
+import '../reorder_flex/reorder_flex.dart';
+import '../reorder_flex/drag_target_inteceptor.dart';
import 'board_column_data.dart';
typedef OnColumnDragStarted = void Function(int index);
@@ -79,7 +79,15 @@ class BoardColumnWidget extends StatefulWidget {
final BoardColumnFooterBuilder? footBuilder;
- BoardColumnWidget({
+ final EdgeInsets margin;
+
+ final EdgeInsets itemMargin;
+
+ final double cornerRadius;
+
+ final Color backgroundColor;
+
+ const BoardColumnWidget({
Key? key,
this.headerBuilder,
this.footBuilder,
@@ -90,8 +98,11 @@ class BoardColumnWidget extends StatefulWidget {
this.onDragStarted,
this.scrollController,
this.onDragEnded,
- double? spacing,
- }) : config = ReorderFlexConfig(spacing: spacing),
+ this.margin = EdgeInsets.zero,
+ this.itemMargin = EdgeInsets.zero,
+ this.cornerRadius = 0.0,
+ this.backgroundColor = Colors.transparent,
+ }) : config = const ReorderFlexConfig(),
super(key: key);
@override
@@ -149,12 +160,25 @@ class _BoardColumnWidgetState extends State {
children: children,
);
- return Column(
- children: [
- if (header != null) header,
- Expanded(child: reorderFlex),
- if (footer != null) footer,
- ],
+ return Container(
+ margin: widget.margin,
+ clipBehavior: Clip.hardEdge,
+ decoration: BoxDecoration(
+ color: widget.backgroundColor,
+ borderRadius: BorderRadius.circular(widget.cornerRadius),
+ ),
+ child: Column(
+ children: [
+ if (header != null) header,
+ Expanded(
+ child: Padding(
+ padding: widget.itemMargin,
+ child: reorderFlex,
+ ),
+ ),
+ if (footer != null) footer,
+ ],
+ ),
);
},
opaque: false,
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart
index 24d3cc1a96..2ce739220e 100644
--- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart
@@ -3,7 +3,7 @@ import 'dart:collection';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import '../../utils/log.dart';
-import '../flex/reorder_flex.dart';
+import '../reorder_flex/reorder_flex.dart';
abstract class ColumnItem extends ReoderFlexItem {
bool get isPhantom => false;
@@ -92,10 +92,16 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin {
/// Replace the item at index with the [newItem].
void replace(int index, ColumnItem newItem) {
- final removedItem = columnData._items.removeAt(index);
- columnData._items.insert(index, newItem);
- Log.debug(
- '[$BoardColumnDataController] $columnData replace $removedItem with $newItem at $index');
+ if (columnData._items.isEmpty) {
+ columnData._items.add(newItem);
+ Log.debug('[$BoardColumnDataController] $columnData add $newItem');
+ } else {
+ final removedItem = columnData._items.removeAt(index);
+ columnData._items.insert(index, newItem);
+ Log.debug(
+ '[$BoardColumnDataController] $columnData replace $removedItem with $newItem at $index');
+ }
+
notifyListeners();
}
}
@@ -119,6 +125,6 @@ class BoardColumnData extends ReoderFlexItem with EquatableMixin {
@override
String toString() {
- return 'Column$id';
+ return 'Column:[$id]';
}
}
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart
index fe2fca2c92..06e8ff1a57 100644
--- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart
@@ -4,9 +4,9 @@ import 'package:equatable/equatable.dart';
import '../utils/log.dart';
import 'board_column/board_column_data.dart';
-import 'flex/reorder_flex.dart';
+import 'reorder_flex/reorder_flex.dart';
import 'package:flutter/material.dart';
-import 'phantom/phantom_controller.dart';
+import 'reorder_phantom/phantom_controller.dart';
typedef OnMoveColumn = void Function(int fromIndex, int toIndex);
@@ -79,8 +79,11 @@ class BoardDataController extends ChangeNotifier
int toColumnIndex,
) {
final item = columnController(fromColumnId).removeAt(fromColumnIndex);
- assert(
- columnController(toColumnId).items[toColumnIndex] is PhantomColumnItem);
+
+ if (columnController(toColumnId).items.length > toColumnIndex) {
+ assert(columnController(toColumnId).items[toColumnIndex]
+ is PhantomColumnItem);
+ }
columnController(toColumnId).replace(toColumnIndex, item);
@@ -120,7 +123,7 @@ class BoardDataController extends ChangeNotifier
columnController.removeAt(index);
Log.debug(
- '[$BoardDataController] Column$columnId remove phantom, current count: ${columnController.items.length}');
+ '[$BoardDataController] Column:[$columnId] remove phantom, current count: ${columnController.items.length}');
}
return isExist;
}
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart
index fafdcef774..ea8cc91fab 100644
--- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:provider/provider.dart';
import '../transitions.dart';
abstract class DragTargetData {
@@ -65,6 +67,8 @@ class ReorderDragTarget extends StatefulWidget {
final AnimationController insertAnimationController;
final AnimationController deleteAnimationController;
+ final bool useMoveAnimation;
+
ReorderDragTarget({
Key? key,
required this.child,
@@ -74,6 +78,7 @@ class ReorderDragTarget extends StatefulWidget {
required this.onWillAccept,
required this.insertAnimationController,
required this.deleteAnimationController,
+ required this.useMoveAnimation,
this.onAccept,
this.onLeave,
this.draggableTargetBuilder,
@@ -140,7 +145,10 @@ class _ReorderDragTargetState
data: widget.dragTargetData,
ignoringFeedbackSemantics: false,
feedback: feedbackBuilder,
- childWhenDragging: IgnorePointerWidget(child: widget.child),
+ childWhenDragging: IgnorePointerWidget(
+ useIntrinsicSize: !widget.useMoveAnimation,
+ child: widget.child,
+ ),
onDragStarted: () {
_draggingFeedbackSize = widget._indexGlobalKey.currentContext?.size;
widget.onDragStarted(
@@ -174,11 +182,13 @@ class _ReorderDragTargetState
transform: Matrix4.rotationZ(0),
alignment: FractionalOffset.topLeft,
child: Material(
- elevation: 3.0,
color: Colors.transparent,
borderRadius: BorderRadius.zero,
clipBehavior: Clip.hardEdge,
- child: ConstrainedBox(constraints: constraints, child: child),
+ child: ConstrainedBox(
+ constraints: constraints,
+ child: Opacity(opacity: 0.6, child: child),
+ ),
),
);
}
@@ -254,10 +264,12 @@ class IgnorePointerWidget extends StatelessWidget {
final sizedChild = useIntrinsicSize
? child
: SizedBox(width: 0.0, height: 0.0, child: child);
+
+ final opacity = useIntrinsicSize ? 0.3 : 0.0;
return IgnorePointer(
ignoring: true,
child: Opacity(
- opacity: 0,
+ opacity: opacity,
child: sizedChild,
),
);
@@ -282,6 +294,82 @@ class PhantomWidget extends StatelessWidget {
}
}
+abstract class DragTargetMovePlaceholderDelegate {
+ void registerPlaceholder(
+ int dragTargetIndex,
+ void Function(int currentDragTargetIndex) callback,
+ );
+
+ void unregisterPlaceholder(int dragTargetIndex);
+}
+
+class DragTargeMovePlaceholder extends StatefulWidget {
+ final double height;
+ final Color color;
+ final Color highlightColor;
+ final int dragTargetIndex;
+ final DragTargetMovePlaceholderDelegate delegate;
+
+ const DragTargeMovePlaceholder({
+ required this.delegate,
+ required this.dragTargetIndex,
+ this.height = 4,
+ this.color = Colors.transparent,
+ this.highlightColor = Colors.lightBlue,
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ State createState() =>
+ _DragTargeMovePlaceholderState();
+}
+
+class _DragTargeMovePlaceholderState extends State {
+ ValueNotifier isHighlight = ValueNotifier(false);
+
+ @override
+ void initState() {
+ widget.delegate.registerPlaceholder(
+ widget.dragTargetIndex,
+ (currentDragTargetIndex) {
+ if (!mounted) return;
+
+ SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
+ if (currentDragTargetIndex == -1) {
+ isHighlight.value = false;
+ } else {
+ isHighlight.value =
+ widget.dragTargetIndex == currentDragTargetIndex;
+ }
+ });
+ },
+ );
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ isHighlight.dispose();
+ widget.delegate.unregisterPlaceholder(widget.dragTargetIndex);
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ChangeNotifierProvider.value(
+ value: isHighlight,
+ child: Consumer>(
+ builder: (context, notifier, child) {
+ return Container(
+ height: widget.height,
+ color: notifier.value ? widget.highlightColor : widget.color,
+ );
+ },
+ ),
+ );
+ }
+}
+
abstract class FakeDragTargetEventTrigger {
void fakeOnDragEnded(VoidCallback callback);
}
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart
index 86152ed0de..da529819dd 100644
--- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart
@@ -30,12 +30,14 @@ abstract class DragTargetInterceptor {
}
abstract class OverlapDragTargetDelegate {
- void didReturnOriginalDragTarget();
- void didCrossOtherDragTarget(
+ void cancel();
+ void moveTo(
String reorderFlexId,
FlexDragTargetData dragTargetData,
int dragTargetIndex,
);
+
+ bool canMoveTo(String dragTargetId);
}
/// [OverlappingDragTargetInteceptor] is used to receive the overlapping
@@ -68,13 +70,11 @@ class OverlappingDragTargetInteceptor extends DragTargetInterceptor {
required String dragTargetId,
required int dragTargetIndex}) {
if (dragTargetId == dragTargetData.reorderFlexId) {
- delegate.didReturnOriginalDragTarget();
+ delegate.cancel();
} else {
- delegate.didCrossOtherDragTarget(
- dragTargetId,
- dragTargetData,
- dragTargetIndex,
- );
+ if (delegate.canMoveTo(dragTargetId)) {
+ delegate.moveTo(dragTargetId, dragTargetData, 0);
+ }
}
return true;
@@ -128,13 +128,13 @@ class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor {
@override
void onAccept(FlexDragTargetData dragTargetData) {
Log.trace(
- '[$CrossReorderFlexDragTargetInterceptor] Column$reorderFlexId on onAccept');
+ '[$CrossReorderFlexDragTargetInterceptor] Column:[$reorderFlexId] on onAccept');
}
@override
void onLeave(FlexDragTargetData dragTargetData) {
Log.trace(
- '[$CrossReorderFlexDragTargetInterceptor] Column$reorderFlexId on leave');
+ '[$CrossReorderFlexDragTargetInterceptor] Column:[$reorderFlexId] on leave');
}
@override
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart
index 9066c987f0..04de8cae7b 100644
--- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart
@@ -41,16 +41,19 @@ class ReorderFlexConfig {
// How long an animation to scroll to an off-screen element
final Duration scrollAnimationDuration = const Duration(milliseconds: 250);
- final double? spacing;
+ final bool useMoveAnimation;
- const ReorderFlexConfig({this.spacing});
+ final bool useMovePlaceholder;
+
+ const ReorderFlexConfig({
+ this.useMoveAnimation = true,
+ }) : useMovePlaceholder = !useMoveAnimation;
}
class ReorderFlex extends StatefulWidget {
final ReorderFlexConfig config;
final List children;
- final EdgeInsets? padding;
/// [direction] How to place the children, default is Axis.vertical
final Axis direction;
@@ -81,7 +84,6 @@ class ReorderFlex extends StatefulWidget {
this.onDragStarted,
this.onDragEnded,
this.interceptor,
- this.padding,
this.direction = Axis.vertical,
}) : super(key: key);
@@ -108,8 +110,11 @@ class ReorderFlexState extends State
/// [_animation] controls the dragging animations
late DragTargetAnimation _animation;
+ late ReorderFlexNotifier _notifier;
+
@override
void initState() {
+ _notifier = ReorderFlexNotifier();
dragState = DraggingState(widget.reorderFlexId);
_animation = DragTargetAnimation(
@@ -154,13 +159,14 @@ class ReorderFlexState extends State
for (int i = 0; i < widget.children.length; i += 1) {
Widget child = widget.children[i];
+ children.add(_wrap(child, i));
- if (widget.config.spacing != null) {
- children.add(SizedBox(width: widget.config.spacing!));
- }
-
- final wrapChild = _wrap(child, i);
- children.add(wrapChild);
+ // if (widget.config.useMovePlaceholder) {
+ // children.add(DragTargeMovePlaceholder(
+ // dragTargetIndex: i,
+ // delegate: _notifier,
+ // ));
+ // }
}
final child = _wrapContainer(children);
@@ -199,7 +205,8 @@ class ReorderFlexState extends State
/// [childIndex]: the index of the child in a list
Widget _wrap(Widget child, int childIndex) {
return Builder(builder: (context) {
- final dragTarget = _buildDragTarget(context, child, childIndex);
+ final ReorderDragTarget dragTarget =
+ _buildDragTarget(context, child, childIndex);
int shiftedIndex = childIndex;
if (dragState.isOverlapWithPhantom()) {
@@ -207,7 +214,7 @@ class ReorderFlexState extends State
}
Log.trace(
- 'Rebuild: Column${dragState.id} ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex');
+ 'Rebuild: Column:[${dragState.id}] ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex');
final currentIndex = dragState.currentIndex;
final dragPhantomIndex = dragState.phantomIndex;
@@ -234,15 +241,18 @@ class ReorderFlexState extends State
}
/// Determine the size of the drop area to show under the dragging widget.
- final feedbackSize = dragState.feedbackSize;
+ Size? feedbackSize = Size.zero;
+ if (widget.config.useMoveAnimation) {
+ feedbackSize = dragState.feedbackSize;
+ }
+
Widget appearSpace = _makeAppearSpace(dragSpace, feedbackSize);
Widget disappearSpace = _makeDisappearSpace(dragSpace, feedbackSize);
/// When start dragging, the dragTarget, [ReorderDragTarget], will
/// return a [IgnorePointerWidget] which size is zero.
if (dragState.isPhantomAboveDragTarget()) {
- //the phantom is moving down, i.e. the tile below the phantom is moving up
- Log.trace('index:$childIndex item moving up / phantom moving down');
+ _notifier.updateDragTargetIndex(currentIndex);
if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
return _buildDraggingContainer(children: [
disappearSpace,
@@ -264,8 +274,7 @@ class ReorderFlexState extends State
///
if (dragState.isPhantomBelowDragTarget()) {
- //the phantom is moving up, i.e. the tile above the phantom is moving down
- Log.trace('index:$childIndex item moving down / phantom moving up');
+ _notifier.updateDragTargetIndex(currentIndex);
if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
return _buildDraggingContainer(children: [
appearSpace,
@@ -303,10 +312,7 @@ class ReorderFlexState extends State
}
ReorderDragTarget _buildDragTarget(
- BuildContext builderContext,
- Widget child,
- int dragTargetIndex,
- ) {
+ BuildContext builderContext, Widget child, int dragTargetIndex) {
final ReoderFlexItem reorderFlexItem =
widget.dataSource.items[dragTargetIndex];
return ReorderDragTarget(
@@ -319,14 +325,14 @@ class ReorderFlexState extends State
),
onDragStarted: (draggingWidget, draggingIndex, size) {
Log.debug(
- "[DragTarget] Column${widget.dataSource.identifier} start dragging item at $draggingIndex");
+ "[DragTarget] Column:[${widget.dataSource.identifier}] start dragging item at $draggingIndex");
_startDragging(draggingWidget, draggingIndex, size);
widget.onDragStarted?.call(draggingIndex);
},
onDragEnded: (dragTargetData) {
Log.debug(
- "[DragTarget]: Column${widget.dataSource.identifier} end dragging");
-
+ "[DragTarget]: Column:[${widget.dataSource.identifier}] end dragging");
+ _notifier.updateDragTargetIndex(-1);
setState(() {
if (dragTargetData.reorderFlexId == widget.reorderFlexId) {
_onReordered(
@@ -340,14 +346,11 @@ class ReorderFlexState extends State
});
},
onWillAccept: (FlexDragTargetData dragTargetData) {
- Log.debug('Insert animation: ${_animation.deleteController.status}');
-
if (_animation.deleteController.isAnimating) {
return false;
}
assert(widget.dataSource.items.length > dragTargetIndex);
-
if (_interceptDragTarget(
dragTargetData,
(interceptor) => interceptor.onWillAccept(
@@ -370,6 +373,7 @@ class ReorderFlexState extends State
);
},
onLeave: (dragTargetData) {
+ _notifier.updateDragTargetIndex(-1);
_interceptDragTarget(
dragTargetData,
(interceptor) => interceptor.onLeave(dragTargetData),
@@ -378,6 +382,7 @@ class ReorderFlexState extends State
insertAnimationController: _animation.insertController,
deleteAnimationController: _animation.deleteController,
draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder,
+ useMoveAnimation: widget.config.useMoveAnimation,
child: child,
);
}
@@ -430,7 +435,7 @@ class ReorderFlexState extends State
/// The [willAccept] will be true if the dargTarget is the widget that gets
/// dragged and it is dragged on top of the other dragTargets.
///
- Log.trace(
+ Log.debug(
'[$ReorderDragTarget] ${widget.dataSource.identifier} on will accept, dragIndex:$dragIndex, dragTargetIndex:$dragTargetIndex, count: ${widget.dataSource.items.length}');
bool willAccept =
@@ -442,7 +447,6 @@ class ReorderFlexState extends State
} else {
dragState.updateNextIndex(dragTargetIndex);
}
-
_requestAnimationToNextIndex(isAcceptingNewTarget: true);
});
@@ -467,7 +471,6 @@ class ReorderFlexState extends State
} else {
return SingleChildScrollView(
scrollDirection: widget.direction,
- padding: widget.padding,
controller: _scrollController,
child: child,
);
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart
index a90ee6a83a..accdaa866b 100644
--- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart
@@ -1,6 +1,7 @@
import 'package:flutter/widgets.dart';
import '../transitions.dart';
+import 'drag_target.dart';
mixin ReorderFlexMinxi {
@protected
@@ -86,3 +87,56 @@ extension CurveAnimationController on AnimationController {
);
}
}
+
+class ReorderFlexNotifier extends DragTargetMovePlaceholderDelegate {
+ Map dragTargeEventNotifier = {};
+
+ void updateDragTargetIndex(int index) {
+ for (var notifier in dragTargeEventNotifier.values) {
+ notifier.setDragTargetIndex(index);
+ }
+ }
+
+ DragTargetEventNotifier _notifierFromIndex(int dragTargetIndex) {
+ DragTargetEventNotifier? notifier = dragTargeEventNotifier[dragTargetIndex];
+ if (notifier == null) {
+ final newNotifier = DragTargetEventNotifier();
+ dragTargeEventNotifier[dragTargetIndex] = newNotifier;
+ notifier = newNotifier;
+ }
+
+ return notifier;
+ }
+
+ void dispose() {
+ for (var notifier in dragTargeEventNotifier.values) {
+ notifier.dispose();
+ }
+ }
+
+ @override
+ void registerPlaceholder(
+ int dragTargetIndex,
+ void Function(int dragTargetIndex) callback,
+ ) {
+ _notifierFromIndex(dragTargetIndex).addListener(() {
+ callback.call(_notifierFromIndex(dragTargetIndex).currentDragTargetIndex);
+ });
+ }
+
+ @override
+ void unregisterPlaceholder(int dragTargetIndex) {
+ dragTargeEventNotifier.remove(dragTargetIndex);
+ }
+}
+
+class DragTargetEventNotifier extends ChangeNotifier {
+ int currentDragTargetIndex = -1;
+
+ void setDragTargetIndex(int index) {
+ if (currentDragTargetIndex != index) {
+ currentDragTargetIndex = index;
+ notifyListeners();
+ }
+ }
+}
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart
index 6b68eefd52..266c83d873 100644
--- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart
@@ -1,9 +1,11 @@
+import 'dart:async';
+
import 'package:flutter/material.dart';
import '../../utils/log.dart';
import '../board_column/board_column_data.dart';
-import '../flex/drag_state.dart';
-import '../flex/drag_target.dart';
-import '../flex/drag_target_inteceptor.dart';
+import '../reorder_flex/drag_state.dart';
+import '../reorder_flex/drag_target.dart';
+import '../reorder_flex/drag_target_inteceptor.dart';
import 'phantom_state.dart';
abstract class BoardPhantomControllerDelegate {
@@ -127,8 +129,8 @@ class BoardPhantomController extends OverlapDragTargetDelegate
FlexDragTargetData dragTargetData,
int dragTargetIndex,
) {
- // Log.debug('[$BoardPhantomController] move Column${dragTargetData.reorderFlexId}:${dragTargetData.draggingIndex} '
- // 'to Column$columnId:$index');
+ // Log.debug('[$BoardPhantomController] move Column:[${dragTargetData.reorderFlexId}]:${dragTargetData.draggingIndex} '
+ // 'to Column:[$columnId]:$index');
phantomRecord = PhantomRecord(
toColumnId: columnId,
@@ -177,7 +179,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate
}
@override
- void didReturnOriginalDragTarget() {
+ void cancel() {
if (phantomRecord == null) {
return;
}
@@ -188,7 +190,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate
}
@override
- void didCrossOtherDragTarget(
+ void moveTo(
String reorderFlexId,
FlexDragTargetData dragTargetData,
int dragTargetIndex,
@@ -199,6 +201,12 @@ class BoardPhantomController extends OverlapDragTargetDelegate
dragTargetIndex,
);
}
+
+ @override
+ bool canMoveTo(String dragTargetId) {
+ // TODO: implement shouldReceive
+ return delegate.controller(dragTargetId)?.columnData.items.length == 0;
+ }
}
/// Use [PhantomRecord] to record where to remove the column item and where to
@@ -228,7 +236,7 @@ class PhantomRecord {
return;
}
Log.debug(
- '[$PhantomRecord] Update Column$fromColumnId remove position to $index');
+ '[$PhantomRecord] Update Column:[$fromColumnId] remove position to $index');
fromColumnIndex = index;
}
@@ -238,13 +246,13 @@ class PhantomRecord {
}
Log.debug(
- '[$PhantomRecord] Column$toColumnId update position $toColumnIndex -> $index');
+ '[$PhantomRecord] Column:[$toColumnId] update position $toColumnIndex -> $index');
toColumnIndex = index;
}
@override
String toString() {
- return 'Column$fromColumnId:$fromColumnIndex to Column$toColumnId:$toColumnIndex';
+ return 'Column:[$fromColumnId]:$fromColumnIndex to Column:[$toColumnId]:$toColumnIndex';
}
}
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/appflowy_styled_widgets.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/appflowy_styled_widgets.dart
new file mode 100644
index 0000000000..b802d15dae
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/appflowy_styled_widgets.dart
@@ -0,0 +1,3 @@
+export 'card.dart';
+export 'footer.dart';
+export 'header.dart';
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart
new file mode 100644
index 0000000000..b2e5085649
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart
@@ -0,0 +1,37 @@
+import 'package:flutter/material.dart';
+
+class AppFlowyColumnItemCard extends StatefulWidget {
+ final Widget? child;
+ final Color backgroundColor;
+ final double cornerRadius;
+ final BoxConstraints boxConstraints;
+
+ const AppFlowyColumnItemCard({
+ this.child,
+ this.backgroundColor = Colors.white,
+ this.cornerRadius = 0.0,
+ this.boxConstraints = const BoxConstraints.tightFor(height: 60),
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ State createState() => _AppFlowyColumnItemCardState();
+}
+
+class _AppFlowyColumnItemCardState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.all(4.0),
+ child: Container(
+ constraints: widget.boxConstraints,
+ clipBehavior: Clip.hardEdge,
+ decoration: BoxDecoration(
+ color: widget.backgroundColor,
+ borderRadius: BorderRadius.circular(widget.cornerRadius),
+ ),
+ child: widget.child,
+ ),
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart
new file mode 100644
index 0000000000..7f5655fe60
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart
@@ -0,0 +1,46 @@
+import 'package:flutter/material.dart';
+
+typedef OnFooterAddButtonClick = void Function();
+
+class AppFlowyColumnFooter extends StatefulWidget {
+ final double height;
+ final Widget? icon;
+ final Widget? title;
+ final EdgeInsets margin;
+ final OnFooterAddButtonClick? onAddButtonClick;
+
+ const AppFlowyColumnFooter({
+ this.icon,
+ this.title,
+ this.margin = EdgeInsets.zero,
+ required this.height,
+ this.onAddButtonClick,
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ State createState() => _AppFlowyColumnFooterState();
+}
+
+class _AppFlowyColumnFooterState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: widget.onAddButtonClick,
+ child: SizedBox(
+ height: widget.height,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 10),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ if (widget.icon != null) widget.icon!,
+ if (widget.title != null) widget.title!,
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart
new file mode 100644
index 0000000000..fdebc7ef21
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart
@@ -0,0 +1,74 @@
+import 'package:flutter/material.dart';
+
+typedef OnHeaderAddButtonClick = void Function();
+typedef OnHeaderMoreButtonClick = void Function();
+
+class AppFlowyColumnHeader extends StatefulWidget {
+ final double height;
+ final Widget? icon;
+ final Widget? title;
+ final Widget? addIcon;
+ final Widget? moreIcon;
+ final EdgeInsets margin;
+ final OnHeaderAddButtonClick? onAddButtonClick;
+ final OnHeaderMoreButtonClick? onMoreButtonClick;
+
+ const AppFlowyColumnHeader({
+ required this.height,
+ this.icon,
+ this.title,
+ this.addIcon,
+ this.moreIcon,
+ this.margin = EdgeInsets.zero,
+ this.onAddButtonClick,
+ this.onMoreButtonClick,
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ State createState() => _AppFlowyColumnHeaderState();
+}
+
+class _AppFlowyColumnHeaderState extends State {
+ @override
+ Widget build(BuildContext context) {
+ List children = [];
+
+ if (widget.icon != null) {
+ children.add(widget.icon!);
+ children.add(_hSpace());
+ }
+
+ if (widget.title != null) {
+ children.add(widget.title!);
+ children.add(_hSpace());
+ }
+
+ if (widget.moreIcon != null) {
+ children.add(const Spacer());
+ children.add(
+ IconButton(onPressed: widget.onMoreButtonClick, icon: widget.moreIcon!),
+ );
+ }
+
+ if (widget.addIcon != null) {
+ children.add(
+ IconButton(onPressed: widget.onAddButtonClick, icon: widget.addIcon!),
+ );
+ }
+
+ return SizedBox(
+ height: widget.height,
+ child: Padding(
+ padding: widget.margin,
+ child: Row(
+ children: children,
+ ),
+ ),
+ );
+ }
+
+ Widget _hSpace() {
+ return const SizedBox(width: 6);
+ }
+}