Files
keyboard/CustomKeyboard/Manager/KBEmojiDataProvider.m

271 lines
9.1 KiB
Mathematica
Raw Normal View History

2025-12-15 13:24:43 +08:00
//
// KBEmojiDataProvider.m
// CustomKeyboard
//
#import "KBEmojiDataProvider.h"
#import "KBLocalizationManager.h"
#import "KBConfig.h"
NSString * const KBEmojiRecentsDidChangeNotification = @"KBEmojiRecentsDidChangeNotification";
static NSString * const kKBEmojiJSONFileName = @"emoji_categories";
static NSString * const kKBEmojiRecentsStoreKey = @"KBEmojiRecentEmojis";
static NSString * const kKBEmojiRecentsCategoryId = @"recents";
static const NSUInteger kKBEmojiRecentsLimit = 32;
#pragma mark - Model Implementations
@interface KBEmojiItem ()
@property (nonatomic, copy, readwrite) NSString *value;
@property (nonatomic, copy, readwrite) NSString *name;
@end
@implementation KBEmojiItem
- (instancetype)initWithValue:(NSString *)value name:(NSString *)name {
if (self = [super init]) {
_value = [value copy];
_name = [name copy];
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
KBEmojiItem *item = [[[self class] allocWithZone:zone] initWithValue:self.value name:self.name];
return item;
}
@end
@interface KBEmojiCategory ()
@property (nonatomic, copy, readwrite) NSString *identifier;
@property (nonatomic, copy) NSDictionary<NSString *, NSString *> *titleMap;
@property (nonatomic, copy, readwrite) NSString *displayTitle;
@property (nonatomic, copy, readwrite) NSString *iconSymbol;
@property (nonatomic, assign, readwrite, getter=isDynamic) BOOL dynamic;
@property (nonatomic, copy, readwrite) NSArray<KBEmojiItem *> *items;
@end
@implementation KBEmojiCategory
- (void)refreshDisplayTitleForLanguage:(NSString *)lang {
if (lang.length == 0) {
lang = KBLanguageCodeEnglish;
}
NSString *title = self.titleMap[lang];
if (title.length == 0) {
if ([lang.lowercaseString hasPrefix:@"zh"]) {
title = self.titleMap[@"zh-Hans"] ?: self.titleMap[@"zh-hans"];
}
}
if (title.length == 0) {
title = self.titleMap[@"en"];
}
if (title.length == 0) {
title = self.titleMap.allValues.firstObject;
}
self.displayTitle = title ?: @"";
}
@end
#pragma mark - Data Provider
@interface KBEmojiDataProvider ()
@property (nonatomic, copy) NSArray<KBEmojiCategory *> *categoriesInternal;
@property (nonatomic, strong) NSMutableDictionary<NSString *, KBEmojiItem *> *itemLookup;
@property (nonatomic, strong) NSMutableOrderedSet<NSString *> *recentValues;
@end
@implementation KBEmojiDataProvider
+ (instancetype)shared {
static KBEmojiDataProvider *m;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
m = [KBEmojiDataProvider new];
[[NSNotificationCenter defaultCenter] addObserver:m
selector:@selector(onLocalizationChanged:)
name:KBLocalizationDidChangeNotification
object:nil];
});
return m;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (NSArray<KBEmojiCategory *> *)categories {
[self reloadIfNeeded];
return self.categoriesInternal ?: @[];
}
- (void)reloadIfNeeded {
if (self.categoriesInternal.count > 0) { return; }
[self loadEmojiJSON];
[self refreshLocalizedTitles];
[self loadRecentsFromStore];
[self rebuildRecentsCategory];
}
- (void)loadEmojiJSON {
NSString *path = [[NSBundle mainBundle] pathForResource:kKBEmojiJSONFileName ofType:@"json"];
if (path.length == 0) {
return;
}
NSData *data = [NSData dataWithContentsOfFile:path];
if (data.length == 0) { return; }
NSError *err = nil;
NSDictionary *root = [NSJSONSerialization JSONObjectWithData:data options:0 error:&err];
if (!root || err) {
NSLog(@"[Emoji] failed to parse json: %@", err);
return;
}
NSArray *catArray = root[@"categories"];
if (![catArray isKindOfClass:NSArray.class]) {
return;
}
NSMutableArray<KBEmojiCategory *> *tmpCats = [NSMutableArray arrayWithCapacity:catArray.count];
self.itemLookup = [NSMutableDictionary dictionary];
for (NSDictionary *catDict in catArray) {
if (![catDict isKindOfClass:NSDictionary.class]) continue;
KBEmojiCategory *category = [KBEmojiCategory new];
category.identifier = catDict[@"id"] ?: @"";
NSDictionary *titleMap = catDict[@"title"];
if ([titleMap isKindOfClass:NSDictionary.class]) {
category.titleMap = titleMap;
} else {
category.titleMap = @{};
}
NSString *iconKey = catDict[@"icon"];
category.iconSymbol = [self symbolForIconKey:iconKey];
NSString *type = catDict[@"type"];
category.dynamic = [type.lowercaseString isEqualToString:@"dynamic"];
NSArray *emojiArray = catDict[@"emojis"];
NSMutableArray<KBEmojiItem *> *items = [NSMutableArray arrayWithCapacity:[emojiArray count]];
if ([emojiArray isKindOfClass:NSArray.class]) {
for (NSDictionary *emojiDict in emojiArray) {
if (![emojiDict isKindOfClass:NSDictionary.class]) continue;
NSString *value = emojiDict[@"value"];
if (value.length == 0) continue;
NSString *name = emojiDict[@"name"] ?: @"";
KBEmojiItem *item = [[KBEmojiItem alloc] initWithValue:value name:name];
[items addObject:item];
if (value.length > 0) {
self.itemLookup[value] = item;
}
}
}
category.items = items.copy;
[tmpCats addObject:category];
}
self.categoriesInternal = tmpCats.copy;
}
- (NSString *)symbolForIconKey:(NSString *)key {
static NSDictionary<NSString *, NSString *> *map;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
map = @{
@"emoji_tab_recent": @"🕘",
@"emoji_tab_people": @"😊",
@"emoji_tab_nature": @"🌿",
@"emoji_tab_food": @"🍔",
@"emoji_tab_activity": @"🏀",
@"emoji_tab_travel": @"✈️",
@"emoji_tab_objects": @"💡",
@"emoji_tab_symbols": @"♾",
@"emoji_tab_flags": @"🏳️"
};
});
NSString *symbol = map[key];
return symbol.length ? symbol : @"●";
}
- (void)refreshLocalizedTitles {
NSString *lang = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
for (KBEmojiCategory *cat in self.categoriesInternal) {
[cat refreshDisplayTitleForLanguage:lang];
}
}
- (void)onLocalizationChanged:(__unused NSNotification *)note {
[self refreshLocalizedTitles];
[[NSNotificationCenter defaultCenter] postNotificationName:KBEmojiRecentsDidChangeNotification object:nil];
}
- (void)recordEmojiSelection:(NSString *)emoji {
if (emoji.length == 0) return;
[self reloadIfNeeded];
if (!self.recentValues) {
self.recentValues = [NSMutableOrderedSet orderedSet];
}
[self.recentValues removeObject:emoji];
[self.recentValues insertObject:emoji atIndex:0];
while (self.recentValues.count > kKBEmojiRecentsLimit) {
[self.recentValues removeObjectAtIndex:self.recentValues.count - 1];
}
[self saveRecentsToStore];
[self rebuildRecentsCategory];
[[NSNotificationCenter defaultCenter] postNotificationName:KBEmojiRecentsDidChangeNotification object:nil];
}
- (void)loadRecentsFromStore {
NSUserDefaults *defs = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
if (!defs) { defs = NSUserDefaults.standardUserDefaults; }
NSArray *stored = [defs objectForKey:kKBEmojiRecentsStoreKey];
NSMutableOrderedSet *set = [NSMutableOrderedSet orderedSet];
if ([stored isKindOfClass:NSArray.class]) {
for (id obj in stored) {
if (![obj isKindOfClass:NSString.class]) continue;
NSString *str = (NSString *)obj;
if (str.length == 0) continue;
[set addObject:str];
if (set.count >= kKBEmojiRecentsLimit) break;
}
}
self.recentValues = set;
}
- (void)saveRecentsToStore {
if (!self.recentValues) return;
NSArray *arr = self.recentValues.array;
NSUserDefaults *defs = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
if (!defs) { defs = NSUserDefaults.standardUserDefaults; }
[defs setObject:arr forKey:kKBEmojiRecentsStoreKey];
[defs synchronize];
}
- (void)rebuildRecentsCategory {
KBEmojiCategory *recent = [self categoryForIdentifier:kKBEmojiRecentsCategoryId];
if (!recent) return;
NSArray<NSString *> *values = self.recentValues.array ?: @[];
NSMutableArray<KBEmojiItem *> *items = [NSMutableArray arrayWithCapacity:values.count];
for (NSString *value in values) {
KBEmojiItem *item = self.itemLookup[value];
if (!item) {
item = [[KBEmojiItem alloc] initWithValue:value name:@""];
}
[items addObject:item];
}
recent.items = items.copy;
}
- (KBEmojiCategory *)categoryForIdentifier:(NSString *)identifier {
if (identifier.length == 0) return nil;
for (KBEmojiCategory *cat in self.categoriesInternal) {
if ([cat.identifier isEqualToString:identifier]) {
return cat;
}
}
return nil;
}
@end