mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: emoji picker improvement (#3591)
This commit is contained in:
parent
e9852a3934
commit
eb20c7c117
@ -54,7 +54,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
|
@ -5,8 +5,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_info.dart
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_banner_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
@ -8,6 +8,7 @@ import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
|
||||
import 'package:appflowy/workspace/application/appearance.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -1,4 +0,0 @@
|
||||
export 'src/config.dart';
|
||||
export 'src/emoji_picker.dart';
|
||||
export 'src/emoji_picker_builder.dart';
|
||||
export 'src/models/emoji_model.dart';
|
@ -1,165 +0,0 @@
|
||||
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;
|
||||
|
||||
/// Vertical 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 initialization
|
||||
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;
|
||||
}
|
@ -2,7 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -257,7 +257,7 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
|
||||
controller: _popoverController,
|
||||
offset: const Offset(0, 8),
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
constraints: BoxConstraints.loose(const Size(320, 380)),
|
||||
constraints: BoxConstraints.loose(const Size(300, 250)),
|
||||
child: FlowyButton(
|
||||
leftIconSize: const Size.square(18),
|
||||
useIntrinsicWidth: true,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
@ -8,6 +8,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Add icon menu in Header
|
||||
class EmojiPopover extends StatefulWidget {
|
||||
final EditorState editorState;
|
||||
final Node node;
|
||||
@ -46,17 +47,7 @@ class _EmojiPopoverState extends State<EmojiPopover> {
|
||||
onEmojiSelected: (category, emoji) {
|
||||
widget.onEmojiChanged(emoji);
|
||||
},
|
||||
config: Config(
|
||||
columns: 8,
|
||||
emojiSizeMax: 28,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: Theme.of(context).iconTheme.color!,
|
||||
iconColorSelected: Theme.of(context).colorScheme.onSurface,
|
||||
selectedHoverColor: Theme.of(context).colorScheme.secondary,
|
||||
progressIndicatorColor: Theme.of(context).iconTheme.color!,
|
||||
buttonMode: ButtonMode.CUPERTINO,
|
||||
initCategory: Category.RECENT,
|
||||
),
|
||||
config: buildFlowyEmojiPickerConfig(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -12,7 +12,6 @@ export 'copy_and_paste/custom_paste_command.dart';
|
||||
export 'database/database_view_block_component.dart';
|
||||
export 'database/inline_database_menu_item.dart';
|
||||
export 'database/referenced_database_menu_item.dart';
|
||||
export 'emoji_picker/emoji_menu_item.dart';
|
||||
export 'extensions/flowy_tint_extension.dart';
|
||||
export 'find_and_replace/find_and_replace_menu.dart';
|
||||
export 'font/customize_font_toolbar_item.dart';
|
||||
|
@ -113,17 +113,7 @@ class _EmojiSelectionMenuState extends State<EmojiSelectionMenu> {
|
||||
Widget build(BuildContext context) {
|
||||
return EmojiPicker(
|
||||
onEmojiSelected: (category, emoji) => widget.onSubmitted(emoji),
|
||||
config: const Config(
|
||||
columns: 7,
|
||||
emojiSizeMax: 28,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: Colors.grey,
|
||||
iconColorSelected: Color(0xff333333),
|
||||
indicatorColor: Color(0xff333333),
|
||||
progressIndicatorColor: Color(0xff333333),
|
||||
buttonMode: ButtonMode.CUPERTINO,
|
||||
initCategory: Category.RECENT,
|
||||
),
|
||||
config: buildFlowyEmojiPickerConfig(context),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
export 'emoji_menu_item.dart';
|
||||
export 'src/emji_picker_config.dart';
|
||||
export 'src/emoji_picker.dart';
|
||||
export 'src/emoji_picker_builder.dart';
|
||||
export 'src/flowy_emoji_picker_config.dart';
|
||||
export 'src/models/emoji_model.dart';
|
@ -1,16 +1,19 @@
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'config.dart';
|
||||
import 'emji_picker_config.dart';
|
||||
import 'emoji_picker.dart';
|
||||
import 'emoji_picker_builder.dart';
|
||||
import 'emoji_view_state.dart';
|
||||
import 'models/category_models.dart';
|
||||
import 'models/emoji_category_models.dart';
|
||||
import 'models/emoji_model.dart';
|
||||
|
||||
class DefaultEmojiPickerView extends EmojiPickerBuilder {
|
||||
const DefaultEmojiPickerView(Config config, EmojiViewState state, {Key? key})
|
||||
: super(config, state, key: key);
|
||||
const DefaultEmojiPickerView(
|
||||
EmojiPickerConfig config,
|
||||
EmojiViewState state, {
|
||||
Key? key,
|
||||
}) : super(config, state, key: key);
|
||||
|
||||
@override
|
||||
DefaultEmojiPickerViewState createState() => DefaultEmojiPickerViewState();
|
||||
@ -22,11 +25,12 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
TabController? _tabController;
|
||||
final TextEditingController _emojiController = TextEditingController();
|
||||
final FocusNode _emojiFocusNode = FocusNode();
|
||||
CategoryEmoji searchEmojiList = CategoryEmoji(Category.SEARCH, <Emoji>[]);
|
||||
EmojiCategoryGroup searchEmojiList =
|
||||
EmojiCategoryGroup(EmojiCategory.SEARCH, <Emoji>[]);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
var initCategory = widget.state.categoryEmoji.indexWhere(
|
||||
var initCategory = widget.state.emojiCategoryGroupList.indexWhere(
|
||||
(element) => element.category == widget.config.initCategory,
|
||||
);
|
||||
if (initCategory == -1) {
|
||||
@ -34,7 +38,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
}
|
||||
_tabController = TabController(
|
||||
initialIndex: initCategory,
|
||||
length: widget.state.categoryEmoji.length,
|
||||
length: widget.state.emojiCategoryGroupList.length,
|
||||
vsync: this,
|
||||
);
|
||||
_pageController = PageController(initialPage: initCategory);
|
||||
@ -49,7 +53,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
);
|
||||
} else {
|
||||
searchEmojiList.emoji.clear();
|
||||
for (final element in widget.state.categoryEmoji) {
|
||||
for (final element in widget.state.emojiCategoryGroupList) {
|
||||
searchEmojiList.emoji.addAll(
|
||||
element.emoji.where((item) {
|
||||
return item.name.toLowerCase().contains(query);
|
||||
@ -66,6 +70,8 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
void dispose() {
|
||||
_emojiController.dispose();
|
||||
_emojiFocusNode.dispose();
|
||||
_pageController?.dispose();
|
||||
_tabController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -100,56 +106,50 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final emojiSize = widget.config.getEmojiSize(constraints.maxWidth);
|
||||
final style = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
color: widget.config.bgColor,
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
children: [
|
||||
const VSpace(4),
|
||||
// search bar
|
||||
SizedBox(
|
||||
height: 25.0,
|
||||
height: 32.0,
|
||||
child: TextField(
|
||||
controller: _emojiController,
|
||||
focusNode: _emojiFocusNode,
|
||||
autofocus: true,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
cursorWidth: 1.0,
|
||||
cursorColor: Colors.black,
|
||||
style: style.textTheme.bodyMedium,
|
||||
cursorColor: style.textTheme.bodyMedium?.color,
|
||||
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,
|
||||
contentPadding: const EdgeInsets.all(8),
|
||||
hintText: widget.config.searchHintText,
|
||||
hintStyle: widget.config.serachHintTextStyle,
|
||||
enabledBorder: widget.config.serachBarEnableBorder,
|
||||
focusedBorder: widget.config.serachBarFocusedBorder,
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(4),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBar(
|
||||
labelColor: widget.config.iconColorSelected,
|
||||
unselectedLabelColor: widget.config.iconColor,
|
||||
labelColor: widget.config.selectedCategoryIconColor,
|
||||
unselectedLabelColor: widget.config.categoryIconColor,
|
||||
controller: isEmojiSearching()
|
||||
? TabController(length: 1, vsync: this)
|
||||
: _tabController,
|
||||
labelPadding: EdgeInsets.zero,
|
||||
indicatorColor: widget.config.indicatorColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0),
|
||||
indicatorColor:
|
||||
widget.config.selectedCategoryIconBackgroundColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
indicator: BoxDecoration(
|
||||
border: Border.all(color: Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
color: style.colorScheme.secondary,
|
||||
),
|
||||
onTap: (index) {
|
||||
_pageController!.animateToPage(
|
||||
@ -159,8 +159,8 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
);
|
||||
},
|
||||
tabs: isEmojiSearching()
|
||||
? [_buildCategory(Category.SEARCH, emojiSize)]
|
||||
: widget.state.categoryEmoji
|
||||
? [_buildCategory(EmojiCategory.SEARCH, emojiSize)]
|
||||
: widget.state.emojiCategoryGroupList
|
||||
.asMap()
|
||||
.entries
|
||||
.map<Widget>(
|
||||
@ -179,20 +179,15 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
child: PageView.builder(
|
||||
itemCount: searchEmojiList.emoji.isNotEmpty
|
||||
? 1
|
||||
: widget.state.categoryEmoji.length,
|
||||
: widget.state.emojiCategoryGroupList.length,
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// onPageChanged: (index) {
|
||||
// _tabController!.animateTo(
|
||||
// index,
|
||||
// duration: widget.config.tabIndicatorAnimDuration,
|
||||
// );
|
||||
// },
|
||||
itemBuilder: (context, index) {
|
||||
final CategoryEmoji catEmoji = isEmojiSearching()
|
||||
? searchEmojiList
|
||||
: widget.state.categoryEmoji[index];
|
||||
return _buildPage(emojiSize, catEmoji);
|
||||
final EmojiCategoryGroup emojiCategoryGroup =
|
||||
isEmojiSearching()
|
||||
? searchEmojiList
|
||||
: widget.state.emojiCategoryGroupList[index];
|
||||
return _buildPage(emojiSize, emojiCategoryGroup);
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -203,7 +198,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategory(Category category, double categorySize) {
|
||||
Widget _buildCategory(EmojiCategory category, double categorySize) {
|
||||
return Tab(
|
||||
height: categorySize,
|
||||
child: Icon(
|
||||
@ -229,39 +224,39 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPage(double emojiSize, CategoryEmoji categoryEmoji) {
|
||||
Widget _buildPage(double emojiSize, EmojiCategoryGroup emojiCategoryGroup) {
|
||||
// Display notice if recent has no entries yet
|
||||
final scrollController = ScrollController();
|
||||
|
||||
if (categoryEmoji.category == Category.RECENT &&
|
||||
categoryEmoji.emoji.isEmpty) {
|
||||
if (emojiCategoryGroup.category == EmojiCategory.RECENT &&
|
||||
emojiCategoryGroup.emoji.isEmpty) {
|
||||
return _buildNoRecent();
|
||||
} else if (categoryEmoji.category == Category.SEARCH &&
|
||||
categoryEmoji.emoji.isEmpty) {
|
||||
return const Center(child: Text("No Emoji Found"));
|
||||
} else if (emojiCategoryGroup.category == EmojiCategory.SEARCH &&
|
||||
emojiCategoryGroup.emoji.isEmpty) {
|
||||
return Center(child: Text(widget.config.noEmojiFoundText));
|
||||
}
|
||||
// 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),
|
||||
scrollbarPadding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
handleColor: widget.config.scrollBarHandleColor,
|
||||
showTrack: true,
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
child: GridView.builder(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.all(0),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: widget.config.columns,
|
||||
crossAxisCount: widget.config.emojiNumberPerRow,
|
||||
mainAxisSpacing: widget.config.verticalSpacing,
|
||||
crossAxisSpacing: widget.config.horizontalSpacing,
|
||||
),
|
||||
itemCount: categoryEmoji.emoji.length,
|
||||
itemCount: emojiCategoryGroup.emoji.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = categoryEmoji.emoji[index];
|
||||
return _buildEmoji(emojiSize, categoryEmoji, item);
|
||||
final item = emojiCategoryGroup.emoji[index];
|
||||
return _buildEmoji(emojiSize, emojiCategoryGroup, item);
|
||||
},
|
||||
cacheExtent: 10,
|
||||
),
|
||||
@ -271,21 +266,20 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
|
||||
Widget _buildEmoji(
|
||||
double emojiSize,
|
||||
CategoryEmoji categoryEmoji,
|
||||
EmojiCategoryGroup emojiCategoryGroup,
|
||||
Emoji emoji,
|
||||
) {
|
||||
return _buildButtonWidget(
|
||||
onPressed: () {
|
||||
widget.state.onEmojiSelected(categoryEmoji.category, emoji);
|
||||
widget.state.onEmojiSelected(emojiCategoryGroup.category, emoji);
|
||||
},
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
emoji.emoji,
|
||||
textScaleFactor: 1.0,
|
||||
style: TextStyle(
|
||||
fontSize: emojiSize,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: FlowyHover(
|
||||
child: FittedBox(
|
||||
child: Text(
|
||||
emoji.emoji,
|
||||
style: TextStyle(
|
||||
fontSize: emojiSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -294,10 +288,9 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
|
||||
Widget _buildNoRecent() {
|
||||
return Center(
|
||||
child: FlowyText.regular(
|
||||
child: Text(
|
||||
widget.config.noRecentsText,
|
||||
color: Theme.of(context).colorScheme.tertiary.withAlpha(77),
|
||||
fontSize: widget.config.noRecentsStyle.fontSize,
|
||||
style: widget.config.noRecentsStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
@ -0,0 +1,94 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'models/emoji_category_models.dart';
|
||||
import 'emoji_picker.dart';
|
||||
|
||||
part 'emji_picker_config.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class EmojiPickerConfig with _$EmojiPickerConfig {
|
||||
// private empty constructor is used to make method work in freezed
|
||||
// https://pub.dev/packages/freezed#adding-getters-and-methods-to-our-models
|
||||
const EmojiPickerConfig._();
|
||||
const factory EmojiPickerConfig({
|
||||
@Default(7) int emojiNumberPerRow,
|
||||
// The maximum size(width and height) of emoji
|
||||
// It also depaneds on the screen size and emojiNumberPerRow
|
||||
@Default(32) double emojiSizeMax,
|
||||
// Vertical spacing between emojis
|
||||
@Default(0) double verticalSpacing,
|
||||
// Horizontal spacing between emojis
|
||||
@Default(0) double horizontalSpacing,
|
||||
// The initial [EmojiCategory] that will be selected
|
||||
@Default(EmojiCategory.RECENT) EmojiCategory initCategory,
|
||||
// The background color of the Widget
|
||||
@Default(Color(0xFFEBEFF2)) Color? bgColor,
|
||||
// The color of the category icons
|
||||
@Default(Colors.grey) Color? categoryIconColor,
|
||||
// The color of the category icon when selected
|
||||
@Default(Colors.blue) Color? selectedCategoryIconColor,
|
||||
// The color of the category indicator
|
||||
@Default(Colors.blue) Color? selectedCategoryIconBackgroundColor,
|
||||
// The color of the loading indicator during initialization
|
||||
@Default(Colors.blue) Color? progressIndicatorColor,
|
||||
// The color of the backspace icon button
|
||||
@Default(Colors.blue) Color? backspaceColor,
|
||||
// Show extra tab with recently used emoji
|
||||
@Default(true) bool showRecentsTab,
|
||||
// Limit of recently used emoji that will be saved
|
||||
@Default(28) int recentsLimit,
|
||||
@Default('Search emoji') String searchHintText,
|
||||
TextStyle? serachHintTextStyle,
|
||||
InputBorder? serachBarEnableBorder,
|
||||
InputBorder? serachBarFocusedBorder,
|
||||
// The text to be displayed if no recent emojis to display
|
||||
@Default('No recent emoji') String noRecentsText,
|
||||
TextStyle? noRecentsStyle,
|
||||
// The text to be displayed if no emoji found
|
||||
@Default('No emoji found') String noEmojiFoundText,
|
||||
Color? scrollBarHandleColor,
|
||||
// Duration of tab indicator to animate to next category
|
||||
@Default(kTabScrollDuration) Duration tabIndicatorAnimDuration,
|
||||
// Determines the icon to display for each [EmojiCategory]
|
||||
@Default(EmojiCategoryIcons()) EmojiCategoryIcons emojiCategoryIcons,
|
||||
// Change between Material and Cupertino button style
|
||||
@Default(ButtonMode.MATERIAL) ButtonMode buttonMode,
|
||||
}) = _EmojiPickerConfig;
|
||||
|
||||
/// Get Emoji size based on properties and screen width
|
||||
double getEmojiSize(double width) {
|
||||
final maxSize = width / emojiNumberPerRow;
|
||||
return min(maxSize, emojiSizeMax);
|
||||
}
|
||||
|
||||
/// Returns the icon for the category
|
||||
IconData getIconForCategory(EmojiCategory category) {
|
||||
switch (category) {
|
||||
case EmojiCategory.RECENT:
|
||||
return emojiCategoryIcons.recentIcon;
|
||||
case EmojiCategory.SMILEYS:
|
||||
return emojiCategoryIcons.smileyIcon;
|
||||
case EmojiCategory.ANIMALS:
|
||||
return emojiCategoryIcons.animalIcon;
|
||||
case EmojiCategory.FOODS:
|
||||
return emojiCategoryIcons.foodIcon;
|
||||
case EmojiCategory.TRAVEL:
|
||||
return emojiCategoryIcons.travelIcon;
|
||||
case EmojiCategory.ACTIVITIES:
|
||||
return emojiCategoryIcons.activityIcon;
|
||||
case EmojiCategory.OBJECTS:
|
||||
return emojiCategoryIcons.objectIcon;
|
||||
case EmojiCategory.SYMBOLS:
|
||||
return emojiCategoryIcons.symbolIcon;
|
||||
case EmojiCategory.FLAGS:
|
||||
return emojiCategoryIcons.flagIcon;
|
||||
case EmojiCategory.SEARCH:
|
||||
return emojiCategoryIcons.searchIcon;
|
||||
default:
|
||||
throw Exception('Unsupported EmojiCategory');
|
||||
}
|
||||
}
|
||||
}
|
@ -7,18 +7,16 @@ 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 'models/emoji_category_models.dart';
|
||||
import 'emji_picker_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 {
|
||||
/// The emoji category shown on the category tab
|
||||
enum EmojiCategory {
|
||||
/// Searched emojis
|
||||
SEARCH,
|
||||
|
||||
@ -62,29 +60,32 @@ enum ButtonMode {
|
||||
/// 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);
|
||||
/// as the [EmojiCategory] from which it originated
|
||||
typedef OnEmojiSelected = void Function(EmojiCategory 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);
|
||||
typedef EmojiViewBuilder = Widget Function(
|
||||
EmojiPickerConfig config,
|
||||
EmojiViewState state,
|
||||
);
|
||||
|
||||
/// The Emoji Keyboard widget
|
||||
///
|
||||
/// This widget displays a grid of [Emoji] sorted by [Category]
|
||||
/// This widget displays a grid of [Emoji] sorted by [EmojiCategory]
|
||||
/// 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]
|
||||
/// There is also a bottombar which displays all the possible [EmojiCategory]
|
||||
/// and allow the user to quickly switch to that [EmojiCategory]
|
||||
class EmojiPicker extends StatefulWidget {
|
||||
/// EmojiPicker for flutter
|
||||
const EmojiPicker({
|
||||
Key? key,
|
||||
required this.onEmojiSelected,
|
||||
this.onBackspacePressed,
|
||||
this.config = const Config(),
|
||||
this.config = const EmojiPickerConfig(),
|
||||
this.customWidget,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -98,7 +99,7 @@ class EmojiPicker extends StatefulWidget {
|
||||
final OnBackspacePressed? onBackspacePressed;
|
||||
|
||||
/// Config for customizations
|
||||
final Config config;
|
||||
final EmojiPickerConfig config;
|
||||
|
||||
@override
|
||||
EmojiPickerState createState() => EmojiPickerState();
|
||||
@ -107,8 +108,8 @@ class EmojiPicker extends StatefulWidget {
|
||||
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);
|
||||
List<EmojiCategoryGroup> emojiCategoryGroupList = List.empty(growable: true);
|
||||
List<RecentEmoji> recentEmojiList = List.empty(growable: true);
|
||||
late Future<void> updateEmojiFuture;
|
||||
|
||||
// Prevent emojis to be reloaded with every build
|
||||
@ -123,7 +124,7 @@ class EmojiPickerState extends State<EmojiPicker> {
|
||||
@override
|
||||
void didUpdateWidget(covariant EmojiPicker oldWidget) {
|
||||
if (oldWidget.config != widget.config) {
|
||||
// Config changed - rebuild EmojiPickerView completely
|
||||
// EmojiPickerConfig changed - rebuild EmojiPickerView completely
|
||||
loaded = false;
|
||||
updateEmojiFuture = _updateEmojis();
|
||||
}
|
||||
@ -147,12 +148,12 @@ class EmojiPickerState extends State<EmojiPicker> {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (widget.config.showRecentsTab) {
|
||||
categoryEmoji[0].emoji =
|
||||
recentEmoji.map((e) => e.emoji).toList().cast<Emoji>();
|
||||
emojiCategoryGroupList[0].emoji =
|
||||
recentEmojiList.map((e) => e.emoji).toList().cast<Emoji>();
|
||||
}
|
||||
|
||||
final state = EmojiViewState(
|
||||
categoryEmoji,
|
||||
emojiCategoryGroupList,
|
||||
_getOnEmojiListener(),
|
||||
widget.onBackspacePressed,
|
||||
);
|
||||
@ -168,7 +169,7 @@ class EmojiPickerState extends State<EmojiPicker> {
|
||||
return (category, emoji) {
|
||||
if (widget.config.showRecentsTab) {
|
||||
_addEmojiToRecentlyUsed(emoji).then((value) {
|
||||
if (category != Category.RECENT && mounted) {
|
||||
if (category != EmojiCategory.RECENT && mounted) {
|
||||
setState(() {
|
||||
// rebuild to update recent emoji tab
|
||||
// when it is not current tab
|
||||
@ -182,47 +183,48 @@ class EmojiPickerState extends State<EmojiPicker> {
|
||||
|
||||
// Initialize emoji data
|
||||
Future<void> _updateEmojis() async {
|
||||
categoryEmoji.clear();
|
||||
emojiCategoryGroupList.clear();
|
||||
if (widget.config.showRecentsTab) {
|
||||
recentEmoji = await _getRecentEmojis();
|
||||
recentEmojiList = await _getRecentEmojis();
|
||||
final List<Emoji> recentEmojiMap =
|
||||
recentEmoji.map((e) => e.emoji).toList().cast<Emoji>();
|
||||
categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap));
|
||||
recentEmojiList.map((e) => e.emoji).toList().cast<Emoji>();
|
||||
emojiCategoryGroupList
|
||||
.add(EmojiCategoryGroup(EmojiCategory.RECENT, recentEmojiMap));
|
||||
}
|
||||
categoryEmoji.addAll([
|
||||
CategoryEmoji(
|
||||
Category.SMILEYS,
|
||||
emojiCategoryGroupList.addAll([
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.SMILEYS,
|
||||
await _getAvailableEmojis(emoji_list.smileys, title: 'smileys'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.ANIMALS,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.ANIMALS,
|
||||
await _getAvailableEmojis(emoji_list.animals, title: 'animals'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.FOODS,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.FOODS,
|
||||
await _getAvailableEmojis(emoji_list.foods, title: 'foods'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.ACTIVITIES,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.ACTIVITIES,
|
||||
await _getAvailableEmojis(
|
||||
emoji_list.activities,
|
||||
title: 'activities',
|
||||
),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.TRAVEL,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.TRAVEL,
|
||||
await _getAvailableEmojis(emoji_list.travel, title: 'travel'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.OBJECTS,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.OBJECTS,
|
||||
await _getAvailableEmojis(emoji_list.objects, title: 'objects'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.SYMBOLS,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.SYMBOLS,
|
||||
await _getAvailableEmojis(emoji_list.symbols, title: 'symbols'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.FLAGS,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.FLAGS,
|
||||
await _getAvailableEmojis(emoji_list.flags, title: 'flags'),
|
||||
)
|
||||
]);
|
||||
@ -316,23 +318,23 @@ class EmojiPickerState extends State<EmojiPicker> {
|
||||
// Add an emoji to recently used list or increase its counter
|
||||
Future<void> _addEmojiToRecentlyUsed(Emoji emoji) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final recentEmojiIndex =
|
||||
recentEmoji.indexWhere((element) => element.emoji.emoji == emoji.emoji);
|
||||
final recentEmojiIndex = recentEmojiList
|
||||
.indexWhere((element) => element.emoji.emoji == emoji.emoji);
|
||||
if (recentEmojiIndex != -1) {
|
||||
// Already exist in recent list
|
||||
// Just update counter
|
||||
recentEmoji[recentEmojiIndex].counter++;
|
||||
recentEmojiList[recentEmojiIndex].counter++;
|
||||
} else {
|
||||
recentEmoji.add(RecentEmoji(emoji, 1));
|
||||
recentEmojiList.add(RecentEmoji(emoji, 1));
|
||||
}
|
||||
// Sort by counter desc
|
||||
recentEmoji.sort((a, b) => b.counter - a.counter);
|
||||
recentEmojiList.sort((a, b) => b.counter - a.counter);
|
||||
// Limit entries to recentsLimit
|
||||
recentEmoji = recentEmoji.sublist(
|
||||
recentEmojiList = recentEmojiList.sublist(
|
||||
0,
|
||||
min(widget.config.recentsLimit, recentEmoji.length),
|
||||
min(widget.config.recentsLimit, recentEmojiList.length),
|
||||
);
|
||||
// save locally
|
||||
prefs.setString('recent', jsonEncode(recentEmoji));
|
||||
prefs.setString('recent', jsonEncode(recentEmojiList));
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'config.dart';
|
||||
import 'emji_picker_config.dart';
|
||||
import 'emoji_view_state.dart';
|
||||
|
||||
/// Template class for custom implementation
|
||||
@ -11,7 +11,7 @@ abstract class EmojiPickerBuilder extends StatefulWidget {
|
||||
: super(key: key);
|
||||
|
||||
/// Config for customizations
|
||||
final Config config;
|
||||
final EmojiPickerConfig config;
|
||||
|
||||
/// State that holds current emoji data
|
||||
final EmojiViewState state;
|
@ -1,17 +1,17 @@
|
||||
import 'models/category_models.dart';
|
||||
import 'models/emoji_category_models.dart';
|
||||
import 'emoji_picker.dart';
|
||||
|
||||
/// State that holds current emoji data
|
||||
class EmojiViewState {
|
||||
/// Constructor
|
||||
EmojiViewState(
|
||||
this.categoryEmoji,
|
||||
this.emojiCategoryGroupList,
|
||||
this.onEmojiSelected,
|
||||
this.onBackspacePressed,
|
||||
);
|
||||
|
||||
/// List of all category including their emoji
|
||||
final List<CategoryEmoji> categoryEmoji;
|
||||
/// List of all categories including their emojis
|
||||
final List<EmojiCategoryGroup> emojiCategoryGroupList;
|
||||
|
||||
/// Callback when pressed on emoji
|
||||
final OnEmojiSelected onEmojiSelected;
|
@ -0,0 +1,34 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
EmojiPickerConfig buildFlowyEmojiPickerConfig(BuildContext context) {
|
||||
final style = Theme.of(context);
|
||||
return EmojiPickerConfig(
|
||||
bgColor: style.cardColor,
|
||||
categoryIconColor: style.iconTheme.color,
|
||||
selectedCategoryIconColor: style.colorScheme.onSurface,
|
||||
selectedCategoryIconBackgroundColor: style.colorScheme.primary,
|
||||
progressIndicatorColor: style.colorScheme.primary,
|
||||
backspaceColor: style.colorScheme.primary,
|
||||
searchHintText: LocaleKeys.emoji_search.tr(),
|
||||
serachHintTextStyle: style.textTheme.bodyMedium?.copyWith(
|
||||
color: style.hintColor,
|
||||
),
|
||||
serachBarEnableBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: BorderSide(color: style.dividerColor),
|
||||
),
|
||||
serachBarFocusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: BorderSide(
|
||||
color: style.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
noRecentsText: LocaleKeys.emoji_noRecent.tr(),
|
||||
noRecentsStyle: style.textTheme.bodyMedium,
|
||||
noEmojiFoundText: LocaleKeys.emoji_noEmojiFound.tr(),
|
||||
scrollBarHandleColor: style.colorScheme.onBackground,
|
||||
);
|
||||
}
|
@ -3,13 +3,11 @@ 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);
|
||||
/// EmojiCategory with its emojis
|
||||
class EmojiCategoryGroup {
|
||||
EmojiCategoryGroup(this.category, this.emoji);
|
||||
|
||||
/// Category instance
|
||||
final Category category;
|
||||
final EmojiCategory category;
|
||||
|
||||
/// List of emoji of this category
|
||||
List<Emoji> emoji;
|
||||
@ -20,10 +18,10 @@ class CategoryEmoji {
|
||||
}
|
||||
}
|
||||
|
||||
/// Class that defines the icon representing a [Category]
|
||||
class CategoryIcon {
|
||||
/// Class that defines the icon representing a [EmojiCategory]
|
||||
class EmojiCategoryIcon {
|
||||
/// Icon of Category
|
||||
const CategoryIcon({
|
||||
const EmojiCategoryIcon({
|
||||
required this.icon,
|
||||
this.color = const Color(0xffd3d3d3),
|
||||
this.selectedColor = const Color(0xffb2b2b2),
|
||||
@ -39,14 +37,14 @@ class CategoryIcon {
|
||||
final Color selectedColor;
|
||||
}
|
||||
|
||||
/// Class used to define all the [CategoryIcon] shown for each [Category]
|
||||
/// Class used to define all the [EmojiCategoryIcon] shown for each [EmojiCategory]
|
||||
///
|
||||
/// This allows the keyboard to be personalized by changing icons shown.
|
||||
/// If a [CategoryIcon] is set as null or not defined during initialization,
|
||||
/// If a [EmojiCategoryIcon] is set as null or not defined during initialization,
|
||||
/// the default icons will be used instead
|
||||
class CategoryIcons {
|
||||
class EmojiCategoryIcons {
|
||||
/// Constructor
|
||||
const CategoryIcons({
|
||||
const EmojiCategoryIcons({
|
||||
this.recentIcon = Icons.access_time,
|
||||
this.smileyIcon = Icons.tag_faces,
|
||||
this.animalIcon = Icons.pets,
|
||||
@ -59,33 +57,33 @@ class CategoryIcons {
|
||||
this.searchIcon = Icons.search,
|
||||
});
|
||||
|
||||
/// Icon for [Category.RECENT]
|
||||
/// Icon for [EmojiCategory.RECENT]
|
||||
final IconData recentIcon;
|
||||
|
||||
/// Icon for [Category.SMILEYS]
|
||||
/// Icon for [EmojiCategory.SMILEYS]
|
||||
final IconData smileyIcon;
|
||||
|
||||
/// Icon for [Category.ANIMALS]
|
||||
/// Icon for [EmojiCategory.ANIMALS]
|
||||
final IconData animalIcon;
|
||||
|
||||
/// Icon for [Category.FOODS]
|
||||
/// Icon for [EmojiCategory.FOODS]
|
||||
final IconData foodIcon;
|
||||
|
||||
/// Icon for [Category.ACTIVITIES]
|
||||
/// Icon for [EmojiCategory.ACTIVITIES]
|
||||
final IconData activityIcon;
|
||||
|
||||
/// Icon for [Category.TRAVEL]
|
||||
/// Icon for [EmojiCategory.TRAVEL]
|
||||
final IconData travelIcon;
|
||||
|
||||
/// Icon for [Category.OBJECTS]
|
||||
/// Icon for [EmojiCategory.OBJECTS]
|
||||
final IconData objectIcon;
|
||||
|
||||
/// Icon for [Category.SYMBOLS]
|
||||
/// Icon for [EmojiCategory.SYMBOLS]
|
||||
final IconData symbolIcon;
|
||||
|
||||
/// Icon for [Category.FLAGS]
|
||||
/// Icon for [EmojiCategory.FLAGS]
|
||||
final IconData flagIcon;
|
||||
|
||||
/// Icon for [Category.SEARCH]
|
||||
/// Icon for [EmojiCategory.SEARCH]
|
||||
final IconData searchIcon;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export 'src/config.dart';
|
||||
export 'src/models/emoji_model.dart';
|
||||
export 'src/emoji_picker.dart';
|
||||
export 'src/emoji_picker_builder.dart';
|
@ -1,169 +0,0 @@
|
||||
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.selectedHoverColor = Colors.grey,
|
||||
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;
|
||||
|
||||
/// Vertical 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 background color of the selected category
|
||||
final Color selectedHoverColor;
|
||||
|
||||
/// 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 initialization
|
||||
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;
|
||||
}
|
@ -1,321 +0,0 @@
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.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(() {
|
||||
final String query = _emojiController.text.toLowerCase();
|
||||
if (query.isEmpty) {
|
||||
searchEmojiList.emoji.clear();
|
||||
_pageController!.jumpToPage(
|
||||
_tabController!.index,
|
||||
);
|
||||
} else {
|
||||
searchEmojiList.emoji.clear();
|
||||
for (final 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() {
|
||||
final 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(4.0),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: _emojiController,
|
||||
focusNode: _emojiFocusNode,
|
||||
autofocus: true,
|
||||
cursorWidth: 1.0,
|
||||
cursorColor: Theme.of(context).colorScheme.tertiary,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: FontSizes.s16,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
hintText: "Search emoji",
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(6),
|
||||
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(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
color: widget.config.selectedHoverColor,
|
||||
),
|
||||
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) {
|
||||
final 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,
|
||||
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
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.scaleDown,
|
||||
child: Text(
|
||||
emoji.emoji,
|
||||
textScaleFactor: 1.0,
|
||||
style: TextStyle(
|
||||
fontSize: emojiSize,
|
||||
backgroundColor: Colors.transparent,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNoRecent() {
|
||||
return Center(
|
||||
child: Text(
|
||||
widget.config.noRecentsText,
|
||||
style: widget.config.noRecentsStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,338 +0,0 @@
|
||||
// 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,
|
||||
|
||||
/// Objects 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>();
|
||||
}
|
||||
|
||||
final 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);
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize 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 = {};
|
||||
const delimiter = '|';
|
||||
try {
|
||||
final entries = emoji.values.join(delimiter);
|
||||
final keys = emoji.keys.join(delimiter);
|
||||
final result = (await platform.invokeMethod<String>(
|
||||
'checkAvailability',
|
||||
{'emojiKeys': keys, 'emojiEntries': entries},
|
||||
)) as String;
|
||||
final 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();
|
||||
final emojiJson = prefs.getString(title);
|
||||
if (emojiJson == null) {
|
||||
return null;
|
||||
}
|
||||
final 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();
|
||||
final 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();
|
||||
final emojiJson = prefs.getString('recent');
|
||||
if (emojiJson == null) {
|
||||
return [];
|
||||
}
|
||||
final 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();
|
||||
final 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));
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'config.dart';
|
||||
import 'emoji_view_state.dart';
|
||||
|
||||
/// Template class for custom implementation
|
||||
/// Inherit 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;
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
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(0xffd3d3d3),
|
||||
this.selectedColor = const Color(0xffb2b2b2),
|
||||
});
|
||||
|
||||
/// 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;
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
/// 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,
|
||||
};
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
@ -57,7 +57,7 @@ class DandelionColorScheme extends FlowyColorScheme {
|
||||
main1: _lightDandelionYellow,
|
||||
// cursor color
|
||||
main2: _lightDandelionYellow,
|
||||
shadow: _black,
|
||||
shadow: const Color.fromRGBO(0, 0, 0, 0.15),
|
||||
sidebarBg: _lightDandelionGreen,
|
||||
divider: _lightShader6,
|
||||
topbarBg: _white,
|
||||
@ -111,7 +111,7 @@ class DandelionColorScheme extends FlowyColorScheme {
|
||||
tint9: const Color(0x4d0029FF),
|
||||
main1: _darkMain1,
|
||||
main2: _darkMain1,
|
||||
shadow: _black,
|
||||
shadow: const Color(0xff0F131C),
|
||||
sidebarBg: const Color(0xff232B38),
|
||||
divider: _darkShader3,
|
||||
topbarBg: _darkShader1,
|
||||
|
@ -107,7 +107,7 @@ class LavenderColorScheme extends FlowyColorScheme {
|
||||
tint9: const Color(0x4d0029FF),
|
||||
main1: _darkMain1,
|
||||
main2: _darkMain1,
|
||||
shadow: _black,
|
||||
shadow: const Color(0xff0F131C),
|
||||
sidebarBg: const Color(0xff2D223B),
|
||||
divider: _darkShader3,
|
||||
topbarBg: _darkShader1,
|
||||
|
@ -124,8 +124,8 @@ class ScrollbarState extends State<StyledScrollbar> {
|
||||
// Track color
|
||||
var trackColor = widget.trackColor ??
|
||||
(Theme.of(context).brightness == Brightness.dark
|
||||
? AFThemeExtension.of(context).greyHover.withOpacity(.1)
|
||||
: AFThemeExtension.of(context).greyHover.withOpacity(.3));
|
||||
? AFThemeExtension.of(context).lightGreyHover
|
||||
: AFThemeExtension.of(context).greyHover);
|
||||
|
||||
// Layout the stack, it just contains a child, and
|
||||
return Stack(
|
||||
@ -225,6 +225,7 @@ class ScrollbarListStack extends StatelessWidget {
|
||||
final EdgeInsets? scrollbarPadding;
|
||||
final Color? handleColor;
|
||||
final Color? trackColor;
|
||||
final bool showTrack;
|
||||
final bool autoHideScrollbar;
|
||||
|
||||
const ScrollbarListStack({
|
||||
@ -238,6 +239,7 @@ class ScrollbarListStack extends StatelessWidget {
|
||||
this.handleColor,
|
||||
this.autoHideScrollbar = true,
|
||||
this.trackColor,
|
||||
this.showTrack = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -262,6 +264,7 @@ class ScrollbarListStack extends StatelessWidget {
|
||||
trackColor: trackColor,
|
||||
handleColor: handleColor,
|
||||
autoHideScrollbar: autoHideScrollbar,
|
||||
showTrack: showTrack,
|
||||
),
|
||||
)
|
||||
// The animate will be used by the children that using styled_widget.
|
||||
|
@ -755,6 +755,9 @@
|
||||
"gray": "Gray"
|
||||
},
|
||||
"emoji": {
|
||||
"search": "Search emoji",
|
||||
"noRecent": "No recent emoji",
|
||||
"noEmojiFound": "No emoji found",
|
||||
"filter": "Filter",
|
||||
"random": "Random",
|
||||
"selectSkinTone": "Select skin tone",
|
||||
|
Loading…
Reference in New Issue
Block a user