diff --git a/bruig/flutterui/bruig/lib/components/chat/active_chat.dart b/bruig/flutterui/bruig/lib/components/chat/active_chat.dart index 462bda88..f7efbaa5 100644 --- a/bruig/flutterui/bruig/lib/components/chat/active_chat.dart +++ b/bruig/flutterui/bruig/lib/components/chat/active_chat.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:bruig/components/chat/chat_side_menu.dart'; import 'package:bruig/components/manage_gc.dart'; +import 'package:bruig/components/typing_emoji_panel.dart'; +import 'package:bruig/models/emoji.dart'; import 'package:bruig/models/snackbar.dart'; import 'package:bruig/models/uistate.dart'; import 'package:bruig/screens/chats.dart'; @@ -9,6 +11,7 @@ import 'package:bruig/models/client.dart'; import 'package:flutter/material.dart'; import 'package:bruig/components/profile.dart'; import 'package:bruig/components/chat/messages.dart'; +import 'package:provider/provider.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:bruig/components/chat/input.dart'; @@ -126,8 +129,20 @@ class _ActiveChatState extends State { Column( children: [ Expanded( - child: Messages( - chat, client, _itemScrollController, _itemPositionsListener), + child: Stack(children: [ + Messages(chat, client, _itemScrollController, + _itemPositionsListener), + Positioned( + bottom: 10, + left: 10, + right: 10, + child: Consumer( + builder: (context, typingEmoji, child) => + TypingEmojiPanel( + model: typingEmoji, + focusNode: inputFocusNode, + ))) + ]), ), Container( padding: isScreenSmall diff --git a/bruig/flutterui/bruig/lib/components/chat/input.dart b/bruig/flutterui/bruig/lib/components/chat/input.dart index 5816e3c7..475c6bfc 100644 --- a/bruig/flutterui/bruig/lib/components/chat/input.dart +++ b/bruig/flutterui/bruig/lib/components/chat/input.dart @@ -1,8 +1,10 @@ import 'dart:math'; import 'package:bruig/components/attach_file.dart'; +import 'package:bruig/models/emoji.dart'; import 'package:bruig/models/uistate.dart'; import 'package:bruig/screens/chats.dart'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; import 'package:bruig/components/chat/types.dart'; import 'package:bruig/models/client.dart'; @@ -92,6 +94,7 @@ class _ChatInputState extends State { controller.text = widget.chat.workingMsg; widget.inputFocusNode.noModEnterKeyHandler = sendMsg; widget.inputFocusNode.pasteEventHandler = pasteEvent; + widget.inputFocusNode.addEmojiHandler = addEmoji; } @override @@ -107,12 +110,15 @@ class _ChatInputState extends State { } oldWidget.inputFocusNode.pasteEventHandler = null; widget.inputFocusNode.pasteEventHandler = pasteEvent; + oldWidget.inputFocusNode.addEmojiHandler = null; + widget.inputFocusNode.addEmojiHandler = addEmoji; } @override void dispose() { widget.inputFocusNode.noModEnterKeyHandler = null; widget.inputFocusNode.pasteEventHandler = null; + widget.inputFocusNode.addEmojiHandler = null; super.dispose(); } @@ -145,6 +151,34 @@ class _ChatInputState extends State { embeds = []; }); } + + Provider.of(context, listen: false).clearSelection(); + } + + void addEmoji(Emoji? e) { + if (e != null) { + // Selected emoji from panel eidget. + var oldPos = controller.selection.start; + var newText = controller.selection.textBefore(controller.text) + + e.emoji + + controller.selection.textAfter(controller.text); + widget.chat.workingMsg = newText; + controller.value = TextEditingValue( + text: newText, + selection: TextSelection.collapsed(offset: oldPos + e.emoji.length)); + return; + } + + // Selected emoji from typing panel. + var typingEmoji = Provider.of(context, listen: false); + var newText = typingEmoji.replaceTypedEmojiCode(controller); + if (newText == "") return; + + var oldPos = + controller.selection.start - typingEmoji.lastEmojiCode.length + 1; + widget.chat.workingMsg = newText; + controller.value = TextEditingValue( + text: newText, selection: TextSelection.collapsed(offset: oldPos)); } void attachFile() { @@ -185,10 +219,25 @@ class _ChatInputState extends State { onPressed: attachFile, icon: const Icon(Icons.add_outlined)), const SizedBox(width: 5), + IconButton( + padding: const EdgeInsets.all(0), + iconSize: 25, + onPressed: () { + var emojiModel = + TypingEmojiSelModel.of(context, listen: false); + emojiModel.showAddEmojiPanel.value = + !emojiModel.showAddEmojiPanel.value; + }, + icon: const Icon(Icons.emoji_emotions_outlined)), + const SizedBox(width: 5), Expanded( child: TextField( onChanged: (value) { widget.chat.workingMsg = value; + + // Check if user is typing an emoji code (:foo:). + TypingEmojiSelModel.of(context, listen: false) + .maybeSelectEmojis(controller); }, autofocus: isScreenSmall ? false : true, focusNode: widget.inputFocusNode.inputFocusNode, diff --git a/bruig/flutterui/bruig/lib/components/typing_emoji_panel.dart b/bruig/flutterui/bruig/lib/components/typing_emoji_panel.dart new file mode 100644 index 00000000..8b05091e --- /dev/null +++ b/bruig/flutterui/bruig/lib/components/typing_emoji_panel.dart @@ -0,0 +1,113 @@ +import 'package:bruig/components/containers.dart'; +import 'package:bruig/components/empty_widget.dart'; +import 'package:bruig/components/text.dart'; +import 'package:bruig/models/emoji.dart'; +import 'package:bruig/screens/chats.dart'; +import 'package:bruig/theme_manager.dart'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/material.dart'; + +class TypingEmojiPanel extends StatefulWidget { + final TypingEmojiSelModel model; + final CustomInputFocusNode focusNode; + const TypingEmojiPanel( + {required this.model, required this.focusNode, super.key}); + + @override + State createState() => _TypingEmojiPanelState(); +} + +class _TypingEmojiPanelState extends State { + TypingEmojiSelModel get model => widget.model; + List emojis = []; + int selected = -1; + ScrollController scroll = ScrollController(); + + void updated() { + setState(() { + emojis = model.selectionList.toList(); + }); + } + + void updatedSelected() { + setState(() { + selected = model.selected.value; + var offset = selected.toDouble() * 48; // 48 == height of tile + if (scroll.hasClients && offset >= 0) { + scroll.animateTo(offset, + duration: const Duration(milliseconds: 500), + curve: Easing.standard); + } + }); + } + + void updatedShowAddPanel() { + setState(() {}); + } + + void emojiSelectedOnEmojiPicker(category, emoji) { + if (widget.focusNode.addEmojiHandler != null) { + widget.focusNode.addEmojiHandler!(emoji); + } + } + + @override + void initState() { + super.initState(); + model.addListener(updated); + model.selected.addListener(updatedSelected); + model.showAddEmojiPanel.addListener(updatedShowAddPanel); + } + + @override + void dispose() { + model.removeListener(updated); + model.selected.removeListener(updatedSelected); + model.showAddEmojiPanel.removeListener(updatedShowAddPanel); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var theme = ThemeNotifier.of(context); + + if (model.showAddEmojiPanel.value) { + return EmojiPicker( + onEmojiSelected: emojiSelectedOnEmojiPicker, + config: theme.emojiPickerConfig, + ); + } + + if (!model.isTypingEmoji) { + return const Empty(); + } + + return Box( + color: SurfaceColor.primaryContainer, + child: Container( + constraints: const BoxConstraints(maxHeight: 200), + child: Scrollbar( + thumbVisibility: true, + controller: scroll, + child: ListView.builder( + shrinkWrap: true, + itemCount: emojis.length, + controller: scroll, + itemBuilder: (BuildContext context, int index) { + var e = emojis[index]; + return ListTile( + onTap: () {}, + // hoverColor: Colors.amber, + selectedTileColor: Colors.transparent, + selectedColor: + theme.extraColors.selectedItemOnSurfaceListView, + selected: index == selected, + leading: Txt.H(e.emoji), + title: Txt.M(e.name), + ); + }, + ), + )), + ); + } +} diff --git a/bruig/flutterui/bruig/lib/main.dart b/bruig/flutterui/bruig/lib/main.dart index b29ca4ab..5139ed03 100644 --- a/bruig/flutterui/bruig/lib/main.dart +++ b/bruig/flutterui/bruig/lib/main.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'dart:developer' as developer; import 'package:bruig/components/route_error.dart'; +import 'package:bruig/models/emoji.dart'; import 'package:bruig/models/menus.dart'; import 'package:bruig/models/payments.dart'; import 'package:bruig/models/resources.dart'; @@ -159,6 +160,7 @@ Future runMainApp(Config cfg) async { ChangeNotifierProvider(create: (c) => SnackBarModel()), ChangeNotifierProvider(create: (c) => PaymentsModel()), ChangeNotifierProvider(create: (c) => WalletModel()), + ChangeNotifierProvider(create: (c) => TypingEmojiSelModel()), ], child: App(cfg, globalLogModel, globalShutdownModel), )); diff --git a/bruig/flutterui/bruig/lib/models/emoji.dart b/bruig/flutterui/bruig/lib/models/emoji.dart new file mode 100644 index 00000000..66ca7a81 --- /dev/null +++ b/bruig/flutterui/bruig/lib/models/emoji.dart @@ -0,0 +1,164 @@ +import 'dart:collection'; + +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:retrieval/key_value_trie.dart'; + +final KeyValueTrie> emojiTrie = _initEmojiTrie(); + +KeyValueTrie> _initEmojiTrie() { + final mp = >{}; + + // Gather map of full words to list of emojis. + for (var cat in defaultEmojiSet) { + for (var e in cat.emoji) { + var name = e.name.toLowerCase(); + var words = name.split(" "); + + // Add the emoji code replacing " " with "_". + words.add(name.replaceAll(" ", "_")); + + for (var w in words) { + if (mp.containsKey(w)) { + mp[w]!.add(e); + } else { + mp[w] = [e]; + } + } + } + } + + // Convert to trie for faster prefix search. + final kv = KeyValueTrie>(); + for (var k in mp.keys) { + kv.insert(k, mp[k]!); + } + + return kv; +} + +List findEmojis(String word) { + try { + var lists = emojiTrie.find(word); + return lists.fold([], (res, v) { + for (var e in v) { + if (!res.contains(e)) { + res.add(e); + } + } + return res; + }); + } catch (exception) { + return []; + } +} + +final _lastEmojiRegExp = RegExp(r':(\b\w{2,})$'); + +String lastEmojiCodeFrom(String s) { + var lastMatchIndex = s.lastIndexOf(_lastEmojiRegExp); + if (lastMatchIndex == -1) { + return ""; + } + + var match = _lastEmojiRegExp.firstMatch(s.substring(lastMatchIndex)); + if (match == null) return ""; // Should not happen. + return match.group(1)!; +} + +class TypingEmojiSelModel extends ChangeNotifier { + static TypingEmojiSelModel of(BuildContext context, {listen = true}) => + Provider.of(context, listen: listen); + + List _selectionList = []; + Iterable get selectionList => UnmodifiableListView(_selectionList); + bool get isTypingEmoji => _selectionList.isNotEmpty; + + ValueNotifier selected = ValueNotifier(-1); + String _lastEmojiCode = ""; + String get lastEmojiCode => _lastEmojiCode; + + Emoji? get selectedEmoji => + selected.value > -1 && selected.value < _selectionList.length + ? _selectionList[selected.value] + : null; + + ValueNotifier showAddEmojiPanel = ValueNotifier(false); + + void clearSelection() { + if (showAddEmojiPanel.value) { + showAddEmojiPanel.value = false; + } + if (_selectionList.isEmpty) { + return; + } + + _selectionList.clear(); + selected.value = -1; + _lastEmojiCode = ""; + notifyListeners(); + } + + void maybeSelectEmojis(TextEditingController controller) { + var sel = controller.selection; + if (sel.start != sel.end) { + clearSelection(); + return; + } + + var before = controller.selection.textBefore(controller.text); + var emojiCode = lastEmojiCodeFrom(before); + + // Require 2 chars to start searching. + if (emojiCode.length < 2) { + clearSelection(); + return; + } + + var emojis = findEmojis(emojiCode); + if (emojis.isEmpty) { + clearSelection(); + return; + } + + _selectionList = emojis; + _lastEmojiCode = emojiCode; + notifyListeners(); + selected.value = 0; + } + + void changeSelection(int delta) { + int newSel = selected.value + delta; + if (newSel < 0 || newSel >= _selectionList.length) { + return; + } + + selected.value = newSel; + } + + String replaceTypedEmojiCode(TextEditingController controller) { + if (selectedEmoji == null) { + return ""; + } + + var emoji = selectedEmoji!; + + var sel = controller.selection; + if (sel.start != sel.end) { + clearSelection(); + return ""; + } + + var before = controller.selection.textBefore(controller.text); + var after = controller.selection.textAfter(controller.text); + var emojiCode = lastEmojiCodeFrom(before); + + var emojiCodeStartIndex = before.lastIndexOf(":$emojiCode"); + if (emojiCodeStartIndex == -1) return ""; // Should not happen. + + before = before.substring(0, emojiCodeStartIndex); + var newText = "$before${emoji.emoji}$after"; + return newText; + } +} diff --git a/bruig/flutterui/bruig/lib/models/menus.dart b/bruig/flutterui/bruig/lib/models/menus.dart index fa8f6ed6..4baae0e5 100644 --- a/bruig/flutterui/bruig/lib/models/menus.dart +++ b/bruig/flutterui/bruig/lib/models/menus.dart @@ -4,6 +4,7 @@ import 'package:bruig/components/rename_chat.dart'; import 'package:bruig/components/suggest_kx.dart'; import 'package:bruig/components/trans_reset.dart'; import 'package:bruig/models/client.dart'; +import 'package:bruig/models/emoji.dart'; import 'package:bruig/models/log.dart'; import 'package:bruig/models/notifications.dart'; import 'package:bruig/models/resources.dart'; @@ -73,9 +74,10 @@ final List mainMenu = [ MainMenuItem( "Chat", ChatsScreen.routeName, - (context) => Consumer2( - builder: (context, client, ntfns, child) => - ChatsScreen(client, ntfns)), + (context) => + Consumer3( + builder: (context, client, ntfns, typingEmoji, child) => + ChatsScreen(client, ntfns, typingEmoji)), (context) => const ChatsScreenTitle(), const SidebarSvgIcon("assets/icons/icons-menu-chat.svg"), []), diff --git a/bruig/flutterui/bruig/lib/screens/chats.dart b/bruig/flutterui/bruig/lib/screens/chats.dart index 727f96b1..a9118e3f 100644 --- a/bruig/flutterui/bruig/lib/screens/chats.dart +++ b/bruig/flutterui/bruig/lib/screens/chats.dart @@ -5,10 +5,12 @@ import 'package:bruig/components/chats_list.dart'; import 'package:bruig/components/addressbook/addressbook.dart'; import 'package:bruig/components/text.dart'; import 'package:bruig/models/client.dart'; +import 'package:bruig/models/emoji.dart'; import 'package:bruig/models/notifications.dart'; import 'package:bruig/models/uistate.dart'; import 'package:bruig/screens/needs_out_channel.dart'; import 'package:bruig/theme_manager.dart'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:golib_plugin/definitions.dart'; @@ -72,7 +74,8 @@ class ChatsScreen extends StatefulWidget { static const routeName = '/chat'; final ClientModel client; final AppNotifications ntfns; - const ChatsScreen(this.client, this.ntfns, {super.key}); + final TypingEmojiSelModel typingEmoji; + const ChatsScreen(this.client, this.ntfns, this.typingEmoji, {super.key}); static gotoChatScreenFor(BuildContext context, ChatModel chat) { ClientModel.of(context, listen: false).active = chat; @@ -189,6 +192,8 @@ After the invitation is accepted, you'll be able to chat with them, and if they } } +typedef AddEmojiCallback = Function(Emoji?); + // This class is a hack to pass a FocusNode down the component stack along with // callbacks for the Input() class to know when to send vs when to add new lines // to the input component. There should to be a better way to do this. @@ -215,10 +220,12 @@ class CustomInputFocusNode { Function? noModEnterKeyHandler; + AddEmojiCallback? addEmojiHandler; + // If set, this is called when a paste event ("ctrl+v") is detected. Function? pasteEventHandler; - CustomInputFocusNode() { + CustomInputFocusNode(TypingEmojiSelModel typingEmoji) { inputFocusNode = FocusNode(onKeyEvent: (node, event) { if (event.logicalKey.keyId == LogicalKeyboardKey.controlLeft.keyId) { ctrlLeft = event is KeyDownEvent; @@ -239,6 +246,18 @@ class CustomInputFocusNode { LogicalKeyboardKey.shiftRight.keyId) { shiftRight = event is KeyDownEvent; } else if (event.logicalKey.keyId == LogicalKeyboardKey.enter.keyId) { + // When typing an emoji code and there's a handler, use it. + if (addEmojiHandler != null && typingEmoji.selectedEmoji != null) { + if (event is KeyDownEvent) { + // Ignore KeyDown and only act on KeyUp to ensure no double insertion + // or insertion + sendMsg. + return KeyEventResult.handled; + } + addEmojiHandler!(null); + typingEmoji.clearSelection(); + return KeyEventResult.handled; + } + // When a special handler is set, call it to bypass standard processing // of the key and return the 'handled' result. if (noModEnterKeyHandler != null && !anyMod) { @@ -252,6 +271,18 @@ class CustomInputFocusNode { pasteEventHandler!(); return KeyEventResult.handled; } + } else if (event.logicalKey.keyId == LogicalKeyboardKey.arrowUp.keyId && + typingEmoji.isTypingEmoji) { + if (event is KeyDownEvent) { + typingEmoji.changeSelection(-1); // Move up list of emojis. + } + return KeyEventResult.handled; + } else if (event.logicalKey.keyId == LogicalKeyboardKey.arrowDown.keyId && + typingEmoji.isTypingEmoji) { + if (event is KeyDownEvent) { + typingEmoji.changeSelection(1); + } // Move down list of emojis. + return KeyEventResult.handled; } return KeyEventResult.ignored; @@ -262,7 +293,7 @@ class CustomInputFocusNode { class _ChatsScreenState extends State { ClientModel get client => widget.client; AppNotifications get ntfns => widget.ntfns; - final CustomInputFocusNode inputFocusNode = CustomInputFocusNode(); + late CustomInputFocusNode inputFocusNode; bool hasLNBalance = false; List userPostList = []; Timer? checkLNTimer; @@ -298,6 +329,7 @@ class _ChatsScreenState extends State { @override void initState() { super.initState(); + inputFocusNode = CustomInputFocusNode(widget.typingEmoji); keepCheckingLNHasBalance(); client.ui.showAddressBook.addListener(showAddressBookChanged); } diff --git a/bruig/flutterui/bruig/lib/theme_manager.dart b/bruig/flutterui/bruig/lib/theme_manager.dart index 1ca7d1de..b693d7cc 100644 --- a/bruig/flutterui/bruig/lib/theme_manager.dart +++ b/bruig/flutterui/bruig/lib/theme_manager.dart @@ -4,6 +4,7 @@ import 'package:bruig/util.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:provider/provider.dart'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emoji_picker; import './storage_manager.dart'; enum TextSize { @@ -103,10 +104,12 @@ enum SurfaceColor { class CustomColors { final Color sidebarDivider; final Color successOnSurface; + final Color selectedItemOnSurfaceListView; const CustomColors({ this.sidebarDivider = Colors.black, this.successOnSurface = const Color(0xFF2D882D), + this.selectedItemOnSurfaceListView = Colors.amber, }); } @@ -188,7 +191,7 @@ String appFontSizeKeyForScale(double scale) { return key; } -String _emojifont = Platform.isWindows ? "notoemoji_win" : "notoemoji_unix"; +String emojifont = Platform.isWindows ? "notoemoji_win" : "notoemoji_unix"; final TextTheme _interTextTheme = TextTheme( displayLarge: TextStyle( @@ -196,91 +199,91 @@ final TextTheme _interTextTheme = TextTheme( fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), displayMedium: TextStyle( debugLabel: 'interdisplayMedium', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), displaySmall: TextStyle( debugLabel: 'interdisplaySmall', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), headlineLarge: TextStyle( debugLabel: 'interheadlineLarge', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), headlineMedium: TextStyle( debugLabel: 'interheadlineMedium', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), headlineSmall: TextStyle( debugLabel: 'interheadlineSmall', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), titleLarge: TextStyle( debugLabel: 'intertitleLarge', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), titleMedium: TextStyle( debugLabel: 'intertitleMedium', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), titleSmall: TextStyle( debugLabel: 'intertitleSmall', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), bodyLarge: TextStyle( debugLabel: 'interbodyLarge', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), bodyMedium: TextStyle( debugLabel: 'interbodyMedium', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), bodySmall: TextStyle( debugLabel: 'interbodySmall', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), labelLarge: TextStyle( debugLabel: 'interlabelLarge', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), labelMedium: TextStyle( debugLabel: 'interlabelMedium', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), labelSmall: TextStyle( debugLabel: 'interlabelSmall', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.white70, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), ); final TextTheme _interBlackTextTheme = TextTheme( @@ -289,91 +292,91 @@ final TextTheme _interBlackTextTheme = TextTheme( fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black54, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), displayMedium: TextStyle( debugLabel: 'interdisplayMedium', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black54, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), displaySmall: TextStyle( debugLabel: 'interdisplaySmall', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black54, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), headlineLarge: TextStyle( debugLabel: 'interheadlineLarge', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black54, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), headlineMedium: TextStyle( debugLabel: 'interheadlineMedium', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black54, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), headlineSmall: TextStyle( debugLabel: 'interheadlineSmall', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black87, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), titleLarge: TextStyle( debugLabel: 'intertitleLarge', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black87, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), titleMedium: TextStyle( debugLabel: 'intertitleMedium', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black87, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), titleSmall: TextStyle( debugLabel: 'intertitleSmall', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black87, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), bodyLarge: TextStyle( debugLabel: 'interbodyLarge', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black87, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), bodyMedium: TextStyle( debugLabel: 'interbodyMedium', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black87, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), bodySmall: TextStyle( debugLabel: 'interbodySmall', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black87, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), labelLarge: TextStyle( debugLabel: 'interlabelLarge', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black87, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), labelMedium: TextStyle( debugLabel: 'interlabelMedium', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black87, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), labelSmall: TextStyle( debugLabel: 'interlabelSmall', fontFamily: 'Inter', decoration: TextDecoration.none, color: Colors.black87, - fontFamilyFallback: [_emojifont]), + fontFamilyFallback: [emojifont]), ); class AppTheme { @@ -498,6 +501,7 @@ final appThemes = { ), extraColors: const CustomColors( sidebarDivider: Colors.white, + selectedItemOnSurfaceListView: Color(0xFFFF6F00), ), extraTextStyles: const CustomTextStyles( chatListGcIndicator: TextStyle( @@ -609,6 +613,31 @@ class ThemeNotifier with ChangeNotifier { await StorageManager.saveData(StorageManager.themeModeKey, value); _clearTxtStyleCache(); _rebuildMarkdownStyleSheet(); + + _emojiPickerConfig = emoji_picker.Config( + emojiTextStyle: TextStyle(fontFamily: emojifont), + categoryViewConfig: emoji_picker.CategoryViewConfig( + backgroundColor: colors.secondaryContainer, + iconColor: colors.outline, + iconColorSelected: colors.secondary, + indicatorColor: colors.secondary, + ), + emojiViewConfig: emoji_picker.EmojiViewConfig( + backgroundColor: colors.primaryContainer, + ), + searchViewConfig: emoji_picker.SearchViewConfig( + backgroundColor: colors.secondaryContainer, + ), + bottomActionBarConfig: emoji_picker.BottomActionBarConfig( + backgroundColor: colors.tertiaryContainer, + buttonColor: colors.onSurfaceVariant, + showBackspaceButton: false, + ), + skinToneConfig: emoji_picker.SkinToneConfig( + dialogBackgroundColor: colors.secondaryContainer, + ), + ); + _themeLoaded = true; notifyListeners(); } @@ -790,4 +819,7 @@ class ThemeNotifier with ChangeNotifier { ); _mdStyleSheet.styles["pre"] = _mdStyleSheet.code; } + + late emoji_picker.Config _emojiPickerConfig; + emoji_picker.Config get emojiPickerConfig => _emojiPickerConfig; } diff --git a/bruig/flutterui/bruig/linux/flutter/generated_plugin_registrant.cc b/bruig/flutterui/bruig/linux/flutter/generated_plugin_registrant.cc index b8b6247c..0e83508c 100644 --- a/bruig/flutterui/bruig/linux/flutter/generated_plugin_registrant.cc +++ b/bruig/flutterui/bruig/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -16,6 +17,9 @@ #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) emoji_picker_flutter_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "EmojiPickerFlutterPlugin"); + emoji_picker_flutter_plugin_register_with_registrar(emoji_picker_flutter_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/bruig/flutterui/bruig/linux/flutter/generated_plugins.cmake b/bruig/flutterui/bruig/linux/flutter/generated_plugins.cmake index 4b75b39e..82eb3999 100644 --- a/bruig/flutterui/bruig/linux/flutter/generated_plugins.cmake +++ b/bruig/flutterui/bruig/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + emoji_picker_flutter file_selector_linux golib_plugin image_compression_flutter diff --git a/bruig/flutterui/bruig/macos/Flutter/GeneratedPluginRegistrant.swift b/bruig/flutterui/bruig/macos/Flutter/GeneratedPluginRegistrant.swift index 11e051b4..bf46d5a6 100644 --- a/bruig/flutterui/bruig/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/bruig/flutterui/bruig/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import device_info_plus +import emoji_picker_flutter import file_selector_macos import flutter_image_compress_macos import flutter_local_notifications @@ -25,6 +26,7 @@ import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) diff --git a/bruig/flutterui/bruig/pubspec.lock b/bruig/flutterui/bruig/pubspec.lock index c72dcf94..3ef72b29 100644 --- a/bruig/flutterui/bruig/pubspec.lock +++ b/bruig/flutterui/bruig/pubspec.lock @@ -169,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.3" + emoji_picker_flutter: + dependency: "direct main" + description: + name: emoji_picker_flutter + sha256: "3bf6d4cadc188215570a15c80fd7aeecec312b1cb3168ab08394e0faa4161fcb" + url: "https://pub.dev" + source: hosted + version: "3.0.0" fake_async: dependency: transitive description: @@ -912,6 +920,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + retrieval: + dependency: "direct main" + description: + name: retrieval + sha256: b8fe753d97f2728a513d0e48a240cfe8fff9666523ba6b78da4fa7aa32c805d7 + url: "https://pub.dev" + source: hosted + version: "1.0.1" rxdart: dependency: transitive description: diff --git a/bruig/flutterui/bruig/pubspec.yaml b/bruig/flutterui/bruig/pubspec.yaml index 973bc650..6ccdd042 100644 --- a/bruig/flutterui/bruig/pubspec.yaml +++ b/bruig/flutterui/bruig/pubspec.yaml @@ -75,6 +75,8 @@ dependencies: super_clipboard: ^0.8.22 image_compression_flutter: ^1.0.4 archive: ^3.6.1 + emoji_picker_flutter: ^3.0.0 + retrieval: ^1.0.1 msix_config: display_name: Bison Relay GUI diff --git a/bruig/flutterui/bruig/windows/flutter/generated_plugin_registrant.cc b/bruig/flutterui/bruig/windows/flutter/generated_plugin_registrant.cc index 149dc917..1ca4c153 100644 --- a/bruig/flutterui/bruig/windows/flutter/generated_plugin_registrant.cc +++ b/bruig/flutterui/bruig/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -18,6 +19,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + EmojiPickerFlutterPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); GolibPluginCApiRegisterWithRegistrar( diff --git a/bruig/flutterui/bruig/windows/flutter/generated_plugins.cmake b/bruig/flutterui/bruig/windows/flutter/generated_plugins.cmake index 27165d82..bfbc2523 100644 --- a/bruig/flutterui/bruig/windows/flutter/generated_plugins.cmake +++ b/bruig/flutterui/bruig/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + emoji_picker_flutter file_selector_windows golib_plugin image_compression_flutter