// // 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 *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 *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 *categoriesInternal; @property (nonatomic, strong) NSMutableDictionary *itemLookup; @property (nonatomic, strong) NSMutableOrderedSet *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 *)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 *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 *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 *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 *values = self.recentValues.array ?: @[]; NSMutableArray *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