253 lines
8.4 KiB
Dart
253 lines
8.4 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
|
import 'package:emoji_picker_flutter/src/emoji_picker_internal_utils.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// Emoji Regex
|
|
/// Keycap Sequence '((\u0023|\u002a|[\u0030-\u0039])\ufe0f\u20e3){1}'
|
|
/// Issue: https://github.com/flutter/flutter/issues/36062
|
|
const EmojiRegex =
|
|
r'((\u0023|\u002a|[\u0030-\u0039])\ufe0f\u20e3){1}|\p{Emoji}|\u200D|\uFE0F';
|
|
|
|
/// Helper class that provides extended usage
|
|
class EmojiPickerUtils {
|
|
/// Singleton Constructor
|
|
factory EmojiPickerUtils() {
|
|
return _singleton;
|
|
}
|
|
|
|
EmojiPickerUtils._internal();
|
|
|
|
static final EmojiPickerUtils _singleton = EmojiPickerUtils._internal();
|
|
static final RegExp _whitespaceRegExp = RegExp(r'\s+');
|
|
final List<Emoji> _allAvailableEmojiEntities = [];
|
|
RegExp? _emojiRegExp;
|
|
|
|
/// Returns list of recently used emoji from cache
|
|
Future<List<RecentEmoji>> getRecentEmojis() =>
|
|
Future.value(EmojiPickerInternalUtils().getRecentEmojis());
|
|
|
|
/// Filters out emojis not supported on the platform
|
|
Future<List<CategoryEmoji>> filterUnsupported(List<CategoryEmoji> data) =>
|
|
Future.value(EmojiPickerInternalUtils().filterUnsupported(data));
|
|
|
|
/// Search for related emoticons based on keywords
|
|
Future<List<Emoji>> searchEmoji(
|
|
String search,
|
|
List<CategoryEmoji> emojiSet, {
|
|
bool checkPlatformCompatibility = true,
|
|
}) async {
|
|
if (search.isEmpty) return [];
|
|
|
|
if (_allAvailableEmojiEntities.isEmpty) {
|
|
final emojiPickerInternalUtils = EmojiPickerInternalUtils();
|
|
|
|
final data = [...emojiSet]
|
|
..removeWhere((e) => e.category == Category.RECENT);
|
|
final availableCategoryEmoji = checkPlatformCompatibility
|
|
? await emojiPickerInternalUtils.filterUnsupported(data)
|
|
: data;
|
|
|
|
// Set all the emoji entities
|
|
for (var emojis in availableCategoryEmoji) {
|
|
_allAvailableEmojiEntities.addAll(emojis.emoji);
|
|
}
|
|
}
|
|
|
|
// Split the input string into a list of lowercase keywords
|
|
final keywordSet = search
|
|
.split(_whitespaceRegExp)
|
|
.where((e) => e.isNotEmpty)
|
|
.map((e) => e.toLowerCase())
|
|
.toSet();
|
|
|
|
if (keywordSet.isEmpty) return [];
|
|
|
|
return _allAvailableEmojiEntities.where((emoji) {
|
|
// Perform lowercasing of emoji keywords once
|
|
final emojiKeywordSet = emoji.keywords
|
|
.map((e) => e.toLowerCase())
|
|
.toSet();
|
|
|
|
// Check if first keyword is a prefix of any emoji keyword
|
|
final matchFirstKeyword = emojiKeywordSet.any(
|
|
(emojiKeyword) => emojiKeyword.startsWith(keywordSet.first),
|
|
);
|
|
|
|
var matchKeywords = false;
|
|
if (matchFirstKeyword) {
|
|
// Check if each search keyword is a prefix of any emoji keyword
|
|
// start from second keyword, returns true if empty (only 1 keyword)
|
|
matchKeywords = keywordSet.skip(1).every((keyword) {
|
|
return emojiKeywordSet.any(
|
|
(emojiKeyword) => emojiKeyword.startsWith(keyword),
|
|
);
|
|
});
|
|
} else {
|
|
matchKeywords = false;
|
|
}
|
|
|
|
// Check for an exact match with emoji character
|
|
final matchEmoji = emoji.emoji == search.trim();
|
|
|
|
return matchKeywords || matchEmoji;
|
|
}).toList();
|
|
}
|
|
|
|
/// Add an emoji to recently used list or increase its counter
|
|
Future<void> addEmojiToRecentlyUsed({
|
|
required GlobalKey<EmojiPickerState> key,
|
|
required Emoji emoji,
|
|
Config config = const Config(),
|
|
}) async {
|
|
return EmojiPickerInternalUtils()
|
|
.addEmojiToRecentlyUsed(emoji: emoji, config: config)
|
|
.then(
|
|
(recentEmojiList) =>
|
|
key.currentState?.updateRecentEmoji(recentEmojiList),
|
|
);
|
|
}
|
|
|
|
/// Produce a list of spans to adjust style for emoji characters.
|
|
/// Spans enclosing emojis will have [parentStyle] combined with [emojiStyle].
|
|
/// Other spans will not have an explicit style (this method does not set
|
|
/// [parentStyle] to the whole text.
|
|
List<InlineSpan> setEmojiTextStyle(
|
|
String text, {
|
|
required TextStyle emojiStyle,
|
|
TextStyle? parentStyle,
|
|
}) {
|
|
final composedEmojiStyle = (parentStyle ?? const TextStyle())
|
|
.merge(DefaultEmojiTextStyle)
|
|
.merge(emojiStyle);
|
|
|
|
final spans = <TextSpan>[];
|
|
final matches = getEmojiRegex().allMatches(text).toList();
|
|
var cursor = 0;
|
|
for (final match in matches) {
|
|
if (cursor != match.start) {
|
|
// Non emoji text + following emoji
|
|
spans
|
|
..add(
|
|
TextSpan(
|
|
text: text.substring(cursor, match.start),
|
|
style: parentStyle,
|
|
),
|
|
)
|
|
..add(
|
|
TextSpan(
|
|
text: text.substring(match.start, match.end),
|
|
style: composedEmojiStyle,
|
|
),
|
|
);
|
|
} else {
|
|
if (spans.isEmpty) {
|
|
// Create new span if no previous emoji TextSpan exists
|
|
spans.add(
|
|
TextSpan(
|
|
text: text.substring(match.start, match.end),
|
|
style: composedEmojiStyle,
|
|
),
|
|
);
|
|
} else {
|
|
// Update last span if current text is still emoji
|
|
final lastIndex = spans.length - 1;
|
|
final lastText = spans[lastIndex].text ?? '';
|
|
final currentText = text.substring(match.start, match.end);
|
|
spans[lastIndex] = TextSpan(
|
|
text: '$lastText$currentText',
|
|
style: composedEmojiStyle,
|
|
);
|
|
}
|
|
}
|
|
// Update cursor
|
|
cursor = match.end;
|
|
}
|
|
// Add remaining text
|
|
if (cursor != text.length) {
|
|
spans.add(
|
|
TextSpan(text: text.substring(cursor, text.length), style: parentStyle),
|
|
);
|
|
}
|
|
return spans;
|
|
}
|
|
|
|
/// Applies skin tone to given emoji
|
|
///
|
|
/// Any existing skin tone modifier is stripped first, so re-applying a tone
|
|
/// to an already toned glyph produces a valid single-modifier sequence
|
|
/// instead of an invalid double-modifier one (e.g. 👋🏻🏽).
|
|
Emoji applySkinTone(Emoji emoji, String color) {
|
|
final codeUnits = removeSkinTone(emoji).emoji.codeUnits;
|
|
var result = List<int>.empty(growable: true)
|
|
// Basic emoji without gender (until char 2)
|
|
..addAll(codeUnits.sublist(0, min(codeUnits.length, 2)))
|
|
// Skin tone
|
|
..addAll(color.codeUnits);
|
|
// add the rest of the emoji (gender, etc.) again
|
|
if (codeUnits.length >= 2) {
|
|
result.addAll(codeUnits.sublist(2));
|
|
}
|
|
return emoji.copyWith(emoji: String.fromCharCodes(result));
|
|
}
|
|
|
|
/// Removes any skin tone modifier from the given emoji
|
|
Emoji removeSkinTone(Emoji emoji) =>
|
|
EmojiPickerInternalUtils().removeSkinTone(emoji);
|
|
|
|
/// Returns the emoji that should be displayed (and selected) in the grid,
|
|
/// recents and search results.
|
|
///
|
|
/// When [skinToneConfig] remembers a tone and [rememberedSkinTone] is set,
|
|
/// the toned glyph is returned; otherwise the original emoji is returned
|
|
/// unchanged. The result keeps [Emoji.hasSkinTone] intact so the indicator
|
|
/// and long-press picker keep working on the cell.
|
|
Emoji applyDisplaySkinTone(
|
|
Emoji emoji,
|
|
SkinToneConfig skinToneConfig,
|
|
String? rememberedSkinTone,
|
|
) {
|
|
if (!skinToneConfig.enabled ||
|
|
!skinToneConfig.rememberSkinTone ||
|
|
rememberedSkinTone == null ||
|
|
!emoji.hasSkinTone) {
|
|
return emoji;
|
|
}
|
|
return applySkinTone(emoji, rememberedSkinTone);
|
|
}
|
|
|
|
/// Returns the skin tone modifier contained in [emoji], or `null` when the
|
|
/// emoji carries no skin tone.
|
|
String? extractSkinTone(Emoji emoji) {
|
|
for (final tone in SkinTone.values) {
|
|
if (emoji.emoji.contains(tone)) {
|
|
return tone;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Returns the last remembered skin tone modifier, or `null` if none.
|
|
Future<String?> getRememberedSkinTone() =>
|
|
EmojiPickerInternalUtils().getRememberedSkinTone();
|
|
|
|
/// Persists the remembered skin tone modifier. Passing `null` clears it.
|
|
Future<void> setRememberedSkinTone(String? skinTone) =>
|
|
EmojiPickerInternalUtils().setRememberedSkinTone(skinTone);
|
|
|
|
/// Clears the list of recent emojis
|
|
Future<void> clearRecentEmojis({
|
|
required GlobalKey<EmojiPickerState> key,
|
|
}) async {
|
|
return await EmojiPickerInternalUtils()
|
|
.clearRecentEmojisInLocalStorage()
|
|
.then((_) => key.currentState?.updateRecentEmoji([], refresh: true));
|
|
}
|
|
|
|
/// Returns the emoji regex
|
|
/// Based on https://unicode.org/reports/tr51/
|
|
RegExp getEmojiRegex() {
|
|
return _emojiRegExp ??= RegExp(EmojiRegex, unicode: true);
|
|
}
|
|
}
|