// // KBInputProfileManager.m // KeyBoard // #import "KBInputProfileManager.h" @implementation KBInputProfileLayout @end @implementation KBInputProfile @end @interface KBInputProfileManager () @property (nonatomic, strong) NSArray *profiles; @end @implementation KBInputProfileManager + (instancetype)sharedManager { static KBInputProfileManager *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; } - (instancetype)init { if (self = [super init]) { [self loadProfiles]; } return self; } - (void)loadProfiles { NSString *path = [[NSBundle mainBundle] pathForResource:@"kb_input_profiles" ofType:@"json"]; if (path.length == 0) { path = [[NSBundle mainBundle] pathForResource:@"kb_input_profiles" ofType:@"json" inDirectory:@"Resource"]; } if (path.length == 0) { path = [[NSBundle mainBundle] pathForResource:@"kb_input_profiles" ofType:@"json" inDirectory:@"Shared/Resource"]; } if (!path) { NSLog(@"[KBInputProfileManager] kb_input_profiles.json not found"); self.profiles = [self defaultProfiles]; return; } NSData *data = [NSData dataWithContentsOfFile:path]; if (!data) { NSLog(@"[KBInputProfileManager] Failed to read kb_input_profiles.json"); self.profiles = [self defaultProfiles]; return; } NSError *error = nil; NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (error || ![json isKindOfClass:NSDictionary.class]) { NSLog(@"[KBInputProfileManager] Failed to parse JSON: %@", error); self.profiles = [self defaultProfiles]; return; } NSArray *profilesArray = json[@"profiles"]; if (![profilesArray isKindOfClass:NSArray.class]) { NSLog(@"[KBInputProfileManager] Invalid profiles array"); self.profiles = [self defaultProfiles]; return; } self.profiles = [self parseProfilesFromJSONArray:profilesArray]; if (self.profiles.count == 0) { NSLog(@"[KBInputProfileManager] Parsed profiles is empty, fallback to default"); self.profiles = [self defaultProfiles]; } NSLog(@"[KBInputProfileManager] Loaded %lu profiles", (unsigned long)self.profiles.count); } - (NSArray *)parseProfilesFromJSONArray:(NSArray *)profilesArray { NSMutableArray *result = [NSMutableArray array]; for (NSDictionary *profileDict in profilesArray) { if (![profileDict isKindOfClass:NSDictionary.class]) { continue; } KBInputProfile *profile = [[KBInputProfile alloc] init]; profile.code = [profileDict[@"code"] isKindOfClass:NSString.class] ? profileDict[@"code"] : @""; profile.name = [profileDict[@"name"] isKindOfClass:NSString.class] ? profileDict[@"name"] : @""; profile.defaultSkinZip = [profileDict[@"defaultSkinZip"] isKindOfClass:NSString.class] ? profileDict[@"defaultSkinZip"] : @""; NSArray *layoutsArray = profileDict[@"layouts"]; NSMutableArray *layouts = [NSMutableArray array]; if ([layoutsArray isKindOfClass:NSArray.class]) { for (NSDictionary *layoutDict in layoutsArray) { if (![layoutDict isKindOfClass:NSDictionary.class]) { continue; } KBInputProfileLayout *layout = [[KBInputProfileLayout alloc] init]; layout.variant = [layoutDict[@"variant"] isKindOfClass:NSString.class] ? layoutDict[@"variant"] : @""; layout.title = [layoutDict[@"title"] isKindOfClass:NSString.class] ? layoutDict[@"title"] : @""; layout.profileId = [layoutDict[@"profileId"] isKindOfClass:NSString.class] ? layoutDict[@"profileId"] : @""; layout.layoutJsonId = [layoutDict[@"layoutJsonId"] isKindOfClass:NSString.class] ? layoutDict[@"layoutJsonId"] : @""; layout.suggestionEngine = [layoutDict[@"suggestionEngine"] isKindOfClass:NSString.class] ? layoutDict[@"suggestionEngine"] : @""; [layouts addObject:layout]; } } profile.layouts = [layouts copy]; [result addObject:profile]; } return [result copy]; } - (NSArray *)defaultProfiles { NSArray *fallback = @[ @{ @"code": @"en", @"name": @"English", @"defaultSkinZip": @"normal_them.zip", @"layouts": @[@{@"variant": @"qwerty", @"title": @"QWERTY", @"profileId": @"en_US_qwerty", @"layoutJsonId": @"letters", @"suggestionEngine": @"latin"}] }, @{ @"code": @"es", @"name": @"Español (Latinoamérica)", @"defaultSkinZip": @"西班牙初始皮肤.zip", @"layouts": @[@{@"variant": @"qwerty", @"title": @"QWERTY", @"profileId": @"es_419_qwerty", @"layoutJsonId": @"letters_es", @"suggestionEngine": @"spanish"}] }, @{ @"code": @"pt", @"name": @"Português", @"defaultSkinZip": @"", @"layouts": @[@{@"variant": @"qwerty", @"title": @"QWERTY", @"profileId": @"pt_PT_qwerty", @"layoutJsonId": @"letters_pt", @"suggestionEngine": @"latin"}] }, @{ @"code": @"zh-Hant", @"name": @"繁體中文(台灣)", @"defaultSkinZip": @"", @"layouts": @[ @{@"variant": @"pinyin", @"title": @"拼音(繁體)", @"profileId": @"zh_Hant_TW_pinyin", @"layoutJsonId": @"letters_zh_hant_pinyin", @"suggestionEngine": @"pinyin_traditional"}, @{@"variant": @"bopomofo_full", @"title": @"注音全鍵盤", @"profileId": @"zh_Hant_TW_bopomofo_full", @"layoutJsonId": @"letters_bopomofo_full", @"suggestionEngine": @"bopomofo"}, @{@"variant": @"bopomofo_standard", @"title": @"注音標準", @"profileId": @"zh_Hant_TW_bopomofo_standard", @"layoutJsonId": @"letters_bopomofo_standard", @"suggestionEngine": @"bopomofo"} ] }, @{ @"code": @"id", @"name": @"Bahasa Indonesia", @"defaultSkinZip": @"", @"layouts": @[@{@"variant": @"qwerty", @"title": @"QWERTY", @"profileId": @"id_ID_qwerty", @"layoutJsonId": @"letters_id", @"suggestionEngine": @"latin"}] } ]; return [self parseProfilesFromJSONArray:fallback]; } - (NSArray *)allProfiles { return self.profiles; } - (nullable KBInputProfile *)profileForLanguageCode:(NSString *)languageCode { for (KBInputProfile *profile in self.profiles) { if ([profile.code isEqualToString:languageCode]) { return profile; } } return nil; } - (nullable KBInputProfileLayout *)layoutForProfileId:(NSString *)profileId { for (KBInputProfile *profile in self.profiles) { for (KBInputProfileLayout *layout in profile.layouts) { if ([layout.profileId isEqualToString:profileId]) { return layout; } } } return nil; } - (nullable NSString *)layoutJsonIdForProfileId:(NSString *)profileId { KBInputProfileLayout *layout = [self layoutForProfileId:profileId]; return layout.layoutJsonId; } - (nullable NSString *)suggestionEngineForProfileId:(NSString *)profileId { KBInputProfileLayout *layout = [self layoutForProfileId:profileId]; return layout.suggestionEngine; } @end