271 lines
9.1 KiB
Objective-C
271 lines
9.1 KiB
Objective-C
//
|
|
// 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
|