mirror of
				https://github.com/AppFlowy-IO/AppFlowy.git
				synced 2025-10-31 18:15:09 +00:00 
			
		
		
		
	Merge pull request #376 from gaganyadav80/emoji_picker
feat: add emoji picker
This commit is contained in:
		
						commit
						959b2db9a3
					
				| @ -0,0 +1,6 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1"> | ||||
| <g id="surface1"> | ||||
| <path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,12.941176%,12.941176%);fill-opacity:1;" d="M 8 1.332031 C 11.683594 1.332031 14.667969 4.316406 14.667969 8 C 14.667969 11.683594 11.683594 14.667969 8 14.667969 C 4.316406 14.667969 1.332031 11.683594 1.332031 8 C 1.332031 4.316406 4.316406 1.332031 8 1.332031 Z M 8 2.332031 C 4.871094 2.332031 2.332031 4.871094 2.332031 8 C 2.332031 11.128906 4.871094 13.667969 8 13.667969 C 11.128906 13.667969 13.667969 11.128906 13.667969 8 C 13.667969 4.871094 11.128906 2.332031 8 2.332031 Z M 5.640625 9.855469 C 6.207031 10.574219 7.066406 11 8 11 C 8.929688 11 9.789062 10.574219 10.355469 9.859375 C 10.527344 9.640625 10.84375 9.605469 11.058594 9.777344 C 11.277344 9.949219 11.3125 10.261719 11.140625 10.476562 C 10.386719 11.433594 9.238281 12 8 12 C 6.757812 12 5.609375 11.429688 4.855469 10.476562 C 4.683594 10.257812 4.722656 9.945312 4.9375 9.773438 C 5.15625 9.601562 5.46875 9.640625 5.640625 9.855469 Z M 6 5.835938 C 6.460938 5.835938 6.832031 6.207031 6.832031 6.667969 C 6.832031 7.125 6.460938 7.5 6 7.5 C 5.539062 7.5 5.167969 7.125 5.167969 6.667969 C 5.167969 6.207031 5.539062 5.835938 6 5.835938 Z M 10 5.835938 C 10.460938 5.835938 10.832031 6.207031 10.832031 6.667969 C 10.832031 7.125 10.460938 7.5 10 7.5 C 9.539062 7.5 9.167969 7.125 9.167969 6.667969 C 9.167969 6.207031 9.539062 5.835938 10 5.835938 Z M 10 5.835938 "/> | ||||
| </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1.6 KiB | 
| @ -0,0 +1,6 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1"> | ||||
| <g id="surface1"> | ||||
| <path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,12.941176%,12.941176%);fill-opacity:1;" d="M 11.667969 8 C 13.691406 8 15.332031 9.640625 15.332031 11.667969 C 15.332031 13.691406 13.691406 15.332031 11.667969 15.332031 C 9.640625 15.332031 8 13.691406 8 11.667969 C 8 9.640625 9.640625 8 11.667969 8 Z M 8 1.332031 C 11.683594 1.332031 14.667969 4.316406 14.667969 8 C 14.667969 8.175781 14.660156 8.351562 14.648438 8.523438 C 14.355469 8.246094 14.023438 8.007812 13.664062 7.820312 C 13.570312 4.773438 11.070312 2.332031 8 2.332031 C 4.871094 2.332031 2.332031 4.871094 2.332031 8 C 2.332031 11.070312 4.773438 13.570312 7.820312 13.664062 C 8.007812 14.027344 8.246094 14.355469 8.523438 14.648438 C 8.351562 14.660156 8.175781 14.667969 8 14.667969 C 4.316406 14.667969 1.332031 11.683594 1.332031 8 C 1.332031 4.316406 4.316406 1.332031 8 1.332031 Z M 11.667969 9.332031 L 11.605469 9.339844 C 11.46875 9.363281 11.363281 9.46875 11.339844 9.605469 L 11.332031 9.667969 L 11.332031 11.332031 L 9.664062 11.332031 L 9.605469 11.339844 C 9.46875 11.363281 9.363281 11.46875 9.335938 11.605469 L 9.332031 11.667969 L 9.335938 11.726562 C 9.363281 11.863281 9.46875 11.96875 9.605469 11.996094 L 9.664062 12 L 11.332031 12 L 11.335938 13.667969 L 11.339844 13.730469 C 11.363281 13.863281 11.472656 13.972656 11.609375 13.996094 L 11.667969 14.003906 L 11.726562 13.996094 C 11.863281 13.972656 11.96875 13.863281 11.996094 13.730469 L 12 13.667969 L 12 12 L 13.667969 12 L 13.730469 11.996094 C 13.867188 11.96875 13.972656 11.863281 13.996094 11.726562 L 14.003906 11.667969 L 13.996094 11.605469 C 13.972656 11.46875 13.867188 11.363281 13.730469 11.339844 L 13.667969 11.332031 L 12 11.332031 L 12 9.667969 L 11.996094 9.605469 C 11.96875 9.46875 11.863281 9.363281 11.726562 9.339844 Z M 5.640625 9.855469 C 6.082031 10.414062 6.703125 10.796875 7.394531 10.941406 C 7.355469 11.175781 7.332031 11.417969 7.332031 11.667969 C 7.332031 11.761719 7.335938 11.855469 7.34375 11.945312 C 6.359375 11.785156 5.472656 11.261719 4.855469 10.476562 C 4.683594 10.257812 4.722656 9.945312 4.9375 9.773438 C 5.15625 9.601562 5.46875 9.640625 5.640625 9.855469 Z M 6 5.835938 C 6.460938 5.835938 6.832031 6.207031 6.832031 6.667969 C 6.832031 7.125 6.460938 7.5 6 7.5 C 5.539062 7.5 5.167969 7.125 5.167969 6.667969 C 5.167969 6.207031 5.539062 5.835938 6 5.835938 Z M 10 5.835938 C 10.460938 5.835938 10.832031 6.207031 10.832031 6.667969 C 10.832031 7.125 10.460938 7.5 10 7.5 C 9.539062 7.5 9.167969 7.125 9.167969 6.667969 C 9.167969 6.207031 9.539062 5.835938 10 5.835938 Z M 10 5.835938 "/> | ||||
| </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 2.8 KiB | 
| @ -2,6 +2,7 @@ import 'dart:async'; | ||||
| import 'dart:math'; | ||||
| 
 | ||||
| import 'package:app_flowy/workspace/presentation/stack_page/doc/widget/toolbar/history_button.dart'; | ||||
| import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter_quill/flutter_quill.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| @ -151,6 +152,11 @@ class EditorToolbar extends StatelessWidget implements PreferredSizeWidget { | ||||
|           controller: controller, | ||||
|           iconSize: toolbarIconSize, | ||||
|         ), | ||||
|         FlowyEmojiStyleButton( | ||||
|           normalIcon: 'editor/insert_emoticon', | ||||
|           controller: controller, | ||||
|           tooltipText: "Emoji Picker", | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -0,0 +1,5 @@ | ||||
| export 'src/config.dart'; | ||||
| export 'src/models/emoji_model.dart'; | ||||
| export 'src/emoji_picker.dart'; | ||||
| export 'src/emoji_picker_builder.dart'; | ||||
| export 'src/emoji_button.dart'; | ||||
| @ -0,0 +1,164 @@ | ||||
| import 'dart:math'; | ||||
| 
 | ||||
| import 'package:flutter/material.dart'; | ||||
| 
 | ||||
| import 'models/category_models.dart'; | ||||
| import 'emoji_picker.dart'; | ||||
| 
 | ||||
| /// Config for customizations | ||||
| class Config { | ||||
|   /// Constructor | ||||
|   const Config( | ||||
|       {this.columns = 7, | ||||
|       this.emojiSizeMax = 32.0, | ||||
|       this.verticalSpacing = 0, | ||||
|       this.horizontalSpacing = 0, | ||||
|       this.initCategory = Category.RECENT, | ||||
|       this.bgColor = const Color(0xFFEBEFF2), | ||||
|       this.indicatorColor = Colors.blue, | ||||
|       this.iconColor = Colors.grey, | ||||
|       this.iconColorSelected = Colors.blue, | ||||
|       this.progressIndicatorColor = Colors.blue, | ||||
|       this.backspaceColor = Colors.blue, | ||||
|       this.showRecentsTab = true, | ||||
|       this.recentsLimit = 28, | ||||
|       this.noRecentsText = 'No Recents', | ||||
|       this.noRecentsStyle = const TextStyle(fontSize: 20, color: Colors.black26), | ||||
|       this.tabIndicatorAnimDuration = kTabScrollDuration, | ||||
|       this.categoryIcons = const CategoryIcons(), | ||||
|       this.buttonMode = ButtonMode.MATERIAL}); | ||||
| 
 | ||||
|   /// Number of emojis per row | ||||
|   final int columns; | ||||
| 
 | ||||
|   /// Width and height the emoji will be maximal displayed | ||||
|   /// Can be smaller due to screen size and amount of columns | ||||
|   final double emojiSizeMax; | ||||
| 
 | ||||
|   /// Verical spacing between emojis | ||||
|   final double verticalSpacing; | ||||
| 
 | ||||
|   /// Horizontal spacing between emojis | ||||
|   final double horizontalSpacing; | ||||
| 
 | ||||
|   /// The initial [Category] that will be selected | ||||
|   /// This [Category] will have its button in the bottombar darkened | ||||
|   final Category initCategory; | ||||
| 
 | ||||
|   /// The background color of the Widget | ||||
|   final Color bgColor; | ||||
| 
 | ||||
|   /// The color of the category indicator | ||||
|   final Color indicatorColor; | ||||
| 
 | ||||
|   /// The color of the category icons | ||||
|   final Color iconColor; | ||||
| 
 | ||||
|   /// The color of the category icon when selected | ||||
|   final Color iconColorSelected; | ||||
| 
 | ||||
|   /// The color of the loading indicator during initalization | ||||
|   final Color progressIndicatorColor; | ||||
| 
 | ||||
|   /// The color of the backspace icon button | ||||
|   final Color backspaceColor; | ||||
| 
 | ||||
|   /// Show extra tab with recently used emoji | ||||
|   final bool showRecentsTab; | ||||
| 
 | ||||
|   /// Limit of recently used emoji that will be saved | ||||
|   final int recentsLimit; | ||||
| 
 | ||||
|   /// The text to be displayed if no recent emojis to display | ||||
|   final String noRecentsText; | ||||
| 
 | ||||
|   /// The text style for [noRecentsText] | ||||
|   final TextStyle noRecentsStyle; | ||||
| 
 | ||||
|   /// Duration of tab indicator to animate to next category | ||||
|   final Duration tabIndicatorAnimDuration; | ||||
| 
 | ||||
|   /// Determines the icon to display for each [Category] | ||||
|   final CategoryIcons categoryIcons; | ||||
| 
 | ||||
|   /// Change between Material and Cupertino button style | ||||
|   final ButtonMode buttonMode; | ||||
| 
 | ||||
|   /// Get Emoji size based on properties and screen width | ||||
|   double getEmojiSize(double width) { | ||||
|     final maxSize = width / columns; | ||||
|     return min(maxSize, emojiSizeMax); | ||||
|   } | ||||
| 
 | ||||
|   /// Returns the icon for the category | ||||
|   IconData getIconForCategory(Category category) { | ||||
|     switch (category) { | ||||
|       case Category.RECENT: | ||||
|         return categoryIcons.recentIcon; | ||||
|       case Category.SMILEYS: | ||||
|         return categoryIcons.smileyIcon; | ||||
|       case Category.ANIMALS: | ||||
|         return categoryIcons.animalIcon; | ||||
|       case Category.FOODS: | ||||
|         return categoryIcons.foodIcon; | ||||
|       case Category.TRAVEL: | ||||
|         return categoryIcons.travelIcon; | ||||
|       case Category.ACTIVITIES: | ||||
|         return categoryIcons.activityIcon; | ||||
|       case Category.OBJECTS: | ||||
|         return categoryIcons.objectIcon; | ||||
|       case Category.SYMBOLS: | ||||
|         return categoryIcons.symbolIcon; | ||||
|       case Category.FLAGS: | ||||
|         return categoryIcons.flagIcon; | ||||
|       case Category.SEARCH: | ||||
|         return categoryIcons.searchIcon; | ||||
|       default: | ||||
|         throw Exception('Unsupported Category'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(other) { | ||||
|     return (other is Config) && | ||||
|         other.columns == columns && | ||||
|         other.emojiSizeMax == emojiSizeMax && | ||||
|         other.verticalSpacing == verticalSpacing && | ||||
|         other.horizontalSpacing == horizontalSpacing && | ||||
|         other.initCategory == initCategory && | ||||
|         other.bgColor == bgColor && | ||||
|         other.indicatorColor == indicatorColor && | ||||
|         other.iconColor == iconColor && | ||||
|         other.iconColorSelected == iconColorSelected && | ||||
|         other.progressIndicatorColor == progressIndicatorColor && | ||||
|         other.backspaceColor == backspaceColor && | ||||
|         other.showRecentsTab == showRecentsTab && | ||||
|         other.recentsLimit == recentsLimit && | ||||
|         other.noRecentsText == noRecentsText && | ||||
|         other.noRecentsStyle == noRecentsStyle && | ||||
|         other.tabIndicatorAnimDuration == tabIndicatorAnimDuration && | ||||
|         other.categoryIcons == categoryIcons && | ||||
|         other.buttonMode == buttonMode; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|       columns.hashCode ^ | ||||
|       emojiSizeMax.hashCode ^ | ||||
|       verticalSpacing.hashCode ^ | ||||
|       horizontalSpacing.hashCode ^ | ||||
|       initCategory.hashCode ^ | ||||
|       bgColor.hashCode ^ | ||||
|       indicatorColor.hashCode ^ | ||||
|       iconColor.hashCode ^ | ||||
|       iconColorSelected.hashCode ^ | ||||
|       progressIndicatorColor.hashCode ^ | ||||
|       backspaceColor.hashCode ^ | ||||
|       showRecentsTab.hashCode ^ | ||||
|       recentsLimit.hashCode ^ | ||||
|       noRecentsText.hashCode ^ | ||||
|       noRecentsStyle.hashCode ^ | ||||
|       tabIndicatorAnimDuration.hashCode ^ | ||||
|       categoryIcons.hashCode ^ | ||||
|       buttonMode.hashCode; | ||||
| } | ||||
| @ -0,0 +1,276 @@ | ||||
| import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; | ||||
| import 'package:flutter/cupertino.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| 
 | ||||
| import 'models/category_models.dart'; | ||||
| import 'config.dart'; | ||||
| import 'models/emoji_model.dart'; | ||||
| import 'emoji_picker.dart'; | ||||
| import 'emoji_picker_builder.dart'; | ||||
| import 'emoji_view_state.dart'; | ||||
| 
 | ||||
| class DefaultEmojiPickerView extends EmojiPickerBuilder { | ||||
|   const DefaultEmojiPickerView(Config config, EmojiViewState state, {Key? key}) : super(config, state, key: key); | ||||
| 
 | ||||
|   @override | ||||
|   _DefaultEmojiPickerViewState createState() => _DefaultEmojiPickerViewState(); | ||||
| } | ||||
| 
 | ||||
| class _DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView> with TickerProviderStateMixin { | ||||
|   PageController? _pageController; | ||||
|   TabController? _tabController; | ||||
|   final TextEditingController _emojiController = TextEditingController(); | ||||
|   final FocusNode _emojiFocusNode = FocusNode(); | ||||
|   final CategoryEmoji _categoryEmoji = CategoryEmoji(Category.SEARCH, List.empty(growable: true)); | ||||
|   CategoryEmoji searchEmojiList = CategoryEmoji(Category.SEARCH, <Emoji>[]); | ||||
| 
 | ||||
|   @override | ||||
|   void initState() { | ||||
|     var initCategory = | ||||
|         widget.state.categoryEmoji.indexWhere((element) => element.category == widget.config.initCategory); | ||||
|     if (initCategory == -1) { | ||||
|       initCategory = 0; | ||||
|     } | ||||
|     _tabController = TabController(initialIndex: initCategory, length: widget.state.categoryEmoji.length, vsync: this); | ||||
|     _pageController = PageController(initialPage: initCategory); | ||||
|     _emojiFocusNode.requestFocus(); | ||||
| 
 | ||||
|     _emojiController.addListener(() { | ||||
|       String query = _emojiController.text.toLowerCase(); | ||||
|       if (query.isEmpty) { | ||||
|         searchEmojiList.emoji.clear(); | ||||
|         _pageController!.jumpToPage( | ||||
|           _tabController!.index, | ||||
|         ); | ||||
|       } else { | ||||
|         searchEmojiList.emoji.clear(); | ||||
|         for (var element in widget.state.categoryEmoji) { | ||||
|           searchEmojiList.emoji.addAll( | ||||
|             element.emoji.where((item) { | ||||
|               return item.name.toLowerCase().contains(query); | ||||
|             }).toList(), | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|       setState(() {}); | ||||
|     }); | ||||
|     super.initState(); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _emojiController.dispose(); | ||||
|     _emojiFocusNode.dispose(); | ||||
|     super.dispose(); | ||||
|   } | ||||
| 
 | ||||
|   Widget _buildBackspaceButton() { | ||||
|     if (widget.state.onBackspacePressed != null) { | ||||
|       return Material( | ||||
|         type: MaterialType.transparency, | ||||
|         child: IconButton( | ||||
|             padding: const EdgeInsets.only(bottom: 2), | ||||
|             icon: Icon( | ||||
|               Icons.backspace, | ||||
|               color: widget.config.backspaceColor, | ||||
|             ), | ||||
|             onPressed: () { | ||||
|               widget.state.onBackspacePressed!(); | ||||
|             }), | ||||
|       ); | ||||
|     } | ||||
|     return Container(); | ||||
|   } | ||||
| 
 | ||||
|   bool isEmojiSearching() { | ||||
|     bool result = searchEmojiList.emoji.isNotEmpty || _emojiController.text.isNotEmpty; | ||||
| 
 | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return LayoutBuilder( | ||||
|       builder: (context, constraints) { | ||||
|         final emojiSize = widget.config.getEmojiSize(constraints.maxWidth); | ||||
| 
 | ||||
|         return Container( | ||||
|           color: widget.config.bgColor, | ||||
|           padding: const EdgeInsets.all(5.0), | ||||
|           child: Column( | ||||
|             children: [ | ||||
|               SizedBox( | ||||
|                 height: 25.0, | ||||
|                 child: TextField( | ||||
|                   controller: _emojiController, | ||||
|                   focusNode: _emojiFocusNode, | ||||
|                   autofocus: true, | ||||
|                   style: const TextStyle(fontSize: 14.0), | ||||
|                   cursorWidth: 1.0, | ||||
|                   cursorColor: Colors.black, | ||||
|                   decoration: InputDecoration( | ||||
|                     contentPadding: const EdgeInsets.symmetric(horizontal: 5.0), | ||||
|                     hintText: "Search emoji", | ||||
|                     focusedBorder: OutlineInputBorder( | ||||
|                       borderRadius: BorderRadius.circular(4.0), | ||||
|                       borderSide: const BorderSide(), | ||||
|                       gapPadding: 0.0, | ||||
|                     ), | ||||
|                     border: OutlineInputBorder( | ||||
|                       borderRadius: BorderRadius.circular(4.0), | ||||
|                       borderSide: const BorderSide(), | ||||
|                       gapPadding: 0.0, | ||||
|                     ), | ||||
|                     filled: true, | ||||
|                     fillColor: Colors.white, | ||||
|                     hoverColor: Colors.white, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               Row( | ||||
|                 children: [ | ||||
|                   Expanded( | ||||
|                     child: TabBar( | ||||
|                       labelColor: widget.config.iconColorSelected, | ||||
|                       unselectedLabelColor: widget.config.iconColor, | ||||
|                       controller: isEmojiSearching() ? TabController(length: 1, vsync: this) : _tabController, | ||||
|                       labelPadding: EdgeInsets.zero, | ||||
|                       indicatorColor: widget.config.indicatorColor, | ||||
|                       padding: const EdgeInsets.symmetric(vertical: 5.0), | ||||
|                       indicator: BoxDecoration( | ||||
|                         border: Border.all(color: Colors.transparent), | ||||
|                         borderRadius: BorderRadius.circular(4.0), | ||||
|                         color: Colors.grey.withOpacity(0.5), | ||||
|                       ), | ||||
|                       onTap: (index) { | ||||
|                         _pageController!.animateToPage( | ||||
|                           index, | ||||
|                           duration: widget.config.tabIndicatorAnimDuration, | ||||
|                           curve: Curves.ease, | ||||
|                         ); | ||||
|                       }, | ||||
|                       tabs: isEmojiSearching() | ||||
|                           ? [_buildCategory(Category.SEARCH, emojiSize)] | ||||
|                           : widget.state.categoryEmoji | ||||
|                               .asMap() | ||||
|                               .entries | ||||
|                               .map<Widget>((item) => _buildCategory(item.value.category, emojiSize)) | ||||
|                               .toList(), | ||||
|                     ), | ||||
|                   ), | ||||
|                   _buildBackspaceButton(), | ||||
|                 ], | ||||
|               ), | ||||
|               Flexible( | ||||
|                 child: PageView.builder( | ||||
|                   itemCount: searchEmojiList.emoji.isNotEmpty ? 1 : widget.state.categoryEmoji.length, | ||||
|                   controller: _pageController, | ||||
|                   physics: const NeverScrollableScrollPhysics(), | ||||
|                   // onPageChanged: (index) { | ||||
|                   //   _tabController!.animateTo( | ||||
|                   //     index, | ||||
|                   //     duration: widget.config.tabIndicatorAnimDuration, | ||||
|                   //   ); | ||||
|                   // }, | ||||
|                   itemBuilder: (context, index) { | ||||
|                     CategoryEmoji catEmoji = isEmojiSearching() ? searchEmojiList : widget.state.categoryEmoji[index]; | ||||
|                     return _buildPage(emojiSize, catEmoji); | ||||
|                   }, | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Widget _buildCategory(Category category, double categorySize) { | ||||
|     return Tab( | ||||
|       height: categorySize, | ||||
|       child: Icon( | ||||
|         widget.config.getIconForCategory(category), | ||||
|         size: categorySize / 1.3, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Widget _buildButtonWidget({required VoidCallback onPressed, required Widget child}) { | ||||
|     if (widget.config.buttonMode == ButtonMode.MATERIAL) { | ||||
|       return TextButton( | ||||
|         onPressed: onPressed, | ||||
|         child: child, | ||||
|         style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)), | ||||
|       ); | ||||
|     } | ||||
|     return CupertinoButton(padding: EdgeInsets.zero, onPressed: onPressed, child: child); | ||||
|   } | ||||
| 
 | ||||
|   Widget _buildPage(double emojiSize, CategoryEmoji categoryEmoji) { | ||||
|     // Display notice if recent has no entries yet | ||||
|     final scrollController = ScrollController(); | ||||
| 
 | ||||
|     if (categoryEmoji.category == Category.RECENT && categoryEmoji.emoji.isEmpty) { | ||||
|       return _buildNoRecent(); | ||||
|     } else if (categoryEmoji.category == Category.SEARCH && categoryEmoji.emoji.isEmpty) { | ||||
|       return const Center(child: Text("No Emoji Found")); | ||||
|     } | ||||
|     // Build page normally | ||||
|     return ScrollbarListStack( | ||||
|       axis: Axis.vertical, | ||||
|       controller: scrollController, | ||||
|       barSize: 4.0, | ||||
|       scrollbarPadding: const EdgeInsets.symmetric(horizontal: 5.0), | ||||
|       handleColor: const Color(0xffDFE0E0), | ||||
|       trackColor: const Color(0xffDFE0E0), | ||||
|       child: ScrollConfiguration( | ||||
|         behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), | ||||
|         child: GridView.count( | ||||
|           scrollDirection: Axis.vertical, | ||||
|           physics: const ScrollPhysics(), | ||||
|           controller: scrollController, | ||||
|           shrinkWrap: true, | ||||
|           // primary: true, | ||||
|           padding: const EdgeInsets.all(0), | ||||
|           crossAxisCount: widget.config.columns, | ||||
|           mainAxisSpacing: widget.config.verticalSpacing, | ||||
|           crossAxisSpacing: widget.config.horizontalSpacing, | ||||
|           children: _categoryEmoji.emoji.isNotEmpty | ||||
|               ? _categoryEmoji.emoji.map<Widget>((e) => _buildEmoji(emojiSize, categoryEmoji, e)).toList() | ||||
|               : categoryEmoji.emoji.map<Widget>((item) => _buildEmoji(emojiSize, categoryEmoji, item)).toList(), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Widget _buildEmoji( | ||||
|     double emojiSize, | ||||
|     CategoryEmoji categoryEmoji, | ||||
|     Emoji emoji, | ||||
|   ) { | ||||
|     return _buildButtonWidget( | ||||
|         onPressed: () { | ||||
|           widget.state.onEmojiSelected(categoryEmoji.category, emoji); | ||||
|         }, | ||||
|         child: FittedBox( | ||||
|           fit: BoxFit.fill, | ||||
|           child: Text( | ||||
|             emoji.emoji, | ||||
|             textScaleFactor: 1.0, | ||||
|             style: TextStyle( | ||||
|               fontSize: emojiSize, | ||||
|               backgroundColor: Colors.transparent, | ||||
|             ), | ||||
|           ), | ||||
|         )); | ||||
|   } | ||||
| 
 | ||||
|   Widget _buildNoRecent() { | ||||
|     return Center( | ||||
|         child: Text( | ||||
|       widget.config.noRecentsText, | ||||
|       style: widget.config.noRecentsStyle, | ||||
|       textAlign: TextAlign.center, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,174 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_quill/flutter_quill.dart'; | ||||
| 
 | ||||
| import 'package:app_flowy/workspace/presentation/stack_page/doc/widget/toolbar/toolbar_icon_button.dart'; | ||||
| import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart'; | ||||
| 
 | ||||
| class FlowyEmojiStyleButton extends StatefulWidget { | ||||
|   // final Attribute attribute; | ||||
|   final String normalIcon; | ||||
|   final double iconSize; | ||||
|   final QuillController controller; | ||||
|   final String tooltipText; | ||||
| 
 | ||||
|   const FlowyEmojiStyleButton({ | ||||
|     // required this.attribute, | ||||
|     required this.normalIcon, | ||||
|     required this.controller, | ||||
|     required this.tooltipText, | ||||
|     this.iconSize = defaultIconSize, | ||||
|     Key? key, | ||||
|   }) : super(key: key); | ||||
| 
 | ||||
|   @override | ||||
|   _EmojiStyleButtonState createState() => _EmojiStyleButtonState(); | ||||
| } | ||||
| 
 | ||||
| class _EmojiStyleButtonState extends State<FlowyEmojiStyleButton> { | ||||
|   bool _isToggled = false; | ||||
|   // Style get _selectionStyle => widget.controller.getSelectionStyle(); | ||||
|   final GlobalKey emojiButtonKey = GlobalKey(); | ||||
|   OverlayEntry _entry = OverlayEntry(builder: (context) => Container()); | ||||
|   // final FocusNode _keyFocusNode = FocusNode(); | ||||
| 
 | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     // _isToggled = _getIsToggled(_selectionStyle.attributes); | ||||
|     // widget.controller.addListener(_didChangeEditingValue); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     // debugPrint(MediaQuery.of(context).size.width.toString()); | ||||
|     // debugPrint(MediaQuery.of(context).size.height.toString()); | ||||
| 
 | ||||
|     return ToolbarIconButton( | ||||
|       key: emojiButtonKey, | ||||
|       onPressed: _toggleAttribute, | ||||
|       width: widget.iconSize * kIconButtonFactor, | ||||
|       isToggled: _isToggled, | ||||
|       iconName: widget.normalIcon, | ||||
|       tooltipText: widget.tooltipText, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   // @override | ||||
|   // void didUpdateWidget(covariant FlowyEmojiStyleButton oldWidget) { | ||||
|   //   super.didUpdateWidget(oldWidget); | ||||
|   //   if (oldWidget.controller != widget.controller) { | ||||
|   //     oldWidget.controller.removeListener(_didChangeEditingValue); | ||||
|   //     widget.controller.addListener(_didChangeEditingValue); | ||||
|   //     _isToggled = _getIsToggled(_selectionStyle.attributes); | ||||
|   //   } | ||||
|   // } | ||||
| 
 | ||||
|   // @override | ||||
|   // void dispose() { | ||||
|   //   widget.controller.removeListener(_didChangeEditingValue); | ||||
|   //   super.dispose(); | ||||
|   // } | ||||
| 
 | ||||
|   // void _didChangeEditingValue() { | ||||
|   //   setState(() => _isToggled = _getIsToggled(_selectionStyle.attributes)); | ||||
|   // } | ||||
| 
 | ||||
|   // bool _getIsToggled(Map<String, Attribute> attrs) { | ||||
|   //   return _entry.mounted; | ||||
|   // } | ||||
| 
 | ||||
|   void _toggleAttribute() { | ||||
|     if (_entry.mounted) { | ||||
|       _entry.remove(); | ||||
|       setState(() => _isToggled = false); | ||||
|     } else { | ||||
|       RenderBox box = emojiButtonKey.currentContext?.findRenderObject() as RenderBox; | ||||
|       Offset position = box.localToGlobal(Offset.zero); | ||||
| 
 | ||||
|       // final window = await getWindowInfo(); | ||||
| 
 | ||||
|       _entry = OverlayEntry( | ||||
|         builder: (BuildContext context) => BuildEmojiPickerView( | ||||
|           controller: widget.controller, | ||||
|           offset: position, | ||||
|         ), | ||||
|       ); | ||||
| 
 | ||||
|       Overlay.of(context)!.insert(_entry); | ||||
|       setState(() => _isToggled = true); | ||||
|     } | ||||
| 
 | ||||
|     //TODO @gaganyadav80: INFO: throws error when using TextField with FlowyOverlay. | ||||
| 
 | ||||
|     // FlowyOverlay.of(context).insertWithRect( | ||||
|     //   widget: BuildEmojiPickerView(controller: widget.controller), | ||||
|     //   identifier: 'overlay_emoji_picker', | ||||
|     //   anchorPosition: Offset(position.dx + 40, position.dy - 10), | ||||
|     //   anchorSize: window.frame.size, | ||||
|     //   anchorDirection: AnchorDirection.topLeft, | ||||
|     //   style: FlowyOverlayStyle(blur: true), | ||||
|     // ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class BuildEmojiPickerView extends StatefulWidget { | ||||
|   const BuildEmojiPickerView({Key? key, required this.controller, this.offset}) : super(key: key); | ||||
| 
 | ||||
|   final QuillController controller; | ||||
|   final Offset? offset; | ||||
| 
 | ||||
|   @override | ||||
|   State<BuildEmojiPickerView> createState() => _BuildEmojiPickerViewState(); | ||||
| } | ||||
| 
 | ||||
| class _BuildEmojiPickerViewState extends State<BuildEmojiPickerView> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Stack( | ||||
|       children: [ | ||||
|         Positioned( | ||||
|           //TODO @gaganyadav80: Not sure about the calculated position. | ||||
|           top: widget.offset!.dy - MediaQuery.of(context).size.height / 2.83 - 30, | ||||
|           left: widget.offset!.dx - MediaQuery.of(context).size.width / 3.92 + 40, | ||||
|           child: Material( | ||||
|             borderRadius: BorderRadius.circular(8.0), | ||||
|             child: SizedBox( | ||||
|               //TODO @gaganyadav80: FIXIT: Gets too large when fullscreen. | ||||
|               height: MediaQuery.of(context).size.height / 2.83 + 20, | ||||
|               width: MediaQuery.of(context).size.width / 3.92, | ||||
|               child: ClipRRect( | ||||
|                 borderRadius: BorderRadius.circular(8.0), | ||||
|                 child: EmojiPicker( | ||||
|                   onEmojiSelected: (category, emoji) => insertEmoji(emoji), | ||||
|                   config: const Config( | ||||
|                     columns: 8, | ||||
|                     emojiSizeMax: 28, | ||||
|                     bgColor: Color(0xffF2F2F2), | ||||
|                     iconColor: Colors.grey, | ||||
|                     iconColorSelected: Color(0xff333333), | ||||
|                     indicatorColor: Color(0xff333333), | ||||
|                     progressIndicatorColor: Color(0xff333333), | ||||
|                     buttonMode: ButtonMode.CUPERTINO, | ||||
|                     initCategory: Category.RECENT, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   void insertEmoji(Emoji emoji) { | ||||
|     final baseOffset = widget.controller.selection.baseOffset; | ||||
|     final extentOffset = widget.controller.selection.extentOffset; | ||||
|     final replaceLen = extentOffset - baseOffset; | ||||
|     final selection = widget.controller.selection.copyWith( | ||||
|       baseOffset: baseOffset + emoji.emoji.length, | ||||
|       extentOffset: baseOffset + emoji.emoji.length, | ||||
|     ); | ||||
| 
 | ||||
|     widget.controller.replaceText(baseOffset, replaceLen, emoji.emoji, selection); | ||||
|   } | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -0,0 +1,292 @@ | ||||
| // ignore_for_file: constant_identifier_names | ||||
| 
 | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
| import 'dart:math'; | ||||
| 
 | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'models/category_models.dart'; | ||||
| import 'config.dart'; | ||||
| import 'default_emoji_picker_view.dart'; | ||||
| import 'models/emoji_model.dart'; | ||||
| import 'emoji_lists.dart' as emoji_list; | ||||
| import 'emoji_view_state.dart'; | ||||
| import 'models/recent_emoji_model.dart'; | ||||
| 
 | ||||
| /// All the possible categories that [Emoji] can be put into | ||||
| /// | ||||
| /// All [Category] are shown in the category bar | ||||
| enum Category { | ||||
|   /// Searched emojis | ||||
|   SEARCH, | ||||
| 
 | ||||
|   /// Recent emojis | ||||
|   RECENT, | ||||
| 
 | ||||
|   /// Smiley emojis | ||||
|   SMILEYS, | ||||
| 
 | ||||
|   /// Animal emojis | ||||
|   ANIMALS, | ||||
| 
 | ||||
|   /// Food emojis | ||||
|   FOODS, | ||||
| 
 | ||||
|   /// Activity emojis | ||||
|   ACTIVITIES, | ||||
| 
 | ||||
|   /// Travel emojis | ||||
|   TRAVEL, | ||||
| 
 | ||||
|   /// Ojects emojis | ||||
|   OBJECTS, | ||||
| 
 | ||||
|   /// Sumbol emojis | ||||
|   SYMBOLS, | ||||
| 
 | ||||
|   /// Flag emojis | ||||
|   FLAGS, | ||||
| } | ||||
| 
 | ||||
| /// Enum to alter the keyboard button style | ||||
| enum ButtonMode { | ||||
|   /// Android button style - gives the button a splash color with ripple effect | ||||
|   MATERIAL, | ||||
| 
 | ||||
|   /// iOS button style - gives the button a fade out effect when pressed | ||||
|   CUPERTINO | ||||
| } | ||||
| 
 | ||||
| /// Callback function for when emoji is selected | ||||
| /// | ||||
| /// The function returns the selected [Emoji] as well | ||||
| /// as the [Category] from which it originated | ||||
| typedef OnEmojiSelected = void Function(Category category, Emoji emoji); | ||||
| 
 | ||||
| /// Callback function for backspace button | ||||
| typedef OnBackspacePressed = void Function(); | ||||
| 
 | ||||
| /// Callback function for custom view | ||||
| typedef EmojiViewBuilder = Widget Function(Config config, EmojiViewState state); | ||||
| 
 | ||||
| /// The Emoji Keyboard widget | ||||
| /// | ||||
| /// This widget displays a grid of [Emoji] sorted by [Category] | ||||
| /// which the user can horizontally scroll through. | ||||
| /// | ||||
| /// There is also a bottombar which displays all the possible [Category] | ||||
| /// and allow the user to quickly switch to that [Category] | ||||
| class EmojiPicker extends StatefulWidget { | ||||
|   /// EmojiPicker for flutter | ||||
|   const EmojiPicker({ | ||||
|     Key? key, | ||||
|     required this.onEmojiSelected, | ||||
|     this.onBackspacePressed, | ||||
|     this.config = const Config(), | ||||
|     this.customWidget, | ||||
|   }) : super(key: key); | ||||
| 
 | ||||
|   /// Custom widget | ||||
|   final EmojiViewBuilder? customWidget; | ||||
| 
 | ||||
|   /// The function called when the emoji is selected | ||||
|   final OnEmojiSelected onEmojiSelected; | ||||
| 
 | ||||
|   /// The function called when backspace button is pressed | ||||
|   final OnBackspacePressed? onBackspacePressed; | ||||
| 
 | ||||
|   /// Config for customizations | ||||
|   final Config config; | ||||
| 
 | ||||
|   @override | ||||
|   _EmojiPickerState createState() => _EmojiPickerState(); | ||||
| } | ||||
| 
 | ||||
| class _EmojiPickerState extends State<EmojiPicker> { | ||||
|   static const platform = MethodChannel('emoji_picker_flutter'); | ||||
| 
 | ||||
|   List<CategoryEmoji> categoryEmoji = List.empty(growable: true); | ||||
|   List<RecentEmoji> recentEmoji = List.empty(growable: true); | ||||
|   late Future<void> updateEmojiFuture; | ||||
| 
 | ||||
|   // Prevent emojis to be reloaded with every build | ||||
|   bool loaded = false; | ||||
| 
 | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     updateEmojiFuture = _updateEmojis(); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   void didUpdateWidget(covariant EmojiPicker oldWidget) { | ||||
|     if (oldWidget.config != widget.config) { | ||||
|       // Config changed - rebuild EmojiPickerView completely | ||||
|       loaded = false; | ||||
|       updateEmojiFuture = _updateEmojis(); | ||||
|     } | ||||
|     super.didUpdateWidget(oldWidget); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (!loaded) { | ||||
|       // Load emojis | ||||
|       updateEmojiFuture.then( | ||||
|         (value) => WidgetsBinding.instance!.addPostFrameCallback((_) { | ||||
|           if (!mounted) return; | ||||
|           setState(() { | ||||
|             loaded = true; | ||||
|           }); | ||||
|         }), | ||||
|       ); | ||||
| 
 | ||||
|       // Show loading indicator | ||||
|       return const Center(child: CircularProgressIndicator()); | ||||
|     } | ||||
|     if (widget.config.showRecentsTab) { | ||||
|       categoryEmoji[0].emoji = recentEmoji.map((e) => e.emoji).toList().cast<Emoji>(); | ||||
|     } | ||||
| 
 | ||||
|     var state = EmojiViewState( | ||||
|       categoryEmoji, | ||||
|       _getOnEmojiListener(), | ||||
|       widget.onBackspacePressed, | ||||
|     ); | ||||
| 
 | ||||
|     // Build | ||||
|     return widget.customWidget == null | ||||
|         ? DefaultEmojiPickerView(widget.config, state) | ||||
|         : widget.customWidget!(widget.config, state); | ||||
|   } | ||||
| 
 | ||||
|   // Add recent emoji handling to tap listener | ||||
|   OnEmojiSelected _getOnEmojiListener() { | ||||
|     return (category, emoji) { | ||||
|       if (widget.config.showRecentsTab) { | ||||
|         _addEmojiToRecentlyUsed(emoji).then((value) { | ||||
|           if (category != Category.RECENT && mounted) { | ||||
|             setState(() { | ||||
|               // rebuild to update recent emoji tab | ||||
|               // when it is not current tab | ||||
|             }); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|       widget.onEmojiSelected(category, emoji); | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   // Initalize emoji data | ||||
|   Future<void> _updateEmojis() async { | ||||
|     categoryEmoji.clear(); | ||||
|     if (widget.config.showRecentsTab) { | ||||
|       recentEmoji = await _getRecentEmojis(); | ||||
|       final List<Emoji> recentEmojiMap = recentEmoji.map((e) => e.emoji).toList().cast<Emoji>(); | ||||
|       categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap)); | ||||
|     } | ||||
|     categoryEmoji.addAll([ | ||||
|       CategoryEmoji(Category.SMILEYS, await _getAvailableEmojis(emoji_list.smileys, title: 'smileys')), | ||||
|       CategoryEmoji(Category.ANIMALS, await _getAvailableEmojis(emoji_list.animals, title: 'animals')), | ||||
|       CategoryEmoji(Category.FOODS, await _getAvailableEmojis(emoji_list.foods, title: 'foods')), | ||||
|       CategoryEmoji(Category.ACTIVITIES, await _getAvailableEmojis(emoji_list.activities, title: 'activities')), | ||||
|       CategoryEmoji(Category.TRAVEL, await _getAvailableEmojis(emoji_list.travel, title: 'travel')), | ||||
|       CategoryEmoji(Category.OBJECTS, await _getAvailableEmojis(emoji_list.objects, title: 'objects')), | ||||
|       CategoryEmoji(Category.SYMBOLS, await _getAvailableEmojis(emoji_list.symbols, title: 'symbols')), | ||||
|       CategoryEmoji(Category.FLAGS, await _getAvailableEmojis(emoji_list.flags, title: 'flags')) | ||||
|     ]); | ||||
|   } | ||||
| 
 | ||||
|   // Get available emoji for given category title | ||||
|   Future<List<Emoji>> _getAvailableEmojis(Map<String, String> map, {required String title}) async { | ||||
|     Map<String, String>? newMap; | ||||
| 
 | ||||
|     // Get Emojis cached locally if available | ||||
|     newMap = await _restoreFilteredEmojis(title); | ||||
| 
 | ||||
|     if (newMap == null) { | ||||
|       // Check if emoji is available on this platform | ||||
|       newMap = await _getPlatformAvailableEmoji(map); | ||||
|       // Save available Emojis to local storage for faster loading next time | ||||
|       if (newMap != null) { | ||||
|         await _cacheFilteredEmojis(title, newMap); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // Map to Emoji Object | ||||
|     return newMap!.entries.map<Emoji>((entry) => Emoji(entry.key, entry.value)).toList(); | ||||
|   } | ||||
| 
 | ||||
|   // Check if emoji is available on current platform | ||||
|   Future<Map<String, String>?> _getPlatformAvailableEmoji(Map<String, String> emoji) async { | ||||
|     if (Platform.isAndroid) { | ||||
|       Map<String, String>? filtered = {}; | ||||
|       var delimiter = '|'; | ||||
|       try { | ||||
|         var entries = emoji.values.join(delimiter); | ||||
|         var keys = emoji.keys.join(delimiter); | ||||
|         var result = (await platform | ||||
|             .invokeMethod<String>('checkAvailability', {'emojiKeys': keys, 'emojiEntries': entries})) as String; | ||||
|         var resultKeys = result.split(delimiter); | ||||
|         for (var i = 0; i < resultKeys.length; i++) { | ||||
|           filtered[resultKeys[i]] = emoji[resultKeys[i]]!; | ||||
|         } | ||||
|       } on PlatformException catch (_) { | ||||
|         filtered = null; | ||||
|       } | ||||
|       return filtered; | ||||
|     } else { | ||||
|       return emoji; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Restore locally cached emoji | ||||
|   Future<Map<String, String>?> _restoreFilteredEmojis(String title) async { | ||||
|     final prefs = await SharedPreferences.getInstance(); | ||||
|     var emojiJson = prefs.getString(title); | ||||
|     if (emojiJson == null) { | ||||
|       return null; | ||||
|     } | ||||
|     var emojis = Map<String, String>.from(jsonDecode(emojiJson) as Map<String, dynamic>); | ||||
|     return emojis; | ||||
|   } | ||||
| 
 | ||||
|   // Stores filtered emoji locally for faster access next time | ||||
|   Future<void> _cacheFilteredEmojis(String title, Map<String, String> emojis) async { | ||||
|     final prefs = await SharedPreferences.getInstance(); | ||||
|     var emojiJson = jsonEncode(emojis); | ||||
|     prefs.setString(title, emojiJson); | ||||
|   } | ||||
| 
 | ||||
|   // Returns list of recently used emoji from cache | ||||
|   Future<List<RecentEmoji>> _getRecentEmojis() async { | ||||
|     final prefs = await SharedPreferences.getInstance(); | ||||
|     var emojiJson = prefs.getString('recent'); | ||||
|     if (emojiJson == null) { | ||||
|       return []; | ||||
|     } | ||||
|     var json = jsonDecode(emojiJson) as List<dynamic>; | ||||
|     return json.map<RecentEmoji>(RecentEmoji.fromJson).toList(); | ||||
|   } | ||||
| 
 | ||||
|   // Add an emoji to recently used list or increase its counter | ||||
|   Future<void> _addEmojiToRecentlyUsed(Emoji emoji) async { | ||||
|     final prefs = await SharedPreferences.getInstance(); | ||||
|     var recentEmojiIndex = recentEmoji.indexWhere((element) => element.emoji.emoji == emoji.emoji); | ||||
|     if (recentEmojiIndex != -1) { | ||||
|       // Already exist in recent list | ||||
|       // Just update counter | ||||
|       recentEmoji[recentEmojiIndex].counter++; | ||||
|     } else { | ||||
|       recentEmoji.add(RecentEmoji(emoji, 1)); | ||||
|     } | ||||
|     // Sort by counter desc | ||||
|     recentEmoji.sort((a, b) => b.counter - a.counter); | ||||
|     // Limit entries to recentsLimit | ||||
|     recentEmoji = recentEmoji.sublist(0, min(widget.config.recentsLimit, recentEmoji.length)); | ||||
|     // save locally | ||||
|     prefs.setString('recent', jsonEncode(recentEmoji)); | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,17 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| 
 | ||||
| import 'config.dart'; | ||||
| import 'emoji_view_state.dart'; | ||||
| 
 | ||||
| /// Template class for custom implementation | ||||
| /// Inhert this class to create your own EmojiPicker | ||||
| abstract class EmojiPickerBuilder extends StatefulWidget { | ||||
|   /// Constructor | ||||
|   const EmojiPickerBuilder(this.config, this.state, {Key? key}) : super(key: key); | ||||
| 
 | ||||
|   /// Config for customizations | ||||
|   final Config config; | ||||
| 
 | ||||
|   /// State that holds current emoji data | ||||
|   final EmojiViewState state; | ||||
| } | ||||
| @ -0,0 +1,21 @@ | ||||
| import 'models/category_models.dart'; | ||||
| import 'emoji_picker.dart'; | ||||
| 
 | ||||
| /// State that holds current emoji data | ||||
| class EmojiViewState { | ||||
|   /// Constructor | ||||
|   EmojiViewState( | ||||
|     this.categoryEmoji, | ||||
|     this.onEmojiSelected, | ||||
|     this.onBackspacePressed, | ||||
|   ); | ||||
| 
 | ||||
|   /// List of all category including their emoji | ||||
|   final List<CategoryEmoji> categoryEmoji; | ||||
| 
 | ||||
|   /// Callback when pressed on emoji | ||||
|   final OnEmojiSelected onEmojiSelected; | ||||
| 
 | ||||
|   /// Callback when pressed on backspace | ||||
|   final OnBackspacePressed? onBackspacePressed; | ||||
| } | ||||
| @ -0,0 +1,91 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| 
 | ||||
| import 'emoji_model.dart'; | ||||
| import '../emoji_picker.dart'; | ||||
| 
 | ||||
| /// Container for Category and their emoji | ||||
| class CategoryEmoji { | ||||
|   /// Constructor | ||||
|   CategoryEmoji(this.category, this.emoji); | ||||
| 
 | ||||
|   /// Category instance | ||||
|   final Category category; | ||||
| 
 | ||||
|   /// List of emoji of this category | ||||
|   List<Emoji> emoji; | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'Name: $category, Emoji: $emoji'; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// Class that defines the icon representing a [Category] | ||||
| class CategoryIcon { | ||||
|   /// Icon of Category | ||||
|   const CategoryIcon({ | ||||
|     required this.icon, | ||||
|     this.color = const Color.fromRGBO(211, 211, 211, 1), | ||||
|     this.selectedColor = const Color.fromRGBO(178, 178, 178, 1), | ||||
|   }); | ||||
| 
 | ||||
|   /// The icon to represent the category | ||||
|   final IconData icon; | ||||
| 
 | ||||
|   /// The default color of the icon | ||||
|   final Color color; | ||||
| 
 | ||||
|   /// The color of the icon once the category is selected | ||||
|   final Color selectedColor; | ||||
| } | ||||
| 
 | ||||
| /// Class used to define all the [CategoryIcon] shown for each [Category] | ||||
| /// | ||||
| /// This allows the keyboard to be personalized by changing icons shown. | ||||
| /// If a [CategoryIcon] is set as null or not defined during initialization, | ||||
| /// the default icons will be used instead | ||||
| class CategoryIcons { | ||||
|   /// Constructor | ||||
|   const CategoryIcons({ | ||||
|     this.recentIcon = Icons.access_time, | ||||
|     this.smileyIcon = Icons.tag_faces, | ||||
|     this.animalIcon = Icons.pets, | ||||
|     this.foodIcon = Icons.fastfood, | ||||
|     this.activityIcon = Icons.directions_run, | ||||
|     this.travelIcon = Icons.location_city, | ||||
|     this.objectIcon = Icons.lightbulb_outline, | ||||
|     this.symbolIcon = Icons.emoji_symbols, | ||||
|     this.flagIcon = Icons.flag, | ||||
|     this.searchIcon = Icons.search, | ||||
|   }); | ||||
| 
 | ||||
|   /// Icon for [Category.RECENT] | ||||
|   final IconData recentIcon; | ||||
| 
 | ||||
|   /// Icon for [Category.SMILEYS] | ||||
|   final IconData smileyIcon; | ||||
| 
 | ||||
|   /// Icon for [Category.ANIMALS] | ||||
|   final IconData animalIcon; | ||||
| 
 | ||||
|   /// Icon for [Category.FOODS] | ||||
|   final IconData foodIcon; | ||||
| 
 | ||||
|   /// Icon for [Category.ACTIVITIES] | ||||
|   final IconData activityIcon; | ||||
| 
 | ||||
|   /// Icon for [Category.TRAVEL] | ||||
|   final IconData travelIcon; | ||||
| 
 | ||||
|   /// Icon for [Category.OBJECTS] | ||||
|   final IconData objectIcon; | ||||
| 
 | ||||
|   /// Icon for [Category.SYMBOLS] | ||||
|   final IconData symbolIcon; | ||||
| 
 | ||||
|   /// Icon for [Category.FLAGS] | ||||
|   final IconData flagIcon; | ||||
| 
 | ||||
|   /// Icon for [Category.SEARCH] | ||||
|   final IconData searchIcon; | ||||
| } | ||||
| @ -0,0 +1,32 @@ | ||||
| /// A class to store data for each individual emoji | ||||
| class Emoji { | ||||
|   /// Emoji constructor | ||||
|   const Emoji(this.name, this.emoji); | ||||
| 
 | ||||
|   /// The name or description for this emoji | ||||
|   final String name; | ||||
| 
 | ||||
|   /// The unicode string for this emoji | ||||
|   /// | ||||
|   /// This is the string that should be displayed to view the emoji | ||||
|   final String emoji; | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     // return 'Name: $name, Emoji: $emoji'; | ||||
|     return name; | ||||
|   } | ||||
| 
 | ||||
|   /// Parse Emoji from json | ||||
|   static Emoji fromJson(Map<String, dynamic> json) { | ||||
|     return Emoji(json['name'] as String, json['emoji'] as String); | ||||
|   } | ||||
| 
 | ||||
|   ///  Encode Emoji to json | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return { | ||||
|       'name': name, | ||||
|       'emoji': emoji, | ||||
|     }; | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,30 @@ | ||||
| import 'emoji_model.dart'; | ||||
| 
 | ||||
| /// Class that holds an recent emoji | ||||
| /// Recent Emoji has an instance of the emoji | ||||
| /// And a counter, which counts how often this emoji | ||||
| /// has been used before | ||||
| class RecentEmoji { | ||||
|   /// Constructor | ||||
|   RecentEmoji(this.emoji, this.counter); | ||||
| 
 | ||||
|   /// Emoji instance | ||||
|   final Emoji emoji; | ||||
| 
 | ||||
|   /// Counter how often emoji has been used before | ||||
|   int counter = 0; | ||||
| 
 | ||||
|   /// Parse RecentEmoji from json | ||||
|   static RecentEmoji fromJson(dynamic json) { | ||||
|     return RecentEmoji( | ||||
|       Emoji.fromJson(json['emoji'] as Map<String, dynamic>), | ||||
|       json['counter'] as int, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Encode RecentEmoji to json | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         'emoji': emoji, | ||||
|         'counter': counter, | ||||
|       }; | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Nathan.fooo
						Nathan.fooo