feat: callout (#1732)

* feat: add callout plugin

* refactor: add SelectionMenuItem.node factory

makes calloutMenuItem more readable

* feat: add color picker

* feat: add popover to callout

* feat: add emoji to callout

* fix: store tint name

* fix: remove leading underscores

* fix: revert export of editor_entry

* refactor: move color tint names to appflowy_editor

* fix: #1732 only re-insert text node if it's parent is text node too while deleting

* docs: doc comment for SelectionMenuItem.node

* fix: disable callout plugin

should be re-enabled after #1753 is done

* fix: typo

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
abichinger 2023-01-30 03:56:19 +01:00 committed by GitHub
parent 3de4e1cb12
commit 000569a836
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 806 additions and 34 deletions

View File

@ -1,7 +1,7 @@
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
@ -108,6 +108,8 @@ class _DocumentPageState extends State<DocumentPage> {
kMathEquationType: MathEquationNodeWidgetBuidler(),
// Code Block
kCodeBlockType: CodeBlockNodeWidgetBuilder(),
// Card
kCalloutType: CalloutNodeWidgetBuilder(),
},
shortcutEvents: [
// Divider

View File

@ -73,5 +73,23 @@
"backgroundColorPink": "Pink background",
"@backgroundColorPink": {},
"backgroundColorRed": "Red background",
"@backgroundColorRed": {}
"@backgroundColorRed": {},
"tint1": "Tint 1",
"tint2": "Tint 2",
"tint3": "Tint 3",
"tint4": "Tint 4",
"tint5": "Tint 5",
"tint6": "Tint 6",
"tint7": "Tint 7",
"tint8": "Tint 8",
"tint9": "Tint 9",
"lightLightTint1": "Purple",
"lightLightTint2": "Pink",
"lightLightTint3": "Light Pink",
"lightLightTint4": "Orange",
"lightLightTint5": "Yellow",
"lightLightTint6": "Lime",
"lightLightTint7": "Green",
"lightLightTint8": "Aqua",
"lightLightTint9": "Blue"
}

View File

@ -11,6 +11,7 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart';
@ -40,28 +41,28 @@ import 'messages_zh-TW.dart' as messages_zh_tw;
typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = {
'bn_BN': () => new Future.value(null),
'ca': () => new Future.value(null),
'cs_CZ': () => new Future.value(null),
'de_DE': () => new Future.value(null),
'en': () => new Future.value(null),
'es_VE': () => new Future.value(null),
'fr_CA': () => new Future.value(null),
'fr_FR': () => new Future.value(null),
'hi_IN': () => new Future.value(null),
'hu_HU': () => new Future.value(null),
'id_ID': () => new Future.value(null),
'it_IT': () => new Future.value(null),
'ja_JP': () => new Future.value(null),
'ml_IN': () => new Future.value(null),
'nl_NL': () => new Future.value(null),
'pl_PL': () => new Future.value(null),
'pt_BR': () => new Future.value(null),
'pt_PT': () => new Future.value(null),
'ru_RU': () => new Future.value(null),
'tr_TR': () => new Future.value(null),
'zh_CN': () => new Future.value(null),
'zh_TW': () => new Future.value(null),
'bn_BN': () => new SynchronousFuture(null),
'ca': () => new SynchronousFuture(null),
'cs_CZ': () => new SynchronousFuture(null),
'de_DE': () => new SynchronousFuture(null),
'en': () => new SynchronousFuture(null),
'es_VE': () => new SynchronousFuture(null),
'fr_CA': () => new SynchronousFuture(null),
'fr_FR': () => new SynchronousFuture(null),
'hi_IN': () => new SynchronousFuture(null),
'hu_HU': () => new SynchronousFuture(null),
'id_ID': () => new SynchronousFuture(null),
'it_IT': () => new SynchronousFuture(null),
'ja_JP': () => new SynchronousFuture(null),
'ml_IN': () => new SynchronousFuture(null),
'nl_NL': () => new SynchronousFuture(null),
'pl_PL': () => new SynchronousFuture(null),
'pt_BR': () => new SynchronousFuture(null),
'pt_PT': () => new SynchronousFuture(null),
'ru_RU': () => new SynchronousFuture(null),
'tr_TR': () => new SynchronousFuture(null),
'zh_CN': () => new SynchronousFuture(null),
'zh_TW': () => new SynchronousFuture(null),
};
MessageLookupByLibrary? _findExact(String localeName) {
@ -116,18 +117,18 @@ MessageLookupByLibrary? _findExact(String localeName) {
}
/// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) async {
Future<bool> initializeMessages(String localeName) {
var availableLocale = Intl.verifiedLocale(
localeName, (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null);
if (availableLocale == null) {
return new Future.value(false);
return new SynchronousFuture(false);
}
var lib = _deferredLibraries[availableLocale];
await (lib == null ? new Future.value(false) : lib());
lib == null ? new SynchronousFuture(false) : lib();
initializeInternalMessageLookup(() => new CompositeMessageLookup());
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
return new Future.value(true);
return new SynchronousFuture(true);
}
bool _messagesExistFor(String locale) {

View File

@ -63,11 +63,29 @@ class MessageLookup extends MessageLookupByLibrary {
"highlight": MessageLookupByLibrary.simpleMessage("Highlight"),
"image": MessageLookupByLibrary.simpleMessage("Image"),
"italic": MessageLookupByLibrary.simpleMessage("Italic"),
"lightLightTint1": MessageLookupByLibrary.simpleMessage("Purple"),
"lightLightTint2": MessageLookupByLibrary.simpleMessage("Pink"),
"lightLightTint3": MessageLookupByLibrary.simpleMessage("Light Pink"),
"lightLightTint4": MessageLookupByLibrary.simpleMessage("Orange"),
"lightLightTint5": MessageLookupByLibrary.simpleMessage("Yellow"),
"lightLightTint6": MessageLookupByLibrary.simpleMessage("Lime"),
"lightLightTint7": MessageLookupByLibrary.simpleMessage("Green"),
"lightLightTint8": MessageLookupByLibrary.simpleMessage("Aqua"),
"lightLightTint9": MessageLookupByLibrary.simpleMessage("Blue"),
"link": MessageLookupByLibrary.simpleMessage("Link"),
"numberedList": MessageLookupByLibrary.simpleMessage("Numbered List"),
"quote": MessageLookupByLibrary.simpleMessage("Quote"),
"strikethrough": MessageLookupByLibrary.simpleMessage("Strikethrough"),
"text": MessageLookupByLibrary.simpleMessage("Text"),
"tint1": MessageLookupByLibrary.simpleMessage("Tint 1"),
"tint2": MessageLookupByLibrary.simpleMessage("Tint 2"),
"tint3": MessageLookupByLibrary.simpleMessage("Tint 3"),
"tint4": MessageLookupByLibrary.simpleMessage("Tint 4"),
"tint5": MessageLookupByLibrary.simpleMessage("Tint 5"),
"tint6": MessageLookupByLibrary.simpleMessage("Tint 6"),
"tint7": MessageLookupByLibrary.simpleMessage("Tint 7"),
"tint8": MessageLookupByLibrary.simpleMessage("Tint 8"),
"tint9": MessageLookupByLibrary.simpleMessage("Tint 9"),
"underline": MessageLookupByLibrary.simpleMessage("Underline")
};
}

View File

@ -420,6 +420,186 @@ class AppFlowyEditorLocalizations {
args: [],
);
}
/// `Tint 1`
String get tint1 {
return Intl.message(
'Tint 1',
name: 'tint1',
desc: '',
args: [],
);
}
/// `Tint 2`
String get tint2 {
return Intl.message(
'Tint 2',
name: 'tint2',
desc: '',
args: [],
);
}
/// `Tint 3`
String get tint3 {
return Intl.message(
'Tint 3',
name: 'tint3',
desc: '',
args: [],
);
}
/// `Tint 4`
String get tint4 {
return Intl.message(
'Tint 4',
name: 'tint4',
desc: '',
args: [],
);
}
/// `Tint 5`
String get tint5 {
return Intl.message(
'Tint 5',
name: 'tint5',
desc: '',
args: [],
);
}
/// `Tint 6`
String get tint6 {
return Intl.message(
'Tint 6',
name: 'tint6',
desc: '',
args: [],
);
}
/// `Tint 7`
String get tint7 {
return Intl.message(
'Tint 7',
name: 'tint7',
desc: '',
args: [],
);
}
/// `Tint 8`
String get tint8 {
return Intl.message(
'Tint 8',
name: 'tint8',
desc: '',
args: [],
);
}
/// `Tint 9`
String get tint9 {
return Intl.message(
'Tint 9',
name: 'tint9',
desc: '',
args: [],
);
}
/// `Purple`
String get lightLightTint1 {
return Intl.message(
'Purple',
name: 'lightLightTint1',
desc: '',
args: [],
);
}
/// `Pink`
String get lightLightTint2 {
return Intl.message(
'Pink',
name: 'lightLightTint2',
desc: '',
args: [],
);
}
/// `Light Pink`
String get lightLightTint3 {
return Intl.message(
'Light Pink',
name: 'lightLightTint3',
desc: '',
args: [],
);
}
/// `Orange`
String get lightLightTint4 {
return Intl.message(
'Orange',
name: 'lightLightTint4',
desc: '',
args: [],
);
}
/// `Yellow`
String get lightLightTint5 {
return Intl.message(
'Yellow',
name: 'lightLightTint5',
desc: '',
args: [],
);
}
/// `Lime`
String get lightLightTint6 {
return Intl.message(
'Lime',
name: 'lightLightTint6',
desc: '',
args: [],
);
}
/// `Green`
String get lightLightTint7 {
return Intl.message(
'Green',
name: 'lightLightTint7',
desc: '',
args: [],
);
}
/// `Aqua`
String get lightLightTint8 {
return Intl.message(
'Aqua',
name: 'lightLightTint8',
desc: '',
args: [],
);
}
/// `Blue`
String get lightLightTint9 {
return Intl.message(
'Blue',
name: 'lightLightTint9',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate

View File

@ -53,6 +53,81 @@ class SelectionMenuItem {
editorState.apply(transaction);
}
}
/// Creates a selection menu entry for inserting a [Node].
/// [name] and [iconData] define the appearance within the selection menu.
///
/// The insert position is determined by the result of [replace] and
/// [insertBefore]
/// If no values are provided for [replace] and [insertBefore] the node is
/// inserted after the current selection.
/// [replace] takes precedence over [insertBefore]
///
/// [updateSelection] can be used to update the selection after the node
/// has been inserted.
factory SelectionMenuItem.node({
required String name,
required IconData iconData,
required List<String> keywords,
required Node Function(EditorState editorState) nodeBuilder,
bool Function(EditorState editorState, TextNode textNode)? insertBefore,
bool Function(EditorState editorState, TextNode textNode)? replace,
Selection? Function(
EditorState editorState,
Path insertPath,
bool replaced,
bool insertedBefore,
)?
updateSelection,
}) {
return SelectionMenuItem(
name: () => name,
icon: (editorState, onSelected) => Icon(
iconData,
color: onSelected
? editorState.editorStyle.selectionMenuItemSelectedIconColor
: editorState.editorStyle.selectionMenuItemIconColor,
size: 18.0,
),
keywords: keywords,
handler: (editorState, _, __) {
final selection =
editorState.service.selectionService.currentSelection.value;
final textNodes = editorState
.service.selectionService.currentSelectedNodes
.whereType<TextNode>();
if (textNodes.length != 1 || selection == null) {
return;
}
final textNode = textNodes.first;
final node = nodeBuilder(editorState);
final transaction = editorState.transaction;
final bReplace = replace?.call(editorState, textNode) ?? false;
final bInsertBefore =
insertBefore?.call(editorState, textNode) ?? false;
//default insert after
var path = textNode.path.next;
if (bReplace) {
path = textNode.path;
} else if (bInsertBefore) {
path = textNode.path;
}
transaction
..insertNode(path, node)
..afterSelection = updateSelection?.call(
editorState, path, bReplace, bInsertBefore) ??
selection;
if (bReplace) {
transaction.deleteNode(textNode);
}
editorState.apply(transaction);
},
);
}
}
class SelectionMenuWidget extends StatefulWidget {

View File

@ -120,7 +120,8 @@ KeyEventResult _backDeleteToPreviousTextNode(
) {
if (textNode.next == null &&
textNode.children.isEmpty &&
textNode.parent?.parent != null) {
textNode.parent?.parent != null &&
textNode.parent is TextNode) {
transaction
..deleteNode(textNode)
..insertNode(textNode.parent!.path.next, textNode)

View File

@ -1,5 +1,7 @@
library appflowy_editor_plugins;
// Callout
export 'src/callout/callout_node_widget.dart';
// Code Block
export 'src/code_block/code_block_node_widget.dart';
export 'src/code_block/code_block_shortcut_event.dart';
@ -9,4 +11,4 @@ export 'src/divider/divider_shortcut_event.dart';
// Emoji Picker
export 'src/emoji_picker/emoji_menu_item.dart';
// Math Equation
export 'src/math_ equation/math_equation_node_widget.dart';
export 'src/math_ equation/math_equation_node_widget.dart';

View File

@ -0,0 +1,291 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/src/emoji_picker/emoji_menu_item.dart';
import 'package:appflowy_editor_plugins/src/extensions/theme_extension.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/color_picker.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
const String kCalloutType = 'callout';
const String kCalloutAttrColor = 'color';
const String kCalloutAttrEmoji = 'emoji';
SelectionMenuItem calloutMenuItem = SelectionMenuItem.node(
name: 'Callout',
iconData: Icons.note,
keywords: ['callout'],
nodeBuilder: (editorState) {
final node = Node(type: kCalloutType);
node.insert(TextNode.empty());
return node;
},
replace: (_, textNode) => textNode.toPlainText().isEmpty,
updateSelection: (_, path, __, ___) {
return Selection.single(path: [...path, 0], startOffset: 0);
},
);
class CalloutNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
@override
Widget build(NodeWidgetContext<Node> context) {
return _CalloutWidget(
key: context.node.key,
node: context.node,
editorState: context.editorState,
);
}
@override
NodeValidator<Node> get nodeValidator => (node) => node.type == kCalloutType;
}
class _CalloutWidget extends StatefulWidget {
const _CalloutWidget({
super.key,
required this.node,
required this.editorState,
});
final Node node;
final EditorState editorState;
@override
State<_CalloutWidget> createState() => _CalloutWidgetState();
}
class _CalloutWidgetState extends State<_CalloutWidget> with SelectableMixin {
bool isHover = false;
final PopoverController colorPopoverController = PopoverController();
final PopoverController emojiPopoverController = PopoverController();
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
@override
void initState() {
widget.node.addListener(nodeChanged);
super.initState();
}
@override
void dispose() {
widget.node.removeListener(nodeChanged);
super.dispose();
}
void nodeChanged() {
if (widget.node.children.isEmpty) {
deleteNode();
}
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) {
setState(() {
isHover = true;
});
},
onExit: (_) {
setState(() {
isHover = false;
});
},
child: Stack(
children: [
_buildCallout(),
Positioned(top: 5, right: 5, child: _buildMenu()),
],
),
);
}
Widget _buildCallout() {
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
color: tint.color(context),
),
padding: const EdgeInsets.only(top: 8, bottom: 8, left: 0, right: 15),
width: double.infinity,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildEmoji(),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: widget.node.children
.map(
(child) => widget.editorState.service.renderPluginService
.buildPluginWidget(
child is TextNode
? NodeWidgetContext<TextNode>(
context: context,
node: child,
editorState: widget.editorState,
)
: NodeWidgetContext<Node>(
context: context,
node: child,
editorState: widget.editorState,
),
),
)
.toList(),
),
),
],
),
);
}
Widget _popover({
required PopoverController controller,
required Widget Function(BuildContext context) popupBuilder,
required Widget child,
Size size = const Size(200, 460),
}) {
return AppFlowyPopover(
controller: controller,
constraints: BoxConstraints.loose(size),
triggerActions: 0,
popupBuilder: popupBuilder,
child: child);
}
Widget _buildMenu() {
return _popover(
controller: colorPopoverController,
popupBuilder: (context) => _buildColorPicker(),
child: isHover
? Wrap(
children: [
FlowyIconButton(
icon: const Icon(Icons.color_lens_outlined),
onPressed: () {
colorPopoverController.show();
},
),
FlowyIconButton(
icon: const Icon(Icons.delete_forever_outlined),
onPressed: () {
deleteNode();
},
)
],
)
: const SizedBox(width: 0),
);
}
Widget _buildColorPicker() {
return FlowyColorPicker(
colors: FlowyTint.values
.map((t) => ColorOption(
color: t.color(context),
name: t.tintName(AppFlowyEditorLocalizations.current),
))
.toList(),
selected: tint.color(context),
onTap: (color, index) {
setColor(FlowyTint.values[index]);
colorPopoverController.close();
},
);
}
Widget _buildEmoji() {
return _popover(
controller: emojiPopoverController,
popupBuilder: (context) => _buildEmojiPicker(),
size: const Size(300, 200),
child: FlowyTextButton(
emoji,
fontSize: 18,
fillColor: Colors.transparent,
onPressed: () {
emojiPopoverController.show();
},
),
);
}
Widget _buildEmojiPicker() {
return EmojiSelectionMenu(
editorState: widget.editorState,
onSubmitted: (emoji) {
setEmoji(emoji.emoji);
emojiPopoverController.close();
},
onExit: () {},
);
}
void setColor(FlowyTint tint) {
final transaction = widget.editorState.transaction
..updateNode(widget.node, {
kCalloutAttrColor: tint.name,
});
widget.editorState.apply(transaction);
}
void setEmoji(String emoji) {
final transaction = widget.editorState.transaction
..updateNode(widget.node, {
kCalloutAttrEmoji: emoji,
});
widget.editorState.apply(transaction);
}
void deleteNode() {
final transaction = widget.editorState.transaction..deleteNode(widget.node);
widget.editorState.apply(transaction);
}
FlowyTint get tint {
final name = widget.node.attributes[kCalloutAttrColor];
return (name is String) ? FlowyTint.fromJson(name) : FlowyTint.tint1;
}
String get emoji {
return widget.node.attributes[kCalloutAttrEmoji] ?? "💡";
}
@override
Position start() => Position(path: widget.node.path, offset: 0);
@override
Position end() => Position(path: widget.node.path, offset: 1);
@override
Position getPositionInOffset(Offset start) => end();
@override
bool get shouldCursorBlink => false;
@override
CursorStyle get cursorStyle => CursorStyle.borderLine;
@override
Rect? getCursorRectInPosition(Position position) {
final size = _renderBox.size;
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
}
@override
List<Rect> getRectsInSelection(Selection selection) =>
[Offset.zero & _renderBox.size];
@override
Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
path: widget.node.path,
startOffset: 0,
endOffset: 1,
);
@override
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
}

View File

@ -0,0 +1,56 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
extension FlowyTintExtension on FlowyTint {
String tintName(
AppFlowyEditorLocalizations l10n, {
ThemeMode? themeMode,
String? theme,
}) {
if (themeMode == ThemeMode.light && theme == BuiltInTheme.light) {
switch (this) {
case FlowyTint.tint1:
return l10n.lightLightTint1;
case FlowyTint.tint2:
return l10n.lightLightTint2;
case FlowyTint.tint3:
return l10n.lightLightTint3;
case FlowyTint.tint4:
return l10n.lightLightTint4;
case FlowyTint.tint5:
return l10n.lightLightTint5;
case FlowyTint.tint6:
return l10n.lightLightTint6;
case FlowyTint.tint7:
return l10n.lightLightTint7;
case FlowyTint.tint8:
return l10n.lightLightTint8;
case FlowyTint.tint9:
return l10n.lightLightTint9;
}
}
switch (this) {
case FlowyTint.tint1:
return l10n.tint1;
case FlowyTint.tint2:
return l10n.tint2;
case FlowyTint.tint3:
return l10n.tint3;
case FlowyTint.tint4:
return l10n.tint4;
case FlowyTint.tint5:
return l10n.tint5;
case FlowyTint.tint6:
return l10n.tint6;
case FlowyTint.tint7:
return l10n.tint7;
case FlowyTint.tint8:
return l10n.tint8;
case FlowyTint.tint9:
return l10n.tint9;
}
}
}

View File

@ -14,8 +14,12 @@ dependencies:
sdk: flutter
appflowy_editor:
path: ../appflowy_editor
flowy_infra_ui:
flowy_infra:
path: ../flowy_infra
flowy_infra_ui:
path: ../flowy_infra_ui
appflowy_popover:
path: ../appflowy_popover
flutter_math_fork: ^0.6.3+1
highlight: ^0.7.0
shared_preferences: ^2.0.15

View File

@ -120,3 +120,47 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
);
}
}
enum FlowyTint {
tint1,
tint2,
tint3,
tint4,
tint5,
tint6,
tint7,
tint8,
tint9;
String toJson() => name;
static FlowyTint fromJson(String json) {
try {
return FlowyTint.values.byName(json);
} catch (_) {
return FlowyTint.tint1;
}
}
Color color(BuildContext context) {
switch (this) {
case FlowyTint.tint1:
return AFThemeExtension.of(context).tint1;
case FlowyTint.tint2:
return AFThemeExtension.of(context).tint2;
case FlowyTint.tint3:
return AFThemeExtension.of(context).tint3;
case FlowyTint.tint4:
return AFThemeExtension.of(context).tint4;
case FlowyTint.tint5:
return AFThemeExtension.of(context).tint5;
case FlowyTint.tint6:
return AFThemeExtension.of(context).tint6;
case FlowyTint.tint7:
return AFThemeExtension.of(context).tint7;
case FlowyTint.tint8:
return AFThemeExtension.of(context).tint8;
case FlowyTint.tint9:
return AFThemeExtension.of(context).tint9;
}
}
}

View File

@ -221,4 +221,4 @@ packages:
version: "6.1.0"
sdks:
dart: ">=2.18.0 <3.0.0"
flutter: ">=2.11.0-0.1.pre"
flutter: ">=3.3.0"

View File

@ -4,8 +4,8 @@ version: 0.0.1
homepage:
environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=1.17.0"
sdk: ">=2.18.0 <3.0.0"
flutter: ">=3.3.0"
dependencies:
flutter:

View File

@ -0,0 +1,80 @@
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
class ColorOption {
const ColorOption({
required this.color,
required this.name,
});
final Color color;
final String name;
}
class FlowyColorPicker extends StatelessWidget {
final List<ColorOption> colors;
final Color? selected;
final Function(Color color, int index)? onTap;
final double separatorSize;
final double iconSize;
final double itemHeight;
const FlowyColorPicker({
Key? key,
required this.colors,
this.selected,
this.onTap,
this.separatorSize = 4,
this.iconSize = 16,
this.itemHeight = 32,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
separatorBuilder: (context, index) {
return VSpace(separatorSize);
},
itemCount: colors.length,
physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return _buildColorOption(colors[index], index);
},
);
}
Widget _buildColorOption(ColorOption option, int i) {
Widget? checkmark;
if (selected == option.color) {
checkmark = svgWidget("grid/checkmark");
}
final colorIcon = SizedBox.square(
dimension: iconSize,
child: Container(
decoration: BoxDecoration(
color: option.color,
shape: BoxShape.circle,
),
),
);
return SizedBox(
height: itemHeight,
child: FlowyButton(
text: FlowyText.medium(option.name),
leftIcon: colorIcon,
rightIcon: checkmark,
onTap: () {
onTap?.call(option.color, i);
},
),
);
}
}