mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
3de4e1cb12
commit
000569a836
@ -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
|
||||
|
@ -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"
|
||||
}
|
@ -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) {
|
||||
|
@ -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")
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user