1
This commit is contained in:
@@ -33,6 +33,11 @@
|
||||
/// 键盘扩展聊天更新的 companionId(键盘写入,主 App 读取后刷新对应聊天记录)
|
||||
#define AppGroup_ChatUpdatedCompanionId @"AppGroup_ChatUpdatedCompanionId"
|
||||
|
||||
/// 当前选中的输入配置(主 App 写入,键盘扩展读取)
|
||||
#define AppGroup_SelectedKeyboardProfileId @"AppGroup_SelectedKeyboardProfileId"
|
||||
#define AppGroup_SelectedKeyboardLanguageCode @"AppGroup_SelectedKeyboardLanguageCode"
|
||||
#define AppGroup_SelectedKeyboardLayoutVariant @"AppGroup_SelectedKeyboardLayoutVariant"
|
||||
|
||||
/// Darwin 跨进程通知:键盘扩展发送聊天消息后通知主 App 刷新
|
||||
#define kKBDarwinChatUpdated @"com.loveKey.nyx.chat.updated"
|
||||
|
||||
|
||||
48
Shared/KBInputProfileManager.h
Normal file
48
Shared/KBInputProfileManager.h
Normal file
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// KBInputProfileManager.h
|
||||
// KeyBoard
|
||||
//
|
||||
// 管理输入配置(语言 + 布局)的统一配置中心
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface KBInputProfileLayout : NSObject
|
||||
@property (nonatomic, copy) NSString *variant;
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
@property (nonatomic, copy) NSString *profileId;
|
||||
@property (nonatomic, copy) NSString *layoutJsonId;
|
||||
@property (nonatomic, copy) NSString *suggestionEngine;
|
||||
@end
|
||||
|
||||
@interface KBInputProfile : NSObject
|
||||
@property (nonatomic, copy) NSString *code;
|
||||
@property (nonatomic, copy) NSString *name;
|
||||
@property (nonatomic, copy) NSString *defaultSkinZip;
|
||||
@property (nonatomic, strong) NSArray<KBInputProfileLayout *> *layouts;
|
||||
@end
|
||||
|
||||
@interface KBInputProfileManager : NSObject
|
||||
|
||||
+ (instancetype)sharedManager;
|
||||
|
||||
/// 获取所有支持的语言配置
|
||||
- (NSArray<KBInputProfile *> *)allProfiles;
|
||||
|
||||
/// 根据语言代码获取配置
|
||||
- (nullable KBInputProfile *)profileForLanguageCode:(NSString *)languageCode;
|
||||
|
||||
/// 根据 profileId 获取布局配置
|
||||
- (nullable KBInputProfileLayout *)layoutForProfileId:(NSString *)profileId;
|
||||
|
||||
/// 根据 profileId 获取对应的 layoutJsonId
|
||||
- (nullable NSString *)layoutJsonIdForProfileId:(NSString *)profileId;
|
||||
|
||||
/// 根据 profileId 获取对应的联想引擎类型
|
||||
- (nullable NSString *)suggestionEngineForProfileId:(NSString *)profileId;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
194
Shared/KBInputProfileManager.m
Normal file
194
Shared/KBInputProfileManager.m
Normal file
@@ -0,0 +1,194 @@
|
||||
//
|
||||
// KBInputProfileManager.m
|
||||
// KeyBoard
|
||||
//
|
||||
|
||||
#import "KBInputProfileManager.h"
|
||||
|
||||
@implementation KBInputProfileLayout
|
||||
@end
|
||||
|
||||
@implementation KBInputProfile
|
||||
@end
|
||||
|
||||
@interface KBInputProfileManager ()
|
||||
@property (nonatomic, strong) NSArray<KBInputProfile *> *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<KBInputProfile *> *)parseProfilesFromJSONArray:(NSArray *)profilesArray {
|
||||
NSMutableArray<KBInputProfile *> *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<KBInputProfileLayout *> *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<KBInputProfile *> *)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",
|
||||
@"defaultSkinZip": @"",
|
||||
@"layouts": @[
|
||||
@{@"variant": @"qwerty", @"title": @"QWERTY", @"profileId": @"es_ES_qwerty", @"layoutJsonId": @"letters", @"suggestionEngine": @"latin"},
|
||||
@{@"variant": @"azerty", @"title": @"AZERTY", @"profileId": @"es_ES_azerty", @"layoutJsonId": @"letters_azerty", @"suggestionEngine": @"latin"},
|
||||
@{@"variant": @"qwertz", @"title": @"QWERTZ", @"profileId": @"es_ES_qwertz", @"layoutJsonId": @"letters_qwertz", @"suggestionEngine": @"latin"}
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"code": @"pt",
|
||||
@"name": @"Português",
|
||||
@"defaultSkinZip": @"",
|
||||
@"layouts": @[@{@"variant": @"qwerty", @"title": @"QWERTY", @"profileId": @"pt_PT_qwerty", @"layoutJsonId": @"letters", @"suggestionEngine": @"latin"}]
|
||||
},
|
||||
@{
|
||||
@"code": @"zh-Hant",
|
||||
@"name": @"繁體中文(台灣)",
|
||||
@"defaultSkinZip": @"",
|
||||
@"layouts": @[
|
||||
@{@"variant": @"pinyin", @"title": @"拼音(繁體)", @"profileId": @"zh_Hant_TW_pinyin", @"layoutJsonId": @"letters", @"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", @"suggestionEngine": @"latin"}]
|
||||
},
|
||||
@{
|
||||
@"code": @"zh-Hans",
|
||||
@"name": @"简体中文",
|
||||
@"defaultSkinZip": @"",
|
||||
@"layouts": @[@{@"variant": @"qwerty", @"title": @"QWERTY", @"profileId": @"zh_Hans_CN_qwerty", @"layoutJsonId": @"letters", @"suggestionEngine": @"pinyin_simplified"}]
|
||||
}
|
||||
];
|
||||
return [self parseProfilesFromJSONArray:fallback];
|
||||
}
|
||||
|
||||
- (NSArray<KBInputProfile *> *)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
|
||||
@@ -17,8 +17,12 @@ typedef NSString *KBLanguageCode NS_EXTENSIBLE_STRING_ENUM;
|
||||
/// 项目内统一使用的语言常量
|
||||
FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeEnglish; // @"en"
|
||||
FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeSimplifiedChinese; // @"zh-Hans"
|
||||
FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeTraditionalChinese; // @"zh-Hant"
|
||||
FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeSpanish; // @"es"
|
||||
FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodePortuguese; // @"pt"
|
||||
FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeIndonesian; // @"id"
|
||||
|
||||
/// 默认支持的语言列表(目前为 @[KBLanguageCodeEnglish, KBLanguageCodeSimplifiedChinese])
|
||||
/// 默认支持的语言列表(当前:en / zh-Hans / zh-Hant / es / pt / id)
|
||||
FOUNDATION_EXPORT NSArray<KBLanguageCode> *KBDefaultSupportedLanguageCodes(void);
|
||||
|
||||
/// 当前语言变更通知(不附带 userInfo)
|
||||
|
||||
@@ -10,13 +10,24 @@
|
||||
/// 语言常量定义
|
||||
KBLanguageCode const KBLanguageCodeEnglish = @"en";
|
||||
KBLanguageCode const KBLanguageCodeSimplifiedChinese = @"zh-Hans";
|
||||
KBLanguageCode const KBLanguageCodeTraditionalChinese = @"zh-Hant";
|
||||
KBLanguageCode const KBLanguageCodeSpanish = @"es";
|
||||
KBLanguageCode const KBLanguageCodePortuguese = @"pt";
|
||||
KBLanguageCode const KBLanguageCodeIndonesian = @"id";
|
||||
|
||||
/// 默认支持语言列表(集中配置)
|
||||
NSArray<KBLanguageCode> *KBDefaultSupportedLanguageCodes(void) {
|
||||
static NSArray<KBLanguageCode> *codes;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
codes = @[KBLanguageCodeEnglish, KBLanguageCodeSimplifiedChinese];
|
||||
codes = @[
|
||||
KBLanguageCodeEnglish,
|
||||
KBLanguageCodeSimplifiedChinese,
|
||||
KBLanguageCodeTraditionalChinese,
|
||||
KBLanguageCodeSpanish,
|
||||
KBLanguageCodePortuguese,
|
||||
KBLanguageCodeIndonesian
|
||||
];
|
||||
});
|
||||
return codes;
|
||||
}
|
||||
|
||||
@@ -140,6 +140,12 @@
|
||||
"Commit" = "Commit";
|
||||
"Nickname" = "Nickname";
|
||||
"Gender" = "Gender";
|
||||
"Input Language" = "Input Language";
|
||||
"Choose Layout" = "Choose Layout";
|
||||
"Multiple Keyboard Layouts" = "Multiple Keyboard Layouts";
|
||||
"This language has a default skin configured. It won't be auto-applied when switching language." = "This language has a default skin configured. It won't be auto-applied when switching language.";
|
||||
"Please configure a default skin for this language before switching." = "Please configure a default skin for this language before switching.";
|
||||
"Default skin install failed. Please check skin resource configuration." = "Default skin install failed. Please check skin resource configuration.";
|
||||
"User ID" = "User ID";
|
||||
"Modify Gender" = "Modify Gender";
|
||||
"Male" = "Male";
|
||||
|
||||
@@ -141,6 +141,12 @@
|
||||
"Commit" = "提交";
|
||||
"Nickname" = "用户名";
|
||||
"Gender" = "性别";
|
||||
"Input Language" = "输入语言";
|
||||
"Choose Layout" = "选择键盘布局";
|
||||
"Multiple Keyboard Layouts" = "多种键盘布局";
|
||||
"This language has a default skin configured. It won't be auto-applied when switching language." = "该语言已配置默认皮肤,切换语言时不会自动应用。";
|
||||
"Please configure a default skin for this language before switching." = "请先为该语言配置默认皮肤";
|
||||
"Default skin install failed. Please check skin resource configuration." = "默认皮肤安装失败,请检查皮肤资源配置";
|
||||
"User ID" = "用户ID";
|
||||
"Modify Gender" = "修改性别";
|
||||
"Male" = "男";
|
||||
|
||||
117
Shared/Resource/kb_input_profiles.json
Normal file
117
Shared/Resource/kb_input_profiles.json
Normal file
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"__comment": "输入配置文件:定义所有支持的语言和布局",
|
||||
"profiles": [
|
||||
{
|
||||
"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",
|
||||
"defaultSkinZip": "",
|
||||
"layouts": [
|
||||
{
|
||||
"variant": "qwerty",
|
||||
"title": "QWERTY",
|
||||
"profileId": "es_ES_qwerty",
|
||||
"layoutJsonId": "letters",
|
||||
"suggestionEngine": "latin"
|
||||
},
|
||||
{
|
||||
"variant": "azerty",
|
||||
"title": "AZERTY",
|
||||
"profileId": "es_ES_azerty",
|
||||
"layoutJsonId": "letters_azerty",
|
||||
"suggestionEngine": "latin"
|
||||
},
|
||||
{
|
||||
"variant": "qwertz",
|
||||
"title": "QWERTZ",
|
||||
"profileId": "es_ES_qwertz",
|
||||
"layoutJsonId": "letters_qwertz",
|
||||
"suggestionEngine": "latin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "pt",
|
||||
"name": "Português",
|
||||
"defaultSkinZip": "",
|
||||
"layouts": [
|
||||
{
|
||||
"variant": "qwerty",
|
||||
"title": "QWERTY",
|
||||
"profileId": "pt_PT_qwerty",
|
||||
"layoutJsonId": "letters",
|
||||
"suggestionEngine": "latin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "zh-Hant",
|
||||
"name": "繁體中文(台灣)",
|
||||
"defaultSkinZip": "",
|
||||
"layouts": [
|
||||
{
|
||||
"variant": "pinyin",
|
||||
"title": "拼音(繁體)",
|
||||
"profileId": "zh_Hant_TW_pinyin",
|
||||
"layoutJsonId": "letters",
|
||||
"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",
|
||||
"suggestionEngine": "latin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "zh-Hans",
|
||||
"name": "简体中文",
|
||||
"defaultSkinZip": "",
|
||||
"layouts": [
|
||||
{
|
||||
"variant": "qwerty",
|
||||
"title": "QWERTY",
|
||||
"profileId": "zh_Hans_CN_qwerty",
|
||||
"layoutJsonId": "letters",
|
||||
"suggestionEngine": "pinyin_simplified"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user