diff --git a/frontend/app_flowy/lib/plugins/document/document_page.dart b/frontend/app_flowy/lib/plugins/document/document_page.dart index d9a9f800c6..e2a072fa6c 100644 --- a/frontend/app_flowy/lib/plugins/document/document_page.dart +++ b/frontend/app_flowy/lib/plugins/document/document_page.dart @@ -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 { kMathEquationType: MathEquationNodeWidgetBuidler(), // Code Block kCodeBlockType: CodeBlockNodeWidgetBuilder(), + // Card + kCalloutType: CalloutNodeWidgetBuilder(), }, shortcutEvents: [ // Divider diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_en.arb b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_en.arb index 4c3a2f835a..6e8575552f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_en.arb +++ b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_en.arb @@ -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" } \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart index 16fd7b9b6b..0f655c1d31 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart @@ -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 LibraryLoader(); Map _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 initializeMessages(String localeName) async { +Future 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) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_en.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_en.dart index a53c9256ad..9705827d12 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_en.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_en.dart @@ -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") }; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart index f4087fd2c8..fdd0bbb60c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart @@ -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 diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart index d5181174cf..0d96853d74 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart @@ -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 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(); + 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 { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index 69335e5145..ac2f14cc1a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -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) diff --git a/frontend/app_flowy/packages/appflowy_editor_plugins/lib/appflowy_editor_plugins.dart b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/appflowy_editor_plugins.dart index 890208c387..7dbf2eb000 100644 --- a/frontend/app_flowy/packages/appflowy_editor_plugins/lib/appflowy_editor_plugins.dart +++ b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/appflowy_editor_plugins.dart @@ -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'; \ No newline at end of file +export 'src/math_ equation/math_equation_node_widget.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/callout/callout_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/callout/callout_node_widget.dart new file mode 100644 index 0000000000..8a42c57035 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/callout/callout_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 { + @override + Widget build(NodeWidgetContext context) { + return _CalloutWidget( + key: context.node.key, + node: context.node, + editorState: context.editorState, + ); + } + + @override + NodeValidator 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( + context: context, + node: child, + editorState: widget.editorState, + ) + : NodeWidgetContext( + 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 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); +} diff --git a/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/extensions/theme_extension.dart b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/extensions/theme_extension.dart new file mode 100644 index 0000000000..11b5f23786 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor_plugins/lib/src/extensions/theme_extension.dart @@ -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; + } + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor_plugins/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor_plugins/pubspec.yaml index a68a92818c..ddb547ca85 100644 --- a/frontend/app_flowy/packages/appflowy_editor_plugins/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor_plugins/pubspec.yaml @@ -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 diff --git a/frontend/app_flowy/packages/flowy_infra/lib/theme_extension.dart b/frontend/app_flowy/packages/flowy_infra/lib/theme_extension.dart index 8569f987cd..bc72732fef 100644 --- a/frontend/app_flowy/packages/flowy_infra/lib/theme_extension.dart +++ b/frontend/app_flowy/packages/flowy_infra/lib/theme_extension.dart @@ -120,3 +120,47 @@ class AFThemeExtension extends ThemeExtension { ); } } + +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; + } + } +} diff --git a/frontend/app_flowy/packages/flowy_infra/pubspec.lock b/frontend/app_flowy/packages/flowy_infra/pubspec.lock index e97f235a59..53f54db884 100644 --- a/frontend/app_flowy/packages/flowy_infra/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_infra/pubspec.lock @@ -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" diff --git a/frontend/app_flowy/packages/flowy_infra/pubspec.yaml b/frontend/app_flowy/packages/flowy_infra/pubspec.yaml index 63f1b66b4e..a361184dbe 100644 --- a/frontend/app_flowy/packages/flowy_infra/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_infra/pubspec.yaml @@ -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: diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/color_picker.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/color_picker.dart new file mode 100644 index 0000000000..5ad7b9632a --- /dev/null +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/color_picker.dart @@ -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 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); + }, + ), + ); + } +}