96 Commits

Author SHA1 Message Date
e5472ebd6e 换了线上api 2026-03-10 17:55:54 +08:00
8a778a6fdc afn和mbhud隐藏问题 2026-03-10 17:29:20 +08:00
2e95a0072a 1 2026-03-10 12:57:01 +08:00
72142b0b71 1 2026-03-10 11:25:10 +08:00
0af7428353 3 2026-03-09 17:34:08 +08:00
c1ace5f53e 1 2026-03-08 21:29:10 +08:00
9fb2e2e694 1 2026-03-07 18:49:34 +08:00
6327f31f11 补充key 2026-03-07 13:43:26 +08:00
cbcf8c4197 key缺少,添加权限多语言 2026-03-07 13:29:29 +08:00
e03287605c Report报国际化错了 2026-03-07 11:33:15 +08:00
987391953a 过滤敏感词 2026-03-06 18:54:43 +08:00
442d56decd 添加隐私 2026-03-06 17:26:15 +08:00
fb74fbed1c 1 2026-03-06 12:53:39 +08:00
33a04186fb 优化联想 2026-03-06 10:45:13 +08:00
bb74a330db 添加变体 2026-03-05 21:21:15 +08:00
3c18579a83 修改我界面顶部我的键盘和设置按钮布局 2026-03-05 19:46:22 +08:00
d25dd38959 1 2026-03-05 18:41:39 +08:00
eaf512be7f 语言逻辑处理
我的项目里有5个国家的语言,如果用户在app里手动切换了国家语言,只要不卸载app,用户在手机设置切换语言,app的语言不要变;如果app被删
  除,重新安装,app语言要跟随手机设置的语言(如果语言对不上,app就显示英语)
  用户没有在app里手动设置过国家语言,用户在手机设置界面切换国家,app要跟随手机设置的语言(如果语言对不上,app就显示英语)。
2026-03-05 17:42:50 +08:00
d8a84dc478 处理上架的问题
1:处理了openurl 拉起问题
2:去掉了http
3 隐私等等
2026-03-05 14:30:07 +08:00
8cc484edcb 1 2026-03-04 21:57:37 +08:00
a61f505f70 统一语言值 2026-03-04 21:27:51 +08:00
8316d42fb3 bug 2026-03-04 21:16:21 +08:00
cb0b8a0aee 3 2026-03-04 20:49:43 +08:00
fe08f8d54a 1 2026-03-04 20:36:53 +08:00
7029209a4d 1 2026-03-04 19:49:07 +08:00
c42ccfbcdf 国家化语言 2026-03-04 19:08:02 +08:00
f2184cf9c6 1 2026-03-04 18:29:32 +08:00
e7567909bc 1 2026-03-04 18:09:11 +08:00
2d02e05956 新增app更新弹窗 2026-03-04 18:05:39 +08:00
973577c6eb 1 2026-03-04 16:11:13 +08:00
5c0cf2b435 清理键盘emoji内存高的问题 2026-03-04 15:06:49 +08:00
fd5de4f197 1 2026-03-04 14:36:59 +08:00
f9da0c40e5 添加联想词库 2026-03-04 14:15:45 +08:00
b1f1ddec7e 2 2026-03-04 13:44:56 +08:00
f30b1d7640 重构 2026-03-04 12:54:57 +08:00
2a122d27a9 1 2026-03-04 10:20:22 +08:00
72069cc737 1 2026-03-03 23:37:52 +08:00
6786a76f41 1 2026-03-03 23:36:23 +08:00
361ccc12d6 修改英文键盘问题 2026-03-03 22:01:07 +08:00
e5e059cf24 1 2026-03-03 21:43:31 +08:00
7adccd60c5 4 2026-03-03 21:31:03 +08:00
4c16ae1736 1 2026-03-03 19:12:24 +08:00
a0c5afc75d 添加繁体 2026-03-03 19:02:28 +08:00
4a26502c41 1 2026-03-03 17:01:33 +08:00
b86801636a 1 2026-03-03 16:48:05 +08:00
bcc8981c06 添加印度尼西亚 2026-03-03 15:52:37 +08:00
211f30d793 3 2026-03-03 14:22:26 +08:00
494efb745e 2 2026-03-03 13:44:51 +08:00
53c406c984 1 2026-03-03 13:14:47 +08:00
2aa5fa8d09 修改在西班牙键盘bar上 ai图标不显示问题 2026-03-02 21:47:37 +08:00
152c7052b4 修复西班牙语bug 2026-03-02 21:13:54 +08:00
2505de0f24 首页加载默认皮肤 2026-03-02 20:52:02 +08:00
fb6db0649c 1 2026-03-02 20:20:28 +08:00
a68fb9657f 1 2026-03-02 16:46:57 +08:00
04cfc35485 添加西班牙词库 2026-03-02 16:34:59 +08:00
d79a1d15bc 3 2026-03-02 16:19:26 +08:00
6e62394feb 1 2026-03-02 14:39:47 +08:00
781e557e80 1 2026-03-02 09:19:06 +08:00
da4649101e chore: remove accidental _spm submodule entry 2026-02-28 21:29:53 +08:00
47291934a2 应用的皮肤不能删除 2026-02-28 21:23:38 +08:00
e619f48f93 移除主App后台音频声明负责审查风险 2026-02-28 18:21:36 +08:00
f55a70681c 处理KBFunctionTagCell正在执行又可以点击别的 2026-02-28 16:03:05 +08:00
cb86f7c32c 处理header 2026-02-28 15:38:12 +08:00
40ef964b8c 添加注销账号 2026-02-28 14:50:27 +08:00
4269fde923 1 2026-02-27 16:28:15 +08:00
c3e037e070 添加隐私,注销功能 2026-02-27 14:49:46 +08:00
a711be4c4d 跨进程 键盘用ai 在主应用里也要显示 2026-02-26 21:47:22 +08:00
69bd2b2af9 1:修改ios26tabbar的问题
2:修改键盘AI点击必须要登录装填
2026-02-26 19:38:17 +08:00
82222afd76 修复 KBChatPanel 发送内容校验 2026-02-25 20:16:31 +08:00
92ca5c6180 Fix KBAICommentInputView弹出位置 2026-02-25 17:13:25 +08:00
851c0d9531 去除假的用户信息 2026-02-25 11:15:04 +08:00
1c9013bede 新增获取客服接口 2026-02-24 20:45:15 +08:00
0a16a4f240 修改KBKeyboardPanelModeFunction 必须要登录状态 2026-02-24 18:04:13 +08:00
27d4b2b817 添加hud容错处理 2026-02-24 16:23:57 +08:00
bc623676ca 修改在手机信息页面,复制短信后,键盘按钮不存在, 背景也不存在 2026-02-24 15:24:23 +08:00
5edf1751ff 修改sign。
键盘里ai回复的bug
2026-02-24 14:59:06 +08:00
0ac47925fd 先提交 2026-02-24 13:38:51 +08:00
635ad932c7 修改vip 2026-02-12 20:06:44 +08:00
cbe0a53cac 删除无用的国家化 2026-02-11 21:16:15 +08:00
5c273c3963 修改bug 2026-02-11 21:09:37 +08:00
c9743cb363 处理第一次不滑动界面不传递数据的问题 2026-02-11 20:52:56 +08:00
f0cb69948e bug修复 2026-02-11 19:40:41 +08:00
0144f9cc6d 1 2026-02-11 19:31:12 +08:00
ae4070ae88 1 2026-02-11 19:18:26 +08:00
a83fd918a8 删除无关代码 2026-02-11 18:58:30 +08:00
4168da618e 1 2026-02-11 18:29:00 +08:00
d2ffada83f 添加KeyboardViewControllerHelp文件夹 2026-02-10 19:41:32 +08:00
76d387e08b 修改UI遮挡后面爱心和评论的问题 2026-02-10 19:15:05 +08:00
ea0df4fb19 修改UI 2026-02-10 19:12:09 +08:00
02323fb5f1 处理键盘闪的问题 2026-02-10 18:53:31 +08:00
3c71797b7b 处理键盘崩溃 2026-02-10 13:22:19 +08:00
4c57f16058 1 2026-02-10 10:21:21 +08:00
cb2e8467a7 处理UI 2026-02-09 19:31:47 +08:00
4dfd6f5cbb 处理UI 2026-02-09 16:53:30 +08:00
e4223b3a4c 处理国际化 2026-02-09 16:30:00 +08:00
3d19403539 修改bug 2026-02-09 14:24:31 +08:00
343 changed files with 224399 additions and 17499 deletions

View File

@@ -0,0 +1,17 @@
{
"permissions": {
"allow": [
"WebSearch",
"Bash(git checkout:*)",
"Bash(xcodebuild:*)",
"Bash(plutil:*)",
"Bash(find:*)",
"Bash(ls:*)",
"Bash(wc:*)",
"Bash(chmod +x:*)",
"Bash(python3:*)",
"Bash(/usr/libexec/PlistBuddy:*)",
"Bash(iconv -f UTF-8 -t UTF-8 \"/Users/mac/Downloads/隐私协议_修改版.txt\" 2>/dev/null | sed -n '290,305p')"
]
}
}

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
# Xcode / build artifacts
_DerivedData/
DerivedData/
*.xcresult/
xcuserdata/
_tmp/
ws.xcworkspace
# Codex / sandbox home mirror
_home/
# SwiftPM artifacts
_spm/
_SourcePackages/
.swiftpm/

View File

@@ -7,7 +7,7 @@
<string>kbkeyboardAppExtension</string>
</array>
<key>NSMicrophoneUsageDescription</key>
<string>需要使用麦克风进行语音输入</string>
<string>Microphone access is required for voice input and speech transcription.</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,724 @@
//
// KeyboardViewController+Chat.m
// CustomKeyboard
//
// Created by Codex on 2026/02/22.
//
#import "KeyboardViewController+Private.h"
#import "KBChatLimitPopView.h"
#import "KBChatMessage.h"
#import "KBChatPanelView.h"
#import "KBFullAccessManager.h"
#import "../Utils/KBExtensionAppLauncher.h"
#import "KBInputBufferManager.h"
#import "KBNetworkManager.h"
#import "KBVM.h"
#import "Masonry.h"
#import <AVFoundation/AVFoundation.h>
static const NSUInteger kKBChatMessageLimit = 6;
@implementation KeyboardViewController (Chat)
#pragma mark - KBChatPanelViewDelegate
- (void)chatPanelView:(KBChatPanelView *)view didSendText:(NSString *)text {
NSString *trim =
[text stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (trim.length == 0) {
return;
}
[self kb_sendChatText:trim];
}
- (void)chatPanelView:(KBChatPanelView *)view
didTapMessage:(KBChatMessage *)message {
if (message.audioFilePath.length == 0) {
return;
}
[self kb_playChatAudioAtPath:message.audioFilePath];
}
- (void)chatPanelView:(KBChatPanelView *)view
didTapVoiceButtonForMessage:(KBChatMessage *)message {
if (!message)
return;
// audioData
if (message.audioData && message.audioData.length > 0) {
[self kb_playChatAudioData:message.audioData];
return;
}
// audioFilePath
if (message.audioFilePath.length > 0) {
[self kb_playChatAudioAtPath:message.audioFilePath];
return;
}
NSLog(@"[Keyboard] 没有音频数据可播放");
}
- (void)chatPanelViewDidTapClose:(KBChatPanelView *)view {
// chatPanelView
[view kb_reloadWithMessages:@[]];
if (self.chatAudioPlayer.isPlaying) {
[self.chatAudioPlayer stop];
}
self.chatAudioPlayer = nil;
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
}
#pragma mark - Chat Helpers
- (void)kb_handleChatSendAction {
if (!self.chatPanelVisible) {
return;
}
[[KBInputBufferManager shared]
refreshFromProxyIfPossible:self.textDocumentProxy];
NSString *fullText = [KBInputBufferManager shared].liveText ?: @"";
// 宿线
NSString *baseline = self.chatPanelBaselineText ?: @"";
NSString *rawText = fullText;
if (baseline.length > 0 && [fullText hasPrefix:baseline]) {
rawText = [fullText substringFromIndex:baseline.length];
}
NSString *trim =
[rawText stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *textToClear = rawText;
if (trim.length == 0) {
//
//
NSString *fullTrim =
[fullText stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (fullTrim.length > 0) {
trim = fullTrim;
textToClear = fullText;
}
}
if (trim.length == 0) {
[KBHUD showInfo:KBLocalized(@"Please enter content")];
return;
}
[self kb_sendChatText:trim];
//
[self kb_clearHostInputForText:textToClear];
}
- (void)kb_sendChatText:(NSString *)text {
if (text.length == 0) {
return;
}
#if DEBUG
NSLog(@"[KB] 发送消息 len=%lu", (unsigned long)text.length);
#endif
KBChatMessage *outgoing = [KBChatMessage userMessageWithText:text];
outgoing.avatarURL = [self kb_sharedUserAvatarURL];
[self.chatPanelView kb_addUserMessage:text];
[self kb_prefetchAvatarForMessage:outgoing];
if (![[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view]) {
[KBHUD showInfo:KBLocalized(@"Please enable Full Access to continue")];
return;
}
// loading
[self.chatPanelView kb_addLoadingAssistantMessage];
//
[self kb_requestChatMessageWithContent:text];
}
#pragma mark - Chat Limit Pop
- (void)kb_showChatLimitPopWithMessage:(NSString *)message {
[self kb_dismissChatLimitPop];
UIControl *mask = [[UIControl alloc] init];
mask.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
mask.alpha = 0.0;
[mask addTarget:self
action:@selector(kb_dismissChatLimitPop)
forControlEvents:UIControlEventTouchUpInside];
[self.contentView addSubview:mask];
[mask mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
CGFloat width = 252.0;
CGFloat height = 252.0 + 18.0 + 53.0 + 18.0 + 28.0;
KBChatLimitPopView *content =
[[KBChatLimitPopView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
content.message = message ?: @"";
content.delegate = self;
[mask addSubview:content];
[content mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(mask);
make.width.mas_equalTo(width);
make.height.mas_equalTo(height);
}];
self.chatLimitMaskView = mask;
[self.contentView bringSubviewToFront:mask];
[UIView animateWithDuration:0.18
animations:^{
mask.alpha = 1.0;
}];
}
- (void)kb_dismissChatLimitPop {
if (!self.chatLimitMaskView) {
return;
}
UIControl *mask = self.chatLimitMaskView;
self.chatLimitMaskView = nil;
[UIView animateWithDuration:0.15
animations:^{
mask.alpha = 0.0;
}
completion:^(__unused BOOL finished) {
[mask removeFromSuperview];
}];
}
- (void)kb_clearHostInputForText:(NSString *)text {
if (text.length == 0) {
return;
}
NSUInteger count = [self kb_composedCharacterCountForString:text];
for (NSUInteger i = 0; i < count; i++) {
[self.textDocumentProxy deleteBackward];
}
[[KBInputBufferManager shared] clearAllLiveText];
[self kb_clearCurrentWord];
}
- (NSUInteger)kb_composedCharacterCountForString:(NSString *)text {
if (text.length == 0) {
return 0;
}
__block NSUInteger count = 0;
[text enumerateSubstringsInRange:NSMakeRange(0, text.length)
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(__unused NSString *substring,
__unused NSRange substringRange,
__unused NSRange enclosingRange,
__unused BOOL *stop) {
count += 1;
}];
return count;
}
- (NSString *)kb_sharedUserAvatarURL {
NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
NSString *url = [ud stringForKey:AppGroup_UserAvatarURL];
return url ?: @"";
}
- (void)kb_prefetchAvatarForMessage:(KBChatMessage *)message {
if (!message || message.avatarImage) {
return;
}
NSString *urlString = message.avatarURL ?: @"";
if (urlString.length == 0) {
return;
}
if (![[KBFullAccessManager shared] hasFullAccess]) {
return;
}
__weak typeof(self) weakSelf = self;
[[KBVM shared] downloadAvatarFromURL:urlString
completion:^(UIImage *image, NSError *error) {
__strong typeof(weakSelf) self = weakSelf;
if (!self || !image)
return;
message.avatarImage = image;
[self kb_reloadChatRowForMessage:message];
}];
}
- (void)kb_reloadChatRowForMessage:(KBChatMessage *)message {
//
//
//
}
- (void)kb_requestChatAudioForText:(NSString *)text {
NSString *mockPath = [self kb_mockChatAudioPath];
if (mockPath.length > 0) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.35 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
NSString *displayText = KBLocalized(@"Voice reply");
KBChatMessage *incoming =
[KBChatMessage messageWithText:displayText
outgoing:NO
audioFilePath:mockPath];
incoming.displayName = KBLocalized(@"AI Assistant");
[self kb_appendChatMessage:incoming];
[self kb_playChatAudioAtPath:mockPath];
});
return;
}
NSDictionary *payload = @{@"message" : text ?: @""};
__weak typeof(self) weakSelf = self;
[[KBNetworkManager shared] POST:API_AI_TALK
jsonBody:payload
headers:nil
completion:^(NSDictionary *json, NSURLResponse *response,
NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) self = weakSelf;
if (!self) {
return;
}
if (error) {
NSString *tip = error.localizedDescription
?: KBLocalized(@"Request failed");
[KBHUD showInfo:tip];
return;
}
NSString *displayText =
[self kb_chatTextFromJSON:json];
NSString *audioURL =
[self kb_chatAudioURLFromJSON:json];
NSString *audioBase64 =
[self kb_chatAudioBase64FromJSON:json];
if (audioURL.length > 0) {
[self kb_downloadChatAudioFromURL:audioURL
displayText:displayText];
return;
}
if (audioBase64.length > 0) {
NSData *data = [[NSData alloc]
initWithBase64EncodedString:audioBase64
options:0];
if (data.length == 0) {
[KBHUD showInfo:KBLocalized(@"Failed to parse audio data")];
return;
}
[self kb_handleChatAudioData:data
fileExtension:@"m4a"
displayText:displayText];
return;
}
[KBHUD showInfo:KBLocalized(@"No audio file received")];
});
}];
}
#pragma mark - New Chat API (with typewriter effect and audio preload)
/// audioId
- (void)kb_requestChatMessageWithContent:(NSString *)content {
if (content.length == 0) {
[self.chatPanelView kb_removeLoadingAssistantMessage];
return;
}
NSInteger companionId = [[KBVM shared] selectedCompanionIdFromAppGroup];
NSLog(@"[KB] 请求聊天: companionId=%ld", (long)companionId);
__weak typeof(self) weakSelf = self;
[[KBVM shared] sendChatMessageWithContent:content
companionId:companionId
completion:^(KBChatResponse *response) {
__strong typeof(weakSelf) self = weakSelf;
if (!self)
return;
if (response.code != 0) {
if (response.code == 50030) {
NSLog(@"[KB] ⚠️ 次数用尽: %@",
response.message);
[self.chatPanelView
kb_removeLoadingAssistantMessage];
[self kb_showChatLimitPopWithMessage:
response.message];
return;
}
NSLog(@"[KB] ❌ 请求失败: %@",
response.message);
[self.chatPanelView
kb_removeLoadingAssistantMessage];
[KBHUD showInfo:response.message
?: KBLocalized(@"Request failed")];
return;
}
NSLog(@"[KB] ✅ 收到回复: %@",
response.data.aiResponse);
if (response.data.aiResponse.length == 0) {
[self.chatPanelView
kb_removeLoadingAssistantMessage];
[KBHUD showInfo:KBLocalized(@"No reply content received")];
return;
}
// AI
NSLog(@"[KB] 准备添加 AI 消息");
[self.chatPanelView
kb_addAssistantMessage:response.data.aiResponse
audioId:response.data.audioId];
NSLog(@"[KB] AI 消息添加完成");
// App persona
[self kb_notifyMainAppChatUpdatedWithCompanionId:companionId];
// audioId
if (response.data.audioId.length > 0) {
[self kb_preloadAudioWithAudioId:
response.data.audioId];
}
}];
}
/// AppGroup persona companionId
- (NSInteger)kb_selectedCompanionId {
return [[KBVM shared] selectedCompanionIdFromAppGroup];
}
#pragma mark - Audio Preload
/// audioURL
- (void)kb_preloadAudioWithAudioId:(NSString *)audioId {
if (audioId.length == 0)
return;
NSLog(@"[Keyboard] 开始预加载音频audioId: %@", audioId);
__weak typeof(self) weakSelf = self;
[[KBVM shared] pollAudioURLWithAudioId:audioId
maxRetries:10
interval:1.0
completion:^(KBAudioResponse *response) {
__strong typeof(weakSelf) self = weakSelf;
if (!self)
return;
if (!response.success ||
response.audioURL.length == 0) {
NSLog(@"[Keyboard] ❌ 预加载音频 URL 获取失败: %@",
response.errorMessage);
return;
}
NSLog(@"[Keyboard] ✅ 预加载音频 URL 获取成功");
//
[[KBVM shared]
downloadAudioFromURL:response.audioURL
completion:^(
KBAudioResponse *audioResponse) {
if (!audioResponse.success) {
NSLog(@"[Keyboard] ❌ 预加载音频下载失败: %@",
audioResponse.errorMessage);
return;
}
// AI
[self.chatPanelView
kb_updateLastAssistantMessageWithAudioData:
audioResponse.audioData
duration:
audioResponse.duration];
NSLog(@"[Keyboard] ✅ 预加载音频完成,音频时长: %.2f秒",
audioResponse.duration);
}];
}];
}
- (void)kb_downloadChatAudioFromURL:(NSString *)audioURL
displayText:(NSString *)displayText {
__weak typeof(self) weakSelf = self;
[[KBVM shared] downloadAudioFromURL:audioURL
completion:^(KBAudioResponse *response) {
__strong typeof(weakSelf) self = weakSelf;
if (!self)
return;
if (!response.success) {
[KBHUD showInfo:response.errorMessage
?: KBLocalized(@"Download failed")];
return;
}
if (!response.audioData ||
response.audioData.length == 0) {
[KBHUD showInfo:KBLocalized(@"No audio data received")];
return;
}
NSString *ext = @"m4a";
NSURL *url = [NSURL URLWithString:audioURL];
if (url.pathExtension.length > 0) {
ext = url.pathExtension;
}
[self kb_handleChatAudioData:response.audioData
fileExtension:ext
displayText:displayText];
}];
}
- (void)kb_handleChatAudioData:(NSData *)data
fileExtension:(NSString *)extension
displayText:(NSString *)displayText {
if (data.length == 0) {
[KBHUD showInfo:KBLocalized(@"Audio data is empty")];
return;
}
NSString *ext = extension.length > 0 ? extension : @"m4a";
NSString *fileName = [NSString
stringWithFormat:@"kb_chat_%@.%@",
@((long long)([NSDate date].timeIntervalSince1970 *
1000)),
ext];
NSString *filePath =
[NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
if (![data writeToFile:filePath atomically:YES]) {
[KBHUD showInfo:KBLocalized(@"Failed to save audio")];
return;
}
NSString *text =
displayText.length > 0 ? displayText : KBLocalized(@"Voice message");
KBChatMessage *incoming =
[KBChatMessage messageWithText:text outgoing:NO audioFilePath:filePath];
incoming.displayName = KBLocalized(@"AI Assistant");
[self kb_appendChatMessage:incoming];
}
- (void)kb_appendChatMessage:(KBChatMessage *)message {
if (!message) {
return;
}
[self.chatMessages addObject:message];
if (self.chatMessages.count > kKBChatMessageLimit) {
NSUInteger overflow = self.chatMessages.count - kKBChatMessageLimit;
NSArray<KBChatMessage *> *removed =
[self.chatMessages subarrayWithRange:NSMakeRange(0, overflow)];
[self.chatMessages removeObjectsInRange:NSMakeRange(0, overflow)];
for (KBChatMessage *msg in removed) {
if (msg.audioFilePath.length > 0) {
NSString *tmpRoot = NSTemporaryDirectory();
if (tmpRoot.length > 0 && [msg.audioFilePath hasPrefix:tmpRoot]) {
[[NSFileManager defaultManager] removeItemAtPath:msg.audioFilePath
error:nil];
}
}
}
}
[self.chatPanelView kb_reloadWithMessages:self.chatMessages];
}
- (NSString *)kb_mockChatAudioPath {
NSString *path = [[NSBundle mainBundle] pathForResource:@"ai_test"
ofType:@"m4a"];
return path ?: @"";
}
- (NSString *)kb_chatTextFromJSON:(NSDictionary *)json {
NSDictionary *data = [self kb_chatDataDictionaryFromJSON:json];
NSString *text =
[self kb_stringValueInDict:data keys:@[ @"text", @"message", @"content" ]];
if (text.length == 0) {
text = [self kb_stringValueInDict:json
keys:@[ @"text", @"message", @"content" ]];
}
return text ?: @"";
}
- (NSString *)kb_chatAudioURLFromJSON:(NSDictionary *)json {
NSDictionary *data = [self kb_chatDataDictionaryFromJSON:json];
NSArray<NSString *> *keys =
@[ @"audioUrl", @"audioURL", @"audio_url", @"url", @"fileUrl",
@"file_url", @"audioFileUrl", @"audio_file_url" ];
NSString *url = [self kb_stringValueInDict:data keys:keys];
if (url.length == 0) {
url = [self kb_stringValueInDict:json keys:keys];
}
return url ?: @"";
}
- (NSString *)kb_chatAudioBase64FromJSON:(NSDictionary *)json {
NSDictionary *data = [self kb_chatDataDictionaryFromJSON:json];
NSArray<NSString *> *keys =
@[ @"audioBase64", @"audio_base64", @"audioData", @"audio_data",
@"base64" ];
NSString *b64 = [self kb_stringValueInDict:data keys:keys];
if (b64.length == 0) {
b64 = [self kb_stringValueInDict:json keys:keys];
}
return b64 ?: @"";
}
- (NSDictionary *)kb_chatDataDictionaryFromJSON:(NSDictionary *)json {
if (![json isKindOfClass:[NSDictionary class]]) {
return @{};
}
id dataObj = json[@"data"] ?: json[@"result"] ?: json[@"response"];
if ([dataObj isKindOfClass:[NSDictionary class]]) {
return (NSDictionary *)dataObj;
}
return @{};
}
- (NSString *)kb_stringValueInDict:(NSDictionary *)dict
keys:(NSArray<NSString *> *)keys {
if (![dict isKindOfClass:[NSDictionary class]]) {
return @"";
}
for (NSString *key in keys) {
id value = dict[key];
if ([value isKindOfClass:[NSString class]] &&
((NSString *)value).length > 0) {
return (NSString *)value;
}
}
return @"";
}
- (void)kb_playChatAudioAtPath:(NSString *)path {
if (path.length == 0) {
return;
}
NSURL *url = [NSURL fileURLWithPath:path];
if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
[KBHUD showInfo:KBLocalized(@"Audio file does not exist")];
return;
}
if (self.chatAudioPlayer && self.chatAudioPlayer.isPlaying) {
NSURL *currentURL = self.chatAudioPlayer.url;
if ([currentURL isEqual:url]) {
[self.chatAudioPlayer stop];
self.chatAudioPlayer = nil;
return;
}
[self.chatAudioPlayer stop];
self.chatAudioPlayer = nil;
}
NSError *sessionError = nil;
AVAudioSession *session = [AVAudioSession sharedInstance];
if ([session respondsToSelector:@selector(setCategory:options:error:)]) {
[session setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionDuckOthers
error:&sessionError];
} else {
[session setCategory:AVAudioSessionCategoryPlayback error:&sessionError];
}
[session setActive:YES error:nil];
NSError *playerError = nil;
AVAudioPlayer *player =
[[AVAudioPlayer alloc] initWithContentsOfURL:url error:&playerError];
if (playerError || !player) {
[KBHUD showInfo:KBLocalized(@"Audio playback failed")];
return;
}
self.chatAudioPlayer = player;
[player prepareToPlay];
[player play];
}
///
- (void)kb_playChatAudioData:(NSData *)audioData {
if (!audioData || audioData.length == 0) {
NSLog(@"[Keyboard] 音频数据为空");
return;
}
//
if (self.chatAudioPlayer && self.chatAudioPlayer.isPlaying) {
[self.chatAudioPlayer stop];
self.chatAudioPlayer = nil;
}
//
NSError *sessionError = nil;
AVAudioSession *session = [AVAudioSession sharedInstance];
if ([session respondsToSelector:@selector(setCategory:options:error:)]) {
[session setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionDuckOthers
error:&sessionError];
} else {
[session setCategory:AVAudioSessionCategoryPlayback error:&sessionError];
}
[session setActive:YES error:nil];
//
NSError *playerError = nil;
AVAudioPlayer *player =
[[AVAudioPlayer alloc] initWithData:audioData error:&playerError];
if (playerError || !player) {
NSLog(@"[Keyboard] 音频播放器初始化失败: %@",
playerError.localizedDescription);
[KBHUD showInfo:KBLocalized(@"Audio playback failed")];
return;
}
self.chatAudioPlayer = player;
player.volume = 1.0;
[player prepareToPlay];
[player play];
NSLog(@"[Keyboard] 开始播放音频,时长: %.2f秒", player.duration);
}
#pragma mark - Notify Main App
/// App persona
- (void)kb_notifyMainAppChatUpdatedWithCompanionId:(NSInteger)companionId {
NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
[ud setInteger:companionId forKey:AppGroup_ChatUpdatedCompanionId];
[ud synchronize];
CFNotificationCenterPostNotification(
CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge CFStringRef)kKBDarwinChatUpdated,
NULL, NULL, true);
NSLog(@"[KB] 已通知主 App 刷新 companionId=%ld 的聊天记录", (long)companionId);
}
#pragma mark - KBChatLimitPopViewDelegate
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view {
[self kb_dismissChatLimitPop];
}
- (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view {
[self kb_dismissChatLimitPop];
NSString *urlString =
[NSString stringWithFormat:@"%@://recharge?src=keyboard&vipType=svip",
KB_APP_SCHEME];
NSURL *scheme = [NSURL URLWithString:urlString];
NSString *ulString = [NSString stringWithFormat:@"%@?src=keyboard&vipType=svip", KB_UL_RECHARGE];
NSURL *ul = [NSURL URLWithString:ulString];
__weak typeof(self) weakSelf = self;
[KBExtensionAppLauncher openPrimaryURL:ul
fallbackURL:scheme
usingInputController:self
source:(self.view ?: (UIResponder *)weakSelf)
completion:^(BOOL success) {
if (success) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD showInfo:KBLocalized(@"Please open the App to finish purchase")];
});
}];
}
@end

View File

@@ -0,0 +1,96 @@
//
// KeyboardViewController+Layout.m
// CustomKeyboard
//
// Created by Codex on 2026/02/22.
//
#import "KeyboardViewController+Private.h"
// 375 稿
static const CGFloat kKBKeyboardBaseHeight = 250.0f;
static const CGFloat kKBChatPanelHeight = 180;
@implementation KeyboardViewController (Layout)
- (CGFloat)kb_portraitWidth {
CGSize s = [UIScreen mainScreen].bounds.size;
return MIN(s.width, s.height);
}
- (CGFloat)kb_keyboardHeightForWidth:(CGFloat)width {
if (width <= 0) {
width = KB_DESIGN_WIDTH;
}
CGFloat scale = width / KB_DESIGN_WIDTH;
CGFloat baseHeight = kKBKeyboardBaseHeight * scale;
CGFloat chatHeight = kKBChatPanelHeight * scale;
if (self.chatPanelVisible) {
return baseHeight + chatHeight;
}
return baseHeight;
}
- (CGFloat)kb_keyboardBaseHeightForWidth:(CGFloat)width {
if (width <= 0) {
width = KB_DESIGN_WIDTH;
}
CGFloat scale = width / KB_DESIGN_WIDTH;
return kKBKeyboardBaseHeight * scale;
}
- (CGFloat)kb_chatPanelHeightForWidth:(CGFloat)width {
if (width <= 0) {
width = KB_DESIGN_WIDTH;
}
CGFloat scale = width / KB_DESIGN_WIDTH;
return kKBChatPanelHeight * scale;
}
- (void)kb_updateKeyboardLayoutIfNeeded {
CGFloat portraitWidth = [self kb_portraitWidth];
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
CGFloat keyboardBaseHeight =
[self kb_keyboardBaseHeightForWidth:portraitWidth];
CGFloat chatPanelHeight = [self kb_chatPanelHeightForWidth:portraitWidth];
CGFloat containerWidth = CGRectGetWidth(self.view.superview.bounds);
if (containerWidth <= 0) {
containerWidth = CGRectGetWidth(self.view.window.bounds);
}
if (containerWidth <= 0) {
containerWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
}
BOOL widthChanged = (fabs(self.kb_lastPortraitWidth - portraitWidth) >= 0.5);
BOOL heightChanged =
(fabs(self.kb_lastKeyboardHeight - keyboardHeight) >= 0.5);
if (!widthChanged && !heightChanged && containerWidth > 0 &&
self.kb_widthConstraint.constant == containerWidth) {
return;
}
self.kb_lastPortraitWidth = portraitWidth;
self.kb_lastKeyboardHeight = keyboardHeight;
if (self.kb_heightConstraint) {
self.kb_heightConstraint.constant = keyboardHeight;
}
if (containerWidth > 0 && self.kb_widthConstraint) {
self.kb_widthConstraint.constant = containerWidth;
}
if (self.contentWidthConstraint) {
[self.contentWidthConstraint setOffset:portraitWidth];
}
if (self.contentHeightConstraint) {
[self.contentHeightConstraint setOffset:keyboardHeight];
}
if (self.keyBoardMainHeightConstraint) {
[self.keyBoardMainHeightConstraint setOffset:keyboardBaseHeight];
}
if (self.chatPanelHeightConstraint) {
[self.chatPanelHeightConstraint setOffset:chatPanelHeight];
}
[self.view layoutIfNeeded];
}
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,545 @@
//
// KeyboardViewController+Panels.m
// CustomKeyboard
//
// Created by Codex on 2026/02/22.
//
#import "KeyboardViewController+Private.h"
#import "KBAuthManager.h"
#import "KBBackspaceUndoManager.h"
#import "KBChatMessage.h"
#import "KBChatPanelView.h"
#import "KBFunctionView.h"
#import "KBFullAccessManager.h"
#import "../Utils/KBExtensionAppLauncher.h"
#import "KBInputBufferManager.h"
#import "KBKey.h"
#import "KBKeyBoardMainView.h"
#import "KBKeyboardSubscriptionView.h"
#import "Masonry.h"
#import <SDWebImage/SDWebImage.h>
#import <AVFoundation/AVAudioPlayer.h>
@implementation KeyboardViewController (Panels)
#pragma mark - Panel Mode
- (void)kb_setPanelMode:(KBKeyboardPanelMode)mode animated:(BOOL)animated {
if (mode == self.kb_panelMode) {
return;
}
KBKeyboardPanelMode fromMode = self.kb_panelMode;
// AI 访
if (mode == KBKeyboardPanelModeFunction &&
![[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view]) {
return;
}
// mode Function
BOOL islogin = YES;
if (mode == KBKeyboardPanelModeFunction) {
[[KBAuthManager shared] reloadFromKeychain];
islogin = KBAuthManager.shared.isLoggedIn;
}
#if DEBUG
if (mode == KBKeyboardPanelModeFunction) {
NSString *token = [KBAuthManager shared].current.accessToken ?: @"";
NSLog(@"[AuthTrace][Ext] tapAI mode=%ld isLoggedIn=%d tokenLen=%lu",
(long)mode, islogin, (unsigned long)token.length);
}
#endif
if (mode == KBKeyboardPanelModeFunction && !islogin) {
[KBHUD showInfo:KBLocalized(@"Please sign in before using AI features")];
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard", KB_UL_LOGIN]];
NSURL *scheme =
[NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME]];
__weak typeof(self) weakSelf = self;
[KBExtensionAppLauncher openPrimaryURL:ul
fallbackURL:scheme
usingInputController:self
source:(self.view ?: (UIResponder *)weakSelf)
completion:^(BOOL success) {
if (success) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD showInfo:KBLocalized(@"Please return to the Home screen and open the app to sign in")];
});
}];
return;
}
self.kb_panelMode = mode;
//
[self kb_ensureKeyBoardMainViewIfNeeded];
// 1) /
[self kb_setSubscriptionPanelVisible:NO animated:animated];
[self kb_setChatPanelVisible:NO animated:animated];
[self kb_setFunctionPanelVisible:NO];
// 2)
switch (mode) {
case KBKeyboardPanelModeFunction:
[self kb_setFunctionPanelVisible:YES];
break;
case KBKeyboardPanelModeChat:
[self kb_setChatPanelVisible:YES animated:animated];
break;
case KBKeyboardPanelModeSubscription:
[self kb_setSubscriptionPanelVisible:YES animated:animated];
break;
case KBKeyboardPanelModeMain:
default:
break;
}
// 3) /
if (mode == KBKeyboardPanelModeFunction) {
[[KBMaiPointReporter sharedReporter]
reportPageExposureWithEventName:@"enter_keyboard_function_panel"
pageId:@"keyboard_function_panel"
extra:nil
completion:nil];
} else if (mode == KBKeyboardPanelModeMain &&
fromMode == KBKeyboardPanelModeFunction) {
[[KBMaiPointReporter sharedReporter]
reportPageExposureWithEventName:@"enter_keyboard_main_panel"
pageId:@"keyboard_main_panel"
extra:nil
completion:nil];
} else if (mode == KBKeyboardPanelModeSubscription) {
[[KBMaiPointReporter sharedReporter]
reportPageExposureWithEventName:@"enter_keyboard_subscription_panel"
pageId:@"keyboard_subscription_panel"
extra:nil
completion:nil];
}
// 4)
if (mode == KBKeyboardPanelModeSubscription) {
[self.contentView bringSubviewToFront:self.subscriptionView];
} else if (mode == KBKeyboardPanelModeChat) {
[self.contentView bringSubviewToFront:self.chatPanelView];
} else if (mode == KBKeyboardPanelModeFunction) {
[self.contentView bringSubviewToFront:self.functionView];
} else {
[self.contentView bringSubviewToFront:self.keyBoardMainView];
}
}
/// /
- (void)showFunctionPanel:(BOOL)show {
if (show) {
[self kb_setPanelMode:KBKeyboardPanelModeFunction animated:NO];
return;
}
if (self.kb_panelMode == KBKeyboardPanelModeFunction) {
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:NO];
}
}
/// /
- (void)showChatPanel:(BOOL)show {
if (show) {
[self kb_setPanelMode:KBKeyboardPanelModeChat animated:YES];
return;
}
if (self.kb_panelMode == KBKeyboardPanelModeChat) {
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
}
}
- (void)kb_setFunctionPanelVisible:(BOOL)visible {
if (visible) {
[self kb_ensureFunctionViewIfNeeded];
}
if (_functionView) {
_functionView.hidden = !visible;
} else if (visible) {
// ensure
self.functionView.hidden = NO;
}
self.keyBoardMainView.hidden = visible;
}
- (void)kb_setChatPanelVisible:(BOOL)visible animated:(BOOL)animated {
if (visible == self.chatPanelVisible) {
return;
}
self.chatPanelVisible = visible;
if (visible) {
// 宿
[[KBInputBufferManager shared]
refreshFromProxyIfPossible:self.textDocumentProxy];
self.chatPanelBaselineText = [KBInputBufferManager shared].liveText ?: @"";
[self kb_ensureChatPanelViewIfNeeded];
self.chatPanelView.hidden = NO;
self.chatPanelView.alpha = 0.0;
if (animated) {
[UIView animateWithDuration:0.2
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
self.chatPanelView.alpha = 1.0;
}
completion:nil];
} else {
self.chatPanelView.alpha = 1.0;
}
} else {
// show/hide
if (!_chatPanelView) {
[self kb_updateKeyboardLayoutIfNeeded];
return;
}
if (animated) {
[UIView animateWithDuration:0.18
delay:0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
self.chatPanelView.alpha = 0.0;
}
completion:^(BOOL finished) {
self.chatPanelView.hidden = YES;
}];
} else {
self.chatPanelView.alpha = 0.0;
self.chatPanelView.hidden = YES;
}
}
[self kb_updateKeyboardLayoutIfNeeded];
}
- (void)kb_setSubscriptionPanelVisible:(BOOL)visible animated:(BOOL)animated {
if (visible) {
KBKeyboardSubscriptionView *panel = self.subscriptionView;
if (!panel.superview) {
panel.hidden = YES;
[self.contentView addSubview:panel];
[panel mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
}
[self.contentView bringSubviewToFront:panel];
panel.hidden = NO;
panel.alpha = 0.0;
CGFloat height = CGRectGetHeight(self.contentView.bounds);
if (height <= 0) {
height = 260;
}
panel.transform = CGAffineTransformMakeTranslation(0, height);
[panel refreshProductsIfNeeded];
if (animated) {
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
panel.alpha = 1.0;
panel.transform = CGAffineTransformIdentity;
}
completion:nil];
} else {
panel.alpha = 1.0;
panel.transform = CGAffineTransformIdentity;
}
return;
}
KBKeyboardSubscriptionView *panel = _subscriptionView;
if (!panel) {
return;
}
if (!panel.superview || panel.hidden) {
return;
}
CGFloat height = CGRectGetHeight(panel.bounds);
if (height <= 0) {
height = CGRectGetHeight(self.contentView.bounds);
}
if (animated) {
[UIView animateWithDuration:0.22
delay:0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
panel.alpha = 0.0;
panel.transform = CGAffineTransformMakeTranslation(0, height);
}
completion:^(BOOL finished) {
panel.hidden = YES;
panel.alpha = 1.0;
panel.transform = CGAffineTransformIdentity;
}];
} else {
panel.hidden = YES;
panel.alpha = 1.0;
panel.transform = CGAffineTransformIdentity;
}
}
// /
- (void)kb_ensureFunctionViewIfNeeded {
if (_functionView && _functionView.superview) {
return;
}
KBFunctionView *v = self.functionView;
if (!v.superview) {
v.hidden = YES;
[self.contentView addSubview:v];
[v mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
}
}
// /
- (void)kb_ensureChatPanelViewIfNeeded {
if (_chatPanelView && _chatPanelView.superview) {
return;
}
CGFloat portraitWidth = [self kb_portraitWidth];
CGFloat chatPanelHeight = [self kb_chatPanelHeightForWidth:portraitWidth];
KBChatPanelView *v = self.chatPanelView;
if (!v.superview) {
[self.contentView addSubview:v];
[v mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.contentView);
make.bottom.equalTo(self.keyBoardMainView.mas_top);
self.chatPanelHeightConstraint =
make.height.mas_equalTo(chatPanelHeight);
}];
v.hidden = YES;
}
}
//
- (void)kb_ensureKeyBoardMainViewIfNeeded {
if (_keyBoardMainView && _keyBoardMainView.superview) {
return;
}
CGFloat portraitWidth = [self kb_portraitWidth];
CGFloat keyboardBaseHeight =
[self kb_keyboardBaseHeightForWidth:portraitWidth];
KBKeyBoardMainView *v = self.keyBoardMainView;
if (!v.superview) {
[self.contentView addSubview:v];
[v mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.contentView);
make.bottom.equalTo(self.contentView);
self.keyBoardMainHeightConstraint =
make.height.mas_equalTo(keyboardBaseHeight);
}];
}
[self.contentView bringSubviewToFront:v];
}
// //
- (void)kb_releaseMemoryWhenKeyboardHidden {
[KBHUD setContainerView:nil];
self.bgImageView.image = nil;
self.kb_cachedGradientImage = nil;
[self.kb_defaultGradientLayer removeFromSuperlayer];
self.kb_defaultGradientLayer = nil;
[[SDImageCache sharedImageCache] clearMemory];
// /
if (self.chatAudioPlayer) {
[self.chatAudioPlayer stop];
self.chatAudioPlayer = nil;
}
if (_chatMessages.count > 0) {
NSString *tmpRoot = NSTemporaryDirectory();
for (KBChatMessage *msg in _chatMessages.copy) {
if (tmpRoot.length > 0 && msg.audioFilePath.length > 0 &&
[msg.audioFilePath hasPrefix:tmpRoot]) {
[[NSFileManager defaultManager] removeItemAtPath:msg.audioFilePath
error:nil];
}
}
[_chatMessages removeAllObjects];
}
if (_keyBoardMainView) {
[_keyBoardMainView removeFromSuperview];
_keyBoardMainView = nil;
}
self.keyBoardMainHeightConstraint = nil;
if (_functionView) {
[_functionView removeFromSuperview];
_functionView = nil;
}
if (_chatPanelView) {
[_chatPanelView removeFromSuperview];
_chatPanelView = nil;
}
self.chatPanelVisible = NO;
self.kb_panelMode = KBKeyboardPanelModeMain;
if (_subscriptionView) {
[_subscriptionView removeFromSuperview];
_subscriptionView = nil;
}
}
// MARK: - KBKeyBoardMainViewDelegate
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView
didTapKey:(KBKey *)key {
switch (key.type) {
case KBKeyTypeCharacter: {
[[KBBackspaceUndoManager shared] registerNonClearAction];
NSString *text = key.output ?: key.title ?: @"";
[self.textDocumentProxy insertText:text];
[self kb_updateCurrentWordWithInsertedText:text];
[[KBInputBufferManager shared] appendText:text];
} break;
case KBKeyTypeBackspace:
[[KBInputBufferManager shared]
refreshFromProxyIfPossible:self.textDocumentProxy];
[[KBInputBufferManager shared]
prepareSnapshotForDeleteWithContextBefore:
self.textDocumentProxy.documentContextBeforeInput
after:
self.textDocumentProxy
.documentContextAfterInput];
[[KBBackspaceUndoManager shared]
captureAndDeleteBackwardFromProxy:self.textDocumentProxy
count:1];
[self kb_scheduleContextRefreshResetSuppression:NO];
[[KBInputBufferManager shared] applyHoldDeleteCount:1];
break;
case KBKeyTypeSpace:
[[KBBackspaceUndoManager shared] registerNonClearAction];
[self.textDocumentProxy insertText:@" "];
[self kb_clearCurrentWord];
[[KBInputBufferManager shared] appendText:@" "];
break;
case KBKeyTypeReturn:
if (self.chatPanelVisible) {
[self kb_handleChatSendAction];
break;
}
[[KBBackspaceUndoManager shared] registerNonClearAction];
[self.textDocumentProxy insertText:@"\n"];
[self kb_clearCurrentWord];
[[KBInputBufferManager shared] appendText:@"\n"];
break;
case KBKeyTypeGlobe:
[self advanceToNextInputMode];
break;
case KBKeyTypeCustom:
[[KBBackspaceUndoManager shared] registerNonClearAction];
//
[self kb_setPanelMode:KBKeyboardPanelModeFunction animated:NO];
[self kb_clearCurrentWord];
break;
case KBKeyTypeModeChange:
case KBKeyTypeShift:
// KBKeyBoardMainView/KBKeyboardView
break;
}
}
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView
didTapToolActionAtIndex:(NSInteger)index {
NSDictionary *extra = @{@"index" : @(index)};
[[KBMaiPointReporter sharedReporter]
reportClickWithEventName:@"click_keyboard_toolbar_action"
pageId:@"keyboard_main_panel"
elementId:@"toolbar_action"
extra:extra
completion:nil];
if (index == 0) {
[self kb_setPanelMode:KBKeyboardPanelModeFunction animated:YES];
[self kb_clearCurrentWord];
return;
}
if (index == 1) {
[self kb_setPanelMode:KBKeyboardPanelModeChat animated:YES];
return;
}
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
}
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView
didSelectEmoji:(NSString *)emoji {
if (emoji.length == 0) {
return;
}
[[KBBackspaceUndoManager shared] registerNonClearAction];
[self.textDocumentProxy insertText:emoji];
[self kb_clearCurrentWord];
[[KBInputBufferManager shared] appendText:emoji];
}
- (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView {
[[KBMaiPointReporter sharedReporter]
reportClickWithEventName:@"click_keyboard_undo_btn"
pageId:@"keyboard_main_panel"
elementId:@"undo_btn"
extra:nil
completion:nil];
[[KBBackspaceUndoManager shared] performUndoFromResponder:self.view];
[self kb_scheduleContextRefreshResetSuppression:YES];
}
// MARK: - KBFunctionViewDelegate
- (void)functionView:(KBFunctionView *)functionView
didTapToolActionAtIndex:(NSInteger)index {
// index == 0
if (index == 0) {
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:NO];
}
}
- (void)functionView:(KBFunctionView *_Nullable)functionView
didRightTapToolActionAtIndex:(NSInteger)index {
[[KBMaiPointReporter sharedReporter]
reportClickWithEventName:@"click_keyboard_function_right_action"
pageId:@"keyboard_function_panel"
elementId:@"right_action"
extra:@{@"action" : @"login_or_recharge"}
completion:nil];
if (!KBAuthManager.shared.isLoggedIn) {
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard", KB_UL_LOGIN]];
NSURL *scheme =
[NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME]];
__weak typeof(self) weakSelf = self;
[KBExtensionAppLauncher openPrimaryURL:ul
fallbackURL:scheme
usingInputController:self
source:(self.view ?: (UIResponder *)weakSelf)
completion:nil];
return;
}
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard", KB_UL_RECHARGE]];
NSURL *scheme =
[NSURL URLWithString:[NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME]];
__weak typeof(self) weakSelf = self;
[KBExtensionAppLauncher openPrimaryURL:ul
fallbackURL:scheme
usingInputController:self
source:(self.view ?: (UIResponder *)weakSelf)
completion:^(BOOL success) {
if (success) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD showInfo:KBLocalized(@"This app does not allow the keyboard to open the main app directly. Please return to the Home screen and open the app manually to recharge")];
});
}];
}
- (void)functionViewDidRequestSubscription:(KBFunctionView *)functionView {
[self showSubscriptionPanel];
}
@end

View File

@@ -0,0 +1,152 @@
//
// KeyboardViewController+Private.h
// CustomKeyboard
//
// Created by Codex on 2026/02/22.
//
#import "KeyboardViewController.h"
#import "Masonry.h"
@class AVAudioPlayer;
@class CAGradientLayer;
@class KBChatMessage;
@class KBChatPanelView;
@class KBFunctionView;
@class KBKeyBoardMainView;
@class KBKeyboardSubscriptionView;
@class KBSuggestionEngine;
@protocol KBChatLimitPopViewDelegate;
@protocol KBChatPanelViewDelegate;
@protocol KBFunctionViewDelegate;
@protocol KBKeyBoardMainViewDelegate;
@protocol KBKeyboardSubscriptionViewDelegate;
typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
KBKeyboardPanelModeMain = 0,
KBKeyboardPanelModeFunction,
KBKeyboardPanelModeChat,
KBKeyboardPanelModeSubscription,
};
@interface KeyboardViewController () <KBKeyBoardMainViewDelegate,
KBFunctionViewDelegate,
KBKeyboardSubscriptionViewDelegate,
KBChatPanelViewDelegate,
KBChatLimitPopViewDelegate>
{
UIButton *_nextKeyboardButton;
UIView *_contentView;
KBKeyBoardMainView *_keyBoardMainView;
KBFunctionView *_functionView;
UIImageView *_bgImageView;
KBChatPanelView *_chatPanelView;
KBKeyboardSubscriptionView *_subscriptionView;
KBSuggestionEngine *_suggestionEngine;
NSString *_currentWord;
UIControl *_chatLimitMaskView;
MASConstraint *_contentWidthConstraint;
MASConstraint *_contentHeightConstraint;
MASConstraint *_keyBoardMainHeightConstraint;
MASConstraint *_chatPanelHeightConstraint;
NSLayoutConstraint *_kb_heightConstraint;
NSLayoutConstraint *_kb_widthConstraint;
CGFloat _kb_lastPortraitWidth;
CGFloat _kb_lastKeyboardHeight;
UIImage *_kb_cachedGradientImage;
CGSize _kb_cachedGradientSize;
CAGradientLayer *_kb_defaultGradientLayer;
NSString *_kb_lastAppliedThemeKey;
NSMutableArray<KBChatMessage *> *_chatMessages;
AVAudioPlayer *_chatAudioPlayer;
BOOL _suppressSuggestions;
BOOL _chatPanelVisible;
NSString *_chatPanelBaselineText;
id _kb_fullAccessObserverToken;
id _kb_skinObserverToken;
id _kb_localizationObserverToken;
KBKeyboardPanelMode _kb_panelMode;
}
@property(nonatomic, strong)
UIButton *nextKeyboardButton; // 系统“下一个键盘”按钮(可选)
@property(nonatomic, strong) UIView *contentView;
@property(nonatomic, strong) KBKeyBoardMainView
*keyBoardMainView; // 功能面板视图点击工具栏第0个时显示
@property(nonatomic, strong)
KBFunctionView *functionView; // 功能面板视图点击工具栏第0个时显示
@property(nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
@property(nonatomic, strong) KBChatPanelView *chatPanelView;
@property(nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
@property(nonatomic, strong) KBSuggestionEngine *suggestionEngine;
@property(nonatomic, copy) NSString *currentWord;
@property(nonatomic, assign) BOOL suppressSuggestions;
@property(nonatomic, strong) UIControl *chatLimitMaskView;
@property(nonatomic, strong) MASConstraint *contentWidthConstraint;
@property(nonatomic, strong) MASConstraint *contentHeightConstraint;
@property(nonatomic, strong) MASConstraint *keyBoardMainHeightConstraint;
@property(nonatomic, strong) MASConstraint *chatPanelHeightConstraint;
@property(nonatomic, strong) NSLayoutConstraint *kb_heightConstraint;
@property(nonatomic, strong) NSLayoutConstraint *kb_widthConstraint;
@property(nonatomic, assign) CGFloat kb_lastPortraitWidth;
@property(nonatomic, assign) CGFloat kb_lastKeyboardHeight;
@property(nonatomic, strong) UIImage *kb_cachedGradientImage;
@property(nonatomic, assign) CGSize kb_cachedGradientSize;
@property(nonatomic, strong, nullable) CAGradientLayer *kb_defaultGradientLayer;
@property(nonatomic, copy, nullable) NSString *kb_lastAppliedThemeKey;
@property(nonatomic, strong) NSMutableArray<KBChatMessage *> *chatMessages;
@property(nonatomic, strong) AVAudioPlayer *chatAudioPlayer;
@property(nonatomic, assign) BOOL chatPanelVisible;
@property(nonatomic, copy) NSString *chatPanelBaselineText; // 打开聊天面板时宿主输入框已有的文本
@property(nonatomic, strong, nullable) id kb_fullAccessObserverToken;
@property(nonatomic, strong, nullable) id kb_skinObserverToken;
@property(nonatomic, strong, nullable) id kb_localizationObserverToken;
@property(nonatomic, assign) KBKeyboardPanelMode kb_panelMode;
@property(nonatomic, strong, nullable) id kb_appGroupObserverToken;
@end
@interface KeyboardViewController (KBPrivate)
// UI
- (void)setupUI;
- (nullable KBFunctionView *)kb_functionViewIfCreated;
// Panels
- (void)showFunctionPanel:(BOOL)show;
- (void)showChatPanel:(BOOL)show;
- (void)showSubscriptionPanel;
- (void)hideSubscriptionPanel;
- (void)kb_setPanelMode:(KBKeyboardPanelMode)mode animated:(BOOL)animated;
- (void)kb_ensureFunctionViewIfNeeded;
- (void)kb_ensureChatPanelViewIfNeeded;
- (void)kb_ensureKeyBoardMainViewIfNeeded;
- (void)kb_releaseMemoryWhenKeyboardHidden;
// Suggestions
- (void)kb_updateCurrentWordWithInsertedText:(NSString *)text;
- (void)kb_clearCurrentWord;
- (void)kb_scheduleContextRefreshResetSuppression:(BOOL)resetSuppression;
- (void)kb_refreshCurrentWordFromDocumentContextResetSuppression:
(BOOL)resetSuppression;
- (void)kb_updateSuggestionsForCurrentWord;
// Chat
- (void)kb_handleChatSendAction;
// Theme
- (void)kb_applyTheme;
- (void)kb_applyDefaultSkinIfNeeded;
- (void)kb_consumePendingShopSkin;
- (void)kb_registerDarwinSkinInstallObserver;
- (void)kb_unregisterDarwinSkinInstallObserver;
// Layout
- (CGFloat)kb_portraitWidth;
- (CGFloat)kb_keyboardHeightForWidth:(CGFloat)width;
- (CGFloat)kb_keyboardBaseHeightForWidth:(CGFloat)width;
- (CGFloat)kb_chatPanelHeightForWidth:(CGFloat)width;
- (void)kb_updateKeyboardLayoutIfNeeded;
@end

View File

@@ -0,0 +1,157 @@
//
// KeyboardViewController+Subscription.m
// CustomKeyboard
//
// Created by Codex on 2026/02/22.
//
#import "KeyboardViewController+Private.h"
#import "KBAuthManager.h"
#import "KBFullAccessManager.h"
#import "../Utils/KBExtensionAppLauncher.h"
#import "KBKeyboardSubscriptionProduct.h"
#import "KBKeyboardSubscriptionView.h"
@implementation KeyboardViewController (Subscription)
- (void)showSubscriptionPanel {
// 1) 访
if (![[KBFullAccessManager shared] hasFullAccess]) {
// 访
// [KBHUD showInfo:KBLocalized(@"Processing...")];
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view];
return;
}
//
// 2) -> App App
if (!KBAuthManager.shared.isLoggedIn) {
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard", KB_UL_LOGIN]];
NSURL *scheme =
[NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME]];
__weak typeof(self) weakSelf = self;
[KBExtensionAppLauncher openPrimaryURL:ul
fallbackURL:scheme
usingInputController:self
source:(self.view ?: (UIResponder *)weakSelf)
completion:nil];
return;
}
[self kb_setPanelMode:KBKeyboardPanelModeSubscription animated:YES];
}
- (void)hideSubscriptionPanel {
if (self.kb_panelMode != KBKeyboardPanelModeSubscription) {
return;
}
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
}
#pragma mark - KBKeyboardSubscriptionViewDelegate
- (void)subscriptionViewDidTapClose:(KBKeyboardSubscriptionView *)view {
[[KBMaiPointReporter sharedReporter]
reportClickWithEventName:@"click_keyboard_subscription_close_btn"
pageId:@"keyboard_subscription_panel"
elementId:@"close_btn"
extra:nil
completion:nil];
[self hideSubscriptionPanel];
}
- (void)subscriptionView:(KBKeyboardSubscriptionView *)view
didTapPurchaseForProduct:(KBKeyboardSubscriptionProduct *)product {
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
if ([product.productId isKindOfClass:NSString.class] &&
product.productId.length > 0) {
extra[@"product_id"] = product.productId;
}
[[KBMaiPointReporter sharedReporter]
reportClickWithEventName:@"click_keyboard_subscription_product_btn"
pageId:@"keyboard_subscription_panel"
elementId:@"product_btn"
extra:extra.copy
completion:nil];
[self hideSubscriptionPanel];
[self kb_openRechargeForProduct:product];
}
- (void)subscriptionViewDidTapAgreement:(KBKeyboardSubscriptionView *)view {
(void)view;
[self hideSubscriptionPanel];
NSString *query = [NSString stringWithFormat:@"type=%@&src=keyboard",
@"membership"];
NSString *ulString = [NSString stringWithFormat:@"%@?%@", KB_UL_LEGAL, query];
NSString *schemeString =
[NSString stringWithFormat:@"%@://legal?%@", KB_APP_SCHEME, query];
NSURL *ul = [NSURL URLWithString:ulString];
NSURL *scheme = [NSURL URLWithString:schemeString];
__weak typeof(self) weakSelf = self;
[KBExtensionAppLauncher openPrimaryURL:ul
fallbackURL:scheme
usingInputController:self
source:(self.view ?: (UIResponder *)weakSelf)
completion:^(BOOL success) {
if (success) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD showInfo:KBLocalized(@"Please open the App to view the agreement")];
});
}];
}
#pragma mark - Actions
- (void)kb_openRechargeForProduct:(KBKeyboardSubscriptionProduct *)product {
if (![product isKindOfClass:KBKeyboardSubscriptionProduct.class] ||
product.productId.length == 0) {
[KBHUD showInfo:KBLocalized(@"Product unavailable")];
return;
}
NSString *encodedId = [self.class kb_urlEncodedString:product.productId];
NSString *title = [product displayTitle];
NSString *encodedTitle = [self.class kb_urlEncodedString:title];
NSMutableArray<NSString *> *params =
[NSMutableArray arrayWithObjects:@"autoPay=1", @"prefill=1", nil];
if (encodedId.length) {
[params addObject:[NSString stringWithFormat:@"productId=%@", encodedId]];
}
if (encodedTitle.length) {
[params
addObject:[NSString stringWithFormat:@"productTitle=%@", encodedTitle]];
}
NSString *query = [params componentsJoinedByString:@"&"];
NSString *urlString = [NSString
stringWithFormat:@"%@://recharge?src=keyboard&%@", KB_APP_SCHEME, query];
NSURL *scheme = [NSURL URLWithString:urlString];
NSString *ulString = [NSString stringWithFormat:@"%@?src=keyboard&%@", KB_UL_RECHARGE, query];
NSURL *ul = [NSURL URLWithString:ulString];
__weak typeof(self) weakSelf = self;
[KBExtensionAppLauncher openPrimaryURL:ul
fallbackURL:scheme
usingInputController:self
source:(self.view ?: (UIResponder *)weakSelf)
completion:^(BOOL success) {
if (success) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD showInfo:KBLocalized(@"Please open the App to finish purchase")];
});
}];
}
+ (NSString *)kb_urlEncodedString:(NSString *)value {
if (value.length == 0) {
return @"";
}
NSString *reserved = @"!*'();:@&=+$,/?%#[]";
NSMutableCharacterSet *allowed =
[[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
[allowed removeCharactersInString:reserved];
return [value stringByAddingPercentEncodingWithAllowedCharacters:allowed]
?: @"";
}
@end

View File

@@ -0,0 +1,221 @@
//
// KeyboardViewController+Suggestions.m
// CustomKeyboard
//
// Created by Codex on 2026/02/22.
//
#import "KeyboardViewController+Private.h"
#import "KBBackspaceUndoManager.h"
#import "KBInputBufferManager.h"
#import "KBKeyBoardMainView.h"
#import "KBSuggestionEngine.h"
@implementation KeyboardViewController (Suggestions)
// MARK: - Suggestions
- (void)kb_updateCurrentWordWithInsertedText:(NSString *)text {
if (text.length == 0) {
return;
}
if ([self kb_isAlphabeticString:text]) {
NSString *current = self.currentWord ?: @"";
self.currentWord = [current stringByAppendingString:text];
self.suppressSuggestions = NO;
[self kb_updateSuggestionsForCurrentWord];
} else {
[self kb_clearCurrentWord];
}
}
- (void)kb_clearCurrentWord {
self.currentWord = @"";
[self.keyBoardMainView kb_setSuggestions:@[]];
self.suppressSuggestions = NO;
}
- (void)kb_scheduleContextRefreshResetSuppression:(BOOL)resetSuppression {
dispatch_async(dispatch_get_main_queue(), ^{
[self kb_refreshCurrentWordFromDocumentContextResetSuppression:
resetSuppression];
});
}
- (void)kb_refreshCurrentWordFromDocumentContextResetSuppression:
(BOOL)resetSuppression {
NSString *context = self.textDocumentProxy.documentContextBeforeInput ?: @"";
NSString *word = [self kb_extractTrailingWordFromContext:context];
self.currentWord = word ?: @"";
if (resetSuppression) {
self.suppressSuggestions = NO;
}
[self kb_updateSuggestionsForCurrentWord];
}
- (NSString *)kb_extractTrailingWordFromContext:(NSString *)context {
if (context.length == 0) {
return @"";
}
NSCharacterSet *letters = [self kb_allowedSuggestionCharacterSet];
NSInteger idx = (NSInteger)context.length - 1;
while (idx >= 0) {
unichar ch = [context characterAtIndex:(NSUInteger)idx];
if (![letters characterIsMember:ch]) {
break;
}
idx -= 1;
}
NSUInteger start = (NSUInteger)(idx + 1);
if (start >= context.length) {
return @"";
}
return [context substringFromIndex:start];
}
- (BOOL)kb_isAlphabeticString:(NSString *)text {
if (text.length == 0) {
return NO;
}
NSCharacterSet *letters = [self kb_allowedSuggestionCharacterSet];
for (NSUInteger i = 0; i < text.length; i++) {
if (![letters characterIsMember:[text characterAtIndex:i]]) {
return NO;
}
}
return YES;
}
- (NSCharacterSet *)kb_allowedSuggestionCharacterSet {
switch (self.suggestionEngine.engineType) {
case KBSuggestionEngineTypeSpanish:
return [self kb_spanishSuggestionCharacterSet];
case KBSuggestionEngineTypeBopomofo:
return [self kb_bopomofoSuggestionCharacterSet];
case KBSuggestionEngineTypeLatin:
case KBSuggestionEngineTypeEnglish:
case KBSuggestionEngineTypePortuguese:
case KBSuggestionEngineTypeIndonesian:
case KBSuggestionEngineTypePinyinSimplified:
case KBSuggestionEngineTypePinyinTraditional:
default:
return [self kb_latinSuggestionCharacterSet];
}
}
- (NSCharacterSet *)kb_latinSuggestionCharacterSet {
static NSCharacterSet *set = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
set = [NSCharacterSet characterSetWithCharactersInString:
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
"áÁàÀâÂãÃäÄåÅæÆçÇ"
"éÉèÈêÊëË"
"íÍìÌîÎïÏ"
"ñÑ"
"óÓòÒôÔõÕöÖøØ"
"úÚùÙûÛüÜ"
"ýÝÿ"];
});
return set;
}
- (NSCharacterSet *)kb_spanishSuggestionCharacterSet {
static NSCharacterSet *set = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
set = [NSCharacterSet characterSetWithCharactersInString:
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
"áÁéÉíÍóÓúÚñÑüÜ"];
});
return set;
}
- (NSCharacterSet *)kb_bopomofoSuggestionCharacterSet {
static NSCharacterSet *set = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
set = [NSCharacterSet characterSetWithCharactersInString:
@"ㄅㄆㄇㄈㄉㄊㄋㄌㄍㄎㄏㄐㄑㄒㄓㄔㄕㄖㄗㄘㄙㄧㄨㄩㄚㄛㄜㄝㄞㄟㄠㄡㄢㄣㄤㄥㄦ"
"˙ˊˇˋ"];
});
return set;
}
- (void)kb_updateSuggestionsForCurrentWord {
NSString *prefix = self.currentWord ?: @"";
if (prefix.length == 0) {
[self.keyBoardMainView kb_setSuggestions:@[]];
return;
}
if (self.suppressSuggestions) {
[self.keyBoardMainView kb_setSuggestions:@[]];
return;
}
NSArray<NSString *> *items =
[self.suggestionEngine suggestionsForPrefix:prefix limit:5];
NSArray<NSString *> *cased = [self kb_applyCaseToSuggestions:items
prefix:prefix];
[self.keyBoardMainView kb_setSuggestions:cased];
}
- (NSArray<NSString *> *)kb_applyCaseToSuggestions:(NSArray<NSString *> *)items
prefix:(NSString *)prefix {
if (items.count == 0 || prefix.length == 0) {
return items;
}
BOOL allUpper = [prefix isEqualToString:prefix.uppercaseString];
BOOL firstUpper = [[prefix substringToIndex:1]
isEqualToString:[[prefix substringToIndex:1] uppercaseString]];
if (!allUpper && !firstUpper) {
return items;
}
NSMutableArray<NSString *> *result =
[NSMutableArray arrayWithCapacity:items.count];
for (NSString *word in items) {
if (allUpper) {
[result addObject:word.uppercaseString];
} else {
NSString *first = [[word substringToIndex:1] uppercaseString];
NSString *rest = (word.length > 1) ? [word substringFromIndex:1] : @"";
[result addObject:[first stringByAppendingString:rest]];
}
}
return result.copy;
}
// MARK: - KBKeyBoardMainViewDelegate (Suggestion)
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView
didSelectSuggestion:(NSString *)suggestion {
if (suggestion.length == 0) {
return;
}
NSDictionary *extra = @{@"suggestion_len" : @(suggestion.length)};
// [[KBMaiPointReporter sharedReporter]
// reportClickWithEventName:@"click_keyboard_suggestion_item"
// pageId:@"keyboard_main_panel"
// elementId:@"suggestion_item"
// extra:extra
// completion:nil];
[[KBBackspaceUndoManager shared] registerNonClearAction];
NSString *current = self.currentWord ?: @"";
if (current.length > 0) {
for (NSUInteger i = 0; i < current.length; i++) {
[self.textDocumentProxy deleteBackward];
}
}
[self.textDocumentProxy insertText:suggestion];
self.currentWord = suggestion;
[self.suggestionEngine recordSelection:suggestion];
self.suppressSuggestions = YES;
[self.keyBoardMainView kb_setSuggestions:@[]];
[[KBInputBufferManager shared] replaceTailWithText:suggestion
deleteCount:current.length];
}
@end

View File

@@ -0,0 +1,368 @@
//
// KeyboardViewController+Theme.m
// CustomKeyboard
//
// Created by Codex on 2026/02/22.
//
#import "KeyboardViewController+Private.h"
#import "KBFunctionView.h"
#import "KBKeyBoardMainView.h"
#import "KBSkinInstallBridge.h"
#import "KBSkinManager.h"
#import "UIImage+KBColor.h"
#import <QuartzCore/QuartzCore.h>
static NSString *const kKBDefaultSkinIdLight = @"normal_them";
static NSString *const kKBDefaultSkinZipNameLight = @"normal_them";
static NSString *const kKBDefaultSkinIdDark = @"normal_hei_them";
static NSString *const kKBDefaultSkinZipNameDark = @"normal_hei_them";
// 使 static kb_consumePendingShopSkin
@interface KeyboardViewController (KBSkinShopBridge)
- (void)kb_consumePendingShopSkin;
@end
static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
void *observer, CFStringRef name,
const void *object,
CFDictionaryRef userInfo) {
KeyboardViewController *strongSelf =
(__bridge KeyboardViewController *)observer;
if (!strongSelf) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
if ([strongSelf respondsToSelector:@selector(kb_consumePendingShopSkin)]) {
[strongSelf kb_consumePendingShopSkin];
}
});
}
@implementation KeyboardViewController (Theme)
- (void)kb_registerDarwinSkinInstallObserver {
CFNotificationCenterAddObserver(
CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self), KBSkinInstallNotificationCallback,
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification, NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
}
- (void)kb_unregisterDarwinSkinInstallObserver {
CFNotificationCenterRemoveObserver(
CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification, NULL);
}
- (void)kb_applyTheme {
@autoreleasepool {
KBSkinTheme *t = [KBSkinManager shared].current;
UIImage *img = nil;
BOOL isDefaultTheme = [self kb_isDefaultKeyboardTheme:t];
BOOL isDarkMode = [self kb_isDarkModeActive];
NSString *skinId = t.skinId ?: @"";
NSString *themeKey =
[NSString stringWithFormat:@"%@|default=%d|dark=%d", skinId,
isDefaultTheme, isDarkMode];
BOOL themeChanged =
(self.kb_lastAppliedThemeKey.length == 0 ||
![self.kb_lastAppliedThemeKey isEqualToString:themeKey]);
if (themeChanged) {
self.kb_lastAppliedThemeKey = themeKey;
}
CGSize size = self.bgImageView.bounds.size;
if (isDefaultTheme) {
if (isDarkMode) {
// 使使
//
img = nil;
self.bgImageView.image = nil;
[self.kb_defaultGradientLayer removeFromSuperlayer];
self.kb_defaultGradientLayer = nil;
// 使
if (@available(iOS 13.0, *)) {
// iOS 使 (RGB: 44, 44, 46 in sRGB, #2C2C2E)
// 使
UIColor *kbBgColor =
[UIColor colorWithDynamicProvider:^UIColor *_Nonnull(
UITraitCollection *_Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle ==
UIUserInterfaceStyleDark) {
//
return [UIColor colorWithRed:43.0 / 255.0
green:43.0 / 255.0
blue:43.0 / 255.0
alpha:1.0];
} else {
return [UIColor colorWithRed:209.0 / 255.0
green:211.0 / 255.0
blue:219.0 / 255.0
alpha:1.0];
}
}];
self.contentView.backgroundColor = kbBgColor;
self.bgImageView.backgroundColor = kbBgColor;
} else {
UIColor *darkColor = [UIColor colorWithRed:43.0 / 255.0
green:43.0 / 255.0
blue:43.0 / 255.0
alpha:1.0];
self.contentView.backgroundColor = darkColor;
self.bgImageView.backgroundColor = darkColor;
}
} else {
// 使
if (size.width <= 0 || size.height <= 0) {
[self.view layoutIfNeeded];
size = self.bgImageView.bounds.size;
}
if (size.width <= 0 || size.height <= 0) {
size = self.view.bounds.size;
}
if (size.width <= 0 || size.height <= 0) {
size = [UIScreen mainScreen].bounds.size;
}
UIColor *topColor = [UIColor colorWithHex:0xDEDFE4];
UIColor *bottomColor = [UIColor colorWithHex:0xD1D3DB];
UIColor *resolvedTopColor = topColor;
UIColor *resolvedBottomColor = bottomColor;
if (@available(iOS 13.0, *)) {
resolvedTopColor =
[topColor resolvedColorWithTraitCollection:self.traitCollection];
resolvedBottomColor =
[bottomColor resolvedColorWithTraitCollection:self.traitCollection];
}
CAGradientLayer *layer = self.kb_defaultGradientLayer;
if (!layer) {
layer = [CAGradientLayer layer];
layer.startPoint = CGPointMake(0.5, 0.0);
layer.endPoint = CGPointMake(0.5, 1.0);
[self.bgImageView.layer insertSublayer:layer atIndex:0];
self.kb_defaultGradientLayer = layer;
}
layer.colors =
@[ (id)resolvedTopColor.CGColor, (id)resolvedBottomColor.CGColor ];
layer.frame = (CGRect){CGPointZero, size};
img = nil;
self.bgImageView.image = nil;
self.contentView.backgroundColor = [UIColor clearColor];
self.bgImageView.backgroundColor = [UIColor clearColor];
}
NSLog(@"===");
} else {
// 使
self.contentView.backgroundColor = [UIColor clearColor];
self.bgImageView.backgroundColor = [UIColor clearColor];
[self.kb_defaultGradientLayer removeFromSuperlayer];
self.kb_defaultGradientLayer = nil;
img = [[KBSkinManager shared] currentBackgroundImage];
}
NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil));
[self kb_logSkinDiagnosticsWithTheme:t backgroundImage:img];
self.bgImageView.image = img;
// 使 skinId
if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
// method declared in KBKeyBoardMainView.h
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.keyBoardMainView performSelector:@selector(kb_applyTheme)];
#pragma clang diagnostic pop
}
// 访 self.functionView
KBFunctionView *functionView = [self kb_functionViewIfCreated];
if (functionView &&
[functionView respondsToSelector:@selector(kb_applyTheme)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[functionView performSelector:@selector(kb_applyTheme)];
#pragma clang diagnostic pop
}
}
}
- (BOOL)kb_isDefaultKeyboardTheme:(KBSkinTheme *)theme {
NSString *skinId = theme.skinId ?: @"";
if (skinId.length == 0 || [skinId isEqualToString:@"default"]) {
return YES;
}
if ([skinId isEqualToString:kKBDefaultSkinIdLight]) {
return YES;
}
return [skinId isEqualToString:kKBDefaultSkinIdDark];
}
- (BOOL)kb_isDarkModeActive {
if (@available(iOS 13.0, *)) {
return self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark;
}
return NO;
}
- (NSString *)kb_defaultSkinIdForCurrentStyle {
return [self kb_isDarkModeActive] ? kKBDefaultSkinIdDark
: kKBDefaultSkinIdLight;
}
- (NSString *)kb_defaultSkinZipNameForCurrentStyle {
return [self kb_isDarkModeActive] ? kKBDefaultSkinZipNameDark
: kKBDefaultSkinZipNameLight;
}
- (UIImage *)kb_defaultGradientImageWithSize:(CGSize)size
topColor:(UIColor *)topColor
bottomColor:(UIColor *)bottomColor {
if (size.width <= 0 || size.height <= 0) {
return nil;
}
//
if (self.kb_cachedGradientImage &&
CGSizeEqualToSize(self.kb_cachedGradientSize, size)) {
return self.kb_cachedGradientImage;
}
UIColor *resolvedTopColor = topColor;
UIColor *resolvedBottomColor = bottomColor;
if (@available(iOS 13.0, *)) {
resolvedTopColor =
[topColor resolvedColorWithTraitCollection:self.traitCollection];
resolvedBottomColor =
[bottomColor resolvedColorWithTraitCollection:self.traitCollection];
}
CAGradientLayer *layer = [CAGradientLayer layer];
layer.frame = CGRectMake(0, 0, size.width, size.height);
layer.startPoint = CGPointMake(0.5, 0.0);
layer.endPoint = CGPointMake(0.5, 1.0);
layer.colors =
@[ (id)resolvedTopColor.CGColor, (id)resolvedBottomColor.CGColor ];
UIGraphicsBeginImageContextWithOptions(size, YES, 0);
[layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.kb_cachedGradientImage = image;
self.kb_cachedGradientSize = size;
return image;
}
- (void)kb_logSkinDiagnosticsWithTheme:(KBSkinTheme *)theme
backgroundImage:(UIImage *)image {
#if DEBUG
NSString *skinId = theme.skinId ?: @"";
NSString *name = theme.name ?: @"";
NSMutableArray<NSString *> *roots = [NSMutableArray array];
NSURL *containerURL = [[NSFileManager defaultManager]
containerURLForSecurityApplicationGroupIdentifier:AppGroup];
if (containerURL.path.length > 0) {
[roots addObject:containerURL.path];
}
NSString *cacheRoot = NSSearchPathForDirectoriesInDomains(
NSCachesDirectory, NSUserDomainMask, YES)
.firstObject;
if (cacheRoot.length > 0) {
[roots addObject:cacheRoot];
}
NSFileManager *fm = [NSFileManager defaultManager];
NSMutableArray<NSString *> *lines = [NSMutableArray array];
for (NSString *root in roots) {
NSString *iconsDir = [[root stringByAppendingPathComponent:@"Skins"]
stringByAppendingPathComponent:skinId];
iconsDir = [iconsDir stringByAppendingPathComponent:@"icons"];
BOOL isDir = NO;
BOOL exists = [fm fileExistsAtPath:iconsDir isDirectory:&isDir] && isDir;
NSArray *contents =
exists ? [fm contentsOfDirectoryAtPath:iconsDir error:nil] : nil;
NSUInteger count = contents.count;
BOOL hasQ =
exists &&
[fm fileExistsAtPath:[iconsDir
stringByAppendingPathComponent:@"key_q.png"]];
BOOL hasQUp =
exists && [fm fileExistsAtPath:[iconsDir stringByAppendingPathComponent:
@"key_q_up.png"]];
BOOL hasDel =
exists && [fm fileExistsAtPath:[iconsDir stringByAppendingPathComponent:
@"key_del.png"]];
BOOL hasShift =
exists &&
[fm fileExistsAtPath:[iconsDir
stringByAppendingPathComponent:@"key_up.png"]];
BOOL hasShiftUpper =
exists && [fm fileExistsAtPath:[iconsDir stringByAppendingPathComponent:
@"key_up_upper.png"]];
NSString *line = [NSString
stringWithFormat:@"root=%@ icons=%@ exist=%d count=%tu key_q=%d "
@"key_q_up=%d key_del=%d key_up=%d key_up_upper=%d",
root, iconsDir, exists, count, hasQ, hasQUp, hasDel,
hasShift, hasShiftUpper];
[lines addObject:line];
}
NSLog(@"[Keyboard] theme id=%@ name=%@ hasBg=%d\n%@", skinId, name,
(image != nil), [lines componentsJoinedByString:@"\n"]);
#endif
}
- (void)kb_consumePendingShopSkin {
KBWeakSelf [KBSkinInstallBridge
consumePendingRequestFromBundle:NSBundle.mainBundle
completion:^(BOOL success,
NSError *_Nullable error) {
if (!success) {
if (error) {
NSLog(@"[Keyboard] skin request failed: %@",
error);
[KBHUD
showInfo:KBLocalized(
@"Theme resource preparation failed, please try again later")];
}
return;
}
[weakSelf kb_applyTheme];
[KBHUD showInfo:KBLocalized(
@"Theme updated, try it now")];
}];
}
- (void)kb_applyDefaultSkinIfNeeded {
NSDictionary *pending = [KBSkinInstallBridge pendingRequestPayload];
if (pending.count > 0) {
return;
}
NSString *currentId = [KBSkinManager shared].current.skinId ?: @"";
BOOL isDefault =
(currentId.length == 0 || [currentId isEqualToString:@"default"]);
BOOL isLightDefault = [currentId isEqualToString:kKBDefaultSkinIdLight];
BOOL isDarkDefault = [currentId isEqualToString:kKBDefaultSkinIdDark];
if (!isDefault && !isLightDefault && !isDarkDefault) {
//
return;
}
NSString *targetId = [self kb_defaultSkinIdForCurrentStyle];
if (currentId.length > 0 && [currentId isEqualToString:targetId]) {
return;
}
NSError *applyError = nil;
if ([KBSkinInstallBridge applyInstalledSkinWithId:targetId error:&applyError]) {
return;
}
// zip App bundle
// App zip
if (applyError) {
NSLog(@"[Keyboard] default skin %@ not installed in AppGroup yet: %@",
targetId, applyError);
}
}
@end

View File

@@ -0,0 +1,143 @@
//
// KeyboardViewController+UI.m
// CustomKeyboard
//
// Created by Codex on 2026/02/22.
//
#import "KeyboardViewController+Private.h"
#import "KBChatMessage.h"
#import "KBChatPanelView.h"
#import "KBFunctionView.h"
#import "KBKeyBoardMainView.h"
#import "KBKeyboardSubscriptionView.h"
#import "Masonry.h"
@implementation KeyboardViewController (UI)
- (void)setupUI {
self.view.translatesAutoresizingMaskIntoConstraints = NO;
//
CGFloat portraitWidth = [self kb_portraitWidth];
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
CGFloat keyboardBaseHeight = [self kb_keyboardBaseHeightForWidth:portraitWidth];
CGFloat screenWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
// FIX: iOS 26
// iOS 26 self.view view
// view
// UI
// 0
// viewWillAppear:
// iOS 18
NSLayoutConstraint *h = [self.view.heightAnchor constraintEqualToConstant:0];
NSLayoutConstraint *w =
[self.view.widthAnchor constraintEqualToConstant:screenWidth];
self.kb_heightConstraint = h;
self.kb_widthConstraint = w;
h.priority = UILayoutPriorityRequired;
w.priority = UILayoutPriorityRequired;
[NSLayoutConstraint activateConstraints:@[ h, w ]];
// UIInputView
if ([self.view isKindOfClass:[UIInputView class]]) {
UIInputView *iv = (UIInputView *)self.view;
if ([iv respondsToSelector:@selector(setAllowsSelfSizing:)]) {
iv.allowsSelfSizing = NO;
}
}
//
[self.view addSubview:self.contentView];
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view);
make.bottom.equalTo(self.view);
self.contentWidthConstraint = make.width.mas_equalTo(portraitWidth);
self.contentHeightConstraint = make.height.mas_equalTo(keyboardHeight);
}];
//
[self.contentView addSubview:self.bgImageView];
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
[self.contentView addSubview:self.keyBoardMainView];
[self.keyBoardMainView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.contentView);
make.bottom.equalTo(self.contentView);
self.keyBoardMainHeightConstraint =
make.height.mas_equalTo(keyboardBaseHeight);
}];
//
self.contentView.hidden = YES;
}
#pragma mark - Lazy
- (nullable KBFunctionView *)kb_functionViewIfCreated {
return _functionView;
}
- (UIView *)contentView {
if (!_contentView) {
_contentView = [[UIView alloc] init];
_contentView.backgroundColor = [UIColor clearColor];
}
return _contentView;
}
- (UIImageView *)bgImageView {
if (!_bgImageView) {
_bgImageView = [[UIImageView alloc] init];
_bgImageView.contentMode = UIViewContentModeScaleAspectFill;
_bgImageView.clipsToBounds = YES;
}
return _bgImageView;
}
- (KBKeyBoardMainView *)keyBoardMainView {
if (!_keyBoardMainView) {
_keyBoardMainView = [[KBKeyBoardMainView alloc] init];
_keyBoardMainView.delegate = self;
}
return _keyBoardMainView;
}
- (KBFunctionView *)functionView {
if (!_functionView) {
_functionView = [[KBFunctionView alloc] init];
_functionView.delegate = self; // Bar
}
return _functionView;
}
- (KBChatPanelView *)chatPanelView {
if (!_chatPanelView) {
NSLog(@"[Keyboard] ⚠️ chatPanelView 被创建!");
_chatPanelView = [[KBChatPanelView alloc] init];
_chatPanelView.delegate = self;
}
return _chatPanelView;
}
- (NSMutableArray<KBChatMessage *> *)chatMessages {
if (!_chatMessages) {
_chatMessages = [NSMutableArray array];
}
return _chatMessages;
}
- (KBKeyboardSubscriptionView *)subscriptionView {
if (!_subscriptionView) {
_subscriptionView = [[KBKeyboardSubscriptionView alloc] init];
_subscriptionView.delegate = self;
_subscriptionView.hidden = YES;
_subscriptionView.alpha = 0.0;
}
return _subscriptionView;
}
@end

View File

@@ -41,6 +41,9 @@ FOUNDATION_EXPORT NSString * const KBEmojiRecentsDidChangeNotification;
/// 更新当前语言对应的分类标题。
- (void)refreshLocalizedTitles;
/// 释放大块缓存emoji 分类与索引),下次访问会重新加载。
- (void)purgeLargeCaches;
@end
NS_ASSUME_NONNULL_END

View File

@@ -195,6 +195,12 @@ static const NSUInteger kKBEmojiRecentsLimit = 32;
}
}
- (void)purgeLargeCaches {
self.categoriesInternal = nil;
self.itemLookup = nil;
self.recentValues = nil;
}
- (void)onLocalizationChanged:(__unused NSNotification *)note {
[self refreshLocalizedTitles];
[[NSNotificationCenter defaultCenter] postNotificationName:KBEmojiRecentsDidChangeNotification object:nil];

View File

@@ -2,12 +2,11 @@
// KBFullAccessManager.m
//
// 访
// 1) UIInputViewController hasFullAccess API
// 1) 使 UIInputViewController.hasFullAccess API
// 2) Unknown Denied
//
#import "KBFullAccessManager.h"
#import <objc/message.h>
#if __has_include("KBNetworkManager.h")
#import "KBNetworkManager.h"
#endif
@@ -62,7 +61,10 @@ NSNotificationName const KBFullAccessChangedNotification = @"KBFullAccessChanged
Class guideCls = NSClassFromString(@"KBFullAccessGuideView");
if (guideCls && [guideCls respondsToSelector:NSSelectorFromString(@"showInView:")]) {
SEL sel = NSSelectorFromString(@"showInView:");
((void (*)(id, SEL, UIView *))objc_msgSend)(guideCls, sel, parent);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[guideCls performSelector:sel withObject:parent];
#pragma clang diagnostic pop
}
#endif
return NO;
@@ -74,13 +76,9 @@ NSNotificationName const KBFullAccessChangedNotification = @"KBFullAccessChanged
- (KBFullAccessState)p_detectFullAccessState {
UIInputViewController *ivc = self.ivc;
if (!ivc) return KBFullAccessStateUnknown;
SEL sel = NSSelectorFromString(@"hasFullAccess");
if ([ivc respondsToSelector:sel]) {
BOOL granted = ((BOOL (*)(id, SEL))objc_msgSend)(ivc, sel);
return granted ? KBFullAccessStateGranted : KBFullAccessStateDenied;
if ([ivc respondsToSelector:@selector(hasFullAccess)]) {
return ivc.hasFullAccess ? KBFullAccessStateGranted : KBFullAccessStateDenied;
}
// Unknown
return KBFullAccessStateUnknown;
}

View File

@@ -0,0 +1,37 @@
//
// KBKeyboardLayoutResolver.h
// CustomKeyboard
//
// 扩展侧布局解析器:根据 profileId 解析对应的布局配置
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBKeyboardLayoutResolver : NSObject
+ (instancetype)sharedResolver;
/// 根据 profileId 获取对应的布局 JSON ID
/// @param profileId 输入配置 ID如 "es_ES_azerty"
/// @return 布局 JSON ID如 "letters_azerty"),如果未找到返回 "letters"
- (NSString *)layoutJsonIdForProfileId:(NSString *)profileId;
/// 根据 profileId 获取对应的联想引擎类型
/// @param profileId 输入配置 ID
/// @return 联想引擎类型(如 "latin", "pinyin_traditional", "bopomofo"
- (NSString *)suggestionEngineForProfileId:(NSString *)profileId;
/// 从 App Group 读取当前选中的 profileId
- (nullable NSString *)currentProfileId;
/// 从 App Group 读取当前选中的语言代码
- (nullable NSString *)currentLanguageCode;
/// 从 App Group 读取当前选中的布局变体
- (nullable NSString *)currentLayoutVariant;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,106 @@
//
// KBKeyboardLayoutResolver.m
// CustomKeyboard
//
#import "KBKeyboardLayoutResolver.h"
#import "KBInputProfileManager.h"
#import "KBConfig.h"
#import "KBLocalizationManager.h"
@implementation KBKeyboardLayoutResolver
+ (instancetype)sharedResolver {
static KBKeyboardLayoutResolver *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
/// App kb_input_profiles.json code
- (NSString *)kb_defaultKeyboardLanguageCodeForAppLanguageCode:(NSString *)appLanguageCode {
NSString *lc = (appLanguageCode ?: @"").lowercaseString;
if ([lc hasPrefix:@"es"]) { return @"es"; }
if ([lc hasPrefix:@"pt"]) { return @"pt"; }
if ([lc hasPrefix:@"id"]) { return @"id"; }
if ([lc hasPrefix:@"zh-hant"]) { return @"zh-Hant-Pinyin"; }
return @"en";
}
- (BOOL)kb_didUserSelectKeyboardProfileInAppGroup:(NSUserDefaults *)appGroup {
return [appGroup boolForKey:AppGroup_DidUserSelectKeyboardProfile];
}
- (nullable KBInputProfileLayout *)kb_defaultLayoutForCurrentAppLanguage {
NSString *appLang = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
NSString *kbLang = [self kb_defaultKeyboardLanguageCodeForAppLanguageCode:appLang];
KBInputProfile *profile = [[KBInputProfileManager sharedManager] profileForLanguageCode:kbLang];
if (!profile) {
profile = [[KBInputProfileManager sharedManager] profileForLanguageCode:@"en"];
}
return profile.layouts.firstObject;
}
- (NSString *)layoutJsonIdForProfileId:(NSString *)profileId {
if (profileId.length == 0) {
return @"letters";
}
NSString *layoutJsonId = [[KBInputProfileManager sharedManager] layoutJsonIdForProfileId:profileId];
if (layoutJsonId.length > 0) {
return layoutJsonId;
}
// 退
NSLog(@"[KBKeyboardLayoutResolver] No layoutJsonId found for profileId: %@, using default 'letters'", profileId);
return @"letters";
}
- (NSString *)suggestionEngineForProfileId:(NSString *)profileId {
if (profileId.length == 0) {
return @"latin";
}
NSString *engine = [[KBInputProfileManager sharedManager] suggestionEngineForProfileId:profileId];
if (engine.length > 0) {
return engine;
}
// 退
NSLog(@"[KBKeyboardLayoutResolver] No suggestionEngine found for profileId: %@, using default 'latin'", profileId);
return @"latin";
}
- (nullable NSString *)currentProfileId {
NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
NSString *profileId = [appGroup stringForKey:AppGroup_SelectedKeyboardProfileId];
if ([self kb_didUserSelectKeyboardProfileInAppGroup:appGroup]) {
return profileId;
}
KBInputProfileLayout *layout = [self kb_defaultLayoutForCurrentAppLanguage];
return layout.profileId.length > 0 ? layout.profileId : profileId;
}
- (nullable NSString *)currentLanguageCode {
NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
NSString *languageCode = [appGroup stringForKey:AppGroup_SelectedKeyboardLanguageCode];
if ([self kb_didUserSelectKeyboardProfileInAppGroup:appGroup]) {
return languageCode;
}
NSString *appLang = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
return [self kb_defaultKeyboardLanguageCodeForAppLanguageCode:appLang];
}
- (nullable NSString *)currentLayoutVariant {
NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
NSString *layoutVariant = [appGroup stringForKey:AppGroup_SelectedKeyboardLayoutVariant];
if ([self kb_didUserSelectKeyboardProfileInAppGroup:appGroup]) {
return layoutVariant;
}
KBInputProfileLayout *layout = [self kb_defaultLayoutForCurrentAppLanguage];
return layout.variant.length > 0 ? layout.variant : layoutVariant;
}
@end

View File

@@ -7,9 +7,22 @@
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, KBSuggestionEngineType) {
KBSuggestionEngineTypeLatin = 0, // 拉丁字母(兼容旧值)
KBSuggestionEngineTypeEnglish, // 英语
KBSuggestionEngineTypeSpanish, // 西班牙语
KBSuggestionEngineTypePortuguese, // 葡萄牙语
KBSuggestionEngineTypeIndonesian, // 印度尼西亚语
KBSuggestionEngineTypePinyinSimplified, // 简体拼音
KBSuggestionEngineTypePinyinTraditional, // 繁体拼音
KBSuggestionEngineTypeBopomofo // 注音(繁体)
};
/// Simple local suggestion engine (prefix match + lightweight ranking).
@interface KBSuggestionEngine : NSObject
@property (nonatomic, assign) KBSuggestionEngineType engineType;
+ (instancetype)shared;
/// Returns suggestions for prefix (lowercase expected), limited by count.
@@ -18,6 +31,9 @@ NS_ASSUME_NONNULL_BEGIN
/// Record a selection to slightly boost ranking next time.
- (void)recordSelection:(NSString *)word;
/// 设置联想引擎类型(根据 profileId 的 suggestionEngine 字段)
- (void)setEngineTypeFromString:(NSString *)engineTypeString;
@end
NS_ASSUME_NONNULL_END

View File

@@ -10,6 +10,14 @@
@property (nonatomic, copy) NSArray<NSString *> *words;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *selectionCounts;
@property (nonatomic, strong) NSSet<NSString *> *priorityWords;
@property (nonatomic, copy) NSArray<NSString *> *traditionalChineseWords;
@property (nonatomic, copy) NSArray<NSString *> *simplifiedChineseWords;
@property (nonatomic, strong) NSDictionary<NSString *, NSArray<NSString *> *> *pinyinToTraditionalMap;
@property (nonatomic, strong) NSDictionary<NSString *, NSArray<NSString *> *> *bopomofoToChineseMap;
@property (nonatomic, copy) NSArray<NSString *> *spanishWords;
@property (nonatomic, copy) NSArray<NSString *> *englishWords;
@property (nonatomic, copy) NSArray<NSString *> *portugueseWords;
@property (nonatomic, copy) NSArray<NSString *> *indonesianWords;
@end
@implementation KBSuggestionEngine
@@ -25,49 +33,51 @@
- (instancetype)init {
if (self = [super init]) {
_engineType = KBSuggestionEngineTypeLatin;
_selectionCounts = [NSMutableDictionary dictionary];
NSArray<NSString *> *defaults = [self.class kb_defaultWords];
_priorityWords = [NSSet setWithArray:defaults];
_words = [self kb_loadWords];
}
return self;
}
- (NSArray<NSString *> *)suggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
if (prefix.length == 0 || limit == 0) { return @[]; }
NSString *lower = prefix.lowercaseString;
NSMutableArray<NSString *> *matches = [NSMutableArray array];
for (NSString *word in self.words) {
if ([word hasPrefix:lower]) {
[matches addObject:word];
if (matches.count >= limit * 3) {
// Avoid scanning too many matches for long lists.
break;
}
}
//
NSUInteger fetchLimit = limit;
if (fetchLimit < 80) {
fetchLimit = MIN((NSUInteger)80, MAX(fetchLimit * 4, fetchLimit));
}
NSArray<NSString *> *raw = nil;
if (matches.count == 0) { return @[]; }
[matches sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
NSInteger ca = self.selectionCounts[a].integerValue;
NSInteger cb = self.selectionCounts[b].integerValue;
if (ca != cb) {
return (cb > ca) ? NSOrderedAscending : NSOrderedDescending;
}
BOOL pa = [self.priorityWords containsObject:a];
BOOL pb = [self.priorityWords containsObject:b];
if (pa != pb) {
return pa ? NSOrderedAscending : NSOrderedDescending;
}
return [a compare:b];
}];
if (matches.count > limit) {
return [matches subarrayWithRange:NSMakeRange(0, limit)];
switch (self.engineType) {
case KBSuggestionEngineTypeEnglish:
raw = [self kb_englishSuggestionsForPrefix:prefix limit:fetchLimit];
break;
case KBSuggestionEngineTypeSpanish:
raw = [self kb_spanishSuggestionsForPrefix:prefix limit:fetchLimit];
break;
case KBSuggestionEngineTypePortuguese:
raw = [self kb_portugueseSuggestionsForPrefix:prefix limit:fetchLimit];
break;
case KBSuggestionEngineTypeIndonesian:
raw = [self kb_indonesianSuggestionsForPrefix:prefix limit:fetchLimit];
break;
case KBSuggestionEngineTypePinyinTraditional:
raw = [self kb_traditionalPinyinSuggestionsForPrefix:prefix limit:fetchLimit];
break;
case KBSuggestionEngineTypePinyinSimplified:
raw = [self kb_simplifiedPinyinSuggestionsForPrefix:prefix limit:fetchLimit];
break;
case KBSuggestionEngineTypeBopomofo:
raw = [self kb_bopomofoSuggestionsForPrefix:prefix limit:fetchLimit];
break;
case KBSuggestionEngineTypeLatin:
default:
raw = [self kb_latinSuggestionsForPrefix:prefix limit:fetchLimit];
break;
}
return matches.copy;
return [self kb_filterSensitiveSuggestions:raw limit:limit];
}
- (void)recordSelection:(NSString *)word {
@@ -164,4 +174,801 @@
];
}
#pragma mark - Engine Type Management
- (void)setEngineTypeFromString:(NSString *)engineTypeString {
if ([engineTypeString isEqualToString:@"latin"]) {
self.engineType = KBSuggestionEngineTypeLatin;
} else if ([engineTypeString isEqualToString:@"spanish"]) {
self.engineType = KBSuggestionEngineTypeSpanish;
} else if ([engineTypeString isEqualToString:@"english"]) {
self.engineType = KBSuggestionEngineTypeEnglish;
} else if ([engineTypeString isEqualToString:@"portuguese"]) {
self.engineType = KBSuggestionEngineTypePortuguese;
} else if ([engineTypeString isEqualToString:@"indonesian"]) {
self.engineType = KBSuggestionEngineTypeIndonesian;
} else if ([engineTypeString isEqualToString:@"pinyin_traditional"]) {
self.engineType = KBSuggestionEngineTypePinyinTraditional;
} else if ([engineTypeString isEqualToString:@"pinyin_simplified"]) {
self.engineType = KBSuggestionEngineTypePinyinSimplified;
} else if ([engineTypeString isEqualToString:@"bopomofo"]) {
self.engineType = KBSuggestionEngineTypeBopomofo;
} else {
self.engineType = KBSuggestionEngineTypeLatin;
}
[self kb_trimCachesForEngineType:self.engineType];
NSLog(@"[KBSuggestionEngine] Engine type set to: %@", engineTypeString);
}
#pragma mark - English Suggestions
- (NSArray<NSString *> *)kb_englishSuggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
if (!self.englishWords) {
self.englishWords = [self kb_loadEnglishWords];
}
NSArray<NSString *> *matches = [self kb_suggestionsFromWordList:self.englishWords
prefix:prefix
limit:limit];
if (matches.count == 0) {
return [self kb_latinSuggestionsForPrefix:prefix limit:limit];
}
return matches;
}
- (NSArray<NSString *> *)kb_loadEnglishWords {
NSString *path = [[NSBundle mainBundle] pathForResource:@"english_words" ofType:@"json"];
if (!path) {
NSLog(@"[KBSuggestionEngine] english_words.json not found, using default words");
return [self.class kb_defaultWords];
}
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
NSLog(@"[KBSuggestionEngine] Failed to read english_words.json");
return [self.class kb_defaultWords];
}
NSError *error = nil;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error || ![json isKindOfClass:NSDictionary.class]) {
NSLog(@"[KBSuggestionEngine] Failed to parse english_words.json: %@", error);
return [self.class kb_defaultWords];
}
NSArray *wordsArray = json[@"words"];
if (![wordsArray isKindOfClass:NSArray.class]) {
NSLog(@"[KBSuggestionEngine] Invalid words array in english_words.json");
return [self.class kb_defaultWords];
}
NSMutableArray<NSString *> *result = [NSMutableArray array];
for (id item in wordsArray) {
if ([item isKindOfClass:NSString.class]) {
[result addObject:item];
}
}
NSLog(@"[KBSuggestionEngine] Loaded %lu English words", (unsigned long)result.count);
return result.count > 0 ? [result copy] : [self.class kb_defaultWords];
}
#pragma mark - Latin Suggestions
- (NSArray<NSString *> *)kb_latinSuggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
if (!self.words) {
self.words = [self kb_loadWords];
}
NSString *lower = prefix.lowercaseString;
NSMutableArray<NSString *> *matches = [NSMutableArray array];
for (NSString *word in self.words) {
if ([word hasPrefix:lower]) {
[matches addObject:word];
if (matches.count >= limit * 3) {
break;
}
}
}
if (matches.count == 0) { return @[]; }
[matches sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
NSInteger ca = self.selectionCounts[a].integerValue;
NSInteger cb = self.selectionCounts[b].integerValue;
if (ca != cb) {
return (cb > ca) ? NSOrderedAscending : NSOrderedDescending;
}
BOOL pa = [self.priorityWords containsObject:a];
BOOL pb = [self.priorityWords containsObject:b];
if (pa != pb) {
return pa ? NSOrderedAscending : NSOrderedDescending;
}
return [a compare:b];
}];
if (matches.count > limit) {
return [matches subarrayWithRange:NSMakeRange(0, limit)];
}
return matches.copy;
}
#pragma mark - Traditional Chinese Pinyin Suggestions
- (NSArray<NSString *> *)kb_traditionalPinyinSuggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
if (!self.pinyinToTraditionalMap) {
self.pinyinToTraditionalMap = [self kb_loadPinyinToTraditionalMap];
}
NSString *lower = prefix.lowercaseString;
NSMutableArray<NSString *> *matches = [NSMutableArray array];
NSArray<NSString *> *directMatches = self.pinyinToTraditionalMap[lower];
if (directMatches.count > 0) {
[matches addObjectsFromArray:directMatches];
}
for (NSString *key in self.pinyinToTraditionalMap) {
if ([key hasPrefix:lower] && ![key isEqualToString:lower]) {
NSArray<NSString *> *candidates = self.pinyinToTraditionalMap[key];
[matches addObjectsFromArray:candidates];
if (matches.count >= limit * 2) {
break;
}
}
}
if (matches.count == 0) {
return [self kb_fallbackTraditionalSuggestions:lower limit:limit];
}
[matches sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
NSInteger ca = self.selectionCounts[a].integerValue;
NSInteger cb = self.selectionCounts[b].integerValue;
if (ca != cb) {
return (cb > ca) ? NSOrderedAscending : NSOrderedDescending;
}
return [a compare:b];
}];
if (matches.count > limit) {
return [matches subarrayWithRange:NSMakeRange(0, limit)];
}
return matches.copy;
}
- (NSArray<NSString *> *)kb_fallbackTraditionalSuggestions:(NSString *)prefix limit:(NSUInteger)limit {
if (!self.traditionalChineseWords) {
self.traditionalChineseWords = [self kb_loadTraditionalChineseWords];
}
NSMutableArray<NSString *> *matches = [NSMutableArray array];
for (NSString *word in self.traditionalChineseWords) {
[matches addObject:word];
if (matches.count >= limit) {
break;
}
}
return matches.copy;
}
#pragma mark - Simplified Chinese Pinyin Suggestions
- (NSArray<NSString *> *)kb_simplifiedPinyinSuggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
if (!self.pinyinToTraditionalMap) {
self.pinyinToTraditionalMap = [self kb_loadPinyinToTraditionalMap];
}
NSString *lower = prefix.lowercaseString;
NSMutableArray<NSString *> *matches = [NSMutableArray array];
NSArray<NSString *> *directMatches = self.pinyinToTraditionalMap[lower];
if (directMatches.count > 0) {
for (NSString *tradChar in directMatches) {
NSString *simplified = [self kb_toSimplified:tradChar];
if (simplified.length > 0) {
[matches addObject:simplified];
}
}
}
for (NSString *key in self.pinyinToTraditionalMap) {
if ([key hasPrefix:lower] && ![key isEqualToString:lower]) {
NSArray<NSString *> *candidates = self.pinyinToTraditionalMap[key];
for (NSString *tradChar in candidates) {
NSString *simplified = [self kb_toSimplified:tradChar];
if (simplified.length > 0) {
[matches addObject:simplified];
}
}
if (matches.count >= limit * 2) {
break;
}
}
}
if (matches.count == 0) {
return [self kb_fallbackSimplifiedSuggestions:lower limit:limit];
}
[matches sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
NSInteger ca = self.selectionCounts[a].integerValue;
NSInteger cb = self.selectionCounts[b].integerValue;
if (ca != cb) {
return (cb > ca) ? NSOrderedAscending : NSOrderedDescending;
}
return [a compare:b];
}];
if (matches.count > limit) {
return [matches subarrayWithRange:NSMakeRange(0, limit)];
}
return matches.copy;
}
- (NSArray<NSString *> *)kb_fallbackSimplifiedSuggestions:(NSString *)prefix limit:(NSUInteger)limit {
if (!self.simplifiedChineseWords) {
self.simplifiedChineseWords = [self kb_loadSimplifiedChineseWords];
}
NSMutableArray<NSString *> *matches = [NSMutableArray array];
for (NSString *word in self.simplifiedChineseWords) {
[matches addObject:word];
if (matches.count >= limit) {
break;
}
}
return matches.copy;
}
- (NSString *)kb_toSimplified:(NSString *)traditional {
static NSDictionary<NSString *, NSString *> *tradToSimpMap = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
tradToSimpMap = @{
@"臺": @"台", @"臺": @"台", @"灣": @"湾", @"語": @"语", @"體": @"体",
@"國": @"国", @"學": @"学", @"時": @"时", @"問": @"问", @"見": @"见",
@"經": @"经", @"動": @"动", @"長": @"长", @"開": @"开", @"關": @"关",
@"無": @"无", @"說": @"说", @"書": @"书", @"電": @"电", @"機": @"机",
@"氣": @"气", @"這": @"这", @"們": @"们", @"個": @"个", @"對": @"对",
@"來": @"来", @"還": @"还", @"過": @"过", @"會": @"会", @"進": @"进",
@"開": @"开", @"頭": @"头", @"點": @"点", @"問": @"问", @"題": @"题",
@"變": @"变", @"條": @"条", @"東": @"东", @"車": @"车", @"錢": @"钱",
@"門": @"门", @"聽": @"听", @"聲": @"声", @"醫": @"医", @"讓": @"让",
@"識": @"识", @"務": @"务", @"農": @"农", @"業": @"业", @"產": @"产",
@"黨": @"党", @"歷": @"历", @"史": @"史", @"後": @"后", @"前": @"前",
@"強": @"强", @"當": @"当", @"應": @"应", @"從": @"从", @"優": @"优",
@"兒": @"儿", @"兩": @"两", @"幾": @"几", @"廣": @"广", @"場": @"场",
@"決": @"决", @"許": @"许", @"設": @"设", @"請": @"请", @"論": @"论",
@"認": @"认", @"斷": @"断", @"離": @"离", @"須": @"须", @"導": @"导",
@"爭": @"争", @"重": @"重", @"輕": @"轻", @"難": @"难", @"極": @"极",
@"據": @"据", @"實": @"实", @"際": @"际", @"標": @"标", @"準": @"准",
@"確": @"确", @"證": @"证", @"驗": @"验", @"權": @"权", @"規": @"规",
@"則": @"则", @"劃": @"划", @"計": @"计", @"劃": @"划", @"術": @"术",
@"藝": @"艺", @"術": @"术", @"選": @"选", @"舉": @"举", @"團": @"团",
@"結": @"结", @"組": @"组", @"織": @"织", @"義": @"义", @"務": @"务",
@"親": @"亲", @"愛": @"爱", @"情": @"情", @"懷": @"怀", @"家": @"家",
@"屬": @"属", @"幫": @"帮", @"助": @"助", @"友": @"友", @"誼": @"谊",
@"謝": @"谢", @"謝": @"谢", @"對": @"对", @"起": @"起", @"早": @"早",
@"安": @"安", @"晚": @"晚", @"請": @"请", @"問": @"问", @"沒": @"没",
@"關": @"关", @"係": @"系", @"加": @"加", @"油": @"油", @"台": @"台",
@"北": @"北", @"高": @"高", @"雄": @"雄", @"中": @"中", @"南": @"南",
@"朋": @"朋", @"友": @"友", @"人": @"人", @"工": @"工", @"作": @"作",
@"習": @"习", @"生": @"生", @"活": @"活", @"地": @"地", @"方": @"方",
@"法": @"法", @"答": @"答", @"喜": @"喜", @"歡": @"欢", @"想": @"想",
@"念": @"念", @"開": @"开", @"心": @"心", @"快": @"快", @"樂": @"乐",
@"美": @"美", @"麗": @"丽", @"漂": @"漂", @"亮": @"亮", @"帥": @"帅",
@"氣": @"气", @"可": @"可", @"愛": @"爱", @"溫": @"温", @"柔": @"柔"
};
});
if (tradToSimpMap[traditional]) {
return tradToSimpMap[traditional];
}
NSMutableString *result = [traditional mutableCopy];
[tradToSimpMap enumerateKeysAndObjectsUsingBlock:^(NSString *trad, NSString *simp, BOOL *stop) {
[result replaceOccurrencesOfString:trad withString:simp options:0 range:NSMakeRange(0, result.length)];
}];
return result.length > 0 ? [result copy] : traditional;
}
#pragma mark - Bopomofo (Zhuyin) Suggestions
- (NSArray<NSString *> *)kb_bopomofoSuggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
if (!self.bopomofoToChineseMap) {
self.bopomofoToChineseMap = [self kb_loadBopomofoToChineseMap];
}
NSMutableArray<NSString *> *matches = [NSMutableArray array];
NSArray<NSString *> *directMatches = self.bopomofoToChineseMap[prefix];
if (directMatches.count > 0) {
[matches addObjectsFromArray:directMatches];
}
for (NSString *key in self.bopomofoToChineseMap) {
if ([key hasPrefix:prefix] && ![key isEqualToString:prefix]) {
NSArray<NSString *> *candidates = self.bopomofoToChineseMap[key];
[matches addObjectsFromArray:candidates];
if (matches.count >= limit * 2) {
break;
}
}
}
if (matches.count == 0) {
return [self kb_fallbackTraditionalSuggestions:prefix limit:limit];
}
[matches sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
NSInteger ca = self.selectionCounts[a].integerValue;
NSInteger cb = self.selectionCounts[b].integerValue;
if (ca != cb) {
return (cb > ca) ? NSOrderedAscending : NSOrderedDescending;
}
return [a compare:b];
}];
if (matches.count > limit) {
return [matches subarrayWithRange:NSMakeRange(0, limit)];
}
return matches.copy;
}
#pragma mark - Chinese Word Loading
- (NSArray<NSString *> *)kb_loadTraditionalChineseWords {
//
//
return @[
@"你好", @"謝謝", @"對不起", @"再見", @"早安",
@"晚安", @"請問", @"不好意思", @"沒關係", @"加油",
@"台灣", @"台北", @"高雄", @"台中", @"台南",
@"朋友", @"家人", @"工作", @"學習", @"生活",
@"時間", @"地點", @"方法", @"問題", @"答案",
@"喜歡", @"愛", @"想念", @"開心", @"快樂",
@"美麗", @"漂亮", @"帥氣", @"可愛", @"溫柔"
];
}
- (NSArray<NSString *> *)kb_loadSimplifiedChineseWords {
return @[
@"你好", @"谢谢", @"对不起", @"再见", @"早安",
@"晚安", @"请问", @"不好意思", @"没关系", @"加油",
@"中国", @"北京", @"上海", @"广州", @"深圳",
@"朋友", @"家人", @"工作", @"学习", @"生活",
@"时间", @"地点", @"方法", @"问题", @"答案",
@"喜欢", @"爱", @"想念", @"开心", @"快乐",
@"美丽", @"漂亮", @"帅气", @"可爱", @"温柔"
];
}
#pragma mark - Pinyin & Bopomofo Map Loading
- (NSDictionary<NSString *, NSArray<NSString *> *> *)kb_loadPinyinToTraditionalMap {
NSString *path = [[NSBundle mainBundle] pathForResource:@"pinyin_to_traditional" ofType:@"json"];
if (!path) {
NSLog(@"[KBSuggestionEngine] pinyin_to_traditional.json not found, using empty map");
return @{};
}
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
NSLog(@"[KBSuggestionEngine] Failed to read pinyin_to_traditional.json");
return @{};
}
NSError *error = nil;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error || ![json isKindOfClass:NSDictionary.class]) {
NSLog(@"[KBSuggestionEngine] Failed to parse pinyin_to_traditional.json: %@", error);
return @{};
}
NSDictionary *mappings = json[@"mappings"];
if (![mappings isKindOfClass:NSDictionary.class]) {
NSLog(@"[KBSuggestionEngine] Invalid mappings in pinyin_to_traditional.json");
return @{};
}
NSMutableDictionary<NSString *, NSArray<NSString *> *> *result = [NSMutableDictionary dictionary];
[mappings enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
if ([obj isKindOfClass:NSArray.class]) {
NSMutableArray<NSString *> *chars = [NSMutableArray array];
for (id item in (NSArray *)obj) {
if ([item isKindOfClass:NSString.class]) {
[chars addObject:item];
}
}
if (chars.count > 0) {
result[key] = [chars copy];
}
}
}];
NSLog(@"[KBSuggestionEngine] Loaded %lu pinyin mappings", (unsigned long)result.count);
return [result copy];
}
- (NSDictionary<NSString *, NSArray<NSString *> *> *)kb_loadBopomofoToChineseMap {
NSString *path = [[NSBundle mainBundle] pathForResource:@"bopomofo_to_chinese" ofType:@"json"];
if (!path) {
NSLog(@"[KBSuggestionEngine] bopomofo_to_chinese.json not found, using empty map");
return @{};
}
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
NSLog(@"[KBSuggestionEngine] Failed to read bopomofo_to_chinese.json");
return @{};
}
NSError *error = nil;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error || ![json isKindOfClass:NSDictionary.class]) {
NSLog(@"[KBSuggestionEngine] Failed to parse bopomofo_to_chinese.json: %@", error);
return @{};
}
NSDictionary *mappings = json[@"mappings"];
if (![mappings isKindOfClass:NSDictionary.class]) {
NSLog(@"[KBSuggestionEngine] Invalid mappings in bopomofo_to_chinese.json");
return @{};
}
NSMutableDictionary<NSString *, NSArray<NSString *> *> *result = [NSMutableDictionary dictionary];
[mappings enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
if ([obj isKindOfClass:NSArray.class]) {
NSMutableArray<NSString *> *chars = [NSMutableArray array];
for (id item in (NSArray *)obj) {
if ([item isKindOfClass:NSString.class]) {
[chars addObject:item];
}
}
if (chars.count > 0) {
result[key] = [chars copy];
}
}
}];
NSLog(@"[KBSuggestionEngine] Loaded %lu bopomofo mappings", (unsigned long)result.count);
return [result copy];
}
#pragma mark - Spanish Suggestions
- (NSArray<NSString *> *)kb_spanishSuggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
if (!self.spanishWords) {
self.spanishWords = [self kb_loadSpanishWords];
}
NSArray<NSString *> *matches = [self kb_suggestionsFromWordList:self.spanishWords
prefix:prefix
limit:limit];
if (matches.count == 0) {
return [self kb_latinSuggestionsForPrefix:prefix limit:limit];
}
return matches;
}
- (NSArray<NSString *> *)kb_loadSpanishWords {
NSString *path = [[NSBundle mainBundle] pathForResource:@"spanish_words" ofType:@"json"];
if (!path) {
NSLog(@"[KBSuggestionEngine] spanish_words.json not found, using default words");
return [self.class kb_defaultWords];
}
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
NSLog(@"[KBSuggestionEngine] Failed to read spanish_words.json");
return [self.class kb_defaultWords];
}
NSError *error = nil;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error || ![json isKindOfClass:NSDictionary.class]) {
NSLog(@"[KBSuggestionEngine] Failed to parse spanish_words.json: %@", error);
return [self.class kb_defaultWords];
}
NSArray *wordsArray = json[@"words"];
if (![wordsArray isKindOfClass:NSArray.class]) {
NSLog(@"[KBSuggestionEngine] Invalid words array in spanish_words.json");
return [self.class kb_defaultWords];
}
NSMutableArray<NSString *> *result = [NSMutableArray array];
for (id item in wordsArray) {
if ([item isKindOfClass:NSString.class]) {
[result addObject:item];
}
}
NSLog(@"[KBSuggestionEngine] Loaded %lu Spanish words", (unsigned long)result.count);
return result.count > 0 ? [result copy] : [self.class kb_defaultWords];
}
#pragma mark - Portuguese Suggestions
- (NSArray<NSString *> *)kb_portugueseSuggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
if (!self.portugueseWords) {
self.portugueseWords = [self kb_loadPortugueseWords];
}
NSArray<NSString *> *matches = [self kb_suggestionsFromWordList:self.portugueseWords
prefix:prefix
limit:limit];
if (matches.count == 0) {
return [self kb_latinSuggestionsForPrefix:prefix limit:limit];
}
return matches;
}
- (NSArray<NSString *> *)kb_loadPortugueseWords {
NSString *path = [[NSBundle mainBundle] pathForResource:@"portuguese_words" ofType:@"json"];
if (!path) {
NSLog(@"[KBSuggestionEngine] portuguese_words.json not found, using default words");
return [self.class kb_defaultWords];
}
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
NSLog(@"[KBSuggestionEngine] Failed to read portuguese_words.json");
return [self.class kb_defaultWords];
}
NSError *error = nil;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error || ![json isKindOfClass:NSDictionary.class]) {
NSLog(@"[KBSuggestionEngine] Failed to parse portuguese_words.json: %@", error);
return [self.class kb_defaultWords];
}
NSArray *wordsArray = json[@"words"];
if (![wordsArray isKindOfClass:NSArray.class]) {
NSLog(@"[KBSuggestionEngine] Invalid words array in portuguese_words.json");
return [self.class kb_defaultWords];
}
NSMutableArray<NSString *> *result = [NSMutableArray array];
for (id item in wordsArray) {
if ([item isKindOfClass:NSString.class]) {
[result addObject:item];
}
}
NSLog(@"[KBSuggestionEngine] Loaded %lu Portuguese words", (unsigned long)result.count);
return result.count > 0 ? [result copy] : [self.class kb_defaultWords];
}
#pragma mark - Indonesian Suggestions
- (NSArray<NSString *> *)kb_indonesianSuggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
if (!self.indonesianWords) {
self.indonesianWords = [self kb_loadIndonesianWords];
}
NSArray<NSString *> *matches = [self kb_suggestionsFromWordList:self.indonesianWords
prefix:prefix
limit:limit];
if (matches.count == 0) {
return [self kb_latinSuggestionsForPrefix:prefix limit:limit];
}
return matches;
}
- (NSArray<NSString *> *)kb_loadIndonesianWords {
NSString *path = [[NSBundle mainBundle] pathForResource:@"indonesian_words" ofType:@"json"];
if (!path) {
NSLog(@"[KBSuggestionEngine] indonesian_words.json not found, using default words");
return [self.class kb_defaultWords];
}
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
NSLog(@"[KBSuggestionEngine] Failed to read indonesian_words.json");
return [self.class kb_defaultWords];
}
NSError *error = nil;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error || ![json isKindOfClass:NSDictionary.class]) {
NSLog(@"[KBSuggestionEngine] Failed to parse indonesian_words.json: %@", error);
return [self.class kb_defaultWords];
}
NSArray *wordsArray = json[@"words"];
if (![wordsArray isKindOfClass:NSArray.class]) {
NSLog(@"[KBSuggestionEngine] Invalid words array in indonesian_words.json");
return [self.class kb_defaultWords];
}
NSMutableArray<NSString *> *result = [NSMutableArray array];
for (id item in wordsArray) {
if ([item isKindOfClass:NSString.class]) {
[result addObject:item];
}
}
NSLog(@"[KBSuggestionEngine] Loaded %lu Indonesian words", (unsigned long)result.count);
return result.count > 0 ? [result copy] : [self.class kb_defaultWords];
}
#pragma mark - Word List Helpers
- (NSArray<NSString *> *)kb_suggestionsFromWordList:(NSArray<NSString *> *)words
prefix:(NSString *)prefix
limit:(NSUInteger)limit {
NSString *lower = prefix.lowercaseString;
NSMutableArray<NSString *> *matches = [NSMutableArray array];
for (NSString *word in words) {
if ([word hasPrefix:lower]) {
[matches addObject:word];
if (matches.count >= limit * 2) {
break;
}
}
}
if (matches.count == 0) { return @[]; }
[matches sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
NSInteger ca = self.selectionCounts[a].integerValue;
NSInteger cb = self.selectionCounts[b].integerValue;
if (ca != cb) {
return (cb > ca) ? NSOrderedAscending : NSOrderedDescending;
}
return [a compare:b];
}];
if (matches.count > limit) {
return [matches subarrayWithRange:NSMakeRange(0, limit)];
}
return matches.copy;
}
- (void)kb_trimCachesForEngineType:(KBSuggestionEngineType)engineType {
switch (engineType) {
case KBSuggestionEngineTypeEnglish:
self.spanishWords = nil;
self.portugueseWords = nil;
self.indonesianWords = nil;
self.words = nil;
self.traditionalChineseWords = nil;
self.simplifiedChineseWords = nil;
self.pinyinToTraditionalMap = nil;
self.bopomofoToChineseMap = nil;
break;
case KBSuggestionEngineTypeSpanish:
self.englishWords = nil;
self.portugueseWords = nil;
self.indonesianWords = nil;
self.words = nil;
self.traditionalChineseWords = nil;
self.simplifiedChineseWords = nil;
self.pinyinToTraditionalMap = nil;
self.bopomofoToChineseMap = nil;
break;
case KBSuggestionEngineTypePortuguese:
self.englishWords = nil;
self.spanishWords = nil;
self.indonesianWords = nil;
self.words = nil;
self.traditionalChineseWords = nil;
self.simplifiedChineseWords = nil;
self.pinyinToTraditionalMap = nil;
self.bopomofoToChineseMap = nil;
break;
case KBSuggestionEngineTypeIndonesian:
self.englishWords = nil;
self.spanishWords = nil;
self.portugueseWords = nil;
self.words = nil;
self.traditionalChineseWords = nil;
self.simplifiedChineseWords = nil;
self.pinyinToTraditionalMap = nil;
self.bopomofoToChineseMap = nil;
break;
case KBSuggestionEngineTypePinyinTraditional:
case KBSuggestionEngineTypePinyinSimplified:
self.words = nil;
self.englishWords = nil;
self.spanishWords = nil;
self.portugueseWords = nil;
self.indonesianWords = nil;
self.bopomofoToChineseMap = nil;
break;
case KBSuggestionEngineTypeBopomofo:
self.words = nil;
self.englishWords = nil;
self.spanishWords = nil;
self.portugueseWords = nil;
self.indonesianWords = nil;
self.pinyinToTraditionalMap = nil;
self.simplifiedChineseWords = nil;
break;
case KBSuggestionEngineTypeLatin:
default:
self.englishWords = nil;
self.spanishWords = nil;
self.portugueseWords = nil;
self.indonesianWords = nil;
self.traditionalChineseWords = nil;
self.simplifiedChineseWords = nil;
self.pinyinToTraditionalMap = nil;
self.bopomofoToChineseMap = nil;
break;
}
}
#pragma mark - Safety Filter
- (NSArray<NSString *> *)kb_filterSensitiveSuggestions:(NSArray<NSString *> *)items
limit:(NSUInteger)limit {
if (items.count == 0 || limit == 0) { return @[]; }
NSMutableOrderedSet<NSString *> *result = [NSMutableOrderedSet orderedSet];
for (id item in items) {
if (![item isKindOfClass:NSString.class]) { continue; }
NSString *word = (NSString *)item;
if (word.length == 0) { continue; }
if ([self kb_isSensitiveSuggestion:word]) { continue; }
[result addObject:word];
if (result.count >= limit) { break; }
}
return result.array ?: @[];
}
- (BOOL)kb_isSensitiveSuggestion:(NSString *)word {
NSString *normalized = [self kb_normalizedSuggestionToken:word];
if (normalized.length == 0) { return YES; }
if ([[self.class kb_blockedSuggestionWords] containsObject:normalized]) {
return YES;
}
for (NSString *fragment in [self.class kb_blockedSuggestionFragments]) {
if ([normalized containsString:fragment]) {
return YES;
}
}
return NO;
}
- (NSString *)kb_normalizedSuggestionToken:(NSString *)word {
if (![word isKindOfClass:NSString.class]) { return @""; }
NSString *value = [[word stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]
lowercaseString];
if (value.length == 0) { return @""; }
value = [value stringByFoldingWithOptions:NSDiacriticInsensitiveSearch
locale:[NSLocale currentLocale]];
NSMutableCharacterSet *trimSet = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
[trimSet formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]];
return [value stringByTrimmingCharactersInSet:trimSet];
}
+ (NSSet<NSString *> *)kb_blockedSuggestionWords {
static NSSet<NSString *> *words = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//
words = [NSSet setWithArray:@[
@"sex", @"sexy", @"porn", @"porno", @"xxx", @"nude", @"naked",
@"fuck", @"fucking", @"shit", @"bitch", @"penis", @"vagina",
@"boob", @"rape", @"cocaine", @"heroin", @"drug", @"drugs",
@"kill", @"murder", @"gun", @"weapon",
@"sexo", @"porno", @"pornografia", @"violacion", @"violacao",
@"drogas", @"cocaina", @"heroina", @"arma", @"matar", @"muerte",
@"pene",
@"色情", @"裸露", @"裸体", @"裸聊", @"裸照",
@"强奸", @"毒品", @"海洛因", @"可卡因",
@"枪", @"武器", @"杀人", @"谋杀"
]];
});
return words;
}
+ (NSArray<NSString *> *)kb_blockedSuggestionFragments {
static NSArray<NSString *> *fragments = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
fragments = @[
@"porn", @"fuck", @"rape", @"cocaine", @"heroin",
@"色情", @"裸聊", @"裸照", @"强奸", @"毒品", @"杀人"
];
});
return fragments;
}
@end

View File

@@ -73,7 +73,11 @@ NS_ASSUME_NONNULL_BEGIN
@end
@interface KBKeyboardLayout : NSObject
@property (nonatomic, strong, nullable) NSNumber *rowSpacing;
@property (nonatomic, strong, nullable) NSNumber *topInset;
@property (nonatomic, strong, nullable) NSNumber *bottomInset;
@property (nonatomic, strong, nullable) NSArray<KBKeyboardRowConfig *> *rows;
@property (nonatomic, strong, nullable) NSArray<KBKeyboardRowConfig *> *shiftRows;
@end
@interface KBKeyboardLayoutConfig : NSObject

View File

@@ -8,6 +8,7 @@
#import "KBConfig.h"
static NSString * const kKBKeyboardLayoutConfigFileName = @"kb_keyboard_layout_config";
static NSString * const kKBKeyboardLayoutI18nFileName = @"kb_keyboard_layouts_i18n";
@implementation KBKeyboardLayoutMetrics
@end
@@ -81,7 +82,7 @@ static NSString * const kKBKeyboardLayoutConfigFileName = @"kb_keyboard_layout_c
@implementation KBKeyboardLayout
+ (NSDictionary *)mj_objectClassInArray {
return @{ @"rows": [KBKeyboardRowConfig class] };
return @{ @"rows": [KBKeyboardRowConfig class], @"shiftRows": [KBKeyboardRowConfig class] };
}
@end
@@ -92,13 +93,110 @@ static NSString * const kKBKeyboardLayoutConfigFileName = @"kb_keyboard_layout_c
static KBKeyboardLayoutConfig *config = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *path = [[NSBundle mainBundle] pathForResource:kKBKeyboardLayoutConfigFileName ofType:@"json"];
NSData *data = path.length ? [NSData dataWithContentsOfFile:path] : nil;
config = data ? [KBKeyboardLayoutConfig configFromJSONData:data] : nil;
config = [[KBKeyboardLayoutConfig alloc] init];
[config kb_loadMainConfig];
[config kb_loadI18nConfig];
});
return config;
}
- (void)kb_loadMainConfig {
NSString *path = [[NSBundle mainBundle] pathForResource:kKBKeyboardLayoutConfigFileName ofType:@"json"];
NSData *data = path.length ? [NSData dataWithContentsOfFile:path] : nil;
if (data.length == 0) { return; }
KBKeyboardLayoutConfig *mainConfig = [KBKeyboardLayoutConfig configFromJSONData:data];
if (mainConfig) {
self.metrics = mainConfig.metrics;
self.designWidth = mainConfig.designWidth;
self.keyDefs = mainConfig.keyDefs;
self.layouts = mainConfig.layouts;
}
}
- (NSArray<KBKeyboardRowConfig *> *)kb_mergeRowsFromBase:(NSArray<KBKeyboardRowConfig *> *)baseRows
override:(NSArray<KBKeyboardRowConfig *> *)overrideRows {
if (baseRows.count == 0) { return overrideRows ?: @[]; }
if (overrideRows.count == 0) { return baseRows; }
NSUInteger maxCount = MAX(baseRows.count, overrideRows.count);
NSMutableArray<KBKeyboardRowConfig *> *merged = [NSMutableArray arrayWithCapacity:maxCount];
for (NSUInteger i = 0; i < maxCount; i++) {
KBKeyboardRowConfig *baseRow = (i < baseRows.count) ? baseRows[i] : nil;
KBKeyboardRowConfig *overrideRow = (i < overrideRows.count) ? overrideRows[i] : nil;
if (!baseRow) {
if (overrideRow) { [merged addObject:overrideRow]; }
continue;
}
if (!overrideRow) {
[merged addObject:baseRow];
continue;
}
KBKeyboardRowConfig *row = [KBKeyboardRowConfig new];
row.height = baseRow.height ?: overrideRow.height;
row.insetLeft = baseRow.insetLeft ?: overrideRow.insetLeft;
row.insetRight = baseRow.insetRight ?: overrideRow.insetRight;
row.gap = baseRow.gap ?: overrideRow.gap;
row.align = baseRow.align.length > 0 ? baseRow.align : overrideRow.align;
BOOL hasOverrideItems = [overrideRow.items isKindOfClass:[NSArray class]] && ((NSArray *)overrideRow.items).count > 0;
row.items = hasOverrideItems ? overrideRow.items : baseRow.items;
row.segments = overrideRow.segments ?: baseRow.segments;
[merged addObject:row];
}
return merged.copy;
}
- (void)kb_loadI18nConfig {
NSString *path = [[NSBundle mainBundle] pathForResource:kKBKeyboardLayoutI18nFileName ofType:@"json"];
NSData *data = path.length ? [NSData dataWithContentsOfFile:path] : nil;
if (data.length == 0) {
NSLog(@"[KBKeyboardLayoutConfig] i18n layout file not found");
return;
}
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error || ![json isKindOfClass:[NSDictionary class]]) {
NSLog(@"[KBKeyboardLayoutConfig] Failed to parse i18n layout file: %@", error);
return;
}
NSDictionary *dict = (NSDictionary *)json;
NSDictionary *layoutsRaw = dict[@"layouts"];
if (![layoutsRaw isKindOfClass:[NSDictionary class]]) {
NSLog(@"[KBKeyboardLayoutConfig] No layouts found in i18n file");
return;
}
NSMutableDictionary<NSString *, KBKeyboardLayout *> *mergedLayouts = [NSMutableDictionary dictionaryWithDictionary:self.layouts ?: @{}];
[layoutsRaw enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if (![key isKindOfClass:[NSString class]] || ![obj isKindOfClass:[NSDictionary class]]) {
return;
}
KBKeyboardLayout *layout = [KBKeyboardLayout mj_objectWithKeyValues:obj];
if (!layout) { return; }
KBKeyboardLayout *baseLayout = mergedLayouts[key];
if (!baseLayout) {
mergedLayouts[key] = layout;
return;
}
KBKeyboardLayout *mergedLayout = [KBKeyboardLayout new];
mergedLayout.rowSpacing = baseLayout.rowSpacing ?: layout.rowSpacing;
mergedLayout.topInset = baseLayout.topInset ?: layout.topInset;
mergedLayout.bottomInset = baseLayout.bottomInset ?: layout.bottomInset;
mergedLayout.rows = [self kb_mergeRowsFromBase:baseLayout.rows override:layout.rows];
mergedLayout.shiftRows = [self kb_mergeRowsFromBase:baseLayout.shiftRows override:layout.shiftRows];
mergedLayouts[key] = mergedLayout;
}];
self.layouts = mergedLayouts.copy;
NSLog(@"[KBKeyboardLayoutConfig] Loaded %lu i18n layouts, total: %lu",
(unsigned long)layoutsRaw.count, (unsigned long)self.layouts.count);
}
+ (instancetype)configFromJSONData:(NSData *)data {
if (data.length == 0) { return nil; }
NSError *error = nil;

View File

@@ -41,38 +41,10 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
}
- (void)getSignWithParare:(NSDictionary *)bodyParams{
NSString *appId = @"loveKeyboard";
NSString *secret = @"kZJM39HYvhxwbJkG1fmquQRVkQiLAh2H"; //
NSString *timestamp = [KBSignUtils currentTimestamp];
NSString *nonce = [KBSignUtils generateNonceWithLength:16];
// 1.
NSMutableDictionary<NSString *, NSString *> *signParams = [NSMutableDictionary dictionary];
signParams[@"appId"] = appId;
signParams[@"timestamp"] = timestamp;
signParams[@"nonce"] = nonce;
// body
[bodyParams enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if ([obj isKindOfClass:[NSString class]]) {
signParams[key] = obj;
} else {
signParams[key] = [obj description];
}
}];
NSString *sign = [KBSignUtils signWithParams:signParams secret:secret];
//
NSDictionary<NSString *, NSString *> *signHeaders = [KBSignUtils signHeadersWithBodyParams:bodyParams];
NSMutableDictionary<NSString *, NSString *> *headers =
[self.defaultHeaders mutableCopy] ?: [NSMutableDictionary dictionary];
if (sign.length > 0) {
headers[@"X-Sign"] = sign;
}
headers[@"X-App-Id"] = appId;
headers[@"X-Timestamp"] = timestamp;
headers[@"X-Nonce"] = nonce;
// copy
[headers addEntriesFromDictionary:signHeaders ?: @{}];
self.defaultHeaders = headers;
}

View File

@@ -225,7 +225,10 @@ static NSString * const kKBStreamSplitToken = @"<SPLIT>";
}
if (payload.length > 0) {
if (self.loggingEnabled) {
NSLog(@"[KBStream] SSE raw payload: %@", payload);
#if DEBUG
NSLog(@"[KBStream] SSE raw payload len=%lu",
(unsigned long)(payload ?: @"").length);
#endif
}
NSString *llmText = nil;
if ([self processLLMChunkPayload:payload output:&llmText]) {
@@ -278,7 +281,10 @@ static NSString * const kKBStreamSplitToken = @"<SPLIT>";
}
if (payload.length > 0) {
if (self.loggingEnabled) {
NSLog(@"[KBStream] SSE raw payload: %@", payload);
#if DEBUG
NSLog(@"[KBStream] SSE raw payload len=%lu",
(unsigned long)(payload ?: @"").length);
#endif
}
NSString *delta = nil;
if ((NSInteger)payload.length >= self.deliveredCharCount) {

View File

@@ -6,6 +6,8 @@
//
#import "NetworkStreamHandler.h"
#import <Security/Security.h>
#import "KBLocalizationManager.h"
@interface NetworkStreamHandler ()
@@ -100,7 +102,11 @@
//
[request setValue:@"text/html, application/xhtml+xml, application/xml; q=0.9, image/avif, image/webp, image/apng, */*; q=0.8, application/signed-exchange; v=b3; q=0.7" forHTTPHeaderField:@"Accept"];
[request setValue:@"gzip, deflate" forHTTPHeaderField:@"Accept-Encoding"];
[request setValue:@"zh-CN, zh; q=0.9, ko; q=0.8, ja; q=0.7" forHTTPHeaderField:@"Accept-Language"];
NSString *lang = [[KBLocalizationManager shared] currentLanguageHeaderValue];
if (lang.length == 0) {
lang = @"en";
}
[request setValue:lang forHTTPHeaderField:@"Accept-Language"];
[request setValue:@"keep-alive" forHTTPHeaderField:@"Connection"];
[request setValue:@"1" forHTTPHeaderField:@"Upgrade-Insecure-Requests"];
@@ -243,8 +249,26 @@ didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
// SSL
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
SecTrustRef trust = challenge.protectionSpace.serverTrust;
if (!trust) {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
return;
}
BOOL ok = NO;
if (@available(iOS 13.0, *)) {
ok = SecTrustEvaluateWithError(trust, nil);
} else {
SecTrustResultType result = kSecTrustResultInvalid;
OSStatus status = SecTrustEvaluate(trust, &result);
ok = (status == errSecSuccess) &&
(result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}
if (ok) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:trust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
} else {
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}

View File

@@ -31,10 +31,16 @@
#define KB_UL_LOGIN KB_UL_BASE @"/login"
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
// 在扩展内,启用 URL Bridge仅在显式的用户点击动作中使用
// 这样即便宿主 App如备忘录拒绝 extensionContext openURL,仍可通过响应链兜底拉起容器 App。
// 说明:
// - `extensionContext openURL:` 是 Apple 官方推荐方式,但部分宿主 App尤其是“B 类应用”)
// 可能会拦截该调用,导致无法直接唤起容器 App
// 如你要走更稳妥的上架策略:把该宏改为 0仅保留 extensionContext 方案)。
#ifndef KB_URL_BRIDGE_ENABLE
#if DEBUG
#define KB_URL_BRIDGE_ENABLE 1
#else
#define KB_URL_BRIDGE_ENABLE 1
#endif
#endif

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeUserID</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeOtherUserContent</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeProductInteraction</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryActiveKeyboards</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>3EC4.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
</array>
</dict>
</plist>

Binary file not shown.

View File

@@ -0,0 +1,262 @@
/* 西班牙语(拉丁美洲)键盘皮肤映射 */
/* Spanish (Latin America) Keyboard Skin Icon Map */
/* 字母 q小写 */
"letter_q_lower" = "key_q";
/* 字母 Q大写 */
"letter_q_upper" = "key_q_up";
/* 字母 w小写 */
"letter_w_lower" = "key_w";
/* 字母 W大写 */
"letter_w_upper" = "key_w_up";
/* 字母 e小写 */
"letter_e_lower" = "key_e";
/* 字母 E大写 */
"letter_e_upper" = "key_e_up";
/* 字母 r小写 */
"letter_r_lower" = "key_r";
/* 字母 R大写 */
"letter_r_upper" = "key_r_up";
/* 字母 t小写 */
"letter_t_lower" = "key_t";
/* 字母 T大写 */
"letter_t_upper" = "key_t_up";
/* 字母 y小写 */
"letter_y_lower" = "key_y";
/* 字母 Y大写 */
"letter_y_upper" = "key_y_up";
/* 字母 u小写 */
"letter_u_lower" = "key_u";
/* 字母 U大写 */
"letter_u_upper" = "key_u_up";
/* 字母 i小写 */
"letter_i_lower" = "key_i";
/* 字母 I大写 */
"letter_i_upper" = "key_i_up";
/* 字母 o小写 */
"letter_o_lower" = "key_o";
/* 字母 O大写 */
"letter_o_upper" = "key_o_up";
/* 字母 p小写 */
"letter_p_lower" = "key_p";
/* 字母 P大写 */
"letter_p_upper" = "key_p_up";
/* 字母 a小写 */
"letter_a_lower" = "key_a";
/* 字母 A大写 */
"letter_a_upper" = "key_a_up";
/* 字母 s小写 */
"letter_s_lower" = "key_s";
/* 字母 S大写 */
"letter_s_upper" = "key_s_up";
/* 字母 d小写 */
"letter_d_lower" = "key_d";
/* 字母 D大写 */
"letter_d_upper" = "key_d_up";
/* 字母 f小写 */
"letter_f_lower" = "key_f";
/* 字母 F大写 */
"letter_f_upper" = "key_f_up";
/* 字母 g小写 */
"letter_g_lower" = "key_g";
/* 字母 G大写 */
"letter_g_upper" = "key_g_up";
/* 字母 h小写 */
"letter_h_lower" = "key_h";
/* 字母 H大写 */
"letter_h_upper" = "key_h_up";
/* 字母 j小写 */
"letter_j_lower" = "key_j";
/* 字母 J大写 */
"letter_j_upper" = "key_j_up";
/* 字母 k小写 */
"letter_k_lower" = "key_k";
/* 字母 K大写 */
"letter_k_upper" = "key_k_up";
/* 字母 l小写 */
"letter_l_lower" = "key_l";
/* 字母 L大写 */
"letter_l_upper" = "key_l_up";
/* 字母 ñ(小写)- 西班牙语专用 */
"letter_ñ_lower" = "key_ñ";
/* 字母 Ñ(大写)- 西班牙语专用 */
"letter_ñ_upper" = "key_ñ_up";
/* 字母 ñ(基础映射) */
"letter_ñ" = "key_ñ";
/* 字母 z小写 */
"letter_z_lower" = "key_z";
/* 字母 Z大写 */
"letter_z_upper" = "key_z_up";
/* 字母 x小写 */
"letter_x_lower" = "key_x";
/* 字母 X大写 */
"letter_x_upper" = "key_x_up";
/* 字母 c小写 */
"letter_c_lower" = "key_c";
/* 字母 C大写 */
"letter_c_upper" = "key_c_up";
/* 字母 v小写 */
"letter_v_lower" = "key_v";
/* 字母 V大写 */
"letter_v_upper" = "key_v_up";
/* 字母 b小写 */
"letter_b_lower" = "key_b";
/* 字母 B大写 */
"letter_b_upper" = "key_b_up";
/* 字母 n小写 */
"letter_n_lower" = "key_n";
/* 字母 N大写 */
"letter_n_upper" = "key_n_up";
/* 字母 m小写 */
"letter_m_lower" = "key_m";
/* 字母 M大写 */
"letter_m_upper" = "key_m_up";
/* 数字 1 */
"digit_1" = "key_1";
/* 数字 2 */
"digit_2" = "key_2";
/* 数字 3 */
"digit_3" = "key_3";
/* 数字 4 */
"digit_4" = "key_4";
/* 数字 5 */
"digit_5" = "key_5";
/* 数字 6 */
"digit_6" = "key_6";
/* 数字 7 */
"digit_7" = "key_7";
/* 数字 8 */
"digit_8" = "key_8";
/* 数字 9 */
"digit_9" = "key_9";
/* 数字 0 */
"digit_0" = "key_0";
/* '-' */
"sym_minus" = "key_minus";
/* '/' */
"sym_slash" = "key_slash";
/* ':' */
"sym_colon" = "key_colon";
/* ';' */
"sym_semicolon" = "key_semicolon";
/* '(' */
"sym_paren_l" = "key_paren_l";
/* ')' */
"sym_paren_r" = "key_paren_r";
/* '$' */
"sym_dollar" = "key_dollar";
/* '&' */
"sym_amp" = "key_amp";
/* '@' */
"sym_at" = "key_at";
/* 双引号 " */
"sym_quote_double" = "key_quote_d";
/* ',' */
"sym_comma" = "key_comma";
/* '.' */
"sym_dot" = "key_dot";
/* '?' */
"sym_question" = "key_question";
/* '!' */
"sym_exclam" = "key_exclam";
/* 单引号 ' */
"sym_quote_single" = "key_quote";
/* '¿' - 西班牙语专用 */
"sym_question_inv" = "key_question_inv";
/* '¡' - 西班牙语专用 */
"sym_exclam_inv" = "key_exclam_inv";
/* '[' */
"sym_bracket_l" = "key_bracket_l";
/* ']' */
"sym_bracket_r" = "key_bracket_r";
/* '{' */
"sym_brace_l" = "key_brace_l";
/* '}' */
"sym_brace_r" = "key_brace_r";
/* '#' */
"sym_hash" = "key_hash";
/* '%' */
"sym_percent" = "key_percent";
/* '^' */
"sym_caret" = "key_caret";
/* '*' */
"sym_asterisk" = "key_asterisk";
/* '+' */
"sym_plus" = "key_plus";
/* '=' */
"sym_equal" = "key_equal";
/* '_' */
"sym_underscore" = "key_underscore";
/* '\' */
"sym_backslash" = "key_backslash";
/* '|' */
"sym_pipe" = "key_pipe";
/* '~' */
"sym_tilde" = "key_tilde";
/* '<' */
"sym_lt" = "key_lt";
/* '>' */
"sym_gt" = "key_gt";
/* '¥' */
"sym_money" = "key_money";
/* '€' */
"sym_euro" = "key_euro";
/* '£' */
"sym_pound" = "key_pound";
/* '•' */
"sym_bullet" = "key_bullet";
/* 空格键 */
"space" = "key_space";
/* 删除键(⌫) */
"backspace" = "key_del";
/* Shift */
"shift" = "key_up";
/* Shift大写 */
"shift_upper" = "key_up_upper";
/* 字母面板左下角 "123" */
"mode_123" = "key_123";
/* 数字面板左下角 "abc" */
"mode_abc" = "key_abc";
/* 数字面板内 "123 -> #+=" */
"symbols_toggle_more" = "key_symbols_more";
/* 数字面板内 "#+= -> 123" */
"symbols_toggle_123" = "key_symbols_123";
/* 自定义 AI 功能键 */
"ai" = "key_ai";
/* Emoji功能键 */
"emoji_panel" = "key_emoji";
/* 发送/换行键 */
"return" = "key_send";

View File

@@ -0,0 +1,250 @@
/* 印尼语键盘皮肤映射 */
/* Indonesian Keyboard Skin Icon Map */
/* 字母 q小写 */
"letter_q_lower" = "key_q";
/* 字母 Q大写 */
"letter_q_upper" = "key_q_up";
/* 字母 w小写 */
"letter_w_lower" = "key_w";
/* 字母 W大写 */
"letter_w_upper" = "key_w_up";
/* 字母 e小写 */
"letter_e_lower" = "key_e";
/* 字母 E大写 */
"letter_e_upper" = "key_e_up";
/* 字母 r小写 */
"letter_r_lower" = "key_r";
/* 字母 R大写 */
"letter_r_upper" = "key_r_up";
/* 字母 t小写 */
"letter_t_lower" = "key_t";
/* 字母 T大写 */
"letter_t_upper" = "key_t_up";
/* 字母 y小写 */
"letter_y_lower" = "key_y";
/* 字母 Y大写 */
"letter_y_upper" = "key_y_up";
/* 字母 u小写 */
"letter_u_lower" = "key_u";
/* 字母 U大写 */
"letter_u_upper" = "key_u_up";
/* 字母 i小写 */
"letter_i_lower" = "key_i";
/* 字母 I大写 */
"letter_i_upper" = "key_i_up";
/* 字母 o小写 */
"letter_o_lower" = "key_o";
/* 字母 O大写 */
"letter_o_upper" = "key_o_up";
/* 字母 p小写 */
"letter_p_lower" = "key_p";
/* 字母 P大写 */
"letter_p_upper" = "key_p_up";
/* 字母 a小写 */
"letter_a_lower" = "key_a";
/* 字母 A大写 */
"letter_a_upper" = "key_a_up";
/* 字母 s小写 */
"letter_s_lower" = "key_s";
/* 字母 S大写 */
"letter_s_upper" = "key_s_up";
/* 字母 d小写 */
"letter_d_lower" = "key_d";
/* 字母 D大写 */
"letter_d_upper" = "key_d_up";
/* 字母 f小写 */
"letter_f_lower" = "key_f";
/* 字母 F大写 */
"letter_f_upper" = "key_f_up";
/* 字母 g小写 */
"letter_g_lower" = "key_g";
/* 字母 G大写 */
"letter_g_upper" = "key_g_up";
/* 字母 h小写 */
"letter_h_lower" = "key_h";
/* 字母 H大写 */
"letter_h_upper" = "key_h_up";
/* 字母 j小写 */
"letter_j_lower" = "key_j";
/* 字母 J大写 */
"letter_j_upper" = "key_j_up";
/* 字母 k小写 */
"letter_k_lower" = "key_k";
/* 字母 K大写 */
"letter_k_upper" = "key_k_up";
/* 字母 l小写 */
"letter_l_lower" = "key_l";
/* 字母 L大写 */
"letter_l_upper" = "key_l_up";
/* 字母 z小写 */
"letter_z_lower" = "key_z";
/* 字母 Z大写 */
"letter_z_upper" = "key_z_up";
/* 字母 x小写 */
"letter_x_lower" = "key_x";
/* 字母 X大写 */
"letter_x_upper" = "key_x_up";
/* 字母 c小写 */
"letter_c_lower" = "key_c";
/* 字母 C大写 */
"letter_c_upper" = "key_c_up";
/* 字母 v小写 */
"letter_v_lower" = "key_v";
/* 字母 V大写 */
"letter_v_upper" = "key_v_up";
/* 字母 b小写 */
"letter_b_lower" = "key_b";
/* 字母 B大写 */
"letter_b_upper" = "key_b_up";
/* 字母 n小写 */
"letter_n_lower" = "key_n";
/* 字母 N大写 */
"letter_n_upper" = "key_n_up";
/* 字母 m小写 */
"letter_m_lower" = "key_m";
/* 字母 M大写 */
"letter_m_upper" = "key_m_up";
/* 数字 1 */
"digit_1" = "key_1";
/* 数字 2 */
"digit_2" = "key_2";
/* 数字 3 */
"digit_3" = "key_3";
/* 数字 4 */
"digit_4" = "key_4";
/* 数字 5 */
"digit_5" = "key_5";
/* 数字 6 */
"digit_6" = "key_6";
/* 数字 7 */
"digit_7" = "key_7";
/* 数字 8 */
"digit_8" = "key_8";
/* 数字 9 */
"digit_9" = "key_9";
/* 数字 0 */
"digit_0" = "key_0";
/* '-' */
"sym_minus" = "key_minus";
/* '/' */
"sym_slash" = "key_slash";
/* ':' */
"sym_colon" = "key_colon";
/* ';' */
"sym_semicolon" = "key_semicolon";
/* '(' */
"sym_paren_l" = "key_paren_l";
/* ')' */
"sym_paren_r" = "key_paren_r";
/* '$' */
"sym_dollar" = "key_dollar";
/* '&' */
"sym_amp" = "key_amp";
/* '@' */
"sym_at" = "key_at";
/* 双引号 " */
"sym_quote_double" = "key_quote_d";
/* ',' */
"sym_comma" = "key_comma";
/* '.' */
"sym_dot" = "key_dot";
/* '?' */
"sym_question" = "key_question";
/* '!' */
"sym_exclam" = "key_exclam";
/* 单引号 ' */
"sym_quote_single" = "key_quote";
/* '[' */
"sym_bracket_l" = "key_bracket_l";
/* ']' */
"sym_bracket_r" = "key_bracket_r";
/* '{' */
"sym_brace_l" = "key_brace_l";
/* '}' */
"sym_brace_r" = "key_brace_r";
/* '#' */
"sym_hash" = "key_hash";
/* '%' */
"sym_percent" = "key_percent";
/* '^' */
"sym_caret" = "key_caret";
/* '*' */
"sym_asterisk" = "key_asterisk";
/* '+' */
"sym_plus" = "key_plus";
/* '=' */
"sym_equal" = "key_equal";
/* '_' */
"sym_underscore" = "key_underscore";
/* '\' */
"sym_backslash" = "key_backslash";
/* '|' */
"sym_pipe" = "key_pipe";
/* '~' */
"sym_tilde" = "key_tilde";
/* '<' */
"sym_lt" = "key_lt";
/* '>' */
"sym_gt" = "key_gt";
/* '¥' */
"sym_money" = "key_money";
/* '€' */
"sym_euro" = "key_euro";
/* '£' */
"sym_pound" = "key_pound";
/* '•' */
"sym_bullet" = "key_bullet";
/* 空格键 */
"space" = "key_space";
/* 删除键(⌫) */
"backspace" = "key_del";
/* Shift */
"shift" = "key_up";
/* Shift大写 */
"shift_upper" = "key_up_upper";
/* 字母面板左下角 "123" */
"mode_123" = "key_123";
/* 数字面板左下角 "abc" */
"mode_abc" = "key_abc";
/* 数字面板内 "123 -> #+=" */
"symbols_toggle_more" = "key_symbols_more";
/* 数字面板内 "#+= -> 123" */
"symbols_toggle_123" = "key_symbols_123";
/* 自定义 AI 功能键 */
"ai" = "key_ai";
/* Emoji功能键 */
"emoji_panel" = "key_emoji";
/* 发送/换行键 */
"return" = "key_send";

View File

@@ -0,0 +1,250 @@
/* 葡萄牙语键盘皮肤映射 */
/* Portuguese Keyboard Skin Icon Map */
/* 字母 q小写 */
"letter_q_lower" = "key_q";
/* 字母 Q大写 */
"letter_q_upper" = "key_q_up";
/* 字母 w小写 */
"letter_w_lower" = "key_w";
/* 字母 W大写 */
"letter_w_upper" = "key_w_up";
/* 字母 e小写 */
"letter_e_lower" = "key_e";
/* 字母 E大写 */
"letter_e_upper" = "key_e_up";
/* 字母 r小写 */
"letter_r_lower" = "key_r";
/* 字母 R大写 */
"letter_r_upper" = "key_r_up";
/* 字母 t小写 */
"letter_t_lower" = "key_t";
/* 字母 T大写 */
"letter_t_upper" = "key_t_up";
/* 字母 y小写 */
"letter_y_lower" = "key_y";
/* 字母 Y大写 */
"letter_y_upper" = "key_y_up";
/* 字母 u小写 */
"letter_u_lower" = "key_u";
/* 字母 U大写 */
"letter_u_upper" = "key_u_up";
/* 字母 i小写 */
"letter_i_lower" = "key_i";
/* 字母 I大写 */
"letter_i_upper" = "key_i_up";
/* 字母 o小写 */
"letter_o_lower" = "key_o";
/* 字母 O大写 */
"letter_o_upper" = "key_o_up";
/* 字母 p小写 */
"letter_p_lower" = "key_p";
/* 字母 P大写 */
"letter_p_upper" = "key_p_up";
/* 字母 a小写 */
"letter_a_lower" = "key_a";
/* 字母 A大写 */
"letter_a_upper" = "key_a_up";
/* 字母 s小写 */
"letter_s_lower" = "key_s";
/* 字母 S大写 */
"letter_s_upper" = "key_s_up";
/* 字母 d小写 */
"letter_d_lower" = "key_d";
/* 字母 D大写 */
"letter_d_upper" = "key_d_up";
/* 字母 f小写 */
"letter_f_lower" = "key_f";
/* 字母 F大写 */
"letter_f_upper" = "key_f_up";
/* 字母 g小写 */
"letter_g_lower" = "key_g";
/* 字母 G大写 */
"letter_g_upper" = "key_g_up";
/* 字母 h小写 */
"letter_h_lower" = "key_h";
/* 字母 H大写 */
"letter_h_upper" = "key_h_up";
/* 字母 j小写 */
"letter_j_lower" = "key_j";
/* 字母 J大写 */
"letter_j_upper" = "key_j_up";
/* 字母 k小写 */
"letter_k_lower" = "key_k";
/* 字母 K大写 */
"letter_k_upper" = "key_k_up";
/* 字母 l小写 */
"letter_l_lower" = "key_l";
/* 字母 L大写 */
"letter_l_upper" = "key_l_up";
/* 字母 z小写 */
"letter_z_lower" = "key_z";
/* 字母 Z大写 */
"letter_z_upper" = "key_z_up";
/* 字母 x小写 */
"letter_x_lower" = "key_x";
/* 字母 X大写 */
"letter_x_upper" = "key_x_up";
/* 字母 c小写 */
"letter_c_lower" = "key_c";
/* 字母 C大写 */
"letter_c_upper" = "key_c_up";
/* 字母 v小写 */
"letter_v_lower" = "key_v";
/* 字母 V大写 */
"letter_v_upper" = "key_v_up";
/* 字母 b小写 */
"letter_b_lower" = "key_b";
/* 字母 B大写 */
"letter_b_upper" = "key_b_up";
/* 字母 n小写 */
"letter_n_lower" = "key_n";
/* 字母 N大写 */
"letter_n_upper" = "key_n_up";
/* 字母 m小写 */
"letter_m_lower" = "key_m";
/* 字母 M大写 */
"letter_m_upper" = "key_m_up";
/* 数字 1 */
"digit_1" = "key_1";
/* 数字 2 */
"digit_2" = "key_2";
/* 数字 3 */
"digit_3" = "key_3";
/* 数字 4 */
"digit_4" = "key_4";
/* 数字 5 */
"digit_5" = "key_5";
/* 数字 6 */
"digit_6" = "key_6";
/* 数字 7 */
"digit_7" = "key_7";
/* 数字 8 */
"digit_8" = "key_8";
/* 数字 9 */
"digit_9" = "key_9";
/* 数字 0 */
"digit_0" = "key_0";
/* '-' */
"sym_minus" = "key_minus";
/* '/' */
"sym_slash" = "key_slash";
/* ':' */
"sym_colon" = "key_colon";
/* ';' */
"sym_semicolon" = "key_semicolon";
/* '(' */
"sym_paren_l" = "key_paren_l";
/* ')' */
"sym_paren_r" = "key_paren_r";
/* '$' */
"sym_dollar" = "key_dollar";
/* '&' */
"sym_amp" = "key_amp";
/* '@' */
"sym_at" = "key_at";
/* 双引号 " */
"sym_quote_double" = "key_quote_d";
/* ',' */
"sym_comma" = "key_comma";
/* '.' */
"sym_dot" = "key_dot";
/* '?' */
"sym_question" = "key_question";
/* '!' */
"sym_exclam" = "key_exclam";
/* 单引号 ' */
"sym_quote_single" = "key_quote";
/* '[' */
"sym_bracket_l" = "key_bracket_l";
/* ']' */
"sym_bracket_r" = "key_bracket_r";
/* '{' */
"sym_brace_l" = "key_brace_l";
/* '}' */
"sym_brace_r" = "key_brace_r";
/* '#' */
"sym_hash" = "key_hash";
/* '%' */
"sym_percent" = "key_percent";
/* '^' */
"sym_caret" = "key_caret";
/* '*' */
"sym_asterisk" = "key_asterisk";
/* '+' */
"sym_plus" = "key_plus";
/* '=' */
"sym_equal" = "key_equal";
/* '_' */
"sym_underscore" = "key_underscore";
/* '\' */
"sym_backslash" = "key_backslash";
/* '|' */
"sym_pipe" = "key_pipe";
/* '~' */
"sym_tilde" = "key_tilde";
/* '<' */
"sym_lt" = "key_lt";
/* '>' */
"sym_gt" = "key_gt";
/* '¥' */
"sym_money" = "key_money";
/* '€' */
"sym_euro" = "key_euro";
/* '£' */
"sym_pound" = "key_pound";
/* '•' */
"sym_bullet" = "key_bullet";
/* 空格键 */
"space" = "key_space";
/* 删除键(⌫) */
"backspace" = "key_del";
/* Shift */
"shift" = "key_up";
/* Shift大写 */
"shift_upper" = "key_up_upper";
/* 字母面板左下角 "123" */
"mode_123" = "key_123";
/* 数字面板左下角 "abc" */
"mode_abc" = "key_abc";
/* 数字面板内 "123 -> #+=" */
"symbols_toggle_more" = "key_symbols_more";
/* 数字面板内 "#+= -> 123" */
"symbols_toggle_123" = "key_symbols_123";
/* 自定义 AI 功能键 */
"ai" = "key_ai";
/* Emoji功能键 */
"emoji_panel" = "key_emoji";
/* 发送/换行键 */
"return" = "key_send";

View File

@@ -0,0 +1,330 @@
/* 繁体中文键盘皮肤映射 */
/* Traditional Chinese Keyboard Skin Icon Map */
/* 包含:拼音布局 + 注音布局 */
/* ========== 拼音布局(与英文相同)========== */
/* 字母 q小写 */
"letter_q_lower" = "key_q";
/* 字母 Q大写 */
"letter_q_upper" = "key_q_up";
/* 字母 w小写 */
"letter_w_lower" = "key_w";
/* 字母 W大写 */
"letter_w_upper" = "key_w_up";
/* 字母 e小写 */
"letter_e_lower" = "key_e";
/* 字母 E大写 */
"letter_e_upper" = "key_e_up";
/* 字母 r小写 */
"letter_r_lower" = "key_r";
/* 字母 R大写 */
"letter_r_upper" = "key_r_up";
/* 字母 t小写 */
"letter_t_lower" = "key_t";
/* 字母 T大写 */
"letter_t_upper" = "key_t_up";
/* 字母 y小写 */
"letter_y_lower" = "key_y";
/* 字母 Y大写 */
"letter_y_upper" = "key_y_up";
/* 字母 u小写 */
"letter_u_lower" = "key_u";
/* 字母 U大写 */
"letter_u_upper" = "key_u_up";
/* 字母 i小写 */
"letter_i_lower" = "key_i";
/* 字母 I大写 */
"letter_i_upper" = "key_i_up";
/* 字母 o小写 */
"letter_o_lower" = "key_o";
/* 字母 O大写 */
"letter_o_upper" = "key_o_up";
/* 字母 p小写 */
"letter_p_lower" = "key_p";
/* 字母 P大写 */
"letter_p_upper" = "key_p_up";
/* 字母 a小写 */
"letter_a_lower" = "key_a";
/* 字母 A大写 */
"letter_a_upper" = "key_a_up";
/* 字母 s小写 */
"letter_s_lower" = "key_s";
/* 字母 S大写 */
"letter_s_upper" = "key_s_up";
/* 字母 d小写 */
"letter_d_lower" = "key_d";
/* 字母 D大写 */
"letter_d_upper" = "key_d_up";
/* 字母 f小写 */
"letter_f_lower" = "key_f";
/* 字母 F大写 */
"letter_f_upper" = "key_f_up";
/* 字母 g小写 */
"letter_g_lower" = "key_g";
/* 字母 G大写 */
"letter_g_upper" = "key_g_up";
/* 字母 h小写 */
"letter_h_lower" = "key_h";
/* 字母 H大写 */
"letter_h_upper" = "key_h_up";
/* 字母 j小写 */
"letter_j_lower" = "key_j";
/* 字母 J大写 */
"letter_j_upper" = "key_j_up";
/* 字母 k小写 */
"letter_k_lower" = "key_k";
/* 字母 K大写 */
"letter_k_upper" = "key_k_up";
/* 字母 l小写 */
"letter_l_lower" = "key_l";
/* 字母 L大写 */
"letter_l_upper" = "key_l_up";
/* 字母 z小写 */
"letter_z_lower" = "key_z";
/* 字母 Z大写 */
"letter_z_upper" = "key_z_up";
/* 字母 x小写 */
"letter_x_lower" = "key_x";
/* 字母 X大写 */
"letter_x_upper" = "key_x_up";
/* 字母 c小写 */
"letter_c_lower" = "key_c";
/* 字母 C大写 */
"letter_c_upper" = "key_c_up";
/* 字母 v小写 */
"letter_v_lower" = "key_v";
/* 字母 V大写 */
"letter_v_upper" = "key_v_up";
/* 字母 b小写 */
"letter_b_lower" = "key_b";
/* 字母 B大写 */
"letter_b_upper" = "key_b_up";
/* 字母 n小写 */
"letter_n_lower" = "key_n";
/* 字母 N大写 */
"letter_n_upper" = "key_n_up";
/* 字母 m小写 */
"letter_m_lower" = "key_m";
/* 字母 M大写 */
"letter_m_upper" = "key_m_up";
/* ========== 注音符号 ========== */
/* 声母 */
"letter_ㄅ" = "key_bopomofo_b";
"letter_ㄆ" = "key_bopomofo_p";
"letter_ㄇ" = "key_bopomofo_m";
"letter_ㄈ" = "key_bopomofo_f";
"letter_ㄉ" = "key_bopomofo_d";
"letter_ㄊ" = "key_bopomofo_t";
"letter_ㄋ" = "key_bopomofo_n";
"letter_ㄌ" = "key_bopomofo_l";
"letter_ㄍ" = "key_bopomofo_g";
"letter_ㄎ" = "key_bopomofo_k";
"letter_ㄏ" = "key_bopomofo_h";
"letter_ㄐ" = "key_bopomofo_j";
"letter_ㄑ" = "key_bopomofo_q";
"letter_ㄒ" = "key_bopomofo_x";
"letter_ㄓ" = "key_bopomofo_zh";
"letter_ㄔ" = "key_bopomofo_ch";
"letter_ㄕ" = "key_bopomofo_sh";
"letter_ㄖ" = "key_bopomofo_r";
"letter_ㄗ" = "key_bopomofo_z";
"letter_ㄘ" = "key_bopomofo_c";
"letter_ㄙ" = "key_bopomofo_s";
/* 韵母 */
"letter_ㄚ" = "key_bopomofo_a";
"letter_ㄛ" = "key_bopomofo_o";
"letter_ㄜ" = "key_bopomofo_e";
"letter_ㄝ" = "key_bopomofo_eh";
"letter_ㄞ" = "key_bopomofo_ai";
"letter_ㄟ" = "key_bopomofo_ei";
"letter_ㄠ" = "key_bopomofo_au";
"letter_ㄡ" = "key_bopomofo_ou";
"letter_ㄢ" = "key_bopomofo_an";
"letter_ㄣ" = "key_bopomofo_en";
"letter_ㄤ" = "key_bopomofo_ang";
"letter_ㄥ" = "key_bopomofo_eng";
"letter_ㄦ" = "key_bopomofo_er";
"letter_ㄧ" = "key_bopomofo_i";
"letter_ㄨ" = "key_bopomofo_u";
"letter_ㄩ" = "key_bopomofo_iu";
/* 声调 */
"letter_ˊ" = "key_bopomofo_tone2";
"letter_ˇ" = "key_bopomofo_tone3";
"letter_ˋ" = "key_bopomofo_tone4";
"letter_˙" = "key_bopomofo_tone5";
/* ========== 数字 ========== */
/* 数字 1 */
"digit_1" = "key_1";
/* 数字 2 */
"digit_2" = "key_2";
/* 数字 3 */
"digit_3" = "key_3";
/* 数字 4 */
"digit_4" = "key_4";
/* 数字 5 */
"digit_5" = "key_5";
/* 数字 6 */
"digit_6" = "key_6";
/* 数字 7 */
"digit_7" = "key_7";
/* 数字 8 */
"digit_8" = "key_8";
/* 数字 9 */
"digit_9" = "key_9";
/* 数字 0 */
"digit_0" = "key_0";
/* ========== 符号 ========== */
/* '-' */
"sym_minus" = "key_minus";
/* '/' */
"sym_slash" = "key_slash";
/* ':' */
"sym_colon" = "key_colon";
/* ';' */
"sym_semicolon" = "key_semicolon";
/* '(' */
"sym_paren_l" = "key_paren_l";
/* ')' */
"sym_paren_r" = "key_paren_r";
/* '$' */
"sym_dollar" = "key_dollar";
/* '&' */
"sym_amp" = "key_amp";
/* '@' */
"sym_at" = "key_at";
/* 双引号 " */
"sym_quote_double" = "key_quote_d";
/* ',' */
"sym_comma" = "key_comma";
/* '、' 顿号 */
"sym_dun" = "key_dun";
/* '.' */
"sym_dot" = "key_dot";
/* '。' 中文句号 */
"sym_chinese_dot" = "key_chinese_dot";
/* '?' */
"sym_question" = "key_question";
/* '!' */
"sym_exclam" = "key_exclam";
/* 单引号 ' */
"sym_quote_single" = "key_quote";
/* '[' */
"sym_bracket_l" = "key_bracket_l";
/* ']' */
"sym_bracket_r" = "key_bracket_r";
/* '{' */
"sym_brace_l" = "key_brace_l";
/* '}' */
"sym_brace_r" = "key_brace_r";
/* '「' */
"sym_corner_l" = "key_corner_l";
/* '」' */
"sym_corner_r" = "key_corner_r";
/* '#' */
"sym_hash" = "key_hash";
/* '%' */
"sym_percent" = "key_percent";
/* '^' */
"sym_caret" = "key_caret";
/* '*' */
"sym_asterisk" = "key_asterisk";
/* '+' */
"sym_plus" = "key_plus";
/* '=' */
"sym_equal" = "key_equal";
/* '_' */
"sym_underscore" = "key_underscore";
/* '\' */
"sym_backslash" = "key_backslash";
/* '|' */
"sym_pipe" = "key_pipe";
/* '~' */
"sym_tilde" = "key_tilde";
/* '<' */
"sym_lt" = "key_lt";
/* '>' */
"sym_gt" = "key_gt";
/* '¥' */
"sym_money" = "key_money";
/* '€' */
"sym_euro" = "key_euro";
/* '£' */
"sym_pound" = "key_pound";
/* '•' */
"sym_bullet" = "key_bullet";
/* '^_^' 笑脸 */
"sym_face" = "key_face";
/* '—' 长横线 */
"sym_emdash" = "key_emdash";
/* '«' 左双尖括号 */
"sym_guillemet_l" = "key_guillemet_l";
/* '»' 右双尖括号 */
"sym_guillemet_r" = "key_guillemet_r";
/* '《' 左书名号 */
"sym_book_title_l" = "key_book_title_l";
/* '》' 右书名号 */
"sym_book_title_r" = "key_book_title_r";
/* '...' 省略号 */
"sym_ellipsis" = "key_ellipsis";
/* ========== 功能键 ========== */
/* 空格键 */
"space" = "key_space";
/* 删除键(⌫) */
"backspace" = "key_del";
/* Shift */
"shift" = "key_up";
/* Shift大写 */
"shift_upper" = "key_up_upper";
/* 字母面板左下角 "123" */
"mode_123" = "key_123";
/* 数字面板左下角 "abc" */
"mode_abc" = "key_拼音";
/* 数字面板内 "123 -> #+=" */
"symbols_toggle_more" = "key_symbols_more";
/* 数字面板内 "#+= -> 123" */
"symbols_toggle_123" = "key_symbols_123";
/* 自定义 AI 功能键 */
"ai" = "key_ai";
/* Emoji功能键 */
"emoji_panel" = "key_emoji";
/* 发送/换行键 */
"return" = "key_send";

Binary file not shown.

View File

@@ -0,0 +1,345 @@
{
"__comment": "注音符号映射表:注音组合 -> 繁体字候选词列表",
"__comment_symbols": "聲母: ㄅㄆㄇㄈㄉㄊㄋㄌㄍㄎㄏㄐㄑㄒㄓㄔㄕㄖㄗㄘㄙ",
"__comment_vowels": "韻母: ㄚㄛㄜㄝㄞㄟㄠㄡㄢㄣㄤㄥㄦㄧㄨㄩ",
"__comment_tones": "聲調: ˊ(二聲) ˇ(三聲) ˋ(四聲) ˙(輕聲), 無符號為一聲",
"mappings": {
"ㄅㄚ": ["八", "巴", "吧", "爸", "拔", "罷", "霸", "扒", "叭", "芭", "疤", "粑"],
"ㄅㄞ": ["白", "百", "拜", "敗", "柏", "擺", "佰", "佰"],
"ㄅㄢ": ["班", "般", "板", "版", "半", "伴", "扮", "拌", "瓣", "頒", "斑", "搬"],
"ㄅㄤ": ["幫", "邦", "榜", "膀", "綁", "棒", "磅", "邦"],
"ㄅㄠ": ["包", "保", "報", "寶", "抱", "暴", "爆", "薄", "豹", "飽", "堡", "刨", "苞", "胞", "雹"],
"ㄅㄟ": ["北", "被", "背", "備", "悲", "杯", "碑", "輩", "倍", "貝", "臂"],
"ㄅㄣ": ["本", "奔", "笨", "盆", "賁"],
"ㄅㄥ": ["崩", "繃", "蹦", "泵", "甭"],
"ㄅㄧ": ["比", "必", "筆", "畢", "避", "閉", "鼻", "彼", "碧", "壁", "弊", "臂", "秘", "辟", "逼", "幣", "庇", "痹", "匕"],
"ㄅㄧㄝ": ["別", "憋", "癟", "鱉"],
"ㄅㄧㄢ": ["變", "便", "邊", "編", "辯", "遍", "鞭", "辨", "扁", "貶", "匾", "蝙"],
"ㄅㄧㄠ": ["表", "標", "彪", "錶", "鏢", "錶", "裱", "婊"],
"ㄅㄧㄣ": ["賓", "彬", "斌", "瀕", "濱", "殯", "鬢"],
"ㄅㄧㄥ": ["病", "並", "冰", "兵", "餅", "柄", "秉", "稟", "炳", "稟"],
"ㄅㄛ": ["波", "博", "播", "伯", "薄", "泊", "柏", "勃", "搏", "撥", "剝", "脖", "卜", "玻", "柏"],
"ㄅㄨ": ["不", "步", "部", "布", "補", "捕", "簿", "卜", "怖", "哺", "埠", "簿"],
"ㄆㄚ": ["趴", "啪", "葩", "扒"],
"ㄆㄞ": ["排", "拍", "牌", "派", "徘", "湃", "俳"],
"ㄆㄢ": ["判", "盤", "盼", "攀", "畔", "胖", "叛", "潘", "磐", "蹣", "拚", "泮"],
"ㄆㄤ": ["旁", "胖", "龐", "膀", "磅", "彷", "螃", "乓"],
"ㄆㄠ": ["跑", "炮", "泡", "拋", "刨", "袍", "咆", "庖", "匏"],
"ㄆㄟ": ["配", "陪", "培", "賠", "佩", "沛", "裴", "胚", "霈"],
"ㄆㄣ": ["噴", "盆"],
"ㄆㄥ": ["朋", "碰", "彭", "棚", "蓬", "鵬", "捧", "烹", "澎", "怦", "砰", "堋"],
"ㄆㄧ": ["皮", "批", "披", "匹", "疲", "僻", "脾", "劈", "琵", "毗", "啤", "坯", "譬", "霹", "屁", "闢", "紕", "闢"],
"ㄆㄧㄝ": ["撇", "瞥", "苤"],
"ㄆㄧㄢ": ["片", "便", "騙", "偏", "篇", "翩", "扁", "諞", "騙"],
"ㄆㄧㄠ": ["票", "飄", "漂", "瓢", "嫖", "縹", "驃", "飄"],
"ㄆㄧㄣ": ["品", "貧", "頻", "聘", "拼", "拚", "嬪"],
"ㄆㄧㄥ": ["平", "評", "憑", "瓶", "萍", "屏", "蘋", "坪", "秤", "娉", "馮", "萍"],
"ㄆㄛ": ["破", "迫", "婆", "頗", "坡", "潑", "泊", "魄", "粕", "朴", "珀", "叵", "鄱"],
"ㄆㄨ": ["普", "鋪", "樸", "譜", "浦", "葡", "蒲", "僕", "撲", "圃", "濮", "璞", "噗", "莆"],
"ㄇㄚ": ["媽", "馬", "麻", "罵", "嘛", "螞", "碼", "瑪", "抹", "摩", "螞"],
"ㄇㄞ": ["買", "賣", "麥", "埋", "邁", "脈", "霾", "賣"],
"ㄇㄢ": ["滿", "慢", "曼", "漫", "蠻", "瞞", "饅", "蔓", "謾", "墁", "幔", "曼"],
"ㄇㄤ": ["忙", "盲", "茫", "芒", "莽", "氓", "硭", "邙"],
"ㄇㄠ": ["貓", "毛", "矛", "茅", "茂", "冒", "帽", "貌", "貿", "卯", "錨", "耄", "髦", "瑁", "懋", "卯"],
"ㄇㄟ": ["沒", "美", "妹", "每", "梅", "媒", "煤", "眉", "霉", "魅", "玫", "枚", "寐", "昧", "媚", "湄", "鎂", "糜", "梅"],
"ㄇㄣ": ["們", "門", "悶", "燜", "捫", "悶"],
"ㄇㄥ": ["夢", "孟", "猛", "蒙", "盟", "萌", "朦", "檬", "懵", "礞", "甍", "萌"],
"ㄇㄧ": ["米", "密", "迷", "蜜", "祕", "眯", "靡", "糜", "彌", "覓", "冪", "泌", "祕", "謎"],
"ㄇㄧㄝ": ["滅", "蔑", "篾", "乜", "咩"],
"ㄇㄧㄢ": ["面", "免", "棉", "眠", "綿", "勉", "緬", "冕", "娩", "湎", "眄", "冕"],
"ㄇㄧㄠ": ["描", "秒", "妙", "廟", "苗", "瞄", "渺", "淼", "緲", "藐", "喵"],
"ㄇㄧㄣ": ["民", "敏", "名", "皿", "閔", "抿", "泯", "憫", "閔", "愍"],
"ㄇㄧㄥ": ["名", "明", "命", "鳴", "銘", "冥", "茗", "溟", "瞑", "螟", "銘"],
"ㄇㄛ": ["麼", "摸", "磨", "摩", "魔", "膜", "默", "墨", "抹", "末", "莫", "漠", "寞", "陌", "謨", "茉", "驀", "歿", "麼"],
"ㄇㄡ": ["某", "謀", "牟", "眸", "繆", "鍪", "哞"],
"ㄇㄨ": ["目", "母", "木", "幕", "牧", "慕", "墓", "暮", "穆", "睦", "沐", "募", "姆", "拇", "牡", "畝", "慕"],
"ㄈㄚ": ["發", "法", "罰", "乏", "伐", "閥", "筏", "佳", "髮", "法"],
"ㄈㄢ": ["反", "飯", "煩", "繁", "範", "犯", "泛", "番", "翻", "凡", "帆", "返", "販", "礬", "釩", "蕃"],
"ㄈㄤ": ["方", "放", "房", "防", "訪", "仿", "芳", "坊", "妨", "紡", "舫", "肪", "仿"],
"ㄈㄟ": ["非", "飛", "費", "肥", "廢", "匪", "誹", "啡", "菲", "沸", "翡", "吠", "肺", "狒", "妃"],
"ㄈㄣ": ["分", "份", "粉", "奮", "憤", "紛", "芬", "墳", "焚", "氛", "糞", "吩", "汾"],
"ㄈㄥ": ["風", "封", "豐", "峰", "鋒", "蜂", "瘋", "逢", "縫", "鳳", "奉", "諷", "楓", "烽", "豐", "峰"],
"ㄈㄛ": ["佛", "彿"],
"ㄈㄡ": ["否", "縫", "缶"],
"ㄈㄨ": ["父", "夫", "付", "服", "福", "府", "負", "富", "復", "副", "婦", "撫", "附", "幅", "浮", "腐", "符", "弗", "腹", "輻", "敷", "氟", "芙", "敷", "伏", "扶", "俘", "袱", "芙", "斧", "脯", "腑", "滏", "蚨", "跗", "馥"],
"ㄉㄚ": ["大", "打", "答", "達", "搭", "塔", "瘩", "妲", "怛", "耷"],
"ㄉㄞ": ["大", "代", "帶", "待", "袋", "戴", "呆", "貸", "逮", "怠", "殆", "黛", "岱", "迨"],
"ㄉㄢ": ["但", "單", "擔", "膽", "丹", "淡", "蛋", "誕", "彈", "旦", "氮", "耽", "憚", "殫", "瘅", "眈"],
"ㄉㄤ": ["當", "黨", "檔", "擋", "蕩", "宕", "檔", "璫", "璫"],
"ㄉㄠ": ["到", "道", "導", "刀", "倒", "島", "盜", "悼", "搗", "禱", "蹈", "叨", "忉", "氘"],
"ㄉㄜ": ["的", "得", "德", "底", "德"],
"ㄉㄥ": ["等", "燈", "登", "鄧", "瞪", "凳", "蹬", "噔", "嶝"],
"ㄉㄧ": ["的", "地", "第", "低", "底", "敵", "弟", "帝", "抵", "遞", "迪", "滴", "堤", "笛", "締", "嫡", "詆", "邸", "砥", "睇", "鏑"],
"ㄉㄧㄝ": ["爹", "跌", "叠", "蝶", "碟", "諜", "迭", "帖", "耋", "牒", "瓞", "鰈"],
"ㄉㄧㄢ": ["點", "電", "店", "典", "墊", "澱", "殿", "顛", "滇", "碘", "巔", "癲", "惦", "奠", "甸", "阽"],
"ㄉㄧㄠ": ["調", "掉", "吊", "雕", "刁", "釣", "凋", "碉", "貂", "雕"],
"ㄉㄧㄥ": ["定", "訂", "頂", "丁", "釘", "盯", "叮", "鼎", "叮", "丁", "町"],
"ㄉㄨ": ["讀", "都", "度", "獨", "毒", "渡", "杜", "肚", "堵", "賭", "鍍", "督", "篤", "嘟", "睹", "妒", "芏"],
"ㄉㄨㄢ": ["段", "斷", "短", "鍛", "緞", "端", "椴", "煅"],
"ㄉㄨㄟ": ["對", "隊", "堆", "兌", "懟", "憝"],
"ㄉㄨㄣ": ["頓", "噸", "盾", "蹲", "敦", "墩", "燉", "鈍", "囤", "遁", "燉"],
"ㄉㄨㄛ": ["多", "度", "奪", "躲", "朵", "墮", "舵", "跺", "惰", "哆", "垛", "躲", "踱", "剁", "咄"],
"ㄊㄚ": ["他", "她", "它", "塔", "踏", "拓", "榻", "獺", "撻", "闒", "遢", "遢"],
"ㄊㄞ": ["太", "台", "臺", "態", "泰", "抬", "胎", "鮐", "薹", "駘", "炱", "邰", "苔", "颱"],
"ㄊㄢ": ["談", "探", "彈", "壇", "攤", "貪", "嘆", "潭", "坦", "毯", "痰", "檀", "譚", "忐", "袒", "郯", "澹", "覃", "忐", "曇", "忐"],
"ㄊㄤ": ["堂", "唐", "糖", "躺", "趟", "湯", "燙", "塘", "膛", "棠", "搪", "螳", "鏜", "鐋", "耥", "鏜"],
"ㄊㄠ": ["套", "逃", "桃", "陶", "討", "濤", "掏", "滔", "萄", "淘", "燾", "絳", "叨", "洮", "啕", "饕"],
"ㄊㄜ": ["特", "忒", "慝", "鋱", "忒"],
"ㄊㄥ": ["疼", "騰", "藤", "滕", "謄", "疼", "滕"],
"ㄊㄧ": ["提", "題", "體", "替", "踢", "梯", "剔", "蹄", "啼", "惕", "涕", "銻", "倜", "悌", "嚏", "醍", "緹"],
"ㄊㄧㄝ": ["鐵", "貼", "帖", "萜", "帖", "餮"],
"ㄊㄧㄢ": ["天", "田", "填", "甜", "添", "恬", "腆", "殄", "忝", "闐", "祆", "忝"],
"ㄊㄧㄠ": ["條", "跳", "調", "挑", "眺", "佻", "祧", "銚", "髫", "鰷", "調", "眺"],
"ㄊㄧㄥ": ["聽", "停", "庭", "挺", "廳", "廷", "亭", "婷", "艇", "汀", "蜓", "霆", "鋌", "莛", "汀"],
"ㄊㄨ": ["圖", "土", "突", "途", "吐", "兔", "屠", "徒", "凸", "禿", "荼", "釷", "菟", "兔"],
"ㄊㄨㄢ": ["團", "摶", "彖", "湍", "摶"],
"ㄊㄨㄟ": ["推", "退", "腿", "蛻", "頹", "褪", "忒"],
"ㄊㄨㄣ": ["吞", "屯", "臀", "囤", "褪", "豚", "吞"],
"ㄊㄨㄛ": ["脫", "托", "拖", "妥", "拓", "唾", "陀", "沱", "坨", "駝", "鴕", "橐", "砣", "佗", "跎", "坨", "酡"],
"ㄋㄚ": ["那", "拿", "哪", "納", "吶", "娜", "鈉", "衲", "鎿"],
"ㄋㄞ": ["奶", "耐", "乃", "奈", "氖", "萘", "鼐", "氖"],
"ㄋㄢ": ["南", "難", "男", "喃", "楠", "赧", "囝", "囡"],
"ㄋㄤ": ["囊", "囔", "餿"],
"ㄋㄠ": ["腦", "惱", "鬧", "撓", "淖", "鐃", "橈", "鬧", "鬧"],
"ㄋㄜ": ["呢", "訥"],
"ㄋㄟ": ["內", "那", "餒"],
"ㄋㄣ": ["嫩", "恁"],
"ㄋㄥ": ["能"],
"ㄋㄧ": ["你", "妳", "呢", "泥", "尼", "擬", "逆", "妮", "霓", "倪", "匿", "溺", "膩", "旎", "昵", "妮"],
"ㄋㄧㄝ": ["捏", "聶", "孽", "躡", "鎳", "囁", "臬", "涅", "孽"],
"ㄋㄧㄢ": ["年", "念", "黏", "碾", "捻", "撚", "蔦", "念", "唸"],
"ㄋㄧㄤ": ["娘", "釀", "釀"],
"ㄋㄧㄠ": ["鳥", "尿", "裊", "嬲", "蔦", "鳥"],
"ㄋㄧㄣ": ["您"],
"ㄋㄧㄥ": ["寧", "凝", "擰", "檸", "獰", "嚀", "甯", "寧"],
"ㄋㄧㄡ": ["牛", "紐", "扭", "鈕", "妞", "拗", "妞"],
"ㄋㄨ": ["女", "努", "怒", "奴", "弩", "胬", "弩"],
"ㄋㄨㄢ": ["暖"],
"ㄋㄨㄣ": ["嫩", "恁"],
"ㄋㄨㄛ": ["挪", "諾", "懦", "糯", "喏", "懦"],
"ㄌㄚ": ["拉", "啦", "蠟", "辣", "臘", "喇", "落", "啦", "邋"],
"ㄌㄞ": ["來", "賴", "萊", "徠", "賚", "賴", "睞"],
"ㄌㄢ": ["藍", "蘭", "攔", "籃", "懶", "爛", "濫", "覽", "欄", "瀾", "嵐", "襤", "懶", "讕"],
"ㄌㄤ": ["浪", "郎", "狼", "廊", "朗", "琅", "螂", "朗", "郎", "閬"],
"ㄌㄠ": ["老", "勞", "落", "牢", "撈", "澇", "絡", "姥", "佬", "潦", "澇", "癆"],
"ㄌㄜ": ["了", "樂", "勒", "肋", "勒", "肋"],
"ㄌㄟ": ["累", "類", "淚", "雷", "勒", "壘", "蕾", "磊", "擂", "鐳", "儡", "勒", "擂"],
"ㄌㄥ": ["冷", "愣", "楞", "冷"],
"ㄌㄧ": ["裡", "力", "理", "利", "立", "離", "例", "歷", "李", "禮", "麗", "勵", "梨", "厘", "莉", "犁", "黎", "璃", "狸", "漓", "罹", "驪", "鱧", "吏", "栗", "俐", "荔", "痢", "裡", "裏", "裡", "吏", "戾", "蠡", "蜊", "悝", "喱"],
"ㄌㄧㄚ": ["倆"],
"ㄌㄧㄝ": ["列", "烈", "獵", "裂", "劣", "咧", "冽", "捩", "躐", "冽", "洌"],
"ㄌㄧㄢ": ["連", "聯", "臉", "練", "蓮", "戀", "煉", "廉", "憐", "漣", "鐮", "斂", "璉", "斂", "斂"],
"ㄌㄧㄤ": ["兩", "亮", "量", "良", "涼", "梁", "糧", "樑", "諒", "晾", "踉", "靚", "倆", "倆", "粱", "量"],
"ㄌㄧㄠ": ["了", "料", "聊", "療", "遼", "撩", "僚", "燎", "繚", "潦", "寥", "嘹", "撩", "鐐", "獠"],
"ㄌㄧㄝ": ["列", "烈", "獵", "裂", "劣", "咧", "冽", "捩", "躐", "獵", "獵"],
"ㄌㄧㄣ": ["林", "臨", "鄰", "淋", "琳", "霖", "鱗", "麟", "遴", "藺", "吝", "躪", "琳", "淋"],
"ㄌㄧㄥ": ["領", "零", "靈", "令", "另", "玲", "鈴", "陵", "嶺", "凌", "菱", "羚", "翎", "聆", "伶", "拎", "凌", "鈴", "鈴"],
"ㄌㄧㄡ": ["六", "流", "留", "劉", "柳", "溜", "琉", "榴", "硫", "鎏", "鷚", "溜", "溜", "鎦"],
"ㄌㄨ": ["路", "錄", "陸", "綠", "露", "旅", "律", "慮", "呂", "履", "侶", "屢", "濾", "氯", "廬", "爐", "蘆", "盧", "顱", "魯", "擼", "祿", "麓", "碌", "陸", "輅", "輅"],
"ㄌㄨㄢ": ["亂", "卵", "巒", "鑾", "鸞", "欒", "鸞", "鑾"],
"ㄌㄨㄣ": ["論", "輪", "倫", "侖", "綸", "淪", "論", "論"],
"ㄌㄨㄛ": ["落", "羅", "洛", "絡", "邏", "鑼", "籮", "駱", "裸", "螺", "蘿", "摞", "囉", "羅", "邏"],
"ㄍㄚ": ["嘎", "噶", "軋", "噶"],
"ㄍㄞ": ["改", "該", "蓋", "概", "溉", "丐", "芥", "鈣", "蓋", "蓋"],
"ㄍㄢ": ["幹", "感", "敢", "甘", "肝", "趕", "桿", "乾", "贛", "柑", "竿", "尴", "擀", "乾", "乾"],
"ㄍㄤ": ["剛", "鋼", "港", "崗", "綱", "岡", "缸", "槓", "扛", "剛", "崗"],
"ㄍㄠ": ["高", "告", "搞", "稿", "糕", "鎬", "膏", "篙", "稿", "稿"],
"ㄍㄜ": ["個", "各", "歌", "格", "哥", "割", "革", "隔", "閣", "葛", "戈", "擱", "鴿", "胳", "骼", "個", "個"],
"ㄍㄟ": ["給"],
"ㄍㄣ": ["跟", "根", "亙", "艮", "跟"],
"ㄍㄥ": ["更", "耕", "庚", "羹", "耿", "梗", "更", "耕"],
"ㄍㄨ": ["古", "故", "顧", "骨", "谷", "股", "鼓", "固", "孤", "姑", "辜", "沽", "咕", "估", "谷", "谷"],
"ㄍㄨㄚ": ["掛", "瓜", "刮", "寡", "呱", "褂", "掛", "掛"],
"ㄍㄨㄞ": ["怪", "乖", "拐", "乖"],
"ㄍㄨㄢ": ["關", "觀", "管", "官", "館", "慣", "灌", "冠", "罐", "貫", "棺", "倌", "觀", "關"],
"ㄍㄨㄤ": ["光", "廣", "逛", "胱", "光", "光"],
"ㄍㄨㄟ": ["貴", "規", "歸", "鬼", "軌", "櫃", "桂", "跪", "龜", "瑰", "詭", "閨", "圭", "桂", "歸"],
"ㄍㄨㄣ": ["滾", "棍", "滾"],
"ㄍㄨㄛ": ["過", "國", "果", "鍋", "郭", "裹", "渦", "過", "過"],
"ㄎㄚ": ["卡", "咖", "喀", "咔", "卡"],
"ㄎㄞ": ["開", "凱", "楷", "慨", "愷", "鎧", "鍇", "開", "凱"],
"ㄎㄢ": ["看", "砍", "坎", "勘", "刊", "堪", "瞰", "龕", "看", "看"],
"ㄎㄤ": ["康", "抗", "扛", "亢", "糠", "慷", "伉", "康", "康"],
"ㄎㄠ": ["考", "靠", "烤", "拷", "栲", "犒", "考", "考"],
"ㄎㄜ": ["可", "客", "科", "刻", "課", "顆", "克", "渴", "柯", "棵", "磕", "咳", "殼", "坷", "可", "可"],
"ㄎㄣ": ["肯", "懇", "啃", "齦", "肯"],
"ㄎㄥ": ["坑", "吭", "鏗", "坑"],
"ㄎㄨ": ["苦", "哭", "庫", "酷", "枯", "窟", "骷", "苦", "苦"],
"ㄎㄨㄚ": ["跨", "誇", "垮", "挎", "胯", "跨", "跨"],
"ㄎㄨㄞ": ["快", "塊", "筷", "儈", "膾", "快", "快"],
"ㄎㄨㄢ": ["寬", "款", "寬"],
"ㄎㄨㄤ": ["況", "礦", "狂", "框", "曠", "眶", "筐", "匡", "誑", "況", "況"],
"ㄎㄨㄟ": ["虧", "愧", "潰", "窺", "葵", "魁", "饋", "匱", "睽", "聵", "虧", "虧"],
"ㄎㄨㄣ": ["困", "昆", "坤", "捆", "琨", "鯤", "困", "困"],
"ㄎㄨㄛ": ["擴", "括", "闊", "廓", "擴", "擴"],
"ㄏㄚ": ["哈", "蛤", "哈"],
"ㄏㄞ": ["還", "海", "害", "孩", "嗨", "亥", "骸", "氦", "海", "海"],
"ㄏㄢ": ["漢", "寒", "汗", "喊", "韓", "旱", "憾", "悍", "翰", "涵", "酣", "憨", "漢", "漢"],
"ㄏㄤ": ["行", "航", "杭", "巷", "夯", "吭", "行", "行"],
"ㄏㄠ": ["好", "號", "豪", "毫", "浩", "耗", "郝", "蒿", "嚎", "壕", "濠", "好", "好"],
"ㄏㄜ": ["和", "合", "河", "何", "核", "賀", "喝", "赫", "褐", "鶴", "荷", "盒", "禾", "嚇", "呵", "和", "和"],
"ㄏㄟ": ["黑", "嘿", "黑"],
"ㄏㄣ": ["很", "狠", "恨", "痕", "很", "很"],
"ㄏㄥ": ["橫", "恆", "衡", "亨", "哼", "橫", "橫"],
"ㄏㄨ": ["湖", "呼", "戶", "虎", "護", "互", "忽", "胡", "壺", "狐", "糊", "弧", "蝴", "乎", "滬", "戶", "戶"],
"ㄏㄨㄚ": ["話", "花", "化", "華", "畫", "劃", "滑", "嘩", "樺", "驊", "花", "花"],
"ㄏㄨㄞ": ["壞", "懷", "槐", "徊", "壞", "壞"],
"ㄏㄨㄢ": ["還", "換", "環", "歡", "緩", "患", "喚", "幻", "煥", "桓", "宦", "渙", "瘓", "歡", "歡"],
"ㄏㄨㄤ": ["黃", "皇", "荒", "慌", "煌", "晃", "謊", "凰", "惶", "簧", "恍", "黃", "黃"],
"ㄏㄨㄟ": ["會", "回", "灰", "輝", "惠", "慧", "繪", "匯", "毀", "悔", "晦", "賄", "穢", "會", "會"],
"ㄏㄨㄣ": ["婚", "魂", "混", "渾", "昏", "葷", "餛", "婚", "婚"],
"ㄏㄨㄛ": ["活", "火", "或", "夥", "獲", "貨", "禍", "惑", "霍", "豁", "鍬", "鑊", "活", "活"],
"ㄐㄧ": ["幾", "機", "己", "記", "計", "集", "基", "際", "極", "擊", "激", "其", "及", "級", "即", "急", "季", "跡", "技", "績", "輯", "籍", "擠", "吉", "雞", "奇", "肌", "饑", "譏", "磯", "姬", "嫉", "棘", "寂", "冀", "驥", "己", "己"],
"ㄐㄧㄚ": ["家", "加", "價", "假", "架", "佳", "甲", "駕", "嘉", "稼", "嫁", "夾", "頰", "戛", "枷", "家", "家"],
"ㄐㄧㄢ": ["見", "間", "建", "件", "簡", "檢", "堅", "健", "漸", "劍", "鍵", "尖", "肩", "艦", "鑒", "剪", "撿", "踐", "賤", "箭", "澗", "濺", "薦", "餞", "諫", "見", "見"],
"ㄐㄧㄤ": ["將", "江", "強", "講", "降", "獎", "疆", "匠", "蔣", "漿", "僵", "薑", "絳", "將", "將"],
"ㄐㄧㄠ": ["叫", "教", "腳", "角", "交", "覺", "較", "焦", "膠", "驕", "澆", "攪", "椒", "嬌", "郊", "蕉", "矯", "絞", "僥", "佼", "叫", "叫"],
"ㄐㄧㄝ": ["接", "節", "街", "結", "解", "姐", "介", "界", "借", "傑", "潔", "截", "揭", "劫", "捷", "睫", "竭", "桔", "戒", "芥", "藉", "拮", "接", "接"],
"ㄐㄧㄣ": ["進", "金", "近", "今", "緊", "盡", "僅", "勁", "錦", "津", "筋", "巾", "斤", "禁", "襟", "瑾", "進", "進"],
"ㄐㄧㄥ": ["經", "精", "景", "警", "靜", "境", "競", "淨", "鏡", "徑", "驚", "京", "晶", "睛", "莖", "荊", "兢", "涇", "憬", "經", "經"],
"ㄐㄧㄡ": ["就", "九", "久", "酒", "舊", "救", "究", "糾", "舅", "揪", "韭", "灸", "玖", "臼", "就", "就"],
"ㄐㄩ": ["句", "具", "據", "局", "舉", "巨", "聚", "居", "距", "懼", "劇", "鋸", "矩", "拒", "俱", "菊", "橘", "颶", "踞", "遽", "句", "句"],
"ㄐㄩㄢ": ["卷", "捐", "圈", "眷", "倦", "娟", "雋", "涓", "鐫", "卷", "卷"],
"ㄐㄩㄝ": ["決", "覺", "絕", "角", "爵", "掘", "倔", "厥", "譎", "獗", "矍", "嚼", "決", "決"],
"ㄐㄩㄣ": ["軍", "君", "均", "俊", "菌", "竣", "鈞", "峻", "雋", "軍", "軍"],
"ㄑㄧ": ["起", "其", "氣", "期", "七", "奇", "妻", "棋", "齊", "旗", "企", "啟", "器", "棄", "汽", "祈", "騎", "豈", "漆", "契", "砌", "琪", "淇", "岐", "祁", "崎", "祺", "臍", "訖", "磧", "起", "起"],
"ㄑㄧㄚ": ["恰", "洽", "卡", "掐", "髂", "袷", "恰", "恰"],
"ㄑㄧㄢ": ["前", "錢", "千", "簽", "遷", "淺", "欠", "牽", "潛", "鉛", "謙", "乾", "嵌", "譴", "倩", "槍", "嗆", "薔", "牆", "強", "搶", "腔", "羌", "嬙", "檣", "鏘", "鏹", "前", "前"],
"ㄑㄧㄠ": ["橋", "瞧", "巧", "敲", "俏", "殼", "竅", "喬", "翹", "峭", "撬", "憔", "譙", "樵", "橋", "橋"],
"ㄑㄧㄝ": ["切", "且", "茄", "怯", "竊", "妾", "愜", "鍥", "伽", "切", "切"],
"ㄑㄧㄣ": ["親", "琴", "勤", "侵", "秦", "欽", "禽", "寢", "沁", "芹", "擒", "噙", "覃", "親", "親"],
"ㄑㄧㄥ": ["情", "請", "清", "青", "輕", "慶", "傾", "頃", "晴", "擎", "卿", "氫", "罄", "磬", "蜻", "鯖", "綮", "情", "情"],
"ㄑㄩ": ["去", "取", "曲", "區", "趣", "娶", "渠", "屈", "驅", "蛆", "軀", "祛", "瞿", "蛐", "麴", "衢", "去", "去"],
"ㄑㄩㄢ": ["全", "權", "圈", "泉", "拳", "犬", "勸", "券", "詮", "痊", "銓", "蜷", "顴", "全", "全"],
"ㄑㄩㄝ": ["確", "卻", "缺", "雀", "鵲", "闕", "瘸", "榷", "愨", "確", "確"],
"ㄑㄩㄣ": ["群", "裙", "逡", "群", "群"],
"ㄒㄧ": ["西", "系", "息", "希", "席", "習", "細", "喜", "戲", "洗", "惜", "稀", "溪", "錫", "析", "膝", "襲", "昔", "熙", "夕", "兮", "悉", "熄", "嬉", "汐", "犀", "烯", "曦", "奚", "唏", "淅", "嘻", "樨", "蠡", "璽", "徙", "隙", "餼", "覡", "西", "西"],
"ㄒㄧㄚ": ["下", "夏", "嚇", "廈", "峽", "蝦", "瞎", "霞", "轄", "俠", "暇", "遐", "瑕", "匣", "黠", "硤", "罅", "下", "下"],
"ㄒㄧㄢ": ["先", "現", "線", "限", "縣", "顯", "險", "鮮", "獻", "賢", "閒", "仙", "鹹", "羨", "陷", "憲", "餡", "掀", "纖", "閑", "涎", "嫻", "銜", "冼", "燹", "蜆", "筧", "薟", "躚", "先", "先"],
"ㄒㄧㄤ": ["想", "向", "相", "鄉", "香", "響", "享", "像", "象", "項", "巷", "降", "箱", "祥", "湘", "詳", "翔", "襄", "鑲", "廂", "驤", "薌", "餉", "緗", "嚮", "想", "想"],
"ㄒㄧㄠ": ["小", "笑", "效", "消", "校", "銷", "曉", "蕭", "肖", "削", "孝", "宵", "硝", "霄", "淆", "嘯", "驍", "梟", "瀟", "簫", "筱", "嘵", "蟰", "小", "小"],
"ㄒㄧㄝ": ["些", "寫", "謝", "協", "鞋", "血", "歇", "斜", "脅", "諧", "攜", "洩", "卸", "懈", "蟹", "邪", "械", "屑", "偕", "褻", "榭", "廨", "瀣", "薤", "躞", "頡", "擷", "些", "些"],
"ㄒㄧㄣ": ["新", "心", "信", "辛", "欣", "薪", "馨", "鑫", "芯", "鋅", "昕", "忻", "歆", "鐔", "囟", "新", "新"],
"ㄒㄧㄥ": ["行", "星", "形", "性", "姓", "興", "刑", "型", "幸", "杏", "腥", "猩", "邢", "悻", "滎", "餳", "行", "行"],
"ㄒㄩ": ["須", "需", "許", "續", "序", "徐", "虛", "緒", "蓄", "敘", "旭", "恤", "墟", "絮", "婿", "栩", "戌", "詡", "洫", "溆", "酗", "糈", "勖", "昫", "盱", "蓿", "須", "須"],
"ㄒㄩㄢ": ["選", "宣", "懸", "旋", "玄", "軒", "喧", "炫", "渲", "萱", "漩", "璇", "癬", "煊", "諼", "鋗", "選", "選"],
"ㄒㄩㄝ": ["學", "雪", "血", "穴", "謔", "噱", "鱈", "學", "學"],
"ㄒㄩㄣ": ["訊", "迅", "尋", "巡", "訓", "詢", "循", "旬", "熏", "勳", "薰", "潯", "馴", "汛", "遜", "殉", "徇", "巽", "塤", "曛", "窯", "鱘", "訊", "訊"],
"ㄓㄚ": ["炸", "紮", "查", "渣", "扎", "眨", "柵", "詐", "乍", "榨", "吒", "砟", "蚱", "齇", "鮓", "醡", "炸", "炸"],
"ㄓㄞ": ["債", "寨", "齋", "摘", "窄", "翟", "瘵", "齋", "齋"],
"ㄓㄢ": ["站", "展", "戰", "佔", "斬", "瞻", "沾", "詹", "盞", "嶄", "湛", "綻", "輾", "搌", "旃", "站", "站"],
"ㄓㄤ": ["長", "張", "章", "掌", "丈", "帳", "仗", "脹", "障", "彰", "漳", "璋", "嶂", "幛", "瘴", "鄣", "張", "張"],
"ㄓㄠ": ["找", "照", "招", "朝", "趙", "兆", "罩", "肇", "詔", "沼", "爪", "召", "昭", "嘲", "濯", "櫂", "笊", "招", "招"],
"ㄓㄜ": ["這", "著", "者", "折", "哲", "蔗", "遮", "轍", "浙", "褶", "蟄", "鷓", "謫", "輒", "晢", "蜇", "這", "這"],
"ㄓㄣ": ["真", "針", "鎮", "陣", "珍", "震", "振", "診", "枕", "斟", "甄", "臻", "疹", "砧", "貞", "偵", "軫", "縝", "榛", "楨", "賑", "禎", "畛", "圳", "蓁", "真", "真"],
"ㄓㄥ": ["正", "政", "整", "爭", "證", "鄭", "征", "蒸", "掙", "睜", "錚", "崢", "箏", "怔", "拯", "鉦", "幀", "諍", "癥", "正", "正"],
"ㄓㄨ": ["主", "住", "注", "著", "助", "築", "逐", "祝", "豬", "珠", "朱", "諸", "竹", "株", "燭", "矚", "駐", "鑄", "煮", "拄", "囑", "佇", "杼", "渚", "瀦", "躅", "櫫", "褚", "苧", "洙", "麈", "瘃", "主", "主"],
"ㄓㄨㄚ": ["抓", "爪", "抓"],
"ㄓㄨㄞ": ["轉", "拽", "轉"],
"ㄓㄨㄢ": ["專", "轉", "傳", "賺", "磚", "撰", "篆", "饌", "顓", "專", "專"],
"ㄓㄨㄤ": ["裝", "狀", "莊", "撞", "壯", "幢", "妝", "樁", "裝", "裝"],
"ㄓㄨㄟ": ["追", "墜", "綴", "贅", "縋", "惴", "騅", "追", "追"],
"ㄓㄨㄣ": ["準", "諄", "肫", "窀", "準", "準"],
"ㄓㄨㄛ": ["著", "桌", "捉", "卓", "濁", "灼", "酌", "拙", "琢", "茁", "擢", "倬", "涿", "浞", "禚", "斫", "桌", "桌"],
"ㄔㄚ": ["查", "茶", "差", "插", "察", "剎", "叉", "岔", "詫", "差", "差"],
"ㄔㄞ": ["差", "拆", "柴", "豺", "差"],
"ㄔㄢ": ["產", "纏", "禪", "蟬", "鏟", "闡", "顫", "摻", "潺", "產", "產"],
"ㄔㄤ": ["長", "常", "場", "唱", "廠", "昌", "倡", "嘗", "腸", "暢", "償", "長", "長"],
"ㄔㄠ": ["超", "朝", "潮", "吵", "炒", "抄", "鈔", "巢", "嘲", "超", "超"],
"ㄔㄜ": ["車", "徹", "撤", "扯", "澈", "車", "車"],
"ㄔㄣ": ["陳", "晨", "沉", "趁", "襯", "臣", "塵", "辰", "忱", "陳", "陳"],
"ㄔㄥ": ["成", "城", "程", "稱", "承", "誠", "乘", "撐", "橙", "呈", "懲", "成", "成"],
"ㄔㄨ": ["出", "處", "初", "除", "書", "楚", "觸", "儲", "廚", "畜", "鋤", "出", "出"],
"ㄔㄨㄞ": ["揣", "踹", "揣"],
"ㄔㄨㄢ": ["傳", "穿", "船", "川", "串", "喘", "釧", "傳", "傳"],
"ㄔㄨㄤ": ["床", "窗", "創", "闖", "幢", "床", "床"],
"ㄔㄨㄟ": ["吹", "垂", "錘", "捶", "炊", "吹", "吹"],
"ㄔㄨㄣ": ["春", "純", "唇", "淳", "醇", "春", "春"],
"ㄔㄨㄛ": ["戳", "綽", "輟", "齪", "戳"],
"ㄕㄚ": ["殺", "沙", "紗", "傻", "啥", "煞", "莎", "杉", "剎", "砂", "痧", "裟", "鎩", "霎", "殺", "殺"],
"ㄕㄞ": ["曬", "篩", "色", "曬", "曬"],
"ㄕㄢ": ["山", "善", "閃", "衫", "扇", "杉", "刪", "珊", "柵", "膳", "擅", "贍", "汕", "潸", "姍", "煽", "跚", "訕", "疝", "鱔", "山", "山"],
"ㄕㄤ": ["上", "商", "傷", "尚", "賞", "裳", "熵", "觴", "殤", "垧", "上", "上"],
"ㄕㄠ": ["少", "燒", "紹", "稍", "勺", "哨", "韶", "捎", "梢", "芍", "苕", "蛸", "筲", "少", "少"],
"ㄕㄜ": ["社", "設", "射", "蛇", "舌", "捨", "涉", "赦", "攝", "奢", "賒", "麝", "懾", "灄", "社", "社"],
"ㄕㄣ": ["身", "深", "神", "什", "申", "伸", "審", "慎", "腎", "滲", "沈", "參", "甚", "嬸", "砷", "莘", "哂", "瀋", "糝", "身", "身"],
"ㄕㄥ": ["生", "聲", "勝", "升", "省", "聖", "盛", "剩", "繩", "笙", "甥", "晟", "生", "生"],
"ㄕㄨ": ["書", "數", "樹", "輸", "術", "述", "叔", "屬", "暑", "署", "鼠", "束", "疏", "舒", "淑", "梳", "抒", "殊", "蔬", "孰", "贖", "熟", "恕", "庶", "墅", "俞", "澍", "紓", "倏", "毹", "書", "書"],
"ㄕㄨㄚ": ["刷", "耍", "唰", "刷", "刷"],
"ㄕㄨㄞ": ["帥", "率", "摔", "甩", "蟀", "帥", "帥"],
"ㄕㄨㄢ": ["栓", "拴", "閂", "涮", "栓", "栓"],
"ㄕㄨㄤ": ["雙", "爽", "霜", "孀", "雙", "雙"],
"ㄕㄨㄟ": ["水", "說", "稅", "睡", "誰", "水", "水"],
"ㄕㄨㄣ": ["順", "瞬", "舜", "吮", "順", "順"],
"ㄕㄨㄛ": ["說", "數", "碩", "朔", "爍", "鑠", "蒴", "搠", "說", "說"],
"ㄖㄢ": ["然", "燃", "染", "冉", "髯", "蚺", "然", "然"],
"ㄖㄤ": ["讓", "嚷", "壤", "攘", "穰", "瓤", "讓", "讓"],
"ㄖㄠ": ["擾", "繞", "饒", "嬈", "橈", "蕘", "擾", "擾"],
"ㄖㄜ": ["熱", "惹", "喏", "熱", "熱"],
"ㄖㄣ": ["人", "認", "任", "仁", "忍", "刃", "韌", "紉", "妊", "葚", "稔", "人", "人"],
"ㄖㄥ": ["仍", "扔", "仍", "仍"],
"ㄖㄨ": ["如", "入", "儒", "乳", "辱", "孺", "茹", "蠕", "嚅", "濡", "縟", "洳", "如", "如"],
"ㄖㄨㄢ": ["軟", "阮", "軟", "軟"],
"ㄖㄨㄟ": ["瑞", "銳", "蕊", "芮", "蚋", "枘", "瑞", "瑞"],
"ㄖㄨㄣ": ["潤", "閏", "潤", "潤"],
"ㄖㄨㄛ": ["若", "弱", "偌", "箬", "蒻", "若", "若"],
"ㄗㄚ": ["雜", "砸", "咂", "拶", "雜", "雜"],
"ㄗㄞ": ["在", "再", "載", "災", "宰", "栽", "崽", "哉", "在", "在"],
"ㄗㄢ": ["咱", "讚", "暫", "拶", "昝", "簪", "糌", "咱", "咱"],
"ㄗㄤ": ["藏", "臟", "葬", "臧", "奘", "駔", "臟", "臟"],
"ㄗㄠ": ["早", "造", "遭", "燥", "澡", "藻", "棗", "躁", "鑿", "蚤", "皁", "竈", "早", "早"],
"ㄗㄜ": ["則", "責", "擇", "澤", "側", "仄", "迮", "幘", "賾", "箦", "則", "則"],
"ㄗㄟ": ["賊", "賊", "賊"],
"ㄗㄣ": ["怎", "譖", "怎", "怎"],
"ㄗㄥ": ["增", "贈", "憎", "甑", "繒", "罾", "增", "增"],
"ㄗㄨ": ["租", "族", "組", "阻", "卒", "俎", "詛", "菹", "祖", "祖"],
"ㄗㄨㄢ": ["鑽", "纂", "攢", "繵", "躜", "鑽", "鑽"],
"ㄗㄨㄟ": ["最", "罪", "嘴", "醉", "蕞", "最", "最"],
"ㄗㄨㄣ": ["尊", "遵", "樽", "撙", "尊", "尊"],
"ㄗㄨㄛ": ["做", "作", "座", "左", "昨", "佐", "琢", "撮", "唑", "嘬", "怍", "祚", "胙", "做", "做"],
"ㄘㄚ": ["擦", "嚓", "擦", "擦"],
"ㄘㄞ": ["才", "材", "才", "財", "采", "彩", "菜", "猜", "裁", "踩", "才", "才"],
"ㄘㄢ": ["參", "餐", "殘", "慘", "燦", "蠶", "參", "參"],
"ㄘㄤ": ["藏", "倉", "蒼", "艙", "藏", "藏"],
"ㄘㄠ": ["草", "操", "曹", "糙", "槽", "草", "草"],
"ㄘㄜ": ["策", "測", "側", "廁", "冊", "策", "策"],
"ㄘㄥ": ["層", "曾", "蹭", "層", "層"],
"ㄘㄨ": ["粗", "促", "醋", "簇", "猝", "粗", "粗"],
"ㄘㄨㄢ": ["竄", "攢", "篡", "竄", "竄"],
"ㄘㄨㄟ": ["催", "脆", "翠", "粹", "崔", "淬", "萃", "催", "催"],
"ㄘㄨㄣ": ["村", "存", "寸", "磋", "村", "村"],
"ㄘㄨㄛ": ["錯", "措", "搓", "磋", "挫", "錯", "錯"],
"ㄙㄚ": ["撒", "灑", "薩", "卅", "颯", "撒", "撒"],
"ㄙㄞ": ["賽", "塞", "腮", "鰓", "噻", "賽", "賽"],
"ㄙㄢ": ["三", "散", "傘", "參", "霰", "三", "三"],
"ㄙㄤ": ["喪", "桑", "嗓", "顙", "搡", "喪", "喪"],
"ㄙㄠ": ["掃", "嫂", "騷", "搔", "瘙", "繅", "掃", "掃"],
"ㄙㄜ": ["色", "塞", "瑟", "澀", "嗇", "穡", "色", "色"],
"ㄙㄣ": ["森", "森", "森"],
"ㄙㄥ": ["僧", "僧", "僧"],
"ㄙㄨ": ["速", "素", "蘇", "訴", "俗", "塑", "溯", "宿", "粟", "夙", "簌", "愫", "嗉", "謖", "速", "速"],
"ㄙㄨㄢ": ["算", "酸", "蒜", "狻", "算", "算"],
"ㄙㄨㄟ": ["隨", "歲", "雖", "碎", "遂", "穗", "隧", "髓", "祟", "綏", "邃", "燧", "謁", "隨", "隨"],
"ㄙㄨㄣ": ["損", "孫", "筍", "遜", "榫", "蓀", "猻", "損", "損"],
"ㄙㄨㄛ": ["所", "鎖", "索", "縮", "瑣", "嗦", "唆", "梭", "嗩", "娑", "蓑", "所", "所"],
"ㄧㄚ": ["呀", "壓", "牙", "亞", "雅", "鴨", "押", "芽", "涯", "訝", "崖", "啞", "衙", "軋", "蚜", "睚", "痖", "呀", "呀"],
"ㄧㄞ": ["涯", "崖", "睚", "涯"],
"ㄧㄢ": ["言", "研", "眼", "嚴", "演", "驗", "煙", "顏", "鹽", "延", "沿", "燕", "宴", "炎", "掩", "衍", "岩", "艷", "雁", "焰", "厭", "彥", "諺", "堰", "硯", "嫣", "閻", "焉", "淹", "偃", "儼", "兗", "讌", "讞", "筵", "蜓", "鼴", "罨", "剡", "鄢", "閆", "滟", "妍", "琰", "罳", "言", "言"],
"ㄧㄤ": ["樣", "陽", "洋", "養", "央", "揚", "羊", "氧", "仰", "癢", "漾", "殃", "秧", "恙", "颺", "煬", "佯", "瘍", "鞅", "樣", "樣"],
"ㄧㄠ": ["要", "藥", "搖", "遙", "腰", "邀", "耀", "瑤", "姚", "咬", "堯", "鑰", "謠", "夭", "妖", "窯", "杳", "舀", "徭", "珧", "軺", "銚", "鰩", "么", "瘧", "要", "要"],
"ㄧㄝ": ["也", "業", "夜", "葉", "爺", "野", "液", "謁", "頁", "邪", "掖", "曳", "腋", "噎", "鄴", "曄", "燁", "鐺", "也", "也"],
"ㄧㄣ": ["因", "音", "引", "銀", "印", "飲", "隱", "陰", "吟", "尹", "殷", "茵", "蔭", "垠", "夤", "齦", "湮", "氤", "胤", "鄞", "喑", "洇", "狺", "因", "因"],
"ㄧㄥ": ["應", "英", "營", "迎", "影", "贏", "硬", "映", "盈", "穎", "瑩", "鷹", "嬰", "櫻", "瀛", "蠅", "嬴", "罌", "縈", "楹", "熒", "螢", "瀅", "瓔", "鸚", "膺", "瀠", "應", "應"],
"ㄨㄚ": ["挖", "哇", "蛙", "瓦", "娃", "襪", "凹", "媧", "佤", "腽", "挖", "挖"],
"ㄨㄞ": ["外", "歪", "崴", "外", "外"],
"ㄨㄢ": ["完", "晚", "玩", "碗", "彎", "灣", "丸", "婉", "腕", "惋", "宛", "蜿", "豌", "莞", "綰", "剜", "完", "完"],
"ㄨㄤ": ["王", "往", "忘", "亡", "望", "網", "旺", "汪", "妄", "罔", "惘", "輞", "尪", "王", "王"],
"ㄨㄟ": ["為", "位", "未", "委", "圍", "唯", "威", "偉", "危", "尾", "微", "維", "違", "胃", "餵", "味", "慰", "魏", "衛", "畏", "萎", "偽", "娓", "惟", "巍", "緯", "煒", "韋", "薇", "帷", "渭", "猬", "闈", "洧", "沩", "為", "為"],
"ㄨㄣ": ["問", "文", "聞", "溫", "穩", "紋", "吻", "蚊", "雯", "紊", "刎", "璺", "問", "問"],
"ㄨㄥ": ["翁", "嗡", "甕", "蓊", "翁", "翁"],
"ㄩㄢ": ["元", "原", "員", "圓", "院", "源", "遠", "願", "緣", "園", "怨", "冤", "援", "袁", "淵", "猿", "轅", "媛", "垣", "沅", "塬", "圜", "鴛", "鳶", "螈", "爰", "瑗", "掾", "元", "元"],
"ㄩㄝ": ["月", "約", "越", "樂", "曰", "閱", "躍", "悅", "岳", "粵", "鑰", "櫟", "鉞", "瀹", "龠", "刖", "軏", "月", "月"],
"ㄩㄣ": ["雲", "運", "員", "韻", "勻", "允", "孕", "蘊", "暈", "隕", "耘", "紜", "慍", "殞", "惲", "醞", "狁", "鄖", "雲", "雲"],
"ㄦ": ["二", "兒", "耳", "而", "爾", "餌", "洱", "貳", "兒", "兒"]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,80 @@
{
"__comment": "长按字符变体映射languages.<lang>.<baseChar> = 变体数组(第一个建议为 baseChar 本身)。默认只配置小写;大写由代码自动派生。",
"languages": {
"common": {
"__comment": "通用符号长按变体(适用于所有语言)。如需语言特化(西语 ¿/¡ 等),在对应语言下覆盖同名 key 即可。",
"-": ["-", "", "—", ""],
"/": ["/", "\\"],
":": [":", ""],
";": [";", ""],
"(": ["(", "", "[", "{", "<"],
")": [")", "", "]", "}", ">"],
".": [".", "…", "..."],
",": [",", ""],
"\"": ["\"", "“", "”"],
"“": ["“", "”", "\""],
"'": ["'", "", ""],
"": ["", "", "'"],
"?": ["?", ""],
"!": ["!", ""],
"_": ["_", "—"],
"\\": ["\\", "|"],
"|": ["|", "¦"],
"~": ["~", ""],
"<": ["<", "«", ""],
">": [">", "»", ""],
"#": ["#", "№"],
"%": ["%", "‰"],
"*": ["*", "•", "·"],
"+": ["+", "±"],
"=": ["=", "≠", "≈"],
"·": ["·", "•"],
"$": ["$", "€", "£", "¥", "₩"],
"€": ["€", "$", "£", "¥"],
"¥": ["¥", "¥", "$", "€", "£"],
"¥": ["¥", "¥", "$", "€", "£"],
"0": ["0", "°"],
"1": ["1", "¹"],
"2": ["2", "²"],
"3": ["3", "³"]
},
"en": {
"__comment": "英文(通用拉丁增强):用于输入外来词/人名等。仅配置小写;大写自动派生。",
"a": ["a", "à", "á", "â", "ä", "æ", "ã", "å", "ā"],
"c": ["c", "ç"],
"e": ["e", "è", "é", "ê", "ë", "ē", "ė", "ę"],
"i": ["i", "ì", "í", "î", "ï", "ī", "į"],
"n": ["n", "ñ"],
"o": ["o", "ò", "ó", "ô", "ö", "œ", "õ", "ø", "ō"],
"u": ["u", "ù", "ú", "û", "ü", "ū"],
"y": ["y", "ÿ"]
},
"pt": {
"a": ["a", "á", "à", "â", "ã", "ä"],
"e": ["e", "é", "è", "ê", "ë"],
"i": ["i", "í", "ì", "î", "ï"],
"o": ["o", "ó", "ò", "ô", "õ", "ö"],
"u": ["u", "ú", "ù", "û", "ü"],
"c": ["c", "ç"]
},
"es": {
"a": ["a", "á"],
"e": ["e", "é"],
"i": ["i", "í"],
"o": ["o", "ó"],
"u": ["u", "ú", "ü"],
"n": ["n", "ñ"],
"?": ["?", "¿"],
"!": ["!", "¡"]
},
"zh-hant-pinyin": {
"__comment": "繁体拼音长按元音输出声调字符v 用于 ü / ǖǘǚǜ(常见拼音输入习惯)",
"a": ["a", "ā", "á", "ǎ", "à"],
"e": ["e", "ē", "é", "ě", "è"],
"i": ["i", "ī", "í", "ǐ", "ì"],
"o": ["o", "ō", "ó", "ǒ", "ò"],
"u": ["u", "ū", "ú", "ǔ", "ù", "ü"],
"v": ["v", "ü", "ǖ", "ǘ", "ǚ", "ǜ"]
}
}
}

View File

@@ -189,9 +189,321 @@
},
{
"__comment": "字母第二行 asdfghjkl",
"align": "center",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 0,
"insetLeft": 23,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 0,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"letter:a", "letter:s", "letter:d", "letter:f", "letter:g",
"letter:h", "letter:j", "letter:k", "letter:l"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "字母第三行:左 shift中间字母右 backspace",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"segments": {
"__comment": "分段布局left/center/right",
"left": [
{ "id": "shift", "width": "controlWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.controlWidth" }
],
"__comment_left": "左侧固定按钮",
"center": [
"letter:z", "letter:x", "letter:c", "letter:v", "letter:b", "letter:n", "letter:m"
],
"__comment_center": "中间字母键集合,整体居中",
"right": [
{ "id": "backspace", "width": "controlWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.controlWidth" }
],
"__comment_right": "右侧固定按钮"
}
},
{
"__comment": "字母第四行123/emoji/space/send",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"mode_123", "emoji", "space", "send"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
}
]
},
"letters_es": {
"__comment": "西班牙语布局QWERTY",
"rows": [
{
"__comment": "字母第一行 qwertyuiop",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"letter:q", "letter:w", "letter:e", "letter:r", "letter:t",
"letter:y", "letter:u", "letter:i", "letter:o", "letter:p"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "字母第二行 asdfghjkl",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"letter:a", "letter:s", "letter:d", "letter:f", "letter:g",
"letter:h", "letter:j", "letter:k", "letter:l"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "字母第三行:左 shift中间字母右 backspace",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"segments": {
"__comment": "分段布局left/center/right",
"left": [
{ "id": "shift", "width": "controlWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.controlWidth" }
],
"__comment_left": "左侧固定按钮",
"center": [
"letter:z", "letter:x", "letter:c", "letter:v", "letter:b", "letter:n", "letter:m"
],
"__comment_center": "中间字母键集合,整体居中",
"right": [
{ "id": "backspace", "width": "controlWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.controlWidth" }
],
"__comment_right": "右侧固定按钮"
}
},
{
"__comment": "字母第四行123/emoji/space/send",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"mode_123", "emoji", "space", "send"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
}
]
},
"letters_id": {
"__comment": "印度尼西亚语布局QWERTY",
"rows": [
{
"__comment": "字母第一行 qwertyuiop",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"letter:q", "letter:w", "letter:e", "letter:r", "letter:t",
"letter:y", "letter:u", "letter:i", "letter:o", "letter:p"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "字母第二行 asdfghjkl",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 23,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 0,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"letter:a", "letter:s", "letter:d", "letter:f", "letter:g",
"letter:h", "letter:j", "letter:k", "letter:l"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "字母第三行:左 shift中间字母右 backspace",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"segments": {
"__comment": "分段布局left/center/right",
"left": [
{ "id": "shift", "width": "controlWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.controlWidth" }
],
"__comment_left": "左侧固定按钮",
"center": [
"letter:z", "letter:x", "letter:c", "letter:v", "letter:b", "letter:n", "letter:m"
],
"__comment_center": "中间字母键集合,整体居中",
"right": [
{ "id": "backspace", "width": "controlWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.controlWidth" }
],
"__comment_right": "右侧固定按钮"
}
},
{
"__comment": "字母第四行123/emoji/space/send",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"mode_123", "emoji", "space", "send"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
}
]
},
"letters_pt": {
"__comment": "葡萄牙语布局QWERTY",
"rows": [
{
"__comment": "字母第一行 qwertyuiop",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"letter:q", "letter:w", "letter:e", "letter:r", "letter:t",
"letter:y", "letter:u", "letter:i", "letter:o", "letter:p"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "字母第二行 asdfghjkl",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 23,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 0,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"letter:a", "letter:s", "letter:d", "letter:f", "letter:g",
"letter:h", "letter:j", "letter:k", "letter:l"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "字母第三行:左 shift中间字母右 backspace",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"segments": {
"__comment": "分段布局left/center/right",
"left": [
{ "id": "shift", "width": "controlWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.controlWidth" }
],
"__comment_left": "左侧固定按钮",
"center": [
"letter:z", "letter:x", "letter:c", "letter:v", "letter:b", "letter:n", "letter:m"
],
"__comment_center": "中间字母键集合,整体居中",
"right": [
{ "id": "backspace", "width": "controlWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.controlWidth" }
],
"__comment_right": "右侧固定按钮"
}
},
{
"__comment": "字母第四行123/emoji/space/send",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"mode_123", "emoji", "space", "send"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
}
]
},
"letters_zh_hant_pinyin": {
"__comment": "繁体拼音布局QWERTY",
"rows": [
{
"__comment": "字母第一行 qwertyuiop",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"letter:q", "letter:w", "letter:e", "letter:r", "letter:t",
"letter:y", "letter:u", "letter:i", "letter:o", "letter:p"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "字母第二行 asdfghjkl",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 23,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 0,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
@@ -409,6 +721,228 @@
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
}
]
},
"letters_azerty": {
"__comment": "AZERTY 布局(法语)- 下个版本启用",
"rows": [
{
"__comment": "第一行 azertyuiop",
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": [
"letter:a", "letter:z", "letter:e", "letter:r", "letter:t",
"letter:y", "letter:u", "letter:i", "letter:o", "letter:p"
]
},
{
"__comment": "第二行 qsdfghjklm",
"align": "center",
"insetLeft": 0,
"insetRight": 0,
"gap": 5,
"items": [
"letter:q", "letter:s", "letter:d", "letter:f", "letter:g",
"letter:h", "letter:j", "letter:k", "letter:l", "letter:m"
]
},
{
"__comment": "第三行shift + wxcvbn + backspace",
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"segments": {
"left": [
{ "id": "shift", "width": "controlWidth" }
],
"center": [
"letter:w", "letter:x", "letter:c", "letter:v", "letter:b", "letter:n"
],
"right": [
{ "id": "backspace", "width": "controlWidth" }
]
}
},
{
"__comment": "第四行123/emoji/space/send",
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": [
"mode_123", "emoji", "space", "send"
]
}
]
},
"letters_qwertz": {
"__comment": "QWERTZ 布局(德语)- 下个版本启用",
"rows": [
{
"__comment": "第一行 qwertzuiop",
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": [
"letter:q", "letter:w", "letter:e", "letter:r", "letter:t",
"letter:z", "letter:u", "letter:i", "letter:o", "letter:p"
]
},
{
"__comment": "第二行 asdfghjkl",
"align": "center",
"insetLeft": 0,
"insetRight": 0,
"gap": 5,
"items": [
"letter:a", "letter:s", "letter:d", "letter:f", "letter:g",
"letter:h", "letter:j", "letter:k", "letter:l"
]
},
{
"__comment": "第三行shift + yxcvbnm + backspace",
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"segments": {
"left": [
{ "id": "shift", "width": "controlWidth" }
],
"center": [
"letter:y", "letter:x", "letter:c", "letter:v", "letter:b", "letter:n", "letter:m"
],
"right": [
{ "id": "backspace", "width": "controlWidth" }
]
}
},
{
"__comment": "第四行123/emoji/space/send",
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": [
"mode_123", "emoji", "space", "send"
]
}
]
},
"letters_bopomofo_full": {
"__comment": "繁体注音全键盘布局iOS 标准注音排列)",
"__comment_layout": "第一行:ㄅㄉˇˋㄓˊ˙ㄚㄞㄢㄦ | 第二行:ㄆㄊㄍㄐㄔㄗㄧㄛㄟㄣ | 第三行:ㄇㄋㄎㄑㄕㄘㄨㄜㄠㄤ | 第四行:ㄈㄌㄏㄒㄖㄙㄩㄝㄡㄥ",
"rowSpacing": 3,
"topInset": 5,
"bottomInset": 0,
"rows": [
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 6,
"items": [
"letter:ㄅ", "letter:ㄉ", "letter:ˇ", "letter:ˋ", "letter:ㄓ",
"letter:ˊ", "letter:˙", "letter:ㄚ", "letter:ㄞ", "letter:ㄢ", "letter:ㄦ"
]
},
{
"align": "left",
"insetLeft": 15,
"insetRight": 4,
"gap": 5,
"items": [
"letter:ㄆ", "letter:ㄊ", "letter:ㄍ", "letter:ㄐ", "letter:ㄔ",
"letter:ㄗ", "letter:ㄧ", "letter:ㄛ", "letter:ㄟ", "letter:ㄣ"
]
},
{
"align": "left",
"insetLeft": 27,
"insetRight": 4,
"gap": 5,
"items": [
"letter:ㄇ", "letter:ㄋ", "letter:ㄎ", "letter:ㄑ", "letter:ㄕ",
"letter:ㄘ", "letter:ㄨ", "letter:ㄜ", "letter:ㄠ", "letter:ㄤ"
]
},
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": [
"letter:ㄈ", "letter:ㄌ", "letter:ㄏ", "letter:ㄒ", "letter:ㄖ",
"letter:ㄙ", "letter:ㄩ", "letter:ㄝ", "letter:ㄡ", "letter:ㄥ", "backspace"
]
},
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": [
"mode_123", "emoji", "space", "send"
]
}
]
},
"letters_bopomofo_standard": {
"__comment": "繁体注音标准布局(与全键盘相同)",
"rows": [
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": [
"letter:ㄅ", "letter:ㄉ", "letter:ˇ", "letter:ˋ", "letter:ㄓ",
"letter:ˊ", "letter:˙", "letter:ㄚ", "letter:ㄞ", "letter:ㄢ", "letter:ㄦ"
]
},
{
"align": "left",
"insetLeft": 15,
"insetRight": 4,
"gap": 5,
"items": [
"letter:ㄆ", "letter:ㄊ", "letter:ㄍ", "letter:ㄐ", "letter:ㄔ",
"letter:ㄗ", "letter:ㄧ", "letter:ㄛ", "letter:ㄟ", "letter:ㄣ"
]
},
{
"align": "left",
"insetLeft": 27,
"insetRight": 4,
"gap": 5,
"items": [
"letter:ㄇ", "letter:ㄋ", "letter:ㄎ", "letter:ㄑ", "letter:ㄕ",
"letter:ㄘ", "letter:ㄨ", "letter:ㄜ", "letter:ㄠ", "letter:ㄤ"
]
},
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": [
"letter:ㄈ", "letter:ㄌ", "letter:ㄏ", "letter:ㄒ", "letter:ㄖ",
"letter:ㄙ", "letter:ㄩ", "letter:ㄝ", "letter:ㄡ", "letter:ㄥ", "backspace"
]
},
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": [
"mode_123", "emoji", "space", "send"
]
}
]
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,405 @@
{
"__comment": "繁体拼音映射表:拼音 -> 繁体字候选词列表",
"mappings": {
"a": ["阿", "啊", "呀"],
"ai": ["愛", "愛", "艾", "哀", "矮", "礙", "挨", "唉"],
"an": ["安", "按", "暗", "岸", "案", "俺", "鞍"],
"ang": ["昂", "盎"],
"ao": ["奧", "傲", "熬", "澳", "襖", "懊", "敖"],
"ba": ["吧", "把", "八", "爸", "巴", "拔", "罷", "霸", "扒", "叭"],
"bai": ["白", "百", "拜", "敗", "柏", "擺", "佰"],
"ban": ["辦", "班", "般", "板", "版", "半", "伴", "扮", "拌", "瓣", "頒"],
"bang": ["幫", "邦", "榜", "膀", "綁", "棒", "磅"],
"bao": ["包", "保", "報", "寶", "抱", "暴", "爆", "薄", "爆", "豹", "飽", "堡", "刨"],
"bei": ["北", "被", "背", "備", "悲", "杯", "碑", "輩", "倍", "貝"],
"ben": ["本", "奔", "笨", "盆"],
"beng": ["崩", "繃", "蹦", "泵"],
"bi": ["比", "必", "筆", "畢", "避", "閉", "鼻", "彼", "碧", "壁", "弊", "臂", "秘", "辟", "逼"],
"bian": ["變", "便", "邊", "編", "辯", "遍", "鞭", "辨", "扁", "貶"],
"biao": ["表", "標", "彪", "錶", "鏢"],
"bie": ["別", "憋", "癟"],
"bin": ["賓", "彬", "斌", "瀕", "濱"],
"bing": ["病", "並", "冰", "兵", "餅", "柄", "秉", "稟"],
"bo": ["不", "波", "博", "播", "伯", "薄", "泊", "柏", "勃", "搏", "撥", "剝", "脖", "博"],
"bu": ["不", "步", "部", "布", "補", "捕", "簿", "卜", "怖"],
"ca": ["擦", "嚓"],
"cai": ["才", "材", "才", "財", "采", "彩", "菜", "猜", "裁", "踩"],
"can": ["參", "餐", "殘", "慘", "燦", "蠶"],
"cang": ["藏", "倉", "蒼", "艙"],
"cao": ["草", "操", "曹", "糙", "槽"],
"ce": ["策", "測", "側", "廁", "冊"],
"ceng": ["層", "曾", "蹭"],
"cha": ["查", "茶", "差", "插", "察", "剎", "叉", "岔", "詫"],
"chai": ["差", "拆", "柴", "豺"],
"chan": ["產", "纏", "禪", "蟬", "鏟", "闡", "顫", "摻", "潺"],
"chang": ["長", "常", "場", "唱", "廠", "昌", "倡", "嘗", "腸", "暢", "償"],
"chao": ["超", "朝", "潮", "吵", "炒", "抄", "鈔", "巢", "嘲"],
"che": ["車", "徹", "撤", "扯", "澈"],
"chen": ["陳", "晨", "沉", "趁", "襯", "臣", "塵", "辰", "忱"],
"cheng": ["成", "城", "程", "稱", "承", "誠", "乘", "撐", "橙", "呈", "懲", "撐"],
"chi": ["吃", "持", "遲", "池", "尺", "齒", "赤", "翅", "斥", "馳", "癡", "侈"],
"chong": ["充", "衝", "蟲", "重", "崇", "寵", "沖", "憧"],
"chou": ["抽", "愁", "醜", "臭", "仇", "籌", "稠", "綢", "酬", "疇"],
"chu": ["出", "處", "初", "除", "書", "楚", "觸", "儲", "廚", "畜", "鋤"],
"chuai": ["揣", "踹"],
"chuan": ["傳", "穿", "船", "川", "串", "喘", "釧"],
"chuang": ["床", "窗", "創", "闖", "幢"],
"chui": ["吹", "垂", "錘", "捶", "炊"],
"chun": ["春", "純", "唇", "淳", "醇"],
"ci": ["次", "此", "詞", "辭", "慈", "瓷", "磁", "賜", "刺", "茨"],
"cong": ["從", "聰", "匆", "蔥", "叢", "淙"],
"cou": ["湊"],
"cu": ["粗", "促", "醋", "簇", "猝"],
"cuan": ["竄", "攢", "篡"],
"cui": ["催", "脆", "翠", "粹", "崔", "淬", "萃"],
"cun": ["村", "存", "寸", "磋"],
"cuo": ["錯", "措", "搓", "磋", "挫"],
"da": ["大", "打", "答", "達", "搭", "塔", "瘩"],
"dai": ["大", "代", "帶", "待", "袋", "戴", "呆", "貸", "逮", "怠", "殆", "黛"],
"dan": ["但", "單", "擔", "膽", "丹", "淡", "蛋", "誕", "彈", "旦", "氮", "耽"],
"dang": ["當", "黨", "檔", "擋", "蕩", "檔", "宕"],
"dao": ["到", "道", "導", "刀", "倒", "島", "盜", "悼", "搗", "禱", "蹈"],
"de": ["的", "得", "德", "底"],
"dei": ["得"],
"deng": ["等", "燈", "登", "鄧", "瞪", "凳", "蹬"],
"di": ["的", "地", "第", "低", "底", "敵", "弟", "帝", "抵", "遞", "迪", "滴", "堤", "笛", "締"],
"dia": ["嗲"],
"dian": ["點", "電", "店", "典", "墊", "澱", "殿", "顛", "滇", "碘", "巔"],
"diao": ["調", "掉", "吊", "雕", "刁", "釣", "凋", "碉"],
"die": ["爹", "跌", "叠", "蝶", "碟", "諜", "迭", "帖", "耋"],
"ding": ["定", "訂", "頂", "丁", "釘", "盯", "叮", "鼎", "叮"],
"diu": ["丟"],
"dong": ["動", "東", "冬", "懂", "洞", "凍", "棟", "董", "咚"],
"dou": ["都", "鬥", "豆", "抖", "逗", "兜", "痘"],
"du": ["讀", "都", "度", "獨", "毒", "渡", "杜", "肚", "堵", "賭", "鍍", "督"],
"duan": ["段", "斷", "短", "鍛", "緞", "端"],
"dui": ["對", "隊", "堆", "兌", "懟"],
"dun": ["頓", "噸", "盾", "蹲", "敦", "墩", "燉", "鈍"],
"duo": ["多", "度", "奪", "躲", "朵", "墮", "舵", "跺", "惰", "哆"],
"e": ["餓", "惡", "額", "俄", "鵝", "娥", "訛", "峨", "扼", "遏", "鄂", "噩"],
"ei": ["誒"],
"en": ["恩", "摁"],
"er": ["二", "兒", "耳", "而", "爾", "餌", "洱", "貳"],
"fa": ["發", "法", "罰", "乏", "伐", "閥", "筏", "佳"],
"fan": ["反", "飯", "煩", "繁", "範", "犯", "泛", "番", "翻", "凡", "帆", "返", "販", "礬"],
"fang": ["方", "放", "房", "防", "訪", "仿", "芳", "坊", "妨", "紡", "舫"],
"fei": ["非", "飛", "費", "肥", "廢", "匪", "誹", "啡", "菲", "沸", "翡", "吠"],
"fen": ["分", "份", "粉", "奮", "憤", "紛", "芬", "墳", "焚", "氛", "糞"],
"feng": ["風", "封", "豐", "峰", "鋒", "蜂", "瘋", "逢", "縫", "鳳", "奉", "諷", "楓"],
"fo": ["佛"],
"fou": ["否", "縫"],
"fu": ["父", "夫", "付", "服", "福", "府", "負", "富", "復", "副", "婦", "撫", "附", "幅", "浮", "腐", "符", "弗", "腹", "輻", "敷", "氟", "芙", "敷"],
"ga": ["嘎", "噶", "軋"],
"gai": ["改", "該", "蓋", "概", "溉", "丐", "芥", "鈣"],
"gan": ["幹", "感", "敢", "甘", "肝", "趕", "桿", "乾", "贛", "柑", "竿", "尴", "擀"],
"gang": ["剛", "鋼", "港", "崗", "綱", "岡", "缸", "槓", "扛"],
"gao": ["高", "告", "搞", "稿", "糕", "鎬", "膏", "篙"],
"ge": ["個", "各", "歌", "格", "哥", "割", "革", "隔", "閣", "葛", "戈", "擱", "鴿", "胳", "骼"],
"gei": ["給"],
"gen": ["跟", "根", "亙", "艮"],
"geng": ["更", "耕", "庚", "羹", "耿", "梗"],
"gong": ["工", "公", "共", "供", "功", "攻", "宮", "恭", "鞏", "弓", "躬", "拱", "貢"],
"gou": ["狗", "夠", "構", "購", "溝", "鉤", "勾", "苟", "垢", "篝"],
"gu": ["古", "故", "顧", "骨", "谷", "股", "鼓", "固", "孤", "姑", "辜", "沽", "咕", "估"],
"gua": ["掛", "瓜", "刮", "寡", "呱", "褂"],
"guai": ["怪", "乖", "拐"],
"guan": ["關", "觀", "管", "官", "館", "慣", "灌", "冠", "罐", "貫", "棺", "倌"],
"guang": ["光", "廣", "逛", "胱"],
"gui": ["貴", "規", "歸", "鬼", "軌", "櫃", "桂", "跪", "龜", "瑰", "詭", "閨"],
"gun": ["滾", "棍"],
"guo": ["過", "國", "果", "鍋", "郭", "裹", "渦"],
"ha": ["哈", "蛤"],
"hai": ["還", "海", "害", "孩", "嗨", "亥", "骸", "氦"],
"han": ["漢", "寒", "汗", "喊", "韓", "旱", "憾", "悍", "翰", "涵", "酣", "憨"],
"hang": ["行", "航", "杭", "巷", "夯", "吭"],
"hao": ["好", "號", "豪", "毫", "浩", "耗", "郝", "蒿", "嚎", "壕", "濠"],
"he": ["和", "合", "河", "何", "核", "賀", "喝", "赫", "褐", "鶴", "荷", "盒", "禾", "嚇", "呵"],
"hei": ["黑", "嘿"],
"hen": ["很", "狠", "恨", "痕"],
"heng": ["橫", "恆", "衡", "亨", "哼"],
"hong": ["紅", "轟", "洪", "宏", "虹", "鴻", "烘", "弘", "訌", "泓"],
"hou": ["後", "候", "厚", "喉", "猴", "吼", "侯", "吼"],
"hu": ["湖", "呼", "戶", "虎", "護", "互", "忽", "胡", "壺", "狐", "糊", "弧", "蝴", "乎", "滬"],
"hua": ["話", "花", "化", "華", "畫", "劃", "滑", "嘩", "樺", "驊"],
"huai": ["壞", "懷", "槐", "徊"],
"huan": ["還", "換", "環", "歡", "緩", "患", "喚", "幻", "煥", "桓", "宦", "渙", "瘓"],
"huang": ["黃", "皇", "荒", "慌", "煌", "晃", "謊", "凰", "惶", "煌", "簧", "恍"],
"hui": ["會", "回", "灰", "輝", "輝", "惠", "慧", "繪", "匯", "輝", "毀", "悔", "晦", "賄", "穢"],
"hun": ["婚", "魂", "混", "渾", "昏", "葷", "餛"],
"huo": ["活", "火", "或", "夥", "獲", "貨", "禍", "惑", "霍", "豁", "鍬", "鑊"],
"ji": ["幾", "機", "己", "記", "計", "集", "基", "際", "極", "擊", "激", "其", "及", "級", "即", "急", "季", "跡", "技", "績", "輯", "籍", "擠", "吉", "雞", "奇", "肌", "饑", "譏", "磯", "姬", "嫉", "棘", "寂", "冀", "驥"],
"jia": ["家", "加", "價", "假", "架", "佳", "甲", "駕", "嘉", "稼", "嫁", "夾", "頰", "戛", "枷"],
"jian": ["見", "間", "建", "件", "簡", "檢", "堅", "健", "漸", "劍", "鍵", "尖", "肩", "艦", "鑒", "剪", "撿", "踐", "賤", "箭", "澗", "濺", "薦", "餞", "漸", "諫"],
"jiang": ["將", "江", "強", "講", "降", "獎", "疆", "匠", "蔣", "漿", "僵", "薑", "絳"],
"jiao": ["叫", "教", "腳", "角", "交", "覺", "較", "焦", "膠", "驕", "澆", "攪", "椒", "嬌", "郊", "蕉", "矯", "絞", "僥", "佼", "僥"],
"jie": ["接", "節", "街", "結", "解", "姐", "介", "界", "借", "傑", "潔", "截", "揭", "劫", "捷", "睫", "竭", "桔", "戒", "芥", "藉", "拮"],
"jin": ["進", "金", "近", "今", "緊", "盡", "僅", "勁", "錦", "津", "筋", "巾", "斤", "禁", "襟", "瑾"],
"jing": ["經", "精", "景", "警", "靜", "境", "競", "淨", "鏡", "徑", "驚", "京", "晶", "睛", "莖", "荊", "兢", "涇", "憬"],
"jiong": ["窘", "炯", "迥"],
"jiu": ["就", "九", "久", "酒", "舊", "救", "究", "糾", "舅", "揪", "韭", "灸", "玖", "臼"],
"ju": ["句", "具", "據", "局", "舉", "巨", "聚", "居", "距", "懼", "劇", "鋸", "矩", "拒", "俱", "菊", "橘", "颶", "踞", "遽"],
"juan": ["卷", "捐", "圈", "眷", "倦", "娟", "雋", "涓", "鐫"],
"jue": ["決", "覺", "絕", "角", "爵", "掘", "倔", "厥", "譎", "獗", "矍", "嚼"],
"jun": ["軍", "君", "均", "俊", "菌", "竣", "鈞", "峻", "雋"],
"ka": ["卡", "咖", "喀", "咔"],
"kai": ["開", "凱", "楷", "慨", "愷", "鎧", "鍇"],
"kan": ["看", "砍", "坎", "勘", "刊", "堪", "瞰", "龕"],
"kang": ["康", "抗", "扛", "亢", "糠", "慷", "伉"],
"kao": ["考", "靠", "烤", "拷", "栲", "犒"],
"ke": ["可", "客", "科", "刻", "課", "顆", "克", "渴", "柯", "棵", "磕", "咳", "殼", "坷"],
"ken": ["肯", "懇", "啃", "齦"],
"keng": ["坑", "吭", "鏗"],
"kong": ["空", "控", "恐", "孔"],
"kou": ["口", "扣", "叩", "寇", "摳"],
"ku": ["苦", "哭", "庫", "酷", "枯", "窟", "骷"],
"kua": ["跨", "誇", "垮", "挎", "胯"],
"kuai": ["快", "塊", "筷", "儈", "膾"],
"kuan": ["寬", "款"],
"kuang": ["況", "礦", "狂", "框", "曠", "眶", "筐", "匡", "誑"],
"kui": ["虧", "愧", "潰", "窺", "葵", "魁", "饋", "匱", "睽", "聵"],
"kun": ["困", "昆", "坤", "捆", "琨", "鯤"],
"kuo": ["擴", "括", "闊", "廓"],
"la": ["拉", "啦", "蠟", "辣", "臘", "喇", "落"],
"lai": ["來", "賴", "萊", "徠", "賚"],
"lan": ["藍", "蘭", "攔", "籃", "懶", "爛", "濫", "覽", "欄", "瀾", "嵐", "襤"],
"lang": ["浪", "郎", "狼", "廊", "朗", "琅", "螂", "朗"],
"lao": ["老", "勞", "落", "牢", "撈", "澇", "絡", "姥", "佬", "潦"],
"le": ["了", "樂", "勒", "肋"],
"lei": ["累", "類", "淚", "雷", "勒", "壘", "蕾", "磊", "擂", "鐳", "儡"],
"leng": ["冷", "愣", "楞"],
"li": ["裡", "力", "理", "利", "立", "離", "例", "歷", "李", "禮", "麗", "勵", "梨", "厘", "莉", "犁", "黎", "璃", "狸", "漓", "罹", "驪", "鱧", "吏", "栗"],
"lia": ["倆"],
"lian": ["連", "聯", "臉", "練", "蓮", "戀", "煉", "廉", "憐", "漣", "鐮", "斂", "璉"],
"liang": ["兩", "亮", "量", "良", "涼", "梁", "糧", "樑", "諒", "晾", "踉", "靚"],
"liao": ["了", "料", "聊", "療", "遼", "撩", "僚", "燎", "繚", "潦", "寥", "嘹"],
"lie": ["列", "烈", "獵", "裂", "劣", "咧", "冽", "捩", "躐"],
"lin": ["林", "臨", "鄰", "淋", "琳", "霖", "鱗", "麟", "遴", "藺", "吝", "躪"],
"ling": ["領", "零", "靈", "令", "另", "玲", "鈴", "陵", "嶺", "凌", "菱", "羚", "翎", "聆", "伶", "拎"],
"liu": ["六", "流", "留", "劉", "柳", "溜", "琉", "榴", "硫", "溜", "鎏", "鷚"],
"long": ["龍", "隆", "弄", "籠", "聾", "攏", "壟", "朗", "隴"],
"lou": ["樓", "漏", "露", "婁", "摟", "簍", "嘍", "螻"],
"lu": ["路", "錄", "陸", "綠", "露", "旅", "律", "慮", "呂", "履", "侶", "屢", "濾", "氯", "廬", "爐", "蘆", "盧", "顱", "魯", "擼", "祿", "麓"],
"lv": ["綠", "律", "旅", "慮", "呂", "履", "侶", "屢", "濾", "氯"],
"luan": ["亂", "卵", "巒", "鑾", "鸞", "欒"],
"lue": ["略", "掠"],
"lun": ["論", "輪", "倫", "侖", "綸", "淪"],
"luo": ["落", "羅", "洛", "絡", "邏", "鑼", "籮", "駱", "裸", "螺", "蘿", "摞"],
"ma": ["嗎", "媽", "馬", "麻", "罵", "嘛", "螞", "碼", "瑪", "抹", "摩"],
"mai": ["買", "賣", "麥", "埋", "邁", "脈", "霾"],
"man": ["滿", "慢", "曼", "漫", "蠻", "瞞", "饅", "蔓", "謾", "墁", "幔"],
"mang": ["忙", "盲", "茫", "芒", "莽", "氓", "硭"],
"mao": ["貓", "毛", "矛", "茅", "茂", "冒", "帽", "貌", "貿", "卯", "錨", "耄", "髦", "瑁", "懋"],
"me": ["麼"],
"mei": ["沒", "美", "妹", "每", "梅", "媒", "煤", "眉", "霉", "魅", "玫", "枚", "寐", "昧", "媚", "湄", "鎂", "糜"],
"men": ["們", "門", "悶", "燜", "捫"],
"meng": ["夢", "孟", "猛", "蒙", "盟", "萌", "朦", "檬", "懵", "礞", "蠐"],
"mi": ["米", "密", "迷", "蜜", "祕", "祕", "眯", "靡", "糜", "彌", "覓", "冪", "泌"],
"mian": ["面", "免", "棉", "眠", "綿", "勉", "緬", "冕", "娩", "湎", "眄"],
"miao": ["描", "秒", "妙", "廟", "苗", "瞄", "渺", "淼", "緲", "藐"],
"mie": ["滅", "蔑", "篾", "乜"],
"min": ["民", "敏", "名", "皿", "閔", "抿", "泯", "憫", "閔"],
"ming": ["名", "明", "命", "鳴", "銘", "冥", "茗", "溟", "瞑", "螟"],
"miu": ["謬"],
"mo": ["麼", "摸", "磨", "摩", "魔", "膜", "默", "墨", "抹", "末", "莫", "漠", "寞", "陌", "謨", "茉", "驀", "歿"],
"mou": ["某", "謀", "牟", "眸", "繆", "鍪"],
"mu": ["目", "母", "木", "幕", "牧", "慕", "墓", "暮", "穆", "睦", "沐", "募", "姆", "拇", "牡", "畝"],
"na": ["那", "拿", "哪", "納", "吶", "娜", "鈉", "衲"],
"nai": ["奶", "耐", "乃", "奈", "氖", "萘", "鼐"],
"nan": ["南", "難", "男", "喃", "楠", "赧"],
"nang": ["囊", "囔"],
"nao": ["腦", "惱", "鬧", "撓", "淖", "鐃", "橈"],
"ne": ["呢", "訥"],
"nei": ["內", "那"],
"nen": ["嫩", "恁"],
"neng": ["能"],
"ni": ["你", "妳", "呢", "泥", "尼", "擬", "逆", "妮", "霓", "倪", "匿", "溺", "膩", "旎"],
"nian": ["年", "念", "黏", "碾", "捻", "撚", "蔦"],
"niang": ["娘", "釀"],
"niao": ["鳥", "尿", "裊", "嬲"],
"nie": ["捏", "聶", "孽", "躡", "鎳", "囁", "臬", "涅"],
"nin": ["您"],
"ning": ["寧", "凝", "擰", "檸", "獰", "嚀", "甯"],
"niu": ["牛", "紐", "扭", "鈕", "妞", "拗"],
"nong": ["農", "濃", "弄", "膿", "儂"],
"nu": ["女", "努", "怒", "奴", "弩", "胬"],
"nv": ["女"],
"nuan": ["暖"],
"nue": ["虐", "瘧"],
"nuo": ["挪", "諾", "懦", "糯", "喏"],
"o": ["哦", "噢", "喔"],
"ou": ["歐", "偶", "嘔", "藕", "鷗", "漚", "慪"],
"pa": ["怕", "爬", "帕", "趴", "琶", "葩", "耙"],
"pai": ["排", "拍", "牌", "派", "徘", "湃", "俳"],
"pan": ["判", "盤", "盼", "攀", "畔", "胖", "叛", "潘", "磐", "蹣", "拚"],
"pang": ["旁", "胖", "龐", "膀", "磅", "彷", "螃"],
"pao": ["跑", "炮", "泡", "拋", "刨", "袍", "咆", "庖"],
"pei": ["配", "陪", "培", "賠", "佩", "沛", "裴", "胚", "霈"],
"pen": ["盆", "噴"],
"peng": ["朋", "碰", "彭", "棚", "蓬", "鵬", "捧", "烹", "澎", "朋", "怦", "砰", "堋"],
"pi": ["皮", "批", "披", "匹", "疲", "僻", "脾", "劈", "琵", "毗", "啤", "坯", "譬", "霹", "屁", "闢", "紕"],
"pian": ["片", "便", "騙", "偏", "篇", "翩", "扁", "諞"],
"piao": ["票", "飄", "漂", "瓢", "嫖", "縹", "驃"],
"pie": ["撇", "瞥", "苤"],
"pin": ["品", "貧", "頻", "聘", "拼", "拚", "嬪"],
"ping": ["平", "評", "憑", "瓶", "萍", "屏", "蘋", "坪", "萍", "秤", "娉", "馮"],
"po": ["破", "迫", "婆", "頗", "坡", "潑", "泊", "魄", "粕", "朴", "珀", "叵", "鄱"],
"pou": ["剖", "掊", "裒"],
"pu": ["普", "鋪", "樸", "譜", "浦", "葡", "蒲", "僕", "撲", "圃", "濮", "璞", "噗"],
"qi": ["起", "其", "氣", "期", "七", "奇", "妻", "棋", "齊", "旗", "企", "啟", "器", "棄", "汽", "祈", "騎", "豈", "漆", "契", "砌", "琪", "淇", "岐", "祁", "崎", "祺", "臍", "訖", "訖", "磧"],
"qia": ["恰", "洽", "卡", "掐", "髂", "袷"],
"qian": ["前", "錢", "千", "簽", "遷", "淺", "欠", "牽", "潛", "鉛", "謙", "乾", "嵌", "譴", "譴", "倩", "倩", "槍", "嗆", "薔", "牆", "強", "搶", "腔", "嗆", "羌", "嬙", "檣", "鏘", "鏹"],
"qiao": ["橋", "瞧", "巧", "敲", "俏", "殼", "竅", "喬", "翹", "峭", "俏", "撬", "憔", "譙", "樵"],
"qie": ["切", "且", "茄", "怯", "竊", "妾", "愜", "鍥", "伽"],
"qin": ["親", "琴", "勤", "侵", "秦", "欽", "禽", "寢", "沁", "芹", "擒", "噙", "覃"],
"qing": ["情", "請", "清", "青", "輕", "慶", "傾", "頃", "晴", "擎", "卿", "氫", "罄", "磬", "蜻", "鯖", "綮"],
"qiong": ["窮", "瓊", "穹", "煢", "邛", "蛩"],
"qiu": ["求", "球", "秋", "丘", "邱", "囚", "酋", "泅", "俅", "裘", "遒", "賒"],
"qu": ["去", "取", "曲", "區", "趣", "娶", "渠", "屈", "驅", "蛆", "軀", "祛", "瞿", "蛐", "麴", "衢"],
"quan": ["全", "權", "圈", "泉", "拳", "犬", "勸", "券", "詮", "痊", "銓", "蜷", "顴"],
"que": ["確", "卻", "缺", "雀", "鵲", "闕", "瘸", "榷", "愨"],
"qun": ["群", "裙", "逡"],
"ran": ["然", "燃", "染", "冉", "髯", "蚺"],
"rang": ["讓", "嚷", "壤", "攘", "穰", "瓤"],
"rao": ["擾", "繞", "饒", "嬈", "橈", "蕘"],
"re": ["熱", "惹", "喏"],
"ren": ["人", "認", "任", "仁", "忍", "刃", "韌", "紉", "妊", "葚", "稔"],
"reng": ["仍", "扔"],
"ri": ["日"],
"rong": ["容", "榮", "融", "絨", "溶", "蓉", "榕", "戎", "茸", "冗", "嶸", "狨"],
"rou": ["肉", "柔", "揉", "蹂", "鞣", "糅"],
"ru": ["如", "入", "儒", "乳", "辱", "孺", "茹", "蠕", "嚅", "濡", "縟", "洳"],
"ruan": ["軟", "阮"],
"rui": ["瑞", "銳", "蕊", "芮", "蚋", "枘"],
"run": ["潤", "閏"],
"ruo": ["若", "弱", "偌", "箬", "蒻"],
"sa": ["撒", "灑", "薩", "卅", "颯"],
"sai": ["賽", "塞", "腮", "鰓", "噻"],
"san": ["三", "散", "傘", "參", "霰"],
"sang": ["喪", "桑", "嗓", "顙", "搡"],
"sao": ["掃", "嫂", "騷", "搔", "瘙", "繅"],
"se": ["色", "塞", "瑟", "澀", "嗇", "穡"],
"sen": ["森"],
"seng": ["僧"],
"sha": ["殺", "沙", "紗", "傻", "啥", "煞", "莎", "杉", "剎", "砂", "痧", "裟", "鎩", "霎"],
"shai": ["曬", "篩", "色"],
"shan": ["山", "善", "閃", "衫", "扇", "杉", "刪", "珊", "柵", "膳", "擅", "贍", "汕", "潸", "姍", "煽", "跚", "訕", "疝", "鱔"],
"shang": ["上", "商", "傷", "尚", "賞", "裳", "熵", "觴", "殤", "垧"],
"shao": ["少", "燒", "紹", "稍", "勺", "哨", "韶", "捎", "梢", "芍", "苕", "蛸", "筲"],
"she": ["社", "設", "射", "蛇", "舌", "捨", "涉", "赦", "攝", "奢", "賒", "麝", "懾", "灄"],
"shei": ["誰"],
"shen": ["身", "深", "神", "什", "申", "伸", "審", "慎", "腎", "滲", "沈", "參", "甚", "嬸", "砷", "莘", "哂", "瀋", "糝"],
"sheng": ["生", "聲", "勝", "升", "省", "聖", "盛", "剩", "繩", "笙", "甥", "晟"],
"shi": ["是", "時", "事", "實", "十", "使", "史", "市", "世", "師", "施", "式", "示", "石", "室", "士", "視", "試", "食", "駛", "始", "勢", "失", "適", "仕", "飾", "濕", "詩", "屍", "虱", "誓", "嗜", "噬", "柿", "拭", "逝", "螫", "諡", "鈰", "鰣"],
"shou": ["手", "首", "受", "收", "授", "瘦", "獸", "壽", "售", "守", "狩", "綬", "艏"],
"shu": ["書", "數", "樹", "輸", "術", "述", "叔", "屬", "暑", "署", "鼠", "束", "疏", "舒", "淑", "梳", "抒", "殊", "蔬", "孰", "贖", "熟", "恕", "庶", "墅", "俞", "澍", "紓", "倏", "毹"],
"shua": ["刷", "耍", "唰"],
"shuai": ["帥", "率", "摔", "甩", "蟀"],
"shuan": ["栓", "拴", "閂", "涮"],
"shuang": ["雙", "爽", "霜", "孀"],
"shui": ["水", "說", "稅", "睡", "誰"],
"shun": ["順", "瞬", "舜", "吮"],
"shuo": ["說", "數", "碩", "朔", "爍", "鑠", "蒴", "搠"],
"si": ["四", "死", "思", "絲", "私", "司", "斯", "撕", "似", "肆", "寺", "祀", "廝", "嘶", "俬", "巳", "廝"],
"song": ["送", "松", "宋", "頌", "誦", "聳", "嵩", "凇", "菘", "淞"],
"sou": ["搜", "艘", "擻", "叟", "嗖", "餿", "溲", "颼", "瞍"],
"su": ["速", "素", "蘇", "訴", "俗", "塑", "溯", "宿", "粟", "夙", "簌", "愫", "嗉", "謖"],
"suan": ["算", "酸", "蒜", "狻"],
"sui": ["隨", "歲", "雖", "碎", "遂", "穗", "隧", "髓", "遂", "祟", "綏", "邃", "燧", "謁"],
"sun": ["損", "孫", "筍", "遜", "榫", "蓀", "猻"],
"suo": ["所", "鎖", "索", "縮", "瑣", "嗦", "唆", "梭", "嗩", "娑", "蓑"],
"ta": ["他", "她", "它", "塔", "踏", "拓", "榻", "獺", "撻", "闒", "遢"],
"tai": ["太", "台", "臺", "態", "泰", "抬", "胎", "臺", "鮐", "薹", "駘", "炱", "邰"],
"tan": ["談", "探", "彈", "壇", "攤", "貪", "嘆", "潭", "坦", "毯", "痰", "檀", "譚", "忐", "袒", "郯", "澹", "覃"],
"tang": ["堂", "唐", "糖", "躺", "趟", "湯", "燙", "塘", "膛", "棠", "搪", "螳", "鏜", "鏜", "鐋", "耥"],
"tao": ["套", "逃", "桃", "陶", "討", "濤", "掏", "滔", "萄", "淘", "陶", "燾", "絳", "叨"],
"te": ["特", "忒", "慝", "鋱"],
"teng": ["疼", "騰", "藤", "滕", "謄"],
"ti": ["提", "題", "體", "替", "踢", "梯", "剔", "蹄", "啼", "惕", "涕", "銻", "倜", "悌", "嚏"],
"tian": ["天", "田", "填", "甜", "添", "恬", "腆", "殄", "忝", "闐", "祆"],
"tiao": ["條", "跳", "調", "挑", "眺", "佻", "祧", "銚", "髫", "鰷"],
"tie": ["鐵", "貼", "帖", "萜"],
"ting": ["聽", "停", "庭", "挺", "廳", "廷", "亭", "婷", "艇", "汀", "蜓", "霆", "鋌", "莛"],
"tong": ["通", "同", "統", "童", "痛", "銅", "桶", "筒", "桐", "彤", "瞳", "佟", "酮", "嗵", "憧"],
"tou": ["頭", "投", "透", "偷", "骰"],
"tu": ["圖", "土", "突", "途", "吐", "兔", "屠", "徒", "凸", "禿", "荼", "釷", "菟"],
"tuan": ["團", "摶", "彖", "湍"],
"tui": ["推", "退", "腿", "蛻", "頹", "褪"],
"tun": ["吞", "屯", "臀", "囤", "褪", "豚"],
"tuo": ["脫", "托", "拖", "妥", "拓", "唾", "陀", "沱", "坨", "駝", "鴕", "橐", "砣", "佗", "跎"],
"wa": ["挖", "哇", "蛙", "瓦", "娃", "襪", "凹", "媧", "佤", "腽"],
"wai": ["外", "歪", "崴"],
"wan": ["完", "晚", "玩", "碗", "彎", "灣", "丸", "婉", "腕", "惋", "宛", "蜿", "豌", "莞", "綰", "剜"],
"wang": ["王", "往", "忘", "亡", "望", "網", "旺", "汪", "妄", "罔", "惘", "輞", "尪"],
"wei": ["為", "位", "未", "委", "圍", "唯", "威", "偉", "危", "尾", "微", "維", "違", "胃", "餵", "味", "慰", "魏", "衛", "畏", "萎", "偽", "娓", "惟", "巍", "緯", "煒", "韋", "薇", "帷", "渭", "猬", "闈", "洧", "沩"],
"wen": ["問", "文", "聞", "溫", "穩", "紋", "吻", "蚊", "雯", "紊", "刎", "璺", "問"],
"weng": ["翁", "嗡", "甕", "蓊"],
"wo": ["我", "握", "臥", "窩", "沃", "蝸", "幄", "斡", "喔", "倭", "萵", "齷"],
"wu": ["無", "五", "物", "務", "武", "舞", "誤", "惡", "午", "吳", "吾", "屋", "烏", "污", "悟", "霧", "捂", "巫", "嗚", "蕪", "梧", "唔", "戊", "塢", "憮", "嫵", "廡", "忤", "兀", "鵡", "鎢", "浯", "蜈", "齬"],
"xi": ["西", "系", "息", "希", "席", "習", "細", "喜", "戲", "洗", "惜", "稀", "溪", "錫", "析", "膝", "襲", "昔", "熙", "夕", "兮", "悉", "惜", "熄", "嬉", "汐", "犀", "烯", "曦", "奚", "唏", "唶", "淅", "嘻", "樨", "熙", "蠡", "璽", "徙", "隙", "戲", "餼", "覡", "闟"],
"xia": ["下", "夏", "嚇", "廈", "峽", "蝦", "瞎", "霞", "轄", "俠", "暇", "遐", "瑕", "匣", "黠", "硤", "罅"],
"xian": ["先", "現", "線", "限", "縣", "顯", "險", "鮮", "獻", "賢", "閒", "仙", "鹹", "羨", "陷", "憲", "餡", "羨", "掀", "纖", "閑", "涎", "嫻", "銜", "冼", "燹", "蜆", "筧", "薟", "躚"],
"xiang": ["想", "向", "相", "鄉", "香", "響", "享", "像", "象", "項", "巷", "降", "箱", "祥", "湘", "詳", "翔", "享", "襄", "鑲", "廂", "驤", "薌", "餉", "緗", "嚮", "嚮"],
"xiao": ["小", "笑", "效", "消", "校", "銷", "曉", "蕭", "肖", "削", "孝", "宵", "硝", "霄", "淆", "嘯", "驍", "梟", "瀟", "簫", "筱", "驍", "嘵", "蟰"],
"xie": ["些", "寫", "謝", "協", "鞋", "血", "歇", "斜", "脅", "諧", "攜", "洩", "卸", "懈", "蟹", "邪", "械", "屑", "偕", "褻", "榭", "廨", "瀣", "薤", "躞", "頡", "擷"],
"xin": ["新", "心", "信", "辛", "欣", "薪", "馨", "鑫", "芯", "鋅", "昕", "忻", "歆", "鐔", "囟"],
"xing": ["行", "星", "形", "性", "姓", "興", "刑", "型", "幸", "杏", "腥", "猩", "邢", "悻", "滎", "滎", "餳"],
"xiong": ["兄", "胸", "兇", "雄", "熊", "匈", "洶", "夐"],
"xiu": ["修", "休", "秀", "宿", "袖", "秀", "繡", "羞", "臭", "朽", "嗅", "鏽", "饈", "貅", "鵂", "岫"],
"xu": ["須", "需", "許", "續", "序", "徐", "虛", "緒", "蓄", "敘", "旭", "恤", "墟", "絮", "婿", "栩", "戌", "詡", "洫", "溆", "酗", "糈", "勖", "昫", "盱", "蓿"],
"xuan": ["選", "宣", "懸", "旋", "玄", "軒", "喧", "炫", "渲", "萱", "漩", "璇", "癬", "炫", "煊", "諼", "鋗"],
"xue": ["學", "雪", "血", "穴", "謔", "噱", "鱈"],
"xun": ["訊", "迅", "尋", "巡", "訓", "詢", "循", "旬", "熏", "勳", "薰", "潯", "馴", "汛", "遜", "殉", "徇", "巽", "塤", "曛", "窯", "鱘"],
"ya": ["呀", "壓", "牙", "亞", "雅", "鴨", "押", "芽", "涯", "訝", "崖", "啞", "衙", "軋", "蚜", "崖", "睚", "痖"],
"yan": ["言", "研", "眼", "嚴", "演", "驗", "煙", "顏", "鹽", "延", "沿", "燕", "宴", "炎", "掩", "演", "衍", "岩", "研", "艷", "雁", "焰", "厭", "彥", "諺", "堰", "硯", "嫣", "閻", "焉", "淹", "偃", "儼", "兗", "讌", "讞", "筵", "蜓", "鼴", "罨", "剡", "鄢", "閆", "滟", "妍", "琰", "罳"],
"yang": ["樣", "陽", "洋", "養", "央", "揚", "羊", "氧", "仰", "癢", "漾", "殃", "秧", "恙", "颺", "煬", "佯", "瘍", "鞅", "樣"],
"yao": ["要", "藥", "搖", "遙", "腰", "邀", "耀", "瑤", "姚", "咬", "堯", "鑰", "謠", "夭", "妖", "窯", "杳", "舀", "徭", "珧", "軺", "銚", "鰩", "么", "瘧"],
"ye": ["也", "業", "夜", "葉", "爺", "野", "液", "謁", "頁", "邪", "掖", "曳", "腋", "噎", "鄴", "曄", "燁", "鐺"],
"yi": ["一", "以", "已", "意", "義", "議", "易", "藝", "醫", "億", "憶", "移", "依", "疑", "譯", "異", "益", "亦", "役", "抑", "譯", "溢", "宜", "儀", "逸", "怡", "姨", "夷", "遺", "倚", "椅", "伊", "毅", "誼", "翌", "熠", "臆", "肄", "懿", "裔", "縊", "軼", "貽", "漪", "迤", "弋", "噫", "屹", "猗", "嶷", "揖", "壹", "挹", "佚", "詣", "懌", "懿", "曀", "繹", "驛", "羿", "釔", "鐿", "瘞", "苡", "薏", "悒", "挹", "嗌", "峄"],
"yin": ["因", "音", "引", "銀", "印", "飲", "隱", "陰", "吟", "尹", "殷", "茵", "蔭", "垠", "夤", "齦", "湮", "氤", "胤", "鄞", "喑", "洇", "狺"],
"ying": ["應", "英", "營", "迎", "影", "贏", "硬", "映", "盈", "穎", "瑩", "鷹", "嬰", "櫻", "瀛", "蠅", "瀛", "嬴", "罌", "縈", "楹", "熒", "螢", "瀅", "瓔", "鸚", "膺", "瀠", "瀛"],
"yo": ["喲", "唷"],
"yong": ["用", "永", "擁", "勇", "湧", "庸", "泳", "庸", "傭", "踴", "蛹", "恿", "鏞", "傭", "臃", "癰", "邕", "鏞", "墉", "慵", "灉"],
"you": ["有", "又", "由", "友", "右", "優", "油", "遊", "幼", "尤", "憂", "幽", "悠", "誘", "佑", "釉", "柚", "酉", "猶", "黝", "卣", "疣", "蚰", "宥", "侑", "呦", "銪", "牖", "蝣", "蝤", "繇", "輶", "夂"],
"yu": ["與", "於", "語", "雨", "魚", "遇", "欲", "育", "域", "預", "愈", "玉", "宇", "余", "譽", "獄", "漁", "愚", "輿", "寓", "御", "裕", "郁", "喻", "逾", "娛", "吁", "逾", "瑜", "馭", "毓", "諭", "豫", "隅", "昱", "覦", "覦", "歟", "煜", "燠", "聿", "鈺", "嶼", "傴", "圄", "圉", "禺", "芋", "飫", "閾", "嫗", "煜", "鷸", "譽", "瘐", "窳", "餘", "雩", "齬", "禺", "滪", "窳", "肀"],
"yuan": ["元", "原", "員", "圓", "院", "源", "遠", "願", "緣", "園", "怨", "冤", "援", "袁", "淵", "猿", "轅", "媛", "垣", "沅", "塬", "圜", "鴛", "鳶", "螈", "爰", "瑗", "掾", "圜"],
"yue": ["月", "約", "越", "樂", "曰", "閱", "躍", "悅", "岳", "粵", "淵", "曰", "鑰", "櫟", "鉞", "瀹", "龠", "刖", "軏"],
"yun": ["雲", "運", "員", "韻", "勻", "允", "孕", "蘊", "暈", "隕", "耘", "紜", "韻", "慍", "殞", "惲", "醞", "狁", "勻", "鄖"],
"za": ["雜", "砸", "咂", "拶"],
"zai": ["在", "再", "載", "災", "宰", "栽", "崽", "哉"],
"zan": ["咱", "讚", "暫", "讚", "拶", "昝", "簪", "糌"],
"zang": ["藏", "臟", "葬", "臟", "臧", "奘", "駔"],
"zao": ["早", "造", "遭", "燥", "澡", "藻", "棗", "躁", "鑿", "蚤", "皁", "竈"],
"ze": ["則", "責", "擇", "澤", "側", "仄", "迮", "幘", "賾", "箦"],
"zei": ["賊"],
"zen": ["怎", "譖"],
"zeng": ["增", "贈", "憎", "甑", "繒", "罾"],
"zha": ["炸", "紮", "查", "渣", "扎", "眨", "柵", "詐", "乍", "榨", "吒", "砟", "蚱", "齇", "鮓", "醡"],
"zhai": ["債", "寨", "齋", "摘", "窄", "翟", "瘵"],
"zhan": ["站", "展", "戰", "佔", "斬", "瞻", "沾", "詹", "盞", "嶄", "湛", "綻", "輾", "搌", "旃"],
"zhang": ["長", "張", "章", "掌", "丈", "帳", "仗", "脹", "障", "彰", "漳", "璋", "嶂", "幛", "瘴", "鄣"],
"zhao": ["找", "照", "招", "朝", "趙", "兆", "罩", "肇", "詔", "沼", "爪", "召", "昭", "嘲", "濯", "櫂", "笊"],
"zhe": ["這", "著", "者", "折", "哲", "蔗", "遮", "轍", "浙", "褶", "蟄", "鷓", "謫", "輒", "晢", "蜇"],
"zhei": ["這"],
"zhen": ["真", "針", "鎮", "陣", "珍", "震", "振", "診", "枕", "斟", "甄", "臻", "疹", "砧", "貞", "偵", "軫", "縝", "榛", "楨", "賑", "禎", "畛", "圳", "蓁", "斟"],
"zheng": ["正", "政", "整", "爭", "證", "鄭", "征", "蒸", "掙", "睜", "錚", "崢", "箏", "怔", "拯", "鉦", "幀", "諍", "癥"],
"zhi": ["之", "知", "只", "至", "指", "支", "直", "值", "制", "質", "治", "職", "紙", "誌", "置", "智", "植", "枝", "止", "址", "芝", "脂", "肢", "旨", "侄", "稚", "滯", "摯", "緻", "秩", "幟", "峙", "窒", "幟", "炙", "幟", "幟", "卮", "芷", "梔", "趾", "蜘", "躓", "雉", "膣", "騭", "躑", "豸", "幟", "輊", "贄", "鷙", "痣", "蛭", "幟"],
"zhong": ["中", "種", "重", "眾", "終", "鐘", "忠", "腫", "仲", "衷", "鍾", "盅", "舯", "螽", "冢"],
"zhou": ["周", "州", "洲", "舟", "皺", "軸", "宙", "粥", "肘", "帚", "胄", "紂", "咒", "晝", "縐", "碡", "僽"],
"zhu": ["主", "住", "注", "著", "助", "築", "逐", "祝", "豬", "珠", "朱", "諸", "竹", "株", "燭", "矚", "駐", "鑄", "煮", "拄", "囑", "矚", "佇", "杼", "渚", "瀦", "躅", "櫫", "褚", "苧", "洙", "瀦", "麈", "瘃"],
"zhua": ["抓", "爪"],
"zhuai": ["轉", "拽"],
"zhuan": ["專", "轉", "傳", "賺", "磚", "撰", "篆", "饌", "顓"],
"zhuang": ["裝", "狀", "莊", "撞", "壯", "幢", "妝", "樁"],
"zhui": ["追", "墜", "綴", "贅", "縋", "惴", "騅"],
"zhun": ["準", "諄", "肫", "窀"],
"zhuo": ["著", "桌", "捉", "卓", "濁", "灼", "酌", "拙", "琢", "茁", "濁", "擢", "倬", "涿", "浞", "禚", "斫"],
"zi": ["子", "自", "字", "資", "紫", "茲", "姿", "咨", "滋", "孜", "籽", "梓", "漬", "諮", "姊", "孳", "恣", "甾", "輜", "錙", "齜", "耔", "笫"],
"zong": ["總", "從", "縱", "綜", "宗", "棕", "蹤", "鬃", "粽", "偬", "綜", "腙"],
"zou": ["走", "奏", "鄒", "揍", "騶", "諏", "陬", "鯫"],
"zu": ["足", "族", "組", "租", "阻", "卒", "俎", "詛", "菹"],
"zuan": ["鑽", "纂", "攢", "繵", "躜"],
"zui": ["最", "罪", "嘴", "醉", "蕞"],
"zun": ["尊", "遵", "樽", "撙"],
"zuo": ["做", "作", "座", "左", "昨", "佐", "琢", "撮", "唑", "嘬", "怍", "祚", "胙"]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -392,6 +392,7 @@ typedef NS_ENUM(NSInteger, KBClearPhase) {
[[KBInputBufferManager shared] clearPendingClearSnapshot];
[[KBInputBufferManager shared] commitLiveToManual];
}
[self kb_refreshSuggestionsAfterLongPressClear:shouldClear];
}
#pragma mark - Clear Label
@@ -499,6 +500,7 @@ typedef NS_ENUM(NSInteger, KBClearPhase) {
#pragma mark - Clear
- (void)kb_clearAllInput {
[self kb_clearCurrentWordIfPossible];
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
UIInputViewController *ivc = KBFindInputViewController(start);
if (ivc) {
@@ -659,4 +661,36 @@ typedef NS_ENUM(NSInteger, KBClearPhase) {
#endif
}
- (void)kb_clearCurrentWordIfPossible {
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
UIInputViewController *ivc = KBFindInputViewController(start);
if (!ivc) { return; }
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([ivc respondsToSelector:@selector(kb_clearCurrentWord)]) {
[ivc performSelector:@selector(kb_clearCurrentWord)];
}
#pragma clang diagnostic pop
}
- (void)kb_refreshSuggestionsAfterLongPressClear:(BOOL)shouldClear {
NSTimeInterval delay = shouldClear ? 0.06 : 0.0;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[self kb_scheduleContextRefreshResetSuppression:NO];
});
}
- (void)kb_scheduleContextRefreshResetSuppression:(BOOL)resetSuppression {
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
UIInputViewController *ivc = KBFindInputViewController(start);
if (!ivc) { return; }
SEL sel = @selector(kb_scheduleContextRefreshResetSuppression:);
if (![ivc respondsToSelector:sel]) { return; }
void (*func)(id, SEL, BOOL) = (void *)[ivc methodForSelector:sel];
if (func) {
func(ivc, sel, resetSuppression);
}
}
@end

View File

@@ -2,7 +2,7 @@
// KBExtensionAppLauncher.h
// CustomKeyboard
//
// 封装:在键盘扩展中拉起主 AppScheme / Universal Link + 响应链兜底)。
// 封装:在键盘扩展中拉起主 AppScheme / Universal Link
//
#import <UIKit/UIKit.h>
@@ -12,23 +12,24 @@ NS_ASSUME_NONNULL_BEGIN
@interface KBExtensionAppLauncher : NSObject
/// 通用入口:优先尝试 primaryURL失败后尝试 fallbackURL
/// 两者都失败时再通过响应链openURL:)做兜底
/// 均通过 `extensionContext openURL` 发起跳转(避免使用扩展禁用 API/响应链绕行)
/// 若开启 `KB_URL_BRIDGE_ENABLE=1`,会在两次 `extensionContext openURL` 均失败时,
/// - Parameters:
/// - primaryURL: 第一优先尝试的 URL可为 Scheme 或 UL
/// - fallbackURL: 失败时的备用 URL可为 nil
/// - ivc: 当前的 UIInputViewController用于 extensionContext openURL
/// - source: 兜底时用作起点的 responder通常传 self 或 self.view
/// - source: 作为响应链兜底的起点(可为 nil
/// - completion: 最终是否“看起来已成功发起”打开动作(不保证一定跳转到 App
+ (void)openPrimaryURL:(NSURL * _Nullable)primaryURL
fallbackURL:(NSURL * _Nullable)fallbackURL
usingInputController:(UIInputViewController *)ivc
source:(UIResponder *)source
source:(UIResponder * _Nullable)source
completion:(void (^ _Nullable)(BOOL success))completion;
/// 简化版:只针对单一 Scheme 做尝试 + 响应链兜底
/// 简化版:只针对单一 Scheme 做尝试。
+ (void)openScheme:(NSURL *)scheme
usingInputController:(UIInputViewController *)ivc
source:(UIResponder *)source
source:(UIResponder * _Nullable)source
completion:(void (^ _Nullable)(BOOL success))completion;
@end

View File

@@ -4,15 +4,86 @@
//
#import "KBExtensionAppLauncher.h"
#if KB_URL_BRIDGE_ENABLE
#import <objc/message.h>
#endif
@implementation KBExtensionAppLauncher
#if KB_URL_BRIDGE_ENABLE
+ (BOOL)kb_openURLViaResponderChain:(NSURL *)url
source:(nullable UIResponder *)source
completion:(void (^ _Nullable)(BOOL success))completion {
if (!url) {
if (completion) { completion(NO); }
return NO;
}
UIResponder *responder = source;
// openURL:options:completionHandler:
// /宿 App
// options options nil
SEL openURLOptionsSel = NSSelectorFromString(@"openURL:options:completionHandler:");
while (responder) {
if ([responder respondsToSelector:openURLOptionsSel]) {
void (*msgSend)(id, SEL, NSURL *, id, void (^)(BOOL)) = (void *)objc_msgSend;
msgSend(responder, openURLOptionsSel, url, nil, ^(BOOL ok) {
if (completion) { completion(ok); }
});
return YES;
}
responder = responder.nextResponder;
}
// openURL:completionHandler:
responder = source;
SEL openURLCompletionSel = NSSelectorFromString(@"openURL:completionHandler:");
while (responder) {
if ([responder respondsToSelector:openURLCompletionSel]) {
void (*msgSend)(id, SEL, NSURL *, void (^)(BOOL)) = (void *)objc_msgSend;
msgSend(responder, openURLCompletionSel, url, ^(BOOL ok) {
if (completion) { completion(ok); }
});
return YES;
}
responder = responder.nextResponder;
}
// openURL:
responder = source;
SEL openURLSel = NSSelectorFromString(@"openURL:");
while (responder) {
if ([responder respondsToSelector:openURLSel]) {
BOOL (*msgSend)(id, SEL, NSURL *) = (void *)objc_msgSend;
BOOL ok = msgSend(responder, openURLSel, url);
if (completion) { completion(ok); }
return YES;
}
responder = responder.nextResponder;
}
if (completion) { completion(NO); }
return NO;
}
#endif
+ (void)openPrimaryURL:(NSURL * _Nullable)primaryURL
fallbackURL:(NSURL * _Nullable)fallbackURL
usingInputController:(UIInputViewController *)ivc
source:(UIResponder *)source
source:(UIResponder * _Nullable)source
completion:(void (^ _Nullable)(BOOL success))completion {
if (![NSThread isMainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self openPrimaryURL:primaryURL
fallbackURL:fallbackURL
usingInputController:ivc
source:source
completion:completion];
});
return;
}
if (!ivc || (!primaryURL && !fallbackURL)) {
if (completion) { completion(NO); }
return;
@@ -48,19 +119,37 @@
finish(YES);
return;
}
BOOL bridged = [self p_bridgeFirst:first second:second from:source];
finish(bridged);
#if KB_URL_BRIDGE_ENABLE
//
UIResponder *start = (source ?: (UIResponder *)ivc.view ?: (UIResponder *)ivc);
[self kb_openURLViaResponderChain:second
source:start
completion:^(BOOL ok3) {
finish(ok3);
}];
#else
finish(NO);
#endif
}];
} else {
BOOL bridged = [self p_bridgeFirst:first second:nil from:source];
finish(bridged);
#if KB_URL_BRIDGE_ENABLE
UIResponder *start = (source ?: (UIResponder *)ivc.view ?: (UIResponder *)ivc);
[self kb_openURLViaResponderChain:first
source:start
completion:^(BOOL ok3) {
finish(ok3);
}];
#else
finish(NO);
#endif
}
}];
}
+ (void)openScheme:(NSURL *)scheme
usingInputController:(UIInputViewController *)ivc
source:(UIResponder *)source
source:(UIResponder * _Nullable)source
completion:(void (^ _Nullable)(BOOL success))completion {
[self openPrimaryURL:scheme
fallbackURL:nil
@@ -69,53 +158,4 @@
completion:completion];
}
#pragma mark - Private
// openURL: KBURLOpenBridge
+ (BOOL)p_openURLViaResponder:(NSURL *)url from:(UIResponder *)start {
#if KB_URL_BRIDGE_ENABLE
if (!url || !start) return NO;
SEL sel = NSSelectorFromString(@"openURL:");
UIResponder *responder = start;
while (responder) {
@try {
if ([responder respondsToSelector:sel]) {
BOOL handled = NO;
BOOL (*funcBool)(id, SEL, NSURL *) = (BOOL (*)(id, SEL, NSURL *))objc_msgSend;
if (funcBool) {
handled = funcBool(responder, sel, url);
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[responder performSelector:sel withObject:url];
handled = YES;
#pragma clang diagnostic pop
}
return handled;
}
} @catch (__unused NSException *e) {
// ignore and continue
}
responder = responder.nextResponder;
}
return NO;
#else
(void)url; (void)start;
return NO;
#endif
}
+ (BOOL)p_bridgeFirst:(NSURL * _Nullable)first
second:(NSURL * _Nullable)second
from:(UIResponder *)source {
BOOL bridged = NO;
if (first) {
bridged = [self p_openURLViaResponder:first from:source];
}
if (!bridged && second) {
bridged = [self p_openURLViaResponder:second from:source];
}
return bridged;
}
@end

View File

@@ -50,7 +50,7 @@
if (completion) {
KBChatResponse *response = [[KBChatResponse alloc] init];
response.success = NO;
response.message = @"内容为空";
response.message = KBLocalized(@"Content is empty");
completion(response);
}
return;
@@ -101,7 +101,7 @@
if (completion) {
KBAudioResponse *response = [[KBAudioResponse alloc] init];
response.success = NO;
response.errorMessage = @"audioId 为空";
response.errorMessage = KBLocalized(@"audioId is empty");
completion(response);
}
return;
@@ -171,7 +171,7 @@
//
KBAudioResponse *failResponse = [[KBAudioResponse alloc] init];
failResponse.success = NO;
failResponse.errorMessage = [NSString stringWithFormat:@"轮询失败,已重试 %ld 次", (long)maxRetries];
failResponse.errorMessage = [NSString stringWithFormat:KBLocalized(@"Polling failed after %ld retries"), (long)maxRetries];
if (completion) completion(failResponse);
}
}];
@@ -183,7 +183,7 @@
if (completion) {
KBAudioResponse *response = [[KBAudioResponse alloc] init];
response.success = NO;
response.errorMessage = @"URL 为空";
response.errorMessage = KBLocalized(@"URL is empty");
completion(response);
}
return;
@@ -198,7 +198,7 @@
if (error || !data || data.length == 0) {
audioResponse.success = NO;
audioResponse.errorMessage = error.localizedDescription ?: @"下载失败";
audioResponse.errorMessage = error.localizedDescription ?: KBLocalized(@"Download failed");
if (completion) completion(audioResponse);
return;
}

View File

@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
@optional
- (void)subscriptionViewDidTapClose:(KBKeyboardSubscriptionView *)view;
- (void)subscriptionView:(KBKeyboardSubscriptionView *)view didTapPurchaseForProduct:(KBKeyboardSubscriptionProduct *)product;
- (void)subscriptionViewDidTapAgreement:(KBKeyboardSubscriptionView *)view;
@end
/// 键盘内的订阅弹层

View File

@@ -157,7 +157,7 @@ static id KBKeyboardSubscriptionSanitizeJSON(id obj) {
- (void)setupFeatureItems {
NSArray *titles = @[
KBLocalized(@"Wireless Sub-ai\nDialogue"),
KBLocalized(@"Wireless Sub-ai Dialogue"),
KBLocalized(@"Personalized\nKeyboard"),
KBLocalized(@"Chat\nPersona"),
KBLocalized(@"Emotional\nCounseling")
@@ -192,7 +192,11 @@ static id KBKeyboardSubscriptionSanitizeJSON(id obj) {
}
- (void)onTapAgreement {
[KBHUD showInfo:KBLocalized(@"Agreement coming soon")];
if ([self.delegate respondsToSelector:@selector(subscriptionViewDidTapAgreement:)]) {
[self.delegate subscriptionViewDidTapAgreement:self];
return;
}
[KBHUD showInfo:KBLocalized(@"Please open the App to view the agreement")];
}
#pragma mark - Data
@@ -200,7 +204,7 @@ static id KBKeyboardSubscriptionSanitizeJSON(id obj) {
- (void)fetchProducts {
if (self.isLoading) { return; }
if (![[KBFullAccessManager shared] hasFullAccess]) {
[KBHUD showInfo:KBLocalized(@"Enable Full Access to continue")];
[KBHUD showInfo:KBLocalized(@"Please enable Full Access to continue")];
return;
}
self.loading = YES;
@@ -405,7 +409,7 @@ static id KBKeyboardSubscriptionSanitizeJSON(id obj) {
- (UILabel *)agreementLabel {
if (!_agreementLabel) {
_agreementLabel = [[UILabel alloc] init];
_agreementLabel.text = KBLocalized(@"By clicking \"pay\", you agree to the");
_agreementLabel.text = KBLocalized(@"By clicking Pay, you indicate your agreement to the");
_agreementLabel.font = [UIFont systemFontOfSize:11];
_agreementLabel.textColor = [UIColor colorWithHex:0x4A4A4A];
}

View File

@@ -118,7 +118,7 @@ static const NSUInteger kKBChatMessageLimit = 10;
// AI
KBChatMessage *msg = [KBChatMessage assistantMessageWithText:text audioId:audioId];
msg.displayName = KBLocalized(@"AI助手");
msg.displayName = KBLocalized(@"AI Assistant");
NSLog(@"[Panel] 创建 AI 消息needsTypewriter: %d", msg.needsTypewriterEffect);
// 使
@@ -318,11 +318,12 @@ static const NSUInteger kKBChatMessageLimit = 10;
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.hidden = true;
_titleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
_titleLabel.textColor =
[UIColor kb_dynamicColorWithLightColor:[UIColor colorWithHex:0x1B1F1A]
darkColor:[UIColor whiteColor]];
_titleLabel.text = KBLocalized(@"AI对话");
_titleLabel.text = KBLocalized(@"AI Chat");
}
return _titleLabel;
}

View File

@@ -12,7 +12,6 @@ NS_ASSUME_NONNULL_BEGIN
@protocol KBEmojiPanelViewDelegate <NSObject>
- (void)emojiPanelView:(KBEmojiPanelView *)panel didSelectEmoji:(NSString *)emoji;
- (void)emojiPanelViewDidRequestClose:(KBEmojiPanelView *)panel;
- (void)emojiPanelViewDidTapSearch:(KBEmojiPanelView *)panel;
@optional
- (void)emojiPanelViewDidTapDelete:(KBEmojiPanelView *)panel;
@end
@@ -30,6 +29,9 @@ NS_ASSUME_NONNULL_BEGIN
/// 高亮指定分类
- (void)selectCategoryAtIndex:(NSInteger)index;
/// 释放 emoji 数据缓存(隐藏面板时可用)
- (void)purgeEmojiCache;
@end
NS_ASSUME_NONNULL_END

View File

@@ -18,7 +18,6 @@
@property (nonatomic, strong) UIButton *backButton;
@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) KBEmojiBottomBarView *bottomBar;
//@property (nonatomic, strong) UIButton *searchButton;
@property (nonatomic, strong) NSArray<UIButton *> *tabButtons;
@property (nonatomic, strong) KBEmojiDataProvider *dataProvider;
@property (nonatomic, copy) NSArray<KBEmojiCategory *> *categories;
@@ -100,14 +99,6 @@
[self addSubview:self.bottomBar];
[self.bottomBar.deleteButton addTarget:self action:@selector(onDelete) forControlEvents:UIControlEventTouchUpInside];
// self.searchButton = [UIButton buttonWithType:UIButtonTypeSystem];
// self.searchButton.layer.cornerRadius = 20;
// self.searchButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightBold];
// [self.searchButton setTitle:KBLocalized(@"Search") forState:UIControlStateNormal];
// [self.searchButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
// [self.searchButton addTarget:self action:@selector(onSearch) forControlEvents:UIControlEventTouchUpInside];
// [self.bottomBar addSubview:self.searchButton];
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.mas_left).offset(12);
make.right.equalTo(self.mas_right).offset(-12);
@@ -185,6 +176,15 @@
[self updateSelectionToIndex:preserved];
}
- (void)purgeEmojiCache {
[self.dataProvider purgeLargeCaches];
self.categories = @[];
self.currentIndex = NSNotFound;
self.titleLabel.text = @"";
[self rebuildTabButtons];
[self.collectionView reloadData];
}
- (void)rebuildTabButtons {
UIStackView *stackView = self.bottomBar.tabStackView;
if (!stackView) { return; }
@@ -260,12 +260,6 @@
}
}
- (void)onSearch {
if ([self.delegate respondsToSelector:@selector(emojiPanelViewDidTapSearch:)]) {
[self.delegate emojiPanelViewDidTapSearch:self];
}
}
- (void)onDelete {
if ([self.delegate respondsToSelector:@selector(emojiPanelViewDidTapDelete:)]) {
[self.delegate emojiPanelViewDidTapDelete:self];
@@ -294,7 +288,6 @@
}
- (void)onLocalizationChanged {
// [self.searchButton setTitle:KBLocalized(@"Search") forState:UIControlStateNormal];
[self reloadData];
}
@@ -305,8 +298,6 @@
self.backgroundColor = bg;
self.collectionView.backgroundColor = [UIColor clearColor];
self.titleLabel.textColor = theme.keyTextColor ?: [UIColor whiteColor];
UIColor *searchColor = theme.accentColor ?: [UIColor colorWithRed:0.35 green:0.35 blue:0.95 alpha:1];
// self.searchButton.backgroundColor = searchColor;
self.tabNormalColor = [UIColor colorWithWhite:1 alpha:0.08];
self.tabSelectedColor = theme.accentColor ?: [UIColor colorWithWhite:1 alpha:0.25];
[self updateTabHighlightStates];

View File

@@ -67,6 +67,8 @@ static CGFloat const kKBItemSpace = 4;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return kKBItemSpace; }
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
// cellloadingcell
if (self.loadingIndexes.count > 0) { return; }
KBTagItemModel *model = (indexPath.item < self.items.count) ? self.items[indexPath.item] : [KBTagItemModel new];
NSInteger personaId = 0;
if ([model isKindOfClass:KBTagItemModel.class]) {

View File

@@ -79,7 +79,7 @@
self.audioLabel.textColor = textColor;
self.audioIconView.tintColor = textColor;
self.audioLabel.text =
(message.text.length > 0) ? message.text : KBLocalized(@"语音回复");
(message.text.length > 0) ? message.text : KBLocalized(@"Voice reply");
self.messageLabel.hidden = audioMessage;
self.audioIconView.hidden = !audioMessage;
self.audioLabel.hidden = !audioMessage;
@@ -94,7 +94,7 @@
self.nameLabel.hidden = outgoing;
self.nameLabel.textColor = nameColor;
self.nameLabel.text =
(message.displayName.length > 0) ? message.displayName : KBLocalized(@"AI助手");
(message.displayName.length > 0) ? message.displayName : KBLocalized(@"AI Assistant");
// loading
if (message.isLoading && !outgoing) {

View File

@@ -7,7 +7,7 @@
#import "Masonry.h"
#import "KBResponderUtils.h" // UIInputViewController
#import "KBHUD.h"
#import "KBHostAppLauncher.h"
#import "../Utils/KBExtensionAppLauncher.h"
@interface KBFullAccessGuideView ()
@property (nonatomic, strong) UIControl *backdrop;
@@ -159,18 +159,34 @@
// KBResponderUtils.h
// App访宿 UIApplication + Scheme
- (void)onTapGoEnable {
UIInputViewController *ivc = KBFindInputViewController(self);
// responder
UIResponder *start = ivc.view ?: (UIResponder *)self;
// SchemeAppDelegate kbkeyboardAppExtension://settings
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//settings?src=kb_extension", KB_APP_SCHEME]];
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:start];
if (ok) {
[self dismiss];
} else {
NSString *showInfo = [NSString stringWithFormat:KBLocalized(@"Follow: Settings → General → Keyboard → Keyboards → %@ → Allow Full Access"),AppName];
UIInputViewController *ivc = self.ivc ?: KBFindInputViewController(self);
if (!ivc) {
NSString *showInfo = [NSString stringWithFormat:KBLocalized(@"Follow: Settings -> General -> Keyboard -> Keyboards -> %@ -> Allow Full Access"), AppName];
[KBHUD showInfo:showInfo];
return;
}
// Universal Link 退 Scheme
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=kb_extension", KB_UL_SETTINGS]];
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@://settings?src=kb_extension", KB_APP_SCHEME]];
__weak typeof(self) weakSelf = self;
[KBExtensionAppLauncher openPrimaryURL:ul
fallbackURL:scheme
usingInputController:ivc
source:(ivc.view ?: (UIResponder *)weakSelf)
completion:^(BOOL success) {
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) self = weakSelf;
if (!self) {
return;
}
if (success) {
[self dismiss];
} else {
NSString *showInfo = [NSString stringWithFormat:KBLocalized(@"Follow: Settings -> General -> Keyboard -> Keyboards -> %@ -> Allow Full Access"), AppName];
[KBHUD showInfo:showInfo];
}
});
}];
}
@end

View File

@@ -60,7 +60,6 @@
// if (!_placeholderLabelInternal) {
// _placeholderLabelInternal = [[UILabel alloc] init];
// // 稿
// _placeholderLabelInternal.text = KBLocalized(@"Paste Ta's Words");
// _placeholderLabelInternal.textColor = [UIColor colorWithRed:0.20 green:0.64 blue:0.54 alpha:1.0];
// _placeholderLabelInternal.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
// }

View File

@@ -16,9 +16,10 @@
#import "KBFunctionPasteView.h"
#import "KBFunctionTagCell.h"
#import "KBFunctionTagListView.h"
#import "KBHostAppLauncher.h"
#import "KBExtensionAppLauncher.h"
#import "KBInputBufferManager.h"
#import "KBResponderUtils.h" // UIInputViewController
#import "KBSignUtils.h"
#import "KBSkinManager.h"
#import "KBStreamOverlayView.h" //
#import "KBStreamTextView.h" //
@@ -141,76 +142,26 @@
/// #323232 #D0D3DA
+ (UIColor *)kb_backgroundColor {
if (@available(iOS 13.0, *)) {
return [UIColor colorWithDynamicProvider:^UIColor *_Nonnull(
UITraitCollection *_Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
return [UIColor colorWithHex:0x2B2B2B];
} else {
return [UIColor colorWithHex:0xD0D3DA];
}
}];
}
return [UIColor colorWithHex:0xD0D3DA];
}
/// Cell #707070 90%
+ (UIColor *)kb_cellBackgroundColor {
if (@available(iOS 13.0, *)) {
return [UIColor colorWithDynamicProvider:^UIColor *_Nonnull(
UITraitCollection *_Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
return [UIColor colorWithHex:0x707070];
} else {
return [UIColor colorWithWhite:1 alpha:0.9];
}
}];
}
return [UIColor colorWithWhite:1 alpha:0.9];
}
/// Cell #FFFFFF #1B1F1A
+ (UIColor *)kb_cellTextColor {
if (@available(iOS 13.0, *)) {
return [UIColor colorWithDynamicProvider:^UIColor *_Nonnull(
UITraitCollection *_Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
return [UIColor whiteColor];
} else {
return [UIColor colorWithHex:0x1B1F1A];
}
}];
}
return [UIColor colorWithHex:0x1B1F1A];
}
/// Clear
+ (UIColor *)kb_clearButtonTextColor {
if (@available(iOS 13.0, *)) {
return [UIColor colorWithDynamicProvider:^UIColor *_Nonnull(
UITraitCollection *_Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
return [UIColor whiteColor];
} else {
return [UIColor blackColor];
}
}];
}
return [UIColor blackColor];
}
/// #707070 #B9BDC8
+ (UIColor *)kb_deleteButtonBackgroundColor {
if (@available(iOS 13.0, *)) {
return [UIColor colorWithDynamicProvider:^UIColor *_Nonnull(
UITraitCollection *_Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
return [UIColor colorWithHex:0x707070];
} else {
return [UIColor colorWithHex:0xB9BDC8];
}
}];
}
return [UIColor colorWithHex:0xB9BDC8];
}
@@ -326,13 +277,8 @@
// self.itemsInternal = @[KBLocalized(@"Warm hearted man"),
// KBLocalized(@"Warm2 hearted man"),
// KBLocalized(@"Warm3 hearted man"),
// KBLocalized(@"撩女生啊u发顺丰大师傅"),
// KBLocalized(@"Warm = man"),
// KBLocalized(@"Warm hearted man"),
// KBLocalized(@"一枚暖男发放"),
// KBLocalized(@"聊天搭子"),
// KBLocalized(@"表达爱意"),
// KBLocalized(@"更多话术")];
// [self.tagListView setItems:self.itemsInternal];
//}
@@ -435,11 +381,32 @@
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:60];
request.HTTPMethod = @"POST";
[request setValue:@"text/event-stream" forHTTPHeaderField:@"Accept"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
// 401Missing sign headers
NSDictionary<NSString *, NSString *> *signHeaders =
[KBSignUtils signHeadersWithBodyParams:payload];
[signHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj,
BOOL *stop) {
if (key.length == 0 || obj.length == 0) {
return;
}
[request setValue:obj forHTTPHeaderField:key];
}];
NSString *token = KBAuthManager.shared.current.accessToken ?: @"";
if (token.length > 0) {
[request setValue:token forHTTPHeaderField:@"auth-token"];
}
// App Bearer
NSDictionary<NSString *, NSString *> *authHeader =
[[KBAuthManager shared] authorizationHeader];
[authHeader enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj,
BOOL *stop) {
if (key.length == 0 || obj.length == 0) {
return;
}
[request setValue:obj forHTTPHeaderField:key];
}];
request.HTTPBody = bodyData;
self.streamHasOutput = NO;
@@ -463,7 +430,7 @@
__strong typeof(weakSelf) self = weakSelf;
if (!self)
return;
[self kb_handleEventSourceError:event.error];
// [self kb_handleEventSourceError:event.error];
}
forEvent:WJXEventNameError
queue:NSOperationQueue.mainQueue];
@@ -483,7 +450,10 @@
if (event.data.length == 0) {
return;
}
NSLog(@"[KBStream] SSE raw payload: %@", event.data);
#if DEBUG
NSLog(@"[KBStream] SSE raw payload len=%lu",
(unsigned long)(event.data ?: @"").length);
#endif
NSData *jsonData = [event.data dataUsingEncoding:NSUTF8StringEncoding];
if (!jsonData) {
return;
@@ -541,7 +511,7 @@
}
BOOL shouldShowError = (error != nil);
if (shouldShowError) {
[KBHUD showInfo:error.localizedDescription ?: KBLocalized(@"拉取失败")];
[KBHUD showInfo:error.localizedDescription ?: KBLocalized(@"Fetch failed")];
}
if (self.streamOverlay) {
[self.streamOverlay finish];
@@ -558,7 +528,7 @@
BOOL needSubscriptionGuide = (code == KBBizCodeQuotaExhausted);
NSString *msg = KBBizMessageFromJSONObject(payload);
if (msg.length == 0) {
msg = KBLocalized(@"拉取失败");
msg = KBLocalized(@"Fetch failed");
}
NSError *bizError =
[NSError errorWithDomain:@"KBStreamBizError"
@@ -746,7 +716,7 @@
// 1) 访
if (![[KBFullAccessManager shared] hasFullAccess]) {
// 访
[KBHUD showInfo:KBLocalized(@"处理中…")];
[KBHUD showInfo:KBLocalized(@"Processing...")];
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self];
return;
}
@@ -756,12 +726,27 @@
if (!KBAuthManager.shared.isLoggedIn) {
UIInputViewController *ivc = KBFindInputViewController(self);
if (!ivc) {
[KBHUD showInfo:KBLocalized(@"Please return to the Home screen and open the app to sign in")];
return;
}
NSString *schemeStr =
[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
NSURL *scheme = [NSURL URLWithString:schemeStr];
// UIApplication App
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:ivc.view];
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard", KB_UL_LOGIN]];
NSURL *scheme =
[NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME]];
__weak typeof(self) weakSelf = self;
[KBExtensionAppLauncher openPrimaryURL:ul
fallbackURL:scheme
usingInputController:ivc
source:(ivc.view ?: (UIResponder *)weakSelf)
completion:^(BOOL success) {
if (success) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD showInfo:KBLocalized(@"Please return to the Home screen and open the app to sign in")];
});
}];
return;
// if (!ivc) return;
// NSString *encodedTitle = [title
@@ -791,7 +776,6 @@
// [ivc dismissKeyboard];
// BOOL ok = [KBHostAppLauncher openHostAppURL:scheme
// fromResponder:start]; if (!ok) {
// [KBHUD showInfo:KBLocalized(@"请切换到主App完成登录")];
// }else{
//
// }
@@ -839,7 +823,7 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer,
return;
}
[KBHUD showInfo:KBLocalized(@"处理中…")];
[KBHUD showInfo:KBLocalized(@"Processing...")];
UIInputViewController *ivc = KBFindInputViewController(self);
if (!ivc)
@@ -859,37 +843,34 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer,
if (!ul)
return;
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
// extensionContext UL
[ivc.extensionContext
openURL:ul
completionHandler:^(BOOL ok) {
if (ok) {
return;
}
// UL 宿 UIApplication + Scheme
NSURL *scheme = [NSURL
URLWithString:
[NSString
stringWithFormat:
@"%@@//login?src=functionView&index=%ld&title=%@",
KB_APP_SCHEME, (long)indexPath.item,
encodedTitle]];
UIResponder *start = ivc.view ?: (UIResponder *)self;
BOOL ok2 = [KBHostAppLauncher openHostAppURL:scheme
fromResponder:start];
if (!ok2) {
// 访宿 Manager
//
dispatch_async(dispatch_get_main_queue(), ^{
[[KBFullAccessManager shared]
ensureFullAccessOrGuideInView:self];
});
}
}];
});
NSURL *scheme = [NSURL
URLWithString:
[NSString stringWithFormat:
@"%@://login?src=functionView&index=%ld&title=%@",
KB_APP_SCHEME, (long)indexPath.item, encodedTitle]];
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) self = weakSelf;
if (!self) {
return;
}
[KBExtensionAppLauncher
openPrimaryURL:ul
fallbackURL:scheme
usingInputController:ivc
source:(ivc.view ?: (UIResponder *)self)
completion:^(BOOL success) {
if (success) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[[KBFullAccessManager shared]
ensureFullAccessOrGuideInView:self];
});
}];
});
}
#pragma mark - Button Actions

View File

@@ -21,17 +21,12 @@ NS_ASSUME_NONNULL_BEGIN
/// 需求:当 index == 0 时由外部KeyboardViewController决定是否切换到功能面板
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index;
/// 点击了右侧设置按钮
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView;
/// 点击了撤销删除按钮
- (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView;
/// emoji 视图里选择了一个表情
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectEmoji:(NSString *)emoji;
/// emoji 面板点击搜索
- (void)keyBoardMainViewDidTapEmojiSearch:(KBKeyBoardMainView *)keyBoardMainView;
/// 选择了联想词
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion;
@end
@@ -45,6 +40,9 @@ NS_ASSUME_NONNULL_BEGIN
/// 更新联想候选
- (void)kb_setSuggestions:(NSArray<NSString *> *)suggestions;
/// 根据 profileId 重新加载键盘布局
- (void)reloadLayoutWithProfileId:(NSString *)profileId;
@end
NS_ASSUME_NONNULL_END

View File

@@ -45,7 +45,7 @@
[self addSubview:self.suggestionBar];
// /
CGFloat keyboardAreaHeight = KBFit(200.0f);
CGFloat keyboardAreaHeight = KBFit(215.0f);
KBKeyboardLayoutConfig *layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
if (layoutConfig) {
CGFloat configHeight = [layoutConfig keyboardAreaScaledHeight];
@@ -66,14 +66,7 @@
make.bottom.equalTo(self.mas_bottom).offset(-bottomInset);
}];
self.emojiView = [[KBEmojiPanelView alloc] init];
self.emojiView.hidden = YES;
self.emojiView.alpha = 0.0;
self.emojiView.delegate = self;
[self addSubview:self.emojiView];
[self.emojiView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
// emoji
// [self.topBar mas_makeConstraints:^(MASConstraintMaker *make) {
// make.left.right.equalTo(self);
@@ -95,6 +88,10 @@
[self.keyboardView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.topBar.mas_bottom).offset(barSpacing);
}];
// 4
[self.keyboardView reloadKeys];
//
[self kb_applyTheme];
// /
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(kb_undoStateChanged)
@@ -112,9 +109,10 @@
if (self.emojiPanelVisible == visible) return;
self.emojiPanelVisible = visible;
if (visible) {
[self.emojiView reloadData];
self.emojiView.hidden = NO;
[self bringSubviewToFront:self.emojiView];
KBEmojiPanelView *emojiView = [self emojiView];
[emojiView reloadData];
emojiView.hidden = NO;
[self bringSubviewToFront:emojiView];
} else {
self.keyboardView.hidden = NO;
self.topBar.hidden = NO;
@@ -122,19 +120,28 @@
}
void (^changes)(void) = ^{
self.emojiView.alpha = visible ? 1.0 : 0.0;
if (self.emojiView) {
self.emojiView.alpha = visible ? 1.0 : 0.0;
}
self.keyboardView.alpha = visible ? 0.0 : 1.0;
self.topBar.alpha = visible ? 0.0 : 1.0;
self.suggestionBar.alpha = visible ? 0.0 : ([self kb_shouldShowSuggestions] ? 1.0 : 0.0);
};
void (^completion)(BOOL) = ^(BOOL finished) {
self.emojiView.hidden = !visible;
if (self.emojiView) {
self.emojiView.hidden = !visible;
}
self.keyboardView.hidden = visible;
self.topBar.hidden = visible;
if (visible) {
self.suggestionBar.hidden = YES;
} else {
self.suggestionBar.hidden = ![self kb_shouldShowSuggestions];
if (self.emojiView) {
[self.emojiView purgeEmojiCache];
[self.emojiView removeFromSuperview];
self.emojiView = nil;
}
}
};
@@ -150,6 +157,22 @@
[self setEmojiPanelVisible:!self.emojiPanelVisible animated:YES];
}
#pragma mark - Lazy Load
- (KBEmojiPanelView *)emojiView {
if (!_emojiView) {
_emojiView = [[KBEmojiPanelView alloc] init];
_emojiView.hidden = YES;
_emojiView.alpha = 0.0;
_emojiView.delegate = self;
[self addSubview:_emojiView];
[_emojiView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
}
return _emojiView;
}
#pragma mark - KBToolBarDelegate
@@ -160,12 +183,6 @@
}
}
- (void)toolBarDidTapSettings:(KBToolBar *)toolBar {
if ([self.delegate respondsToSelector:@selector(keyBoardMainViewDidTapSettings:)]) {
[self.delegate keyBoardMainViewDidTapSettings:self];
}
}
- (void)toolBarDidTapUndo:(KBToolBar *)toolBar {
if ([self.delegate respondsToSelector:@selector(keyBoardMainViewDidTapUndo:)]) {
[self.delegate keyBoardMainViewDidTapUndo:self];
@@ -238,12 +255,6 @@
[self setEmojiPanelVisible:NO animated:YES];
}
- (void)emojiPanelViewDidTapSearch:(KBEmojiPanelView *)panel {
if ([self.delegate respondsToSelector:@selector(keyBoardMainViewDidTapEmojiSearch:)]) {
[self.delegate keyBoardMainViewDidTapEmojiSearch:self];
}
}
- (void)emojiPanelViewDidTapDelete:(KBEmojiPanelView *)panel {
if ([self.delegate respondsToSelector:@selector(keyBoardMainView:didTapKey:)]) {
KBKey *backspace = [KBKey keyWithTitle:@"" type:KBKeyTypeBackspace];
@@ -289,10 +300,7 @@
- (BOOL)kb_shouldShowSuggestions {
if (self.emojiPanelVisible) { return NO; }
if (![KBBackspaceUndoManager shared].hasUndo && self.suggestionBarHasItems) {
return YES;
}
return NO;
return self.suggestionBarHasItems;
}
- (void)kb_applySuggestionVisibility {
@@ -301,4 +309,13 @@
self.suggestionBar.alpha = shouldShow ? 1.0 : 0.0;
}
- (void)reloadLayoutWithProfileId:(NSString *)profileId {
if (profileId.length == 0) {
NSLog(@"[KBKeyBoardMainView] reloadLayoutWithProfileId: empty profileId");
return;
}
NSLog(@"[KBKeyBoardMainView] Reloading layout with profileId: %@", profileId);
[self.keyboardView reloadLayoutWithProfileId:profileId];
}
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
//
// KBKeyboardInputHandler.h
// CustomKeyboard
//
// Key tap handling helper.
//
#import <Foundation/Foundation.h>
@class KBKey;
NS_ASSUME_NONNULL_BEGIN
typedef void (^KBKeyboardActionHandler)(void);
typedef void (^KBKeyboardKeyTapHandler)(KBKey *key);
@interface KBKeyboardInputHandler : NSObject
@property (nonatomic, copy, nullable) KBKeyboardActionHandler onToggleShift;
@property (nonatomic, copy, nullable) KBKeyboardActionHandler onToggleSymbols;
@property (nonatomic, copy, nullable) KBKeyboardKeyTapHandler onKeyTapped;
- (BOOL)handleKeyTap:(KBKey *)key;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,26 @@
//
// KBKeyboardInputHandler.m
// CustomKeyboard
//
#import "KBKeyboardInputHandler.h"
#import "KBKey.h"
@implementation KBKeyboardInputHandler
- (BOOL)handleKeyTap:(KBKey *)key {
if (!key) { return NO; }
switch (key.type) {
case KBKeyTypeShift:
if (self.onToggleShift) { self.onToggleShift(); }
return YES;
case KBKeyTypeSymbolsToggle:
if (self.onToggleSymbols) { self.onToggleSymbols(); }
return YES;
default:
if (self.onKeyTapped) { self.onKeyTapped(key); }
return YES;
}
}
@end

View File

@@ -0,0 +1,28 @@
//
// KBKeyboardInteractionHandler.h
// CustomKeyboard
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@class KBKeyButton;
NS_ASSUME_NONNULL_BEGIN
@interface KBKeyboardInteractionHandler : NSObject
- (UIView *)resolveHitView:(UIView *)hitView
point:(CGPoint)point
container:(UIView *)container
rowViews:(NSArray<UIView *> *)rowViews;
- (NSArray<KBKeyButton *> *)collectKeyButtonsInView:(UIView *)view;
- (void)showPreviewForButton:(KBKeyButton *)button inContainer:(UIView *)container;
- (void)hidePreview;
- (void)bringPreviewToFrontIfNeededInContainer:(UIView *)container;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,156 @@
//
// KBKeyboardInteractionHandler.m
// CustomKeyboard
//
#import "KBKeyboardInteractionHandler.h"
#import "KBKeyButton.h"
#import "KBKey.h"
#import "KBKeyPreviewView.h"
static const NSTimeInterval kKBPreviewShowDuration = 0.08;
static const NSTimeInterval kKBPreviewHideDuration = 0.06;
@interface KBKeyboardInteractionHandler ()
@property (nonatomic, strong) KBKeyPreviewView *previewView;
@end
@implementation KBKeyboardInteractionHandler
- (UIView *)resolveHitView:(UIView *)hitView
point:(CGPoint)point
container:(UIView *)container
rowViews:(NSArray<UIView *> *)rowViews {
if ([hitView isKindOfClass:[KBKeyButton class]]) {
return hitView;
}
if ([self isHitInsideKeyRows:hitView rowViews:rowViews]) {
KBKeyButton *btn = [self nearestKeyButtonForPoint:point
container:container
rowViews:rowViews];
if (btn) { return btn; }
}
return hitView;
}
- (NSArray<KBKeyButton *> *)collectKeyButtonsInView:(UIView *)view {
if (!view) { return @[]; }
NSMutableArray<KBKeyButton *> *buttons = [NSMutableArray array];
[self collectKeyButtonsInView:view into:buttons];
return buttons.copy;
}
- (void)showPreviewForButton:(KBKeyButton *)button inContainer:(UIView *)container {
if (!button || !container) { return; }
KBKey *key = button.key;
if (key.type != KBKeyTypeCharacter) return;
if (!self.previewView) {
self.previewView = [[KBKeyPreviewView alloc] initWithFrame:CGRectZero];
self.previewView.hidden = YES;
[container addSubview:self.previewView];
} else if (self.previewView.superview != container) {
[container addSubview:self.previewView];
}
[self.previewView configureWithKey:key icon:button.iconView.image];
//
CGRect btnFrameInSelf = [button convertRect:button.bounds toView:container];
CGFloat previewWidth = 42;
CGFloat previewHeight = CGRectGetHeight(btnFrameInSelf) * 1.2;
CGFloat centerX = CGRectGetMidX(btnFrameInSelf);
CGFloat centerY = CGRectGetMinY(btnFrameInSelf) - previewHeight * 0.6;
self.previewView.frame = CGRectMake(0, 0, previewWidth, previewHeight);
self.previewView.center = CGPointMake(centerX, centerY);
self.previewView.alpha = 0.0;
self.previewView.hidden = NO;
[UIView animateWithDuration:kKBPreviewShowDuration
delay:0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut
animations:^{
self.previewView.alpha = 1.0;
}
completion:nil];
}
- (void)hidePreview {
if (!self.previewView || self.previewView.isHidden) return;
[UIView animateWithDuration:kKBPreviewHideDuration
delay:0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn
animations:^{
self.previewView.alpha = 0.0;
}
completion:^(BOOL finished) {
self.previewView.hidden = YES;
}];
}
- (void)bringPreviewToFrontIfNeededInContainer:(UIView *)container {
if (!container) { return; }
if (self.previewView && self.previewView.superview == container) {
[container bringSubviewToFront:self.previewView];
}
}
#pragma mark - Private
- (BOOL)isHitInsideKeyRows:(UIView *)hitView rowViews:(NSArray<UIView *> *)rowViews {
if (!hitView) { return NO; }
if ([rowViews containsObject:hitView]) { return YES; }
for (UIView *row in rowViews) {
if ([hitView isDescendantOfView:row]) { return YES; }
}
return NO;
}
- (KBKeyButton *)nearestKeyButtonForPoint:(CGPoint)point
container:(UIView *)container
rowViews:(NSArray<UIView *> *)rowViews {
if (!container) { return nil; }
KBKeyButton *best = nil;
CGFloat bestDistance = CGFLOAT_MAX;
UIView *targetRow = nil;
for (UIView *row in rowViews) {
CGRect rowFrame = [container convertRect:row.bounds fromView:row];
if (CGRectContainsPoint(rowFrame, point)) {
targetRow = row;
break;
}
}
NSArray<UIView *> *candidateRows = targetRow ? @[targetRow] : rowViews;
for (UIView *row in candidateRows) {
NSArray<KBKeyButton *> *buttons = [self collectKeyButtonsInView:row];
for (KBKeyButton *btn in buttons) {
CGRect frame = [container convertRect:btn.frame fromView:btn.superview];
CGFloat dx = point.x - CGRectGetMidX(frame);
CGFloat dy = point.y - CGRectGetMidY(frame);
CGFloat dist = (dx * dx) + (dy * dy);
if (dist < bestDistance) {
bestDistance = dist;
best = btn;
}
}
}
return best;
}
- (void)collectKeyButtonsInView:(UIView *)view
into:(NSMutableArray<KBKeyButton *> *)buttons {
for (UIView *sub in view.subviews) {
if ([sub isKindOfClass:[KBKeyButton class]]) {
[buttons addObject:(KBKeyButton *)sub];
continue;
}
if (sub.subviews.count > 0) {
[self collectKeyButtonsInView:sub into:buttons];
}
}
}
@end

View File

@@ -0,0 +1,28 @@
//
// KBKeyboardKeyFactory.h
// CustomKeyboard
//
// Key creation helper for keyboard layouts.
//
#import <Foundation/Foundation.h>
@class KBKeyboardLayoutConfig;
@class KBKeyboardKeyDef;
@class KBKey;
NS_ASSUME_NONNULL_BEGIN
@interface KBKeyboardKeyFactory : NSObject
- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig;
- (nullable KBKey *)keyForItemId:(NSString *)itemId shiftOn:(BOOL)shiftOn;
- (nullable KBKey *)keyFromDef:(KBKeyboardKeyDef *)def
identifier:(NSString *)identifier
shiftOn:(BOOL)shiftOn;
- (nullable KBKey *)letterKeyWithChar:(NSString *)charString shiftOn:(BOOL)shiftOn;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,186 @@
//
// KBKeyboardKeyFactory.m
// CustomKeyboard
//
#import "KBKeyboardKeyFactory.h"
#import "KBKeyboardLayoutConfig.h"
#import "KBKey.h"
@interface KBKeyboardKeyFactory ()
@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig;
@end
@implementation KBKeyboardKeyFactory
- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig {
self = [super init];
if (self) {
_layoutConfig = layoutConfig;
}
return self;
}
- (KBKey *)keyForItemId:(NSString *)itemId shiftOn:(BOOL)shiftOn {
if (itemId.length == 0) { return nil; }
KBKeyboardKeyDef *def = [self.layoutConfig keyDefForIdentifier:itemId];
if (def) {
return [self keyFromDef:def identifier:itemId shiftOn:shiftOn];
}
NSRange range = [itemId rangeOfString:@":"];
if (range.location != NSNotFound) {
NSString *prefix = [itemId substringToIndex:range.location];
NSString *value = [itemId substringFromIndex:range.location + 1];
if ([prefix isEqualToString:@"letter"]) {
if (value.length >= 1) {
return [self letterKeyWithChar:value shiftOn:shiftOn];
}
return nil;
}
if ([prefix isEqualToString:@"digit"]) {
NSString *identifier = [NSString stringWithFormat:@"digit_%@", value];
KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter];
k.caseVariant = KBKeyCaseVariantNone;
return k;
}
if ([prefix isEqualToString:@"sym"]) {
NSString *identifier = [self kb_identifierForSymbol:value];
KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter];
k.caseVariant = KBKeyCaseVariantNone;
return k;
}
}
return nil;
}
- (KBKey *)keyFromDef:(KBKeyboardKeyDef *)def
identifier:(NSString *)identifier
shiftOn:(BOOL)shiftOn {
KBKeyType type = [self kb_keyTypeForDef:def];
NSString *title = def.title ?: @"";
if (type == KBKeyTypeShift && shiftOn && def.selectedTitle.length > 0) {
title = def.selectedTitle;
}
NSString *output = @"";
switch (type) {
case KBKeyTypeSpace:
output = @" ";
break;
case KBKeyTypeReturn:
output = @"\n";
break;
default:
output = @"";
break;
}
NSString *finalId = identifier;
if ([identifier isEqualToString:@"emoji"]) {
finalId = KBKeyIdentifierEmojiPanel;
} else if ([identifier isEqualToString:@"send"]) {
finalId = @"return";
}
KBKey *k = [KBKey keyWithIdentifier:finalId title:title output:output type:type];
if (type == KBKeyTypeShift) {
k.caseVariant = shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower;
} else {
k.caseVariant = KBKeyCaseVariantNone;
}
return k;
}
- (KBKey *)letterKeyWithChar:(NSString *)charString shiftOn:(BOOL)shiftOn {
if (charString.length == 0) { return nil; }
NSString *lower = charString.lowercaseString;
NSString *upper = charString.uppercaseString;
NSString *shown = shiftOn ? upper : lower;
NSString *identifier = [NSString stringWithFormat:@"letter_%@", lower];
KBKey *k = [KBKey keyWithIdentifier:identifier
title:shown
output:shown
type:KBKeyTypeCharacter];
k.caseVariant = shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower;
return k;
}
#pragma mark - Private
- (KBKeyType)kb_keyTypeForDef:(KBKeyboardKeyDef *)def {
NSString *type = def.type.lowercaseString;
if ([type isEqualToString:@"shift"]) return KBKeyTypeShift;
if ([type isEqualToString:@"backspace"]) return KBKeyTypeBackspace;
if ([type isEqualToString:@"mode"]) return KBKeyTypeModeChange;
if ([type isEqualToString:@"symbolstoggle"]) return KBKeyTypeSymbolsToggle;
if ([type isEqualToString:@"space"]) return KBKeyTypeSpace;
if ([type isEqualToString:@"return"]) return KBKeyTypeReturn;
if ([type isEqualToString:@"globe"]) return KBKeyTypeGlobe;
return KBKeyTypeCustom;
}
- (NSString *)kb_identifierForSymbol:(NSString *)symbol {
if (symbol.length == 0) { return nil; }
static NSDictionary<NSString *, NSString *> *map = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
map = @{
@"-": @"sym_minus",
@"/": @"sym_slash",
@":": @"sym_colon",
@";": @"sym_semicolon",
@"(": @"sym_paren_l",
@")": @"sym_paren_r",
@"¥": @"sym_money",
@"¥": @"sym_money",
@"&": @"sym_amp",
@"@": @"sym_at",
@"\"": @"sym_quote_double",
@"“": @"sym_quote_double",
@"”": @"sym_quote_double",
@".": @"sym_dot",
@"。": @"sym_chinese_dot",
@",": @"sym_comma",
@"、": @"sym_dun",
@"?": @"sym_question",
@"!": @"sym_exclam",
@"'": @"sym_quote_single",
@"": @"sym_quote_single",
@"": @"sym_quote_single",
@"[": @"sym_bracket_l",
@"]": @"sym_bracket_r",
@"{": @"sym_brace_l",
@"}": @"sym_brace_r",
@"「": @"sym_corner_l",
@"」": @"sym_corner_r",
@"#": @"sym_hash",
@"%": @"sym_percent",
@"^": @"sym_caret",
@"*": @"sym_asterisk",
@"+": @"sym_plus",
@"=": @"sym_equal",
@"_": @"sym_underscore",
@"\\": @"sym_backslash",
@"|": @"sym_pipe",
@"~": @"sym_tilde",
@"<": @"sym_lt",
@">": @"sym_gt",
@"€": @"sym_euro",
@"$": @"sym_dollar",
@"£": @"sym_pound",
@"·": @"sym_bullet",
@"^_^": @"sym_face",
@"—": @"sym_emdash",
@"«": @"sym_guillemet_l",
@"»": @"sym_guillemet_r",
@"《": @"sym_book_title_l",
@"》": @"sym_book_title_r",
@"...": @"sym_ellipsis"
};
});
return map[symbol];
}
@end

View File

@@ -0,0 +1,42 @@
//
// KBKeyboardLayoutEngine.h
// CustomKeyboard
//
// Layout metrics calculation helper.
//
#import <Foundation/Foundation.h>
@class KBKeyboardLayoutConfig;
@class KBKeyboardLayout;
@class KBKeyboardRowConfig;
@class KBKeyboardRowItem;
@class KBKeyboardKeyFactory;
@class KBKey;
NS_ASSUME_NONNULL_BEGIN
@interface KBKeyboardLayoutEngine : NSObject
- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig;
- (CGFloat)rowSpacingForLayout:(KBKeyboardLayout *)layout;
- (CGFloat)topInsetForLayout:(KBKeyboardLayout *)layout;
- (CGFloat)bottomInsetForLayout:(KBKeyboardLayout *)layout;
- (CGFloat)rowHeightForRow:(KBKeyboardRowConfig *)row;
- (CGFloat)gapForRow:(KBKeyboardRowConfig *)row;
- (CGFloat)insetLeftForRow:(KBKeyboardRowConfig *)row;
- (CGFloat)insetRightForRow:(KBKeyboardRowConfig *)row;
- (CGFloat)widthForItem:(KBKeyboardRowItem *)item key:(KBKey *)key;
- (CGFloat)fontSizeForItem:(KBKeyboardRowItem *)item key:(KBKey *)key;
- (CGFloat)fontSizeForFontKey:(NSString *)fontKey;
- (CGFloat)calculateUniformCharKeyWidthForRows:(NSArray<KBKeyboardRowConfig *> *)rows
keyFactory:(KBKeyboardKeyFactory *)keyFactory
shiftOn:(BOOL)shiftOn;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,245 @@
//
// KBKeyboardLayoutEngine.m
// CustomKeyboard
//
#import "KBKeyboardLayoutEngine.h"
#import "KBKeyboardLayoutConfig.h"
#import "KBKeyboardKeyFactory.h"
#import "KBKey.h"
#import "KBConfig.h"
@interface KBKeyboardLayoutEngine ()
@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig;
@end
@implementation KBKeyboardLayoutEngine
- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig {
self = [super init];
if (self) {
_layoutConfig = layoutConfig;
}
return self;
}
- (CGFloat)rowSpacingForLayout:(KBKeyboardLayout *)layout {
KBKeyboardLayoutConfig *config = self.layoutConfig;
NSNumber *layoutSpacing = layout.rowSpacing;
return [self kb_metricValue:layoutSpacing fallback:config.metrics.rowSpacing defaultValue:8.0];
}
- (CGFloat)topInsetForLayout:(KBKeyboardLayout *)layout {
KBKeyboardLayoutConfig *config = self.layoutConfig;
NSNumber *layoutInset = layout.topInset;
return [self kb_metricValue:layoutInset fallback:config.metrics.topInset defaultValue:8.0];
}
- (CGFloat)bottomInsetForLayout:(KBKeyboardLayout *)layout {
KBKeyboardLayoutConfig *config = self.layoutConfig;
NSNumber *layoutInset = layout.bottomInset;
return [self kb_metricValue:layoutInset fallback:config.metrics.bottomInset defaultValue:6.0];
}
- (CGFloat)rowHeightForRow:(KBKeyboardRowConfig *)row {
KBKeyboardLayoutConfig *config = self.layoutConfig;
NSNumber *height = row.height ?: config.metrics.keyHeight;
CGFloat value = [self kb_numberValue:height defaultValue:40.0];
return [self kb_scaledValue:value];
}
- (CGFloat)gapForRow:(KBKeyboardRowConfig *)row {
KBKeyboardLayoutConfig *config = self.layoutConfig;
return [self kb_metricValue:row.gap fallback:config.metrics.gap defaultValue:5.0];
}
- (CGFloat)insetLeftForRow:(KBKeyboardRowConfig *)row {
KBKeyboardLayoutConfig *config = self.layoutConfig;
return [self kb_metricValue:row.insetLeft fallback:config.metrics.edgeInset defaultValue:0.0];
}
- (CGFloat)insetRightForRow:(KBKeyboardRowConfig *)row {
KBKeyboardLayoutConfig *config = self.layoutConfig;
return [self kb_metricValue:row.insetRight fallback:config.metrics.edgeInset defaultValue:0.0];
}
- (CGFloat)widthForItem:(KBKeyboardRowItem *)item key:(KBKey *)key {
CGFloat width = 0.0;
if (item.widthValue.doubleValue > 0.0) {
width = item.widthValue.doubleValue;
} else if (item.width.length > 0) {
if ([item.width.lowercaseString isEqualToString:@"flex"]) {
return 0.0;
}
width = [self kb_metricWidthForKey:item.width];
if (width <= 0.0) {
width = item.width.doubleValue;
}
}
if (width <= 0.0) {
KBKeyboardLayoutMetrics *m = self.layoutConfig.metrics;
if ([item.itemId hasPrefix:@"letter:"] ||
[item.itemId hasPrefix:@"digit:"] ||
[item.itemId hasPrefix:@"sym:"]) {
width = m.letterWidth.doubleValue;
} else if (key.type == KBKeyTypeReturn) {
width = m.sendWidth.doubleValue;
} else if (key.type == KBKeyTypeSpace) {
return 0.0;
} else {
width = m.controlWidth.doubleValue;
}
}
if (width <= 0.0) {
if ([item.itemId hasPrefix:@"letter:"] ||
[item.itemId hasPrefix:@"digit:"] ||
[item.itemId hasPrefix:@"sym:"]) {
width = 32.0;
} else if (key.type == KBKeyTypeReturn) {
width = 88.0;
} else if (key.type == KBKeyTypeSpace) {
return 0.0;
} else {
width = 41.0;
}
}
return width > 0.0 ? [self kb_scaledValue:width] : 0.0;
}
- (CGFloat)fontSizeForItem:(KBKeyboardRowItem *)item key:(KBKey *)key {
NSString *fontKey = nil;
if ([item.itemId hasPrefix:@"letter:"]) {
fontKey = @"letter";
} else if ([item.itemId hasPrefix:@"digit:"]) {
fontKey = @"digit";
} else if ([item.itemId hasPrefix:@"sym:"]) {
fontKey = @"symbol";
} else {
KBKeyboardKeyDef *def = [self.layoutConfig keyDefForIdentifier:item.itemId];
fontKey = def.font;
}
if (fontKey.length == 0) {
switch (key.type) {
case KBKeyTypeModeChange:
case KBKeyTypeSymbolsToggle:
fontKey = @"mode";
break;
case KBKeyTypeSpace:
fontKey = @"space";
break;
case KBKeyTypeReturn:
fontKey = @"send";
break;
default:
fontKey = @"symbol";
break;
}
}
return [self fontSizeForFontKey:fontKey];
}
- (CGFloat)fontSizeForFontKey:(NSString *)fontKey {
KBKeyboardLayoutFonts *fonts = self.layoutConfig.fonts;
CGFloat size = 0.0;
if ([fontKey isEqualToString:@"letter"]) { size = fonts.letter.doubleValue; }
else if ([fontKey isEqualToString:@"digit"]) { size = fonts.digit.doubleValue; }
else if ([fontKey isEqualToString:@"symbol"]) { size = fonts.symbol.doubleValue; }
else if ([fontKey isEqualToString:@"mode"]) { size = fonts.mode.doubleValue; }
else if ([fontKey isEqualToString:@"space"]) { size = fonts.space.doubleValue; }
else if ([fontKey isEqualToString:@"send"]) { size = fonts.send.doubleValue; }
if (size <= 0.0) { size = 18.0; }
return [self kb_scaledValue:size];
}
/// insets/gap/
///
/// 0
- (CGFloat)calculateUniformCharKeyWidthForRows:(NSArray<KBKeyboardRowConfig *> *)rows
keyFactory:(KBKeyboardKeyFactory *)keyFactory
shiftOn:(BOOL)shiftOn {
CGFloat minWidth = CGFLOAT_MAX;
CGFloat maxWidth = 0.0;
BOOL hasCharRow = NO;
CGFloat containerWidth = KBScreenWidth();
for (KBKeyboardRowConfig *row in rows) {
if (row.segments) { continue; } //
NSArray<KBKeyboardRowItem *> *items = [row resolvedItems];
NSUInteger charCount = 0;
CGFloat nonCharWidth = 0.0;
for (KBKeyboardRowItem *item in items) {
BOOL isChar = [item.itemId hasPrefix:@"letter:"] ||
[item.itemId hasPrefix:@"digit:"] ||
[item.itemId hasPrefix:@"sym:"];
if (isChar) {
charCount++;
} else {
KBKey *key = [keyFactory keyForItemId:item.itemId shiftOn:shiftOn];
CGFloat w = [self widthForItem:item key:key];
nonCharWidth += w;
}
}
if (charCount == 0) { continue; } //
hasCharRow = YES;
// 使 insets gap
CGFloat gap = [self gapForRow:row];
CGFloat insetLeft = [self insetLeftForRow:row];
CGFloat insetRight = [self insetRightForRow:row];
CGFloat totalGaps = (items.count > 1) ? (items.count - 1) * gap : 0.0;
CGFloat available = containerWidth - insetLeft - insetRight - totalGaps - nonCharWidth;
CGFloat width = available / charCount;
if (width < minWidth) { minWidth = width; }
if (width > maxWidth) { maxWidth = width; }
}
if (!hasCharRow || minWidth <= 0.0 || minWidth >= CGFLOAT_MAX) { return 0.0; }
//
if (fabs(maxWidth - minWidth) < 0.5) { return 0.0; }
return minWidth;
}
#pragma mark - Helpers
- (CGFloat)kb_scaledValue:(CGFloat)designValue {
if (self.layoutConfig) {
return [self.layoutConfig scaledValue:designValue];
}
return KBFit(designValue);
}
- (CGFloat)kb_numberValue:(NSNumber *)value defaultValue:(CGFloat)defaultValue {
if ([value isKindOfClass:[NSNumber class]]) {
return value.doubleValue;
}
return defaultValue;
}
- (CGFloat)kb_metricValue:(NSNumber *)value fallback:(NSNumber *)fallback defaultValue:(CGFloat)defaultValue {
CGFloat v = [self kb_numberValue:value defaultValue:-1.0];
if (v < 0.0) {
v = [self kb_numberValue:fallback defaultValue:defaultValue];
}
if (v < 0.0) {
v = defaultValue;
}
return [self kb_scaledValue:v];
}
- (CGFloat)kb_metricWidthForKey:(NSString *)key {
KBKeyboardLayoutMetrics *m = self.layoutConfig.metrics;
if ([key isEqualToString:@"letterWidth"]) { return m.letterWidth.doubleValue; }
if ([key isEqualToString:@"controlWidth"]) { return m.controlWidth.doubleValue; }
if ([key isEqualToString:@"sendWidth"]) { return m.sendWidth.doubleValue; }
if ([key isEqualToString:@"symbolsWideWidth"]) { return m.symbolsWideWidth.doubleValue; }
if ([key isEqualToString:@"symbolsSideWidth"]) { return m.symbolsSideWidth.doubleValue; }
return 0.0;
}
@end

View File

@@ -0,0 +1,28 @@
//
// KBKeyboardLegacyBuilder.h
// CustomKeyboard
//
// Legacy layout builder (non-config layout).
//
#import <Foundation/Foundation.h>
@class KBKey;
@class KBBackspaceLongPressHandler;
@class UIView;
NS_ASSUME_NONNULL_BEGIN
@interface KBKeyboardLegacyBuilder : NSObject
- (void)buildRow:(UIView *)row
withKeys:(NSArray<KBKey *> *)keys
edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier
shiftOn:(BOOL)shiftOn
backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler
target:(id)target
action:(SEL)action;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,291 @@
//
// KBKeyboardLegacyBuilder.m
// CustomKeyboard
//
#import "KBKeyboardLegacyBuilder.h"
#import "KBKey.h"
#import "KBKeyButton.h"
#import "KBBackspaceLongPressHandler.h"
#import "KBConfig.h"
#import <Masonry/Masonry.h>
static const CGFloat kKBSpecialKeySquareMultiplier = 1.2;
static const CGFloat kKBReturnWidthMultiplier = 2.4;
static const CGFloat kKBSpaceWidthMultiplier = 3.0;
static inline CGFloat KBLegacyRowHorizontalInset(void) {
return KBFit(6.0f);
}
@implementation KBKeyboardLegacyBuilder
- (void)buildRow:(UIView *)row
withKeys:(NSArray<KBKey *> *)keys
edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier
shiftOn:(BOOL)shiftOn
backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler
target:(id)target
action:(SEL)action {
if (!row || keys.count == 0) { return; }
// 4 使
// 123/ABCEmojiSend Space
BOOL isBottomControlRow = [self kb_isBottomControlRowWithKeys:keys];
CGFloat spacing = 0; //
UIView *previous = nil;
UIView *leftSpacer = nil;
UIView *rightSpacer = nil;
if (edgeSpacerMultiplier > 0.0) {
leftSpacer = [UIView new];
rightSpacer = [UIView new];
leftSpacer.backgroundColor = [UIColor clearColor];
rightSpacer.backgroundColor = [UIColor clearColor];
[row addSubview:leftSpacer];
[row addSubview:rightSpacer];
[leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(row.mas_left).offset(KBLegacyRowHorizontalInset());
make.centerY.equalTo(row);
make.height.mas_equalTo(1);
}];
[rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(row.mas_right).offset(-KBLegacyRowHorizontalInset());
make.centerY.equalTo(row);
make.height.mas_equalTo(1);
}];
}
for (NSInteger i = 0; i < keys.count; i++) {
KBKey *key = keys[i];
KBKeyButton *btn = [[KBKeyButton alloc] init];
btn.key = key;
[btn setTitle:key.title forState:UIControlStateNormal];
//
[btn applyThemeForCurrentKey];
if (target && action) {
[btn addTarget:target action:action forControlEvents:UIControlEventTouchDown];
}
[row addSubview:btn];
if (key.type == KBKeyTypeBackspace) {
[backspaceHandler bindDeleteButton:btn showClearLabel:YES];
}
// Shift
if (key.type == KBKeyTypeShift) {
btn.selected = shiftOn;
}
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.equalTo(row);
if (previous) {
make.left.equalTo(previous.mas_right).offset(spacing);
} else {
if (leftSpacer) {
make.left.equalTo(leftSpacer.mas_right).offset(spacing);
} else {
make.left.equalTo(row.mas_left).offset(KBLegacyRowHorizontalInset());
}
}
}];
//
if (key.type == KBKeyTypeCharacter) {
if (previous && [previous isKindOfClass:[KBKeyButton class]]) {
KBKeyButton *prevBtn = (KBKeyButton *)previous;
if (prevBtn.key.type == KBKeyTypeCharacter) {
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(previous);
}];
}
}
} else {
// special keys:
}
previous = btn;
}
// 使
if (previous) {
[previous mas_makeConstraints:^(MASConstraintMaker *make) {
if (rightSpacer) {
make.right.equalTo(rightSpacer.mas_left).offset(-spacing);
} else {
make.right.equalTo(row.mas_right).offset(-KBLegacyRowHorizontalInset());
}
}];
}
// 123/ABCEmojiSend
// Space
if (isBottomControlRow) {
[self kb_applyBottomControlRowWidthInRow:row];
return;
}
//
KBKeyButton *firstChar = nil;
BOOL hasCharacterInRow = NO;
for (UIView *v in row.subviews) {
if (![v isKindOfClass:[KBKeyButton class]]) continue;
KBKeyButton *b = (KBKeyButton *)v;
if (b.key.type == KBKeyTypeCharacter) {
firstChar = b;
hasCharacterInRow = YES;
break;
}
}
// 使
if (!firstChar) {
for (UIView *v in row.subviews) {
if ([v isKindOfClass:[KBKeyButton class]]) {
firstChar = (KBKeyButton *)v;
break;
}
}
}
if (firstChar) {
// 123/ABC/#+=
// 1:1 123/ABC
if (!hasCharacterInRow &&
(firstChar.key.type == KBKeyTypeModeChange ||
firstChar.key.type == KBKeyTypeSymbolsToggle ||
firstChar.key.type == KBKeyTypeCustom)) {
[firstChar mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(firstChar.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
}];
}
for (UIView *v in row.subviews) {
if (![v isKindOfClass:[KBKeyButton class]]) continue;
KBKeyButton *b = (KBKeyButton *)v;
// self == self * k
if (b == firstChar) continue;
if (b.key.type == KBKeyTypeCharacter) continue;
BOOL isBottomModeKey = (b.key.type == KBKeyTypeModeChange) ||
(b.key.type == KBKeyTypeSymbolsToggle) ||
(b.key.type == KBKeyTypeCustom);
// ~
if (b.key.type == KBKeyTypeShift ||
b.key.type == KBKeyTypeBackspace ||
isBottomModeKey) {
[b mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(b.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
}];
continue;
}
CGFloat multiplier = 1.5;
// Space
if (b.key.type == KBKeyTypeSpace) {
multiplier = kKBSpaceWidthMultiplier;
}
// Send 2.4
else if (b.key.type == KBKeyTypeReturn) {
multiplier = kKBReturnWidthMultiplier;
}
// Globe
else if (b.key.type == KBKeyTypeGlobe) {
multiplier = 1.5;
}
[b mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(firstChar).multipliedBy(multiplier);
}];
}
//
//
//
if (leftSpacer && rightSpacer) {
// 1)
[leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(rightSpacer);
}];
// 2) edgeSpacerMultiplier
[rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(firstChar).multipliedBy(edgeSpacerMultiplier);
}];
}
}
}
#pragma mark - Row Helpers (Bottom Control Row)
// Space + Return ModeChange/SymbolsToggle
//
- (BOOL)kb_isBottomControlRowWithKeys:(NSArray<KBKey *> *)keys {
BOOL hasSpace = NO;
BOOL hasReturn = NO;
BOOL hasModeOrSymbols = NO;
BOOL hasCharacters = NO;
for (KBKey *k in keys) {
if (k.type == KBKeyTypeSpace) {
hasSpace = YES;
} else if (k.type == KBKeyTypeReturn) {
hasReturn = YES;
} else if (k.type == KBKeyTypeModeChange || k.type == KBKeyTypeSymbolsToggle) {
hasModeOrSymbols = YES;
} else if (k.type == KBKeyTypeCharacter) {
hasCharacters = YES;
}
}
return (hasSpace && hasReturn && hasModeOrSymbols && !hasCharacters);
}
- (void)kb_applyBottomControlRowWidthInRow:(UIView *)row {
if (!row) { return; }
KBKeyButton *modeBtn = nil;
KBKeyButton *spaceBtn = nil;
KBKeyButton *retBtn = nil;
NSMutableArray<KBKeyButton *> *customButtons = [NSMutableArray array];
for (UIView *v in row.subviews) {
if (![v isKindOfClass:[KBKeyButton class]]) continue;
KBKeyButton *b = (KBKeyButton *)v;
switch (b.key.type) {
case KBKeyTypeModeChange:
case KBKeyTypeSymbolsToggle:
modeBtn = b;
break;
case KBKeyTypeCustom:
[customButtons addObject:b];
break;
case KBKeyTypeSpace:
spaceBtn = b;
break;
case KBKeyTypeReturn:
retBtn = b;
break;
default:
break;
}
}
if (!modeBtn || customButtons.count == 0 || !spaceBtn || !retBtn) {
return;
}
[modeBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
}];
for (KBKeyButton *custom in customButtons) {
[custom mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
}];
}
[retBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(modeBtn.mas_width).multipliedBy(2.0);
}];
// Space
(void)spaceBtn;
}
@end

View File

@@ -0,0 +1,22 @@
//
// KBKeyboardLegacyLayoutProvider.h
// CustomKeyboard
//
#import <Foundation/Foundation.h>
@class KBKey;
@class KBKeyboardKeyFactory;
NS_ASSUME_NONNULL_BEGIN
@interface KBKeyboardLegacyLayoutProvider : NSObject
- (NSArray<NSArray<KBKey *> *> *)keysForLayoutStyleIsNumbers:(BOOL)isNumbersLayout
shiftOn:(BOOL)shiftOn
symbolsMoreOn:(BOOL)symbolsMoreOn
keyFactory:(KBKeyboardKeyFactory *)keyFactory;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,193 @@
//
// KBKeyboardLegacyLayoutProvider.m
// CustomKeyboard
//
#import "KBKeyboardLegacyLayoutProvider.h"
#import "KBKeyboardKeyFactory.h"
#import "KBKey.h"
@implementation KBKeyboardLegacyLayoutProvider
- (NSArray<NSArray<KBKey *> *> *)keysForLayoutStyleIsNumbers:(BOOL)isNumbersLayout
shiftOn:(BOOL)shiftOn
symbolsMoreOn:(BOOL)symbolsMoreOn
keyFactory:(KBKeyboardKeyFactory *)keyFactory {
if (isNumbersLayout) {
return [self buildKeysForNumbersLayoutWithSymbolsMoreOn:symbolsMoreOn];
}
return [self buildKeysForLettersLayoutWithShiftOn:shiftOn keyFactory:keyFactory];
}
#pragma mark - Letters Layout
- (NSArray<NSArray<KBKey *> *> *)buildKeysForLettersLayoutWithShiftOn:(BOOL)shiftOn
keyFactory:(KBKeyboardKeyFactory *)keyFactory {
// QWERTY
NSArray *r1Letters = @[ @"q", @"w", @"e", @"r", @"t", @"y", @"u", @"i", @"o", @"p" ];
NSArray *r2Letters = @[ @"a", @"s", @"d", @"f", @"g", @"h", @"j", @"k", @"l" ];
NSArray *r3Letters = @[ @"z", @"x", @"c", @"v", @"b", @"n", @"m" ];
NSMutableArray *row1 = [NSMutableArray arrayWithCapacity:r1Letters.count];
for (NSString *s in r1Letters) {
KBKey *key = [keyFactory letterKeyWithChar:s shiftOn:shiftOn];
if (key) { [row1 addObject:key]; }
}
NSMutableArray *row2 = [NSMutableArray arrayWithCapacity:r2Letters.count];
for (NSString *s in r2Letters) {
KBKey *key = [keyFactory letterKeyWithChar:s shiftOn:shiftOn];
if (key) { [row2 addObject:key]; }
}
// Shift + Z...M + Backspace
NSMutableArray *row3 = [NSMutableArray array];
KBKey *shift = [KBKey keyWithIdentifier:@"shift"
title:@"⇧"
output:@""
type:KBKeyTypeShift];
// Shift
shift.caseVariant = shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower;
[row3 addObject:shift];
for (NSString *s in r3Letters) {
KBKey *key = [keyFactory letterKeyWithChar:s shiftOn:shiftOn];
if (key) { [row3 addObject:key]; }
}
KBKey *backspace = [KBKey keyWithIdentifier:@"backspace"
title:@"⌫"
output:@""
type:KBKeyTypeBackspace];
[row3 addObject:backspace];
NSArray *row4 = [self bottomControlRowKeysForLettersLayout];
return @[row1.copy, row2.copy, row3.copy, row4];
}
#pragma mark - Numbers / Symbols Layout
- (NSArray<NSArray<KBKey *> *> *)buildKeysForNumbersLayoutWithSymbolsMoreOn:(BOOL)symbolsMoreOn {
// /3 +
NSArray *r1 = nil;
NSArray *r2 = nil;
NSArray *r3 = nil;
if (!symbolsMoreOn) {
// 123
r1 = @[ [KBKey keyWithIdentifier:@"digit_1" title:@"1" output:@"1" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"digit_2" title:@"2" output:@"2" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"digit_3" title:@"3" output:@"3" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"digit_4" title:@"4" output:@"4" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"digit_5" title:@"5" output:@"5" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"digit_6" title:@"6" output:@"6" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"digit_7" title:@"7" output:@"7" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"digit_8" title:@"8" output:@"8" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"digit_9" title:@"9" output:@"9" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"digit_0" title:@"0" output:@"0" type:KBKeyTypeCharacter] ];
r2 = @[ [KBKey keyWithIdentifier:@"sym_minus" title:@"-" output:@"-" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_slash" title:@"/" output:@"/" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_colon" title:@":" output:@":" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_semicolon" title:@";" output:@";" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_paren_l" title:@"(" output:@"(" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_paren_r" title:@")" output:@")" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_money" title:@"¥" output:@"¥" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_amp" title:@"&" output:@"&" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_at" title:@"@" output:@"@" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_quote_double" title:@"\"" output:@"\"" type:KBKeyTypeCharacter] ];
r3 = [self symbolsCommonThirdRowWithToggleIsMore:NO];
} else {
// #+=123
r1 = @[ [KBKey keyWithIdentifier:@"sym_bracket_l" title:@"[" output:@"[" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_bracket_r" title:@"]" output:@"]" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_brace_l" title:@"{" output:@"{" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_brace_r" title:@"}" output:@"}" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_hash" title:@"#" output:@"#" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_percent" title:@"%" output:@"%" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_caret" title:@"^" output:@"^" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_asterisk" title:@"*" output:@"*" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_plus" title:@"+" output:@"+" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_equal" title:@"=" output:@"=" type:KBKeyTypeCharacter] ];
r2 = @[ [KBKey keyWithIdentifier:@"sym_underscore" title:@"_" output:@"_" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_backslash" title:@"\\" output:@"\\" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_pipe" title:@"|" output:@"|" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_tilde" title:@"~" output:@"~" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_lt" title:@"<" output:@"<" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_gt" title:@">" output:@">" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_dollar" title:@"$" output:@"$" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_euro" title:@"€" output:@"€" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_pound" title:@"£" output:@"£" type:KBKeyTypeCharacter],
[KBKey keyWithIdentifier:@"sym_bullet" title:@"•" output:@"•" type:KBKeyTypeCharacter] ];
r3 = [self symbolsCommonThirdRowWithToggleIsMore:YES];
}
NSArray *r4 = [self bottomControlRowKeysForNumbersLayout];
return @[r1, r2, r3, r4];
}
#pragma mark - Key Factories
- (NSArray<KBKey *> *)symbolsCommonThirdRowWithToggleIsMore:(BOOL)isMorePage {
NSString *identifier = isMorePage ? @"symbols_toggle_123" : @"symbols_toggle_more";
NSString *title = isMorePage ? @"123" : @"#+=";
KBKey *toggle = [KBKey keyWithIdentifier:identifier
title:title
output:@""
type:KBKeyTypeSymbolsToggle];
KBKey *comma = [KBKey keyWithIdentifier:@"sym_comma" title:@"," output:@"," type:KBKeyTypeCharacter];
KBKey *dot = [KBKey keyWithIdentifier:@"sym_dot" title:@"." output:@"." type:KBKeyTypeCharacter];
KBKey *q = [KBKey keyWithIdentifier:@"sym_question" title:@"?" output:@"?" type:KBKeyTypeCharacter];
KBKey *ex = [KBKey keyWithIdentifier:@"sym_exclam" title:@"!" output:@"!" type:KBKeyTypeCharacter];
KBKey *quote = [KBKey keyWithIdentifier:@"sym_quote_single" title:@"'" output:@"'" type:KBKeyTypeCharacter];
KBKey *back = [KBKey keyWithIdentifier:@"backspace"
title:@"⌫"
output:@""
type:KBKeyTypeBackspace];
return @[ toggle, comma, dot, q, ex, quote, back ];
}
- (NSArray<KBKey *> *)bottomControlRowKeysForLettersLayout {
KBKey *mode123 = [KBKey keyWithIdentifier:@"mode_123"
title:@"123"
output:@""
type:KBKeyTypeModeChange];
KBKey *emoji = [KBKey keyWithIdentifier:KBKeyIdentifierEmojiPanel
title:@"😊"
output:@""
type:KBKeyTypeCustom];
KBKey *space = [KBKey keyWithIdentifier:@"space"
title:@"space"
output:@" "
type:KBKeyTypeSpace];
KBKey *ret = [KBKey keyWithIdentifier:@"return"
title:KBLocalized(@"Send")
output:@"\n"
type:KBKeyTypeReturn];
return @[ mode123, emoji, space, ret ];
}
- (NSArray<KBKey *> *)bottomControlRowKeysForNumbersLayout {
KBKey *modeABC = [KBKey keyWithIdentifier:@"mode_abc"
title:@"abc"
output:@""
type:KBKeyTypeModeChange];
KBKey *emoji = [KBKey keyWithIdentifier:KBKeyIdentifierEmojiPanel
title:@"😊"
output:@""
type:KBKeyTypeCustom];
KBKey *space = [KBKey keyWithIdentifier:@"space"
title:@"space"
output:@" "
type:KBKeyTypeSpace];
KBKey *ret = [KBKey keyWithIdentifier:@"return"
title:KBLocalized(@"Send")
output:@"\n"
type:KBKeyTypeReturn];
return @[ modeABC, emoji, space, ret ];
}
@end

View File

@@ -0,0 +1,33 @@
//
// KBKeyboardRowBuilder.h
// CustomKeyboard
//
#import <Foundation/Foundation.h>
@class KBKeyboardLayoutConfig;
@class KBKeyboardLayoutEngine;
@class KBKeyboardKeyFactory;
@class KBKeyboardRowConfig;
@class KBBackspaceLongPressHandler;
@class UIView;
NS_ASSUME_NONNULL_BEGIN
@interface KBKeyboardRowBuilder : NSObject
- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig
layoutEngine:(KBKeyboardLayoutEngine *)layoutEngine
keyFactory:(KBKeyboardKeyFactory *)keyFactory;
- (void)buildRow:(UIView *)row
withRowConfig:(KBKeyboardRowConfig *)rowConfig
uniformCharWidth:(CGFloat)uniformCharWidth
shiftOn:(BOOL)shiftOn
backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler
target:(id)target
action:(SEL)action;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,330 @@
//
// KBKeyboardRowBuilder.m
// CustomKeyboard
//
#import "KBKeyboardRowBuilder.h"
#import "KBKeyboardLayoutConfig.h"
#import "KBKeyboardLayoutEngine.h"
#import "KBKeyboardKeyFactory.h"
#import "KBKeyButton.h"
#import "KBKey.h"
#import "KBBackspaceLongPressHandler.h"
#import "KBSkinManager.h"
#import <Masonry/Masonry.h>
@interface KBKeyboardRowBuilder ()
@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig;
@property (nonatomic, strong) KBKeyboardLayoutEngine *layoutEngine;
@property (nonatomic, strong) KBKeyboardKeyFactory *keyFactory;
@end
@implementation KBKeyboardRowBuilder
- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig
layoutEngine:(KBKeyboardLayoutEngine *)layoutEngine
keyFactory:(KBKeyboardKeyFactory *)keyFactory {
self = [super init];
if (self) {
_layoutConfig = layoutConfig;
_layoutEngine = layoutEngine;
_keyFactory = keyFactory;
}
return self;
}
- (void)buildRow:(UIView *)row
withRowConfig:(KBKeyboardRowConfig *)rowConfig
uniformCharWidth:(CGFloat)uniformCharWidth
shiftOn:(BOOL)shiftOn
backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler
target:(id)target
action:(SEL)action {
if (!row || !rowConfig) { return; }
CGFloat gap = [self.layoutEngine gapForRow:rowConfig];
CGFloat insetLeft = [self.layoutEngine insetLeftForRow:rowConfig];
CGFloat insetRight = [self.layoutEngine insetRightForRow:rowConfig];
if (rowConfig.segments) {
KBKeyboardRowSegments *segments = rowConfig.segments;
NSArray<KBKeyboardRowItem *> *leftItems = [segments leftItems];
NSArray<KBKeyboardRowItem *> *centerItems = [segments centerItems];
NSArray<KBKeyboardRowItem *> *rightItems = [segments rightItems];
UIView *leftContainer = [UIView new];
UIView *centerContainer = [UIView new];
UIView *rightContainer = [UIView new];
[row addSubview:leftContainer];
[row addSubview:centerContainer];
[row addSubview:rightContainer];
[leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(row.mas_left).offset(insetLeft);
make.top.bottom.equalTo(row);
}];
[rightContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(row.mas_right).offset(-insetRight);
make.top.bottom.equalTo(row);
}];
[centerContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(row);
make.top.bottom.equalTo(row);
make.left.greaterThanOrEqualTo(leftContainer.mas_right).offset(gap);
make.right.lessThanOrEqualTo(rightContainer.mas_left).offset(-gap);
}];
if (leftItems.count == 0) {
[leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(0);
}];
}
if (centerItems.count == 0) {
[centerContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(0);
}];
}
if (rightItems.count == 0) {
[rightContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(0);
}];
}
[self kb_buildButtonsInContainer:leftContainer
items:leftItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO
isTopLevelRow:NO
uniformCharWidth:uniformCharWidth
shiftOn:shiftOn
backspaceHandler:backspaceHandler
target:target
action:action];
[self kb_buildButtonsInContainer:centerContainer
items:centerItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO
isTopLevelRow:NO
uniformCharWidth:uniformCharWidth
shiftOn:shiftOn
backspaceHandler:backspaceHandler
target:target
action:action];
[self kb_buildButtonsInContainer:rightContainer
items:rightItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO
isTopLevelRow:NO
uniformCharWidth:uniformCharWidth
shiftOn:shiftOn
backspaceHandler:backspaceHandler
target:target
action:action];
return;
}
BOOL alignCenter = [rowConfig.align.lowercaseString isEqualToString:@"center"];
[self kb_buildButtonsInContainer:row
items:[rowConfig resolvedItems]
gap:gap
insetLeft:insetLeft
insetRight:insetRight
alignCenter:alignCenter
isTopLevelRow:YES
uniformCharWidth:uniformCharWidth
shiftOn:shiftOn
backspaceHandler:backspaceHandler
target:target
action:action];
}
#pragma mark - Private
- (void)kb_buildButtonsInContainer:(UIView *)container
items:(NSArray<KBKeyboardRowItem *> *)items
gap:(CGFloat)gap
insetLeft:(CGFloat)insetLeft
insetRight:(CGFloat)insetRight
alignCenter:(BOOL)alignCenter
isTopLevelRow:(BOOL)isTopLevelRow
uniformCharWidth:(CGFloat)uniformCharWidth
shiftOn:(BOOL)shiftOn
backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler
target:(id)target
action:(SEL)action {
if (items.count == 0) { return; }
UIView *leftSpacer = nil;
UIView *rightSpacer = nil;
if (alignCenter) {
leftSpacer = [UIView new];
rightSpacer = [UIView new];
[container addSubview:leftSpacer];
[container addSubview:rightSpacer];
[leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(container.mas_left).offset(insetLeft);
make.top.bottom.equalTo(container);
}];
[rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(container.mas_right).offset(-insetRight);
make.top.bottom.equalTo(container);
}];
[leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(rightSpacer);
}];
}
BOOL usingUniformWidth = (uniformCharWidth > 0.0);
BOOL allCharacterKeys = YES; //
KBKeyButton *previous = nil;
KBKeyButton *firstCharBtn = nil; //
for (KBKeyboardRowItem *item in items) {
KBKeyButton *btn = [self kb_buttonForItem:item
shiftOn:shiftOn
backspaceHandler:backspaceHandler
target:target
action:action];
if (!btn) { continue; }
[container addSubview:btn];
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.equalTo(container);
if (previous) {
make.left.equalTo(previous.mas_right).offset(gap);
} else {
if (leftSpacer) {
make.left.equalTo(leftSpacer.mas_right).offset(gap);
} else {
make.left.equalTo(container.mas_left).offset(insetLeft);
}
}
}];
// letter/digit/sym使
// shift/backspace/mode 使
BOOL isCharacterKey = [item.itemId hasPrefix:@"letter:"] ||
[item.itemId hasPrefix:@"digit:"] ||
[item.itemId hasPrefix:@"sym:"];
if (!isCharacterKey) { allCharacterKeys = NO; }
if (isCharacterKey && usingUniformWidth) {
// 使
CGFloat w = uniformCharWidth;
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(w);
}];
} else if (isCharacterKey) {
//
if (firstCharBtn) {
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(firstCharBtn);
}];
} else {
firstCharBtn = btn;
}
} else {
CGFloat width = [self.layoutEngine widthForItem:item key:btn.key];
if (width > 0.0) {
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(width);
}];
}
}
previous = btn;
}
if (!previous) { return; }
// 使
BOOL skipRightAnchor = isTopLevelRow && usingUniformWidth && allCharacterKeys;
if (!skipRightAnchor) {
[previous mas_makeConstraints:^(MASConstraintMaker *make) {
if (rightSpacer) {
make.right.equalTo(rightSpacer.mas_left).offset(-gap);
} else {
make.right.equalTo(container.mas_right).offset(-insetRight);
}
}];
}
}
- (KBKeyButton *)kb_buttonForItem:(KBKeyboardRowItem *)item
shiftOn:(BOOL)shiftOn
backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler
target:(id)target
action:(SEL)action {
if (item.itemId.length == 0) { return nil; }
KBKeyboardKeyDef *def = [self.layoutConfig keyDefForIdentifier:item.itemId];
KBKey *key = [self.keyFactory keyForItemId:item.itemId shiftOn:shiftOn];
if (!key) { return nil; }
KBKeyButton *btn = [[KBKeyButton alloc] init];
btn.key = key;
[btn setTitle:key.title forState:UIControlStateNormal];
UIColor *bgColor = [self kb_backgroundColorForItem:item keyDef:def];
if (bgColor) {
btn.customBackgroundColor = bgColor;
}
CGFloat fontSize = [self.layoutEngine fontSizeForItem:item key:key];
if (fontSize > 0.0) {
btn.titleLabel.font = [UIFont systemFontOfSize:fontSize weight:UIFontWeightSemibold];
}
[btn applyThemeForCurrentKey];
if (target && action) {
[btn addTarget:target action:action forControlEvents:UIControlEventTouchDown];
}
if (key.type == KBKeyTypeBackspace) {
[backspaceHandler bindDeleteButton:btn showClearLabel:YES];
}
if (key.type == KBKeyTypeShift) {
btn.selected = shiftOn;
}
[self kb_applySymbolIfNeededForButton:btn keyDef:def fontSize:fontSize];
return btn;
}
- (UIColor *)kb_backgroundColorForItem:(KBKeyboardRowItem *)item keyDef:(KBKeyboardKeyDef *)def {
NSString *hex = def.backgroundColor;
if (hex.length == 0) {
hex = self.layoutConfig.defaultKeyBackground;
}
if (hex.length == 0) { return nil; }
return [KBSkinManager colorFromHexString:hex defaultColor:nil];
}
- (void)kb_applySymbolIfNeededForButton:(KBKeyButton *)button
keyDef:(KBKeyboardKeyDef *)def
fontSize:(CGFloat)fontSize {
if (!button || !def) { return; }
if (button.iconView.image != nil) { return; }
NSString *symbolName = button.isSelected ? def.selectedSymbolName : def.symbolName;
if (symbolName.length == 0) { return; }
UIImage *image = [UIImage systemImageNamed:symbolName];
if (!image) { return; }
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:fontSize weight:UIFontWeightSemibold];
image = [image imageWithConfiguration:config];
button.iconView.image = image;
button.iconView.hidden = NO;
button.iconView.contentMode = UIViewContentModeCenter;
button.titleLabel.hidden = YES;
UIColor *textColor = [KBSkinManager shared].current.keyTextColor ?: [UIColor blackColor];
button.iconView.tintColor = button.isSelected ? [UIColor blackColor] : textColor;
}
@end

View File

@@ -0,0 +1,24 @@
//
// KBKeyboardRowContainerBuilder.h
// CustomKeyboard
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@class KBKeyboardRowConfig;
NS_ASSUME_NONNULL_BEGIN
@interface KBKeyboardRowContainerBuilder : NSObject
- (void)rebuildRowContainersForRows:(NSArray<KBKeyboardRowConfig *> *)rowConfigs
inContainer:(UIView *)container
rowViews:(NSMutableArray<UIView *> *)rowViews
rowSpacing:(CGFloat)rowSpacing
topInset:(CGFloat)topInset
bottomInset:(CGFloat)bottomInset;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,59 @@
//
// KBKeyboardRowContainerBuilder.m
// CustomKeyboard
//
#import "KBKeyboardRowContainerBuilder.h"
#import <Masonry/Masonry.h>
@implementation KBKeyboardRowContainerBuilder
- (void)rebuildRowContainersForRows:(NSArray<KBKeyboardRowConfig *> *)rowConfigs
inContainer:(UIView *)container
rowViews:(NSMutableArray<UIView *> *)rowViews
rowSpacing:(CGFloat)rowSpacing
topInset:(CGFloat)topInset
bottomInset:(CGFloat)bottomInset {
if (!container || !rowViews) { return; }
for (UIView *row in rowViews) {
[row removeFromSuperview];
}
[rowViews removeAllObjects];
NSUInteger rowCount = rowConfigs.count;
if (rowCount == 0) { return; }
UIView *firstRow = nil;
UIView *previousRow = nil;
for (NSUInteger i = 0; i < rowCount; i++) {
UIView *rowView = [UIView new];
[container addSubview:rowView];
[rowViews addObject:rowView];
[rowView mas_makeConstraints:^(MASConstraintMaker *make) {
if (previousRow) {
make.top.equalTo(previousRow.mas_bottom).offset(rowSpacing);
} else {
make.top.equalTo(container.mas_top).offset(topInset);
}
make.left.right.equalTo(container);
//
if (firstRow) {
make.height.equalTo(firstRow);
}
}];
//
if (i == rowCount - 1) {
[rowView mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.equalTo(container.mas_bottom).offset(-bottomInset);
}];
}
if (!firstRow) { firstRow = rowView; }
previousRow = rowView;
}
}
@end

View File

@@ -27,7 +27,9 @@ typedef NS_ENUM(NSInteger, KBKeyboardLayoutStyle) {
@property (nonatomic, assign, getter=isShiftOn) BOOL shiftOn; // 大小写状态
// 在数字布局中,是否显示“更多符号”(#+=)页
@property (nonatomic, assign) BOOL symbolsMoreOn;
@property (nonatomic, copy) NSString *currentLayoutJsonId; // 当前使用的布局 JSON ID
- (void)reloadKeys; // 当布局样式/大小写变化时调用
- (void)reloadLayoutWithProfileId:(NSString *)profileId; // 根据 profileId 重新加载布局
@end

View File

@@ -0,0 +1,853 @@
//
// KBKeyboardView.m
// CustomKeyboard
//
#import "KBKeyboardView.h"
#import "KBKeyButton.h"
#import "KBKey.h"
#import "KBBackspaceLongPressHandler.h"
#import "KBKeyboardLayoutConfig.h"
#import "KBKeyboardLayoutResolver.h"
#import "KBKeyboardKeyFactory.h"
#import "KBKeyboardLayoutEngine.h"
#import "KBKeyboardRowBuilder.h"
#import "KBKeyboardInputHandler.h"
#import "KBKeyboardLegacyBuilder.h"
#import "KBKeyboardLegacyLayoutProvider.h"
#import "KBKeyboardInteractionHandler.h"
#import "KBKeyboardRowContainerBuilder.h"
#import "KBSkinManager.h"
#import <Masonry/Masonry.h>
#import <objc/runtime.h>
//
static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
static const NSTimeInterval kKBKeyVariantLongPressMinDuration = 0.35;
static const CGFloat kKBKeyVariantPopupPaddingX = 8.0;
static const CGFloat kKBKeyVariantPopupPaddingY = 6.0;
static const CGFloat kKBKeyVariantItemWidth = 34.0;
static const CGFloat kKBKeyVariantItemHeight = 40.0;
static const CGFloat kKBKeyVariantPopupAnchorGap = 6.0;
static NSString * const kKBDiacriticsConfigFileName = @"kb_diacritics_map";
static const void *kKBDiacriticsLongPressBoundKey = &kKBDiacriticsLongPressBoundKey;
static inline NSString *kb_normalizeLanguageCode(NSString *languageCode) {
NSString *lc = (languageCode ?: @"").lowercaseString;
return lc.length > 0 ? lc : @"en";
}
static inline NSString *kb_baseLanguageCode(NSString *languageCode) {
NSString *lc = kb_normalizeLanguageCode(languageCode);
NSRange r = [lc rangeOfString:@"-"];
if (r.location == NSNotFound) { return lc; }
if (r.location == 0) { return lc; }
return [lc substringToIndex:r.location];
}
static NSDictionary<NSString *, NSDictionary<NSString *, NSArray<NSString *> *> *> *kb_diacriticsLanguagesMap(void) {
static NSDictionary<NSString *, NSDictionary<NSString *, NSArray<NSString *> *> *> *cached = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *path = [[NSBundle mainBundle] pathForResource:kKBDiacriticsConfigFileName ofType:@"json"];
NSData *data = path.length ? [NSData dataWithContentsOfFile:path] : nil;
if (data.length == 0) { return; }
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error || ![json isKindOfClass:[NSDictionary class]]) {
NSLog(@"[KBKeyboardView] Failed to parse %@.json: %@", kKBDiacriticsConfigFileName, error);
return;
}
NSDictionary *dict = (NSDictionary *)json;
id languages = dict[@"languages"];
if (![languages isKindOfClass:[NSDictionary class]]) { return; }
cached = (NSDictionary *)languages;
});
return cached ?: @{};
}
static inline NSUInteger kb_composedCharacterCount(NSString *string) {
if (string.length == 0) { return 0; }
__block NSUInteger count = 0;
[string enumerateSubstringsInRange:NSMakeRange(0, string.length)
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(__unused NSString *substring,
__unused NSRange substringRange,
__unused NSRange enclosingRange,
__unused BOOL *stop) {
count += 1;
}];
return count;
}
@interface KBKeyVariantPopupView : UIView
@property (nonatomic, copy) NSArray<NSString *> *variants;
@property (nonatomic, assign) NSInteger selectedIndex;
- (void)configureWithVariants:(NSArray<NSString *> *)variants selectedIndex:(NSInteger)selectedIndex;
- (void)updateSelectionWithPointInSelf:(CGPoint)point;
- (nullable NSString *)selectedVariant;
- (CGSize)preferredSize;
- (void)applyTheme;
@end
#pragma mark - KBKeyVariantPopupView
@interface KBKeyVariantPopupView ()
@property (nonatomic, strong) UIView *contentView;
@property (nonatomic, strong) NSMutableArray<UILabel *> *itemLabels;
@end
@implementation KBKeyVariantPopupView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.layer.cornerRadius = 10.0;
self.layer.masksToBounds = NO;
self.layer.borderWidth = 0.5;
self.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.15].CGColor;
self.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.28].CGColor;
self.layer.shadowOpacity = 0.7;
self.layer.shadowOffset = CGSizeMake(0, 2);
self.layer.shadowRadius = 6.0;
[self addSubview:self.contentView];
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self).insets(UIEdgeInsetsMake(kKBKeyVariantPopupPaddingY,
kKBKeyVariantPopupPaddingX,
kKBKeyVariantPopupPaddingY,
kKBKeyVariantPopupPaddingX));
}];
[self applyTheme];
}
return self;
}
- (void)applyTheme {
KBSkinTheme *t = [KBSkinManager shared].current;
UIColor *bg = t.keyBackground ?: [UIColor whiteColor];
self.backgroundColor = bg;
}
- (UIView *)contentView {
if (!_contentView) {
_contentView = [UIView new];
_contentView.backgroundColor = [UIColor clearColor];
}
return _contentView;
}
- (NSMutableArray<UILabel *> *)itemLabels {
if (!_itemLabels) {
_itemLabels = [NSMutableArray array];
}
return _itemLabels;
}
- (CGSize)preferredSize {
NSUInteger count = self.variants.count;
if (count == 0) { return CGSizeMake(0, 0); }
CGFloat width = kKBKeyVariantPopupPaddingX * 2 + kKBKeyVariantItemWidth * (CGFloat)count;
CGFloat height = kKBKeyVariantPopupPaddingY * 2 + kKBKeyVariantItemHeight;
return CGSizeMake(width, height);
}
- (void)configureWithVariants:(NSArray<NSString *> *)variants selectedIndex:(NSInteger)selectedIndex {
self.variants = variants ?: @[];
self.selectedIndex = MAX(0, MIN(selectedIndex, (NSInteger)self.variants.count - 1));
[self.itemLabels makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self.itemLabels removeAllObjects];
UILabel *previous = nil;
for (NSInteger i = 0; i < (NSInteger)self.variants.count; i++) {
UILabel *label = [UILabel new];
label.textAlignment = NSTextAlignmentCenter;
label.text = self.variants[i];
label.font = [UIFont systemFontOfSize:20 weight:UIFontWeightSemibold];
label.layer.cornerRadius = 8.0;
label.layer.masksToBounds = YES;
[self.contentView addSubview:label];
[self.itemLabels addObject:label];
[label mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.equalTo(self.contentView);
make.width.mas_equalTo(kKBKeyVariantItemWidth);
if (previous) {
make.left.equalTo(previous.mas_right);
} else {
make.left.equalTo(self.contentView.mas_left);
}
}];
previous = label;
}
if (previous) {
[previous mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.contentView.mas_right);
}];
}
[self kb_refreshSelectionAppearance];
}
- (void)updateSelectionWithPointInSelf:(CGPoint)point {
if (self.variants.count == 0) { return; }
CGPoint p = [self convertPoint:point toView:self.contentView];
CGFloat contentWidth = CGRectGetWidth(self.contentView.bounds);
if (contentWidth <= 0) { return; }
NSInteger count = (NSInteger)self.variants.count;
CGFloat unit = contentWidth / (CGFloat)count;
NSInteger index = (NSInteger)floor(p.x / MAX(unit, 1.0));
index = MAX(0, MIN(index, count - 1));
if (index == self.selectedIndex) { return; }
self.selectedIndex = index;
[self kb_refreshSelectionAppearance];
}
- (nullable NSString *)selectedVariant {
if (self.selectedIndex < 0 || self.selectedIndex >= (NSInteger)self.variants.count) { return nil; }
return self.variants[self.selectedIndex];
}
- (void)kb_refreshSelectionAppearance {
KBSkinTheme *t = [KBSkinManager shared].current;
UIColor *textColor = t.keyTextColor ?: [UIColor blackColor];
UIColor *selectedBg = t.keyHighlightBackground ?: (t.accentColor ?: [UIColor colorWithWhite:0 alpha:0.12]);
for (NSInteger i = 0; i < (NSInteger)self.itemLabels.count; i++) {
UILabel *label = self.itemLabels[i];
label.textColor = textColor;
label.backgroundColor = (i == self.selectedIndex) ? selectedBg : [UIColor clearColor];
}
}
@end
@interface KBKeyboardView ()
@property (nonatomic, strong) NSMutableArray<UIView *> *rowViews;
@property (nonatomic, strong) NSArray<NSArray<KBKey *> *> *keysForRows;
@property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler;
@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig;
@property (nonatomic, strong) KBKeyboardKeyFactory *keyFactory;
@property (nonatomic, strong) KBKeyboardLayoutEngine *layoutEngine;
@property (nonatomic, strong) KBKeyboardRowBuilder *rowBuilder;
@property (nonatomic, strong) KBKeyboardInputHandler *inputHandler;
@property (nonatomic, strong) KBKeyboardLegacyBuilder *legacyBuilder;
@property (nonatomic, strong) KBKeyboardLegacyLayoutProvider *legacyLayoutProvider;
@property (nonatomic, strong) KBKeyboardInteractionHandler *interactionHandler;
@property (nonatomic, strong) KBKeyboardRowContainerBuilder *rowContainerBuilder;
/// 0
@property (nonatomic, assign) CGFloat kb_uniformCharKeyWidth;
/// 便
@property (nonatomic, assign) CGFloat kb_currentRowSpacing;
/// /便
@property (nonatomic, assign) CGFloat kb_currentTopInset;
@property (nonatomic, assign) CGFloat kb_currentBottomInset;
///
@property (nonatomic, strong) KBKeyVariantPopupView *kb_variantPopupView;
@property (nonatomic, weak) KBKeyButton *kb_variantAnchorButton;
@property (nonatomic, copy) NSString *kb_variantBaseOutput;
@property (nonatomic, assign) BOOL kb_variantBaseDeleted;
@property (nonatomic, assign) NSUInteger kb_variantBaseDeleteCount;
@property (nonatomic, copy) NSString *kb_currentLanguageCode;
@end
@implementation KBKeyboardView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor clearColor];
_layoutStyle = KBKeyboardLayoutStyleLetters;
// Shift
_shiftOn = NO;
_symbolsMoreOn = NO; // 123
// App Group profileId
NSString *profileId = [[KBKeyboardLayoutResolver sharedResolver] currentProfileId];
if (profileId.length > 0) {
_currentLayoutJsonId = [[KBKeyboardLayoutResolver sharedResolver] layoutJsonIdForProfileId:profileId];
NSLog(@"[KBKeyboardView] Loaded profileId: %@, layoutJsonId: %@", profileId, _currentLayoutJsonId);
} else {
_currentLayoutJsonId = @"letters";
NSLog(@"[KBKeyboardView] No profileId found, using default 'letters'");
}
self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self];
[self buildBase];
}
return self;
}
// /
- (void)setLayoutStyle:(KBKeyboardLayoutStyle)layoutStyle {
_layoutStyle = layoutStyle;
if (_layoutStyle != KBKeyboardLayoutStyleNumbers) {
_symbolsMoreOn = NO;
}
}
#pragma mark - Base Layout
- (void)buildBase {
KBKeyboardLayout *layout = [self kb_currentLayout];
NSArray<KBKeyboardRowConfig *> *rows = layout.rows ?: @[];
if (rows.count == 0) {
// Fallback: 4
rows = @[[KBKeyboardRowConfig new], [KBKeyboardRowConfig new],
[KBKeyboardRowConfig new], [KBKeyboardRowConfig new]];
}
CGFloat rowSpacing = [self.layoutEngine rowSpacingForLayout:layout];
CGFloat topInset = [self.layoutEngine topInsetForLayout:layout];
CGFloat bottomInset = [self.layoutEngine bottomInsetForLayout:layout];
self.kb_currentRowSpacing = rowSpacing;
self.kb_currentTopInset = topInset;
self.kb_currentBottomInset = bottomInset;
[self.rowContainerBuilder rebuildRowContainersForRows:rows
inContainer:self
rowViews:self.rowViews
rowSpacing:rowSpacing
topInset:topInset
bottomInset:bottomInset];
[self.interactionHandler bringPreviewToFrontIfNeededInContainer:self];
}
#pragma mark - Public
- (void)reloadKeys {
[self.backspaceHandler bindDeleteButton:nil showClearLabel:NO];
[self kb_hideVariantPopupIfNeeded];
self.kb_currentLanguageCode = [[KBKeyboardLayoutResolver sharedResolver] currentLanguageCode] ?: @"en";
KBKeyboardLayout *layout = [self kb_currentLayout];
CGFloat rowSpacing = [self.layoutEngine rowSpacingForLayout:layout];
CGFloat topInset = [self.layoutEngine topInsetForLayout:layout];
CGFloat bottomInset = [self.layoutEngine bottomInsetForLayout:layout];
NSLog(@"[KBKeyboardView] reloadKeys: layoutName=%@ rows=%lu shiftRows=%lu shiftOn=%d",
self.currentLayoutJsonId, (unsigned long)layout.rows.count, (unsigned long)layout.shiftRows.count, self.shiftOn);
NSArray<KBKeyboardRowConfig *> *rows = nil;
if (self.shiftOn && layout.shiftRows.count > 0) {
rows = layout.shiftRows;
} else {
rows = layout.rows ?: @[];
}
NSLog(@"[KBKeyboardView] reloadKeys: usingRows=%lu currentContainers=%lu",
(unsigned long)rows.count, (unsigned long)self.rowViews.count);
// 4 5
if (rows.count >= 4 &&
(rows.count != self.rowViews.count ||
fabs(self.kb_currentRowSpacing - rowSpacing) > 0.1 ||
fabs(self.kb_currentTopInset - topInset) > 0.1 ||
fabs(self.kb_currentBottomInset - bottomInset) > 0.1)) {
self.kb_currentRowSpacing = rowSpacing;
self.kb_currentTopInset = topInset;
self.kb_currentBottomInset = bottomInset;
[self.rowContainerBuilder rebuildRowContainersForRows:rows
inContainer:self
rowViews:self.rowViews
rowSpacing:rowSpacing
topInset:topInset
bottomInset:bottomInset];
[self.interactionHandler bringPreviewToFrontIfNeededInContainer:self];
}
//
for (UIView *row in self.rowViews) {
[row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
}
if (rows.count < 4) {
NSLog(@"[KBKeyboardView] reloadKeys: rows.count < 4, fallback to legacy");
self.kb_uniformCharKeyWidth = 0.0;
[self kb_buildLegacyLayout];
[self kb_bindVariantLongPressIfNeeded];
return;
}
//
self.kb_uniformCharKeyWidth = [self.layoutEngine calculateUniformCharKeyWidthForRows:rows
keyFactory:self.keyFactory
shiftOn:self.shiftOn];
for (NSUInteger i = 0; i < rows.count && i < self.rowViews.count; i++) {
[self.rowBuilder buildRow:self.rowViews[i]
withRowConfig:rows[i]
uniformCharWidth:self.kb_uniformCharKeyWidth
shiftOn:self.shiftOn
backspaceHandler:self.backspaceHandler
target:self
action:@selector(onKeyTapped:)];
}
NSUInteger totalButtons = [self kb_totalKeyButtonCount];
NSLog(@"[KBKeyboardView] reloadKeys: totalButtons=%lu", (unsigned long)totalButtons);
if (totalButtons == 0) {
NSLog(@"[KBKeyboardView] config layout produced no keys, fallback to legacy.");
for (UIView *row in self.rowViews) {
[row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
}
[self kb_buildLegacyLayout];
[self kb_bindVariantLongPressIfNeeded];
return;
}
[self kb_bindVariantLongPressIfNeeded];
}
- (void)didMoveToWindow {
[super didMoveToWindow];
if (!self.window) { return; }
if ([self kb_totalKeyButtonCount] > 0) { return; }
//
[self reloadKeys];
//
UIView *container = self.superview;
if ([container respondsToSelector:@selector(kb_applyTheme)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[container performSelector:@selector(kb_applyTheme)];
#pragma clang diagnostic pop
}
}
- (NSUInteger)kb_totalKeyButtonCount {
NSUInteger total = 0;
for (UIView *row in self.rowViews) {
total += [self.interactionHandler collectKeyButtonsInView:row].count;
}
return total;
}
#pragma mark - Hit Test
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hit = [super hitTest:point withEvent:event];
return [self.interactionHandler resolveHitView:hit
point:point
container:self
rowViews:self.rowViews];
}
#pragma mark - Config Helpers
- (KBKeyboardLayoutConfig *)kb_layoutConfig {
if (!self.layoutConfig) {
self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
}
return self.layoutConfig;
}
- (KBKeyboardKeyFactory *)keyFactory {
if (!_keyFactory) {
_keyFactory = [[KBKeyboardKeyFactory alloc] initWithLayoutConfig:[self kb_layoutConfig]];
}
return _keyFactory;
}
- (KBKeyboardLayoutEngine *)layoutEngine {
if (!_layoutEngine) {
_layoutEngine = [[KBKeyboardLayoutEngine alloc] initWithLayoutConfig:[self kb_layoutConfig]];
}
return _layoutEngine;
}
- (KBKeyboardRowBuilder *)rowBuilder {
if (!_rowBuilder) {
_rowBuilder = [[KBKeyboardRowBuilder alloc] initWithLayoutConfig:[self kb_layoutConfig]
layoutEngine:self.layoutEngine
keyFactory:self.keyFactory];
}
return _rowBuilder;
}
- (KBKeyboardInputHandler *)inputHandler {
if (!_inputHandler) {
_inputHandler = [[KBKeyboardInputHandler alloc] init];
__weak typeof(self) weakSelf = self;
_inputHandler.onToggleShift = ^{
__strong typeof(weakSelf) self = weakSelf;
if (!self) { return; }
self.shiftOn = !self.shiftOn;
[self reloadKeys];
};
_inputHandler.onToggleSymbols = ^{
__strong typeof(weakSelf) self = weakSelf;
if (!self) { return; }
self.symbolsMoreOn = !self.symbolsMoreOn;
[self reloadKeys];
};
_inputHandler.onKeyTapped = ^(KBKey *key) {
__strong typeof(weakSelf) self = weakSelf;
if (!self) { return; }
if ([self.delegate respondsToSelector:@selector(keyboardView:didTapKey:)]) {
[self.delegate keyboardView:self didTapKey:key];
}
};
}
return _inputHandler;
}
- (KBKeyboardLegacyBuilder *)legacyBuilder {
if (!_legacyBuilder) {
_legacyBuilder = [[KBKeyboardLegacyBuilder alloc] init];
}
return _legacyBuilder;
}
- (KBKeyboardLegacyLayoutProvider *)legacyLayoutProvider {
if (!_legacyLayoutProvider) {
_legacyLayoutProvider = [[KBKeyboardLegacyLayoutProvider alloc] init];
}
return _legacyLayoutProvider;
}
- (KBKeyboardInteractionHandler *)interactionHandler {
if (!_interactionHandler) {
_interactionHandler = [[KBKeyboardInteractionHandler alloc] init];
}
return _interactionHandler;
}
- (KBKeyboardRowContainerBuilder *)rowContainerBuilder {
if (!_rowContainerBuilder) {
_rowContainerBuilder = [[KBKeyboardRowContainerBuilder alloc] init];
}
return _rowContainerBuilder;
}
- (KBKeyboardLayout *)kb_layoutForName:(NSString *)name {
return [[self kb_layoutConfig] layoutForName:name];
}
- (KBKeyboardLayout *)kb_currentLayout {
NSString *baseLayoutName = self.currentLayoutJsonId.length > 0 ? self.currentLayoutJsonId : @"letters";
if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) {
// / letters_es_numbers / letters_es_symbols
// 退 numbers / symbolsMore
NSString *numbersName = [NSString stringWithFormat:@"%@_numbers", baseLayoutName];
NSString *symbolsName = [NSString stringWithFormat:@"%@_symbols", baseLayoutName];
NSString *targetName = self.symbolsMoreOn ? symbolsName : numbersName;
KBKeyboardLayout *layout = [self kb_layoutForName:targetName];
if (layout && layout.rows.count >= 4) {
return layout;
}
// 退
return [self kb_layoutForName:(self.symbolsMoreOn ? @"symbolsMore" : @"numbers")];
}
return [self kb_layoutForName:baseLayoutName];
}
- (void)reloadLayoutWithProfileId:(NSString *)profileId {
if (profileId.length == 0) {
NSLog(@"[KBKeyboardView] reloadLayoutWithProfileId: empty profileId, ignoring");
return;
}
NSString *newLayoutJsonId = [[KBKeyboardLayoutResolver sharedResolver] layoutJsonIdForProfileId:profileId];
if ([newLayoutJsonId isEqualToString:self.currentLayoutJsonId]) {
NSLog(@"[KBKeyboardView] Layout already loaded: %@", newLayoutJsonId);
return;
}
NSLog(@"[KBKeyboardView] Switching layout from %@ to %@", self.currentLayoutJsonId, newLayoutJsonId);
self.currentLayoutJsonId = newLayoutJsonId;
//
[self reloadKeys];
}
- (void)kb_buildLegacyLayout {
self.keysForRows = [self.legacyLayoutProvider keysForLayoutStyleIsNumbers:(self.layoutStyle == KBKeyboardLayoutStyleNumbers)
shiftOn:self.shiftOn
symbolsMoreOn:self.symbolsMoreOn
keyFactory:self.keyFactory];
if (self.keysForRows.count < 4) { return; }
if (self.rowViews.count < 4) { return; }
[self.legacyBuilder buildRow:self.rowViews[0]
withKeys:self.keysForRows[0]
edgeSpacerMultiplier:0.0
shiftOn:self.shiftOn
backspaceHandler:self.backspaceHandler
target:self
action:@selector(onKeyTapped:)];
CGFloat row2Spacer = (self.layoutStyle == KBKeyboardLayoutStyleLetters)
? kKBLettersRow2EdgeSpacerMultiplier : 0.0;
[self.legacyBuilder buildRow:self.rowViews[1]
withKeys:self.keysForRows[1]
edgeSpacerMultiplier:row2Spacer
shiftOn:self.shiftOn
backspaceHandler:self.backspaceHandler
target:self
action:@selector(onKeyTapped:)];
[self.legacyBuilder buildRow:self.rowViews[2]
withKeys:self.keysForRows[2]
edgeSpacerMultiplier:0.0
shiftOn:self.shiftOn
backspaceHandler:self.backspaceHandler
target:self
action:@selector(onKeyTapped:)];
[self.legacyBuilder buildRow:self.rowViews[3]
withKeys:self.keysForRows[3]
edgeSpacerMultiplier:0.0
shiftOn:self.shiftOn
backspaceHandler:self.backspaceHandler
target:self
action:@selector(onKeyTapped:)];
}
#pragma mark - Actions
- (void)onKeyTapped:(KBKeyButton *)sender {
[self.inputHandler handleKeyTap:sender.key];
}
//
- (void)showPreviewForButton:(KBKeyButton *)button {
[self.interactionHandler showPreviewForButton:button inContainer:self];
}
- (void)hidePreview {
[self.interactionHandler hidePreview];
}
#pragma mark - Key Variant (Diacritics)
- (void)kb_hideVariantPopupIfNeeded {
if (!self.kb_variantPopupView || self.kb_variantPopupView.hidden) { return; }
self.kb_variantPopupView.hidden = YES;
self.kb_variantPopupView.alpha = 1.0;
self.kb_variantAnchorButton = nil;
self.kb_variantBaseOutput = nil;
self.kb_variantBaseDeleted = NO;
self.kb_variantBaseDeleteCount = 0;
}
- (void)kb_bindVariantLongPressIfNeeded {
NSString *lc = kb_normalizeLanguageCode(self.kb_currentLanguageCode);
NSString *baseLc = kb_baseLanguageCode(self.kb_currentLanguageCode);
NSDictionary<NSString *, NSDictionary<NSString *, NSArray<NSString *> *> *> *languages = kb_diacriticsLanguagesMap();
if (languages.count == 0) { return; }
NSDictionary *commonMap = [languages[@"common"] isKindOfClass:[NSDictionary class]] ? languages[@"common"] : nil;
if (!commonMap && ![languages[lc] isKindOfClass:[NSDictionary class]] && ![languages[baseLc] isKindOfClass:[NSDictionary class]]) { return; }
for (UIView *row in self.rowViews) {
NSArray<KBKeyButton *> *buttons = [self.interactionHandler collectKeyButtonsInView:row];
for (KBKeyButton *btn in buttons) {
if (![btn isKindOfClass:[KBKeyButton class]]) { continue; }
if (btn.key.type != KBKeyTypeCharacter) { continue; }
NSArray<NSString *> *variants = [self kb_variantsForKey:btn.key languageCode:lc];
if (variants.count <= 1) { continue; }
NSNumber *bound = objc_getAssociatedObject(btn, kKBDiacriticsLongPressBoundKey);
if (bound.boolValue) { continue; }
objc_setAssociatedObject(btn, kKBDiacriticsLongPressBoundKey, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:@selector(onKeyVariantLongPress:)];
longPress.minimumPressDuration = kKBKeyVariantLongPressMinDuration;
longPress.allowableMovement = CGFLOAT_MAX;
longPress.cancelsTouchesInView = NO;
[btn addGestureRecognizer:longPress];
}
}
}
- (void)onKeyVariantLongPress:(UILongPressGestureRecognizer *)gr {
KBKeyButton *button = (KBKeyButton *)gr.view;
if (![button isKindOfClass:[KBKeyButton class]]) { return; }
if (button.key.type != KBKeyTypeCharacter) { return; }
NSString *lc = kb_normalizeLanguageCode(self.kb_currentLanguageCode);
NSArray<NSString *> *variants = [self kb_variantsForKey:button.key languageCode:lc];
if (variants.count <= 1) { return; }
switch (gr.state) {
case UIGestureRecognizerStateBegan: {
[self hidePreview];
self.kb_variantAnchorButton = button;
self.kb_variantBaseOutput = (button.key.output.length > 0 ? button.key.output : button.key.title) ?: @"";
self.kb_variantBaseDeleted = NO;
self.kb_variantBaseDeleteCount = kb_composedCharacterCount(self.kb_variantBaseOutput);
if (@available(iOS 10.0, *)) {
UIImpactFeedbackGenerator *gen = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
[gen prepare];
[gen impactOccurred];
}
if (!self.kb_variantPopupView) {
self.kb_variantPopupView = [[KBKeyVariantPopupView alloc] initWithFrame:CGRectZero];
self.kb_variantPopupView.hidden = YES;
[self addSubview:self.kb_variantPopupView];
} else if (self.kb_variantPopupView.superview != self) {
[self addSubview:self.kb_variantPopupView];
}
[self.kb_variantPopupView applyTheme];
[self.kb_variantPopupView configureWithVariants:variants selectedIndex:0];
CGSize preferred = [self.kb_variantPopupView preferredSize];
CGRect anchorFrame = [button convertRect:button.bounds toView:self];
CGFloat x = CGRectGetMidX(anchorFrame) - preferred.width * 0.5;
CGFloat maxX = CGRectGetWidth(self.bounds) - 6.0 - preferred.width;
x = MIN(MAX(6.0, x), MAX(6.0, maxX));
CGFloat y = CGRectGetMinY(anchorFrame) - preferred.height - kKBKeyVariantPopupAnchorGap;
y = MAX(2.0, y);
self.kb_variantPopupView.frame = CGRectMake(x, y, preferred.width, preferred.height);
self.kb_variantPopupView.alpha = 0.0;
self.kb_variantPopupView.hidden = NO;
[self bringSubviewToFront:self.kb_variantPopupView];
[UIView animateWithDuration:0.12
delay:0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut
animations:^{
self.kb_variantPopupView.alpha = 1.0;
}
completion:nil];
} break;
case UIGestureRecognizerStateChanged: {
if (self.kb_variantPopupView.hidden) { return; }
CGPoint p = [gr locationInView:self.kb_variantPopupView];
[self.kb_variantPopupView updateSelectionWithPointInSelf:p];
if (!self.kb_variantBaseDeleted && self.kb_variantPopupView.selectedIndex != 0) {
[self kb_emitBackspaceCountForVariantReplacement:self.kb_variantBaseDeleteCount];
self.kb_variantBaseDeleted = YES;
}
} break;
case UIGestureRecognizerStateEnded: {
if (self.kb_variantPopupView.hidden) { return; }
CGPoint p = [gr locationInView:self.kb_variantPopupView];
[self.kb_variantPopupView updateSelectionWithPointInSelf:p];
[self kb_commitSelectedVariantAndCleanupWithIdentifier:button.key.identifier];
} break;
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateFailed: {
if (self.kb_variantPopupView.hidden) { return; }
// base
if (self.kb_variantBaseDeleted && self.kb_variantBaseOutput.length > 0) {
[self kb_emitCharacterText:self.kb_variantBaseOutput identifier:button.key.identifier];
}
[self kb_hideVariantPopupIfNeeded];
} break;
default:
break;
}
}
- (void)kb_emitBackspaceCountForVariantReplacement:(NSUInteger)count {
if (count == 0) { count = 1; }
if (![self.delegate respondsToSelector:@selector(keyboardView:didTapKey:)]) { return; }
KBKey *backspace = [KBKey keyWithTitle:@"" type:KBKeyTypeBackspace];
for (NSUInteger i = 0; i < count; i++) {
[self.delegate keyboardView:self didTapKey:backspace];
}
}
- (void)kb_emitCharacterText:(NSString *)text identifier:(NSString *)identifier {
if (text.length == 0) { return; }
if (![self.delegate respondsToSelector:@selector(keyboardView:didTapKey:)]) { return; }
KBKey *key = [KBKey keyWithIdentifier:identifier title:text output:text type:KBKeyTypeCharacter];
[self.delegate keyboardView:self didTapKey:key];
}
- (void)kb_commitSelectedVariantAndCleanupWithIdentifier:(NSString *)identifier {
NSInteger index = self.kb_variantPopupView.selectedIndex;
NSString *selected = [self.kb_variantPopupView selectedVariant] ?: @"";
NSString *base = self.kb_variantBaseOutput ?: @"";
if (index <= 0 || selected.length == 0) {
if (self.kb_variantBaseDeleted && base.length > 0) {
[self kb_emitCharacterText:base identifier:identifier];
}
[self kb_hideVariantPopupIfNeeded];
return;
}
if (!self.kb_variantBaseDeleted) {
[self kb_emitBackspaceCountForVariantReplacement:self.kb_variantBaseDeleteCount];
self.kb_variantBaseDeleted = YES;
}
[self kb_emitCharacterText:selected identifier:identifier];
[self kb_hideVariantPopupIfNeeded];
}
- (NSArray<NSString *> *)kb_variantsForKey:(KBKey *)key languageCode:(NSString *)languageCode {
if (!key || key.type != KBKeyTypeCharacter) { return @[]; }
NSString *base = (key.output.length > 0 ? key.output : key.title) ?: @"";
if (base.length == 0) { return @[]; }
NSDictionary<NSString *, NSDictionary<NSString *, NSArray<NSString *> *> *> *languages = kb_diacriticsLanguagesMap();
NSString *lc = kb_normalizeLanguageCode(languageCode);
NSDictionary<NSString *, NSArray<NSString *> *> *langMap = [languages[lc] isKindOfClass:[NSDictionary class]] ? languages[lc] : nil;
if (langMap.count == 0) {
NSString *baseLc = kb_baseLanguageCode(languageCode);
langMap = [languages[baseLc] isKindOfClass:[NSDictionary class]] ? languages[baseLc] : nil;
}
NSDictionary<NSString *, NSArray<NSString *> *> *commonMap = [languages[@"common"] isKindOfClass:[NSDictionary class]] ? languages[@"common"] : nil;
if (langMap.count == 0 && commonMap.count == 0) { return @[]; }
BOOL hasLetter = NO;
NSCharacterSet *letters = [NSCharacterSet letterCharacterSet];
for (NSUInteger i = 0; i < base.length; i++) {
unichar c = [base characterAtIndex:i];
if ([letters characterIsMember:c]) {
hasLetter = YES;
break;
}
}
BOOL upper = hasLetter && [base isEqualToString:base.uppercaseString] && ![base isEqualToString:base.lowercaseString];
NSString *queryKey = upper ? base.lowercaseString : base;
id raw = langMap[queryKey];
if (!(raw && [raw isKindOfClass:[NSArray class]] && ((NSArray *)raw).count > 0)) {
raw = commonMap[queryKey];
}
if (![raw isKindOfClass:[NSArray class]]) { return @[]; }
NSMutableArray<NSString *> *variants = [NSMutableArray array];
for (id obj in (NSArray *)raw) {
if (![obj isKindOfClass:[NSString class]]) { continue; }
NSString *s = (NSString *)obj;
if (s.length == 0) { continue; }
[variants addObject:s];
}
if (variants.count == 0) { return @[]; }
if (![[variants firstObject] isEqualToString:queryKey]) {
[variants insertObject:queryKey atIndex:0];
}
if (variants.count <= 1) { return @[]; }
if (!upper) {
return variants.copy;
}
NSMutableArray<NSString *> *upperVariants = [NSMutableArray arrayWithCapacity:variants.count];
for (NSString *s in variants) {
[upperVariants addObject:s.uppercaseString];
}
return upperVariants.copy;
}
#pragma mark - Lazy
- (NSMutableArray<UIView *> *)rowViews {
if (!_rowViews) _rowViews = [NSMutableArray array];
return _rowViews;
}
@end

View File

@@ -1,20 +0,0 @@
//
// KBSettingView.h
// CustomKeyboard
//
// 简单的设置页面:左上角返回箭头按钮 + 占位内容区域。
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBSettingView : UIView
/// 左上角返回按钮(外部添加 target 实现返回)
@property (nonatomic, strong, readonly) UIButton *backButton;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,70 +0,0 @@
//
// KBSettingView.m
// CustomKeyboard
//
#import "KBSettingView.h"
#import "Masonry.h"
@interface KBSettingView ()
@property (nonatomic, strong) UIButton *backButtonInternal;
@property (nonatomic, strong) UILabel *titleLabel;
@end
@implementation KBSettingView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
//
self.backgroundColor = [UIColor colorWithWhite:1 alpha:0.96];
[self addSubview:self.backButtonInternal];
[self.backButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.mas_left).offset(10);
make.top.equalTo(self.mas_top).offset(8);
make.width.height.mas_equalTo(32);
}];
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.text = KBLocalized(@"设置");
self.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
self.titleLabel.textColor = [UIColor blackColor];
[self addSubview:self.titleLabel];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.backButtonInternal.mas_centerY);
make.centerX.equalTo(self.mas_centerX);
}];
//
UILabel *place = [[UILabel alloc] init];
place.text = KBLocalized(@"这里是设置内容占位");
place.textColor = [UIColor darkGrayColor];
place.font = [UIFont systemFontOfSize:14];
[self addSubview:place];
[place mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self);
}];
}
return self;
}
#pragma mark - Lazy
- (UIButton *)backButtonInternal {
if (!_backButtonInternal) {
_backButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
_backButtonInternal.layer.cornerRadius = 16;
_backButtonInternal.layer.masksToBounds = YES;
_backButtonInternal.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
[_backButtonInternal setTitle:@"←" forState:UIControlStateNormal]; //
[_backButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
_backButtonInternal.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
}
return _backButtonInternal;
}
#pragma mark - Expose
- (UIButton *)backButton { return self.backButtonInternal; }
@end

View File

@@ -8,15 +8,37 @@
#import "KBSkinManager.h"
@interface KBSuggestionBarView ()
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIStackView *stackView;
@property (nonatomic, strong) UIView *topLineView;
@property (nonatomic, strong) UIView *bottomLineView;
@property (nonatomic, copy) NSArray<NSString *> *items;
@property (nonatomic, strong) UIColor *pillColor;
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, strong) UIColor *separatorColor;
@property (nonatomic, strong) UIColor *highlightColor;
@end
@implementation KBSuggestionBarView
- (NSInteger)kb_buttonTag {
return 10001;
}
- (NSInteger)kb_separatorTag {
return 10002;
}
- (UIImage *)kb_imageWithColor:(UIColor *)color {
if (!color) { color = [UIColor clearColor]; }
CGRect rect = CGRectMake(0, 0, 1, 1);
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(ctx, color.CGColor);
CGContextFillRect(ctx, rect);
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor clearColor];
@@ -26,15 +48,23 @@
}
- (void)setupUI {
[self addSubview:self.scrollView];
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
[self addSubview:self.stackView];
[self.stackView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self);
make.top.equalTo(self);
make.bottom.equalTo(self);
}];
[self.scrollView addSubview:self.stackView];
[self.stackView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.scrollView).insets(UIEdgeInsetsMake(0, 8, 0, 8));
make.height.equalTo(self.scrollView);
[self addSubview:self.topLineView];
[self.topLineView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.top.equalTo(self);
make.height.mas_equalTo(0.5);
}];
[self addSubview:self.bottomLineView];
[self.bottomLineView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.bottom.equalTo(self);
make.height.mas_equalTo(0.5);
}];
[self applyTheme:[KBSkinManager shared].current];
@@ -48,35 +78,75 @@
[view removeFromSuperview];
}
for (NSString *item in self.items) {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
btn.layer.cornerRadius = 12.0;
NSUInteger count = self.items.count;
for (NSUInteger idx = 0; idx < count; idx++) {
NSString *item = self.items[idx];
UIView *container = [UIView new];
container.backgroundColor = UIColor.clearColor;
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.tag = [self kb_buttonTag];
btn.backgroundColor = UIColor.clearColor;
btn.layer.cornerRadius = 8.0;
btn.layer.masksToBounds = YES;
btn.backgroundColor = self.pillColor ?: [UIColor colorWithWhite:1 alpha:0.9];
btn.titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
btn.adjustsImageWhenHighlighted = NO;
btn.titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightRegular];
btn.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
btn.titleLabel.adjustsFontSizeToFitWidth = YES;
btn.titleLabel.minimumScaleFactor = 0.85;
btn.contentEdgeInsets = UIEdgeInsetsMake(0, 12, 0, 12);
[btn setTitle:item forState:UIControlStateNormal];
[btn setTitleColor:self.textColor ?: [UIColor blackColor] forState:UIControlStateNormal];
btn.contentEdgeInsets = UIEdgeInsetsMake(4, 10, 4, 10);
[btn setTitleColor:self.textColor ?: UIColor.blackColor forState:UIControlStateNormal];
if (self.highlightColor) {
[btn setBackgroundImage:[self kb_imageWithColor:self.highlightColor] forState:UIControlStateHighlighted];
}
[btn addTarget:self action:@selector(onTapSuggestion:) forControlEvents:UIControlEventTouchUpInside];
[self.stackView addArrangedSubview:btn];
[container addSubview:btn];
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(container);
}];
if (idx + 1 < count) {
UIView *sep = [UIView new];
sep.tag = [self kb_separatorTag];
sep.userInteractionEnabled = NO;
sep.backgroundColor = self.separatorColor ?: [UIColor colorWithWhite:0 alpha:0.12];
[container addSubview:sep];
[sep mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(container.mas_right);
make.centerY.equalTo(container.mas_centerY);
make.width.mas_equalTo(0.5);
make.height.equalTo(container.mas_height).multipliedBy(0.55);
}];
}
[self.stackView addArrangedSubview:container];
}
self.hidden = (self.items.count == 0);
}
- (void)applyTheme:(KBSkinTheme *)theme {
UIColor *bg = theme.keyBackground ?: [UIColor whiteColor];
UIColor *text = theme.keyTextColor ?: [UIColor blackColor];
UIColor *barBg = [UIColor colorWithHex:0xD1D3DB];
UIColor *barBg = theme.keyboardBackground ?: [UIColor colorWithWhite:0.95 alpha:1.0];
UIColor *text = theme.keyTextColor ?: UIColor.blackColor;
UIColor *highlight = theme.keyHighlightBackground ?: [UIColor colorWithWhite:0.85 alpha:1.0];
self.backgroundColor = barBg;
self.pillColor = bg;
self.textColor = text;
self.separatorColor = [text colorWithAlphaComponent:0.12];
self.highlightColor = [highlight colorWithAlphaComponent:0.25];
self.topLineView.backgroundColor = [text colorWithAlphaComponent:0.08];
self.bottomLineView.backgroundColor = [text colorWithAlphaComponent:0.06];
for (UIView *view in self.stackView.arrangedSubviews) {
if (![view isKindOfClass:[UIButton class]]) { continue; }
UIButton *btn = (UIButton *)view;
btn.backgroundColor = self.pillColor;
[btn setTitleColor:self.textColor forState:UIControlStateNormal];
UIButton *btn = (UIButton *)[view viewWithTag:[self kb_buttonTag]];
if ([btn isKindOfClass:UIButton.class]) {
[btn setTitleColor:self.textColor ?: UIColor.blackColor forState:UIControlStateNormal];
[btn setBackgroundImage:[self kb_imageWithColor:self.highlightColor] forState:UIControlStateHighlighted];
}
UIView *sep = [view viewWithTag:[self kb_separatorTag]];
if (sep) {
sep.backgroundColor = self.separatorColor ?: [UIColor colorWithWhite:0 alpha:0.12];
}
}
}
@@ -92,23 +162,33 @@
#pragma mark - Lazy
- (UIScrollView *)scrollView {
if (!_scrollView) {
_scrollView = [[UIScrollView alloc] init];
_scrollView.showsHorizontalScrollIndicator = NO;
_scrollView.alwaysBounceHorizontal = YES;
}
return _scrollView;
}
- (UIStackView *)stackView {
if (!_stackView) {
_stackView = [[UIStackView alloc] init];
_stackView.axis = UILayoutConstraintAxisHorizontal;
_stackView.alignment = UIStackViewAlignmentCenter;
_stackView.spacing = 8.0;
_stackView.alignment = UIStackViewAlignmentFill;
_stackView.distribution = UIStackViewDistributionFillEqually;
_stackView.spacing = 0.0;
_stackView.layoutMarginsRelativeArrangement = YES;
_stackView.layoutMargins = UIEdgeInsetsMake(0, 8, 0, 8);
}
return _stackView;
}
- (UIView *)topLineView {
if (!_topLineView) {
_topLineView = [UIView new];
_topLineView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.06];
}
return _topLineView;
}
- (UIView *)bottomLineView {
if (!_bottomLineView) {
_bottomLineView = [UIView new];
_bottomLineView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.04];
}
return _bottomLineView;
}
@end

View File

@@ -15,8 +15,6 @@ NS_ASSUME_NONNULL_BEGIN
@optional
/// 左侧功能按钮点击index 从 0 开始)
- (void)toolBar:(KBToolBar *)toolBar didTapActionAtIndex:(NSInteger)index;
/// 右侧设置按钮点击
- (void)toolBarDidTapSettings:(KBToolBar *)toolBar;
/// 右侧撤销删除按钮点击
- (void)toolBarDidTapUndo:(KBToolBar *)toolBar;
@end
@@ -31,7 +29,6 @@ NS_ASSUME_NONNULL_BEGIN
/// 暴露按钮以便外部定制(只读;首次访问时懒加载创建)
@property (nonatomic, strong, readonly) NSArray<UIButton *> *leftButtons;
@property (nonatomic, strong, readonly) UIButton *settingsButton;
@property (nonatomic, strong, readonly) UIButton *undoButton;
/// 应用皮肤(更新 AI 按钮背景图)

View File

@@ -9,17 +9,19 @@
#import "KBResponderUtils.h" // UIInputViewController
#import "KBBackspaceUndoManager.h"
#import "KBSkinManager.h"
#import <ImageIO/ImageIO.h>
@interface KBToolBar ()
@property (nonatomic, strong) UIView *leftContainer;
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
//@property (nonatomic, strong) UIButton *settingsButtonInternal;
@property (nonatomic, strong) UIButton *globeButtonInternal; //
@property (nonatomic, strong) UIImageView *avatarImageView; // AppGroup persona_cover.jpg
@property (nonatomic, strong) UIButton *undoButtonInternal; //
@property (nonatomic, assign) BOOL kbNeedsInputModeSwitchKey;
@property (nonatomic, assign) BOOL kbUndoVisible;
@property (nonatomic, assign) BOOL kbAvatarVisible;
@property (nonatomic, copy, nullable) NSString *kb_cachedPersonaCoverPath;
@property (nonatomic, strong, nullable) UIImage *kb_cachedPersonaCoverImage;
@end
@implementation KBToolBar
@@ -39,6 +41,10 @@
selector:@selector(kb_undoStateChanged)
name:KBBackspaceUndoStateDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(kb_skinDidChange)
name:KBSkinDidChangeNotification
object:nil];
}
return self;
}
@@ -58,10 +64,6 @@
return self.undoButtonInternal;
}
//- (UIButton *)settingsButton {
// return self.settingsButtonInternal;
//}
- (void)setLeftButtonTitles:(NSArray<NSString *> *)leftButtonTitles {
_leftButtonTitles = [leftButtonTitles copy];
// Update titles if buttons already exist
@@ -77,18 +79,10 @@
- (void)setupUI {
[self addSubview:self.leftContainer];
// [self addSubview:self.settingsButtonInternal];
[self addSubview:self.globeButtonInternal];
[self addSubview:self.undoButtonInternal];
[self addSubview:self.avatarImageView];
//
// [self.settingsButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
// make.right.equalTo(self.mas_right).offset(-12);
// make.centerY.equalTo(self.mas_centerY);
// make.width.height.mas_equalTo(32);
// }];
//
[self.globeButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.mas_left).offset(12);
@@ -180,9 +174,14 @@
KBSkinManager *skinManager = [KBSkinManager shared];
UIImage *icon = [skinManager iconImageForKeyIdentifier:kKBAIKeyIdentifier caseVariant:0];
NSString *skinId = skinManager.current.skinId ?: @"";
BOOL usingDefaultSkin = (skinId.length == 0 || [skinId isEqualToString:@"default"]);
if (!icon && usingDefaultSkin) {
NSLog(@"[KBToolBar] kb_updateAIButtonAppearance: skinId=%@ icon=%@",
skinId, icon ? @"有" : @"nil");
// ai 使
if (!icon) {
icon = [UIImage imageNamed:@"ai_key_icon"];
NSLog(@"[KBToolBar] fallback to bundled ai_key_icon: %@", icon ? @"有" : @"nil");
}
if (icon) {
@@ -256,10 +255,41 @@
[[containerURL path] stringByAppendingPathComponent:@"persona_cover.jpg"];
if (imagePath.length == 0 ||
![[NSFileManager defaultManager] fileExistsAtPath:imagePath]) {
self.kb_cachedPersonaCoverPath = nil;
self.kb_cachedPersonaCoverImage = nil;
return nil;
}
return [UIImage imageWithContentsOfFile:imagePath];
if (self.kb_cachedPersonaCoverImage &&
[self.kb_cachedPersonaCoverPath isEqualToString:imagePath]) {
return self.kb_cachedPersonaCoverImage;
}
// 40pt full decode JPG
NSUInteger maxPixel = 256;
NSURL *url = [NSURL fileURLWithPath:imagePath];
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
if (!source) {
return nil;
}
NSDictionary *opts = @{
(__bridge id)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
(__bridge id)kCGImageSourceCreateThumbnailWithTransform : @YES,
(__bridge id)kCGImageSourceThumbnailMaxPixelSize : @(maxPixel),
};
CGImageRef cg = CGImageSourceCreateThumbnailAtIndex(source, 0, (__bridge CFDictionaryRef)opts);
CFRelease(source);
if (!cg) {
return nil;
}
UIImage *img = [UIImage imageWithCGImage:cg
scale:[UIScreen mainScreen].scale
orientation:UIImageOrientationUp];
CGImageRelease(cg);
self.kb_cachedPersonaCoverPath = imagePath;
self.kb_cachedPersonaCoverImage = img;
return img;
}
#pragma mark - Actions
@@ -270,12 +300,6 @@
}
}
- (void)onSettings {
if ([self.delegate respondsToSelector:@selector(toolBarDidTapSettings:)]) {
[self.delegate toolBarDidTapSettings:self];
}
}
- (void)onUndo {
if ([self.delegate respondsToSelector:@selector(toolBarDidTapUndo:)]) {
[self.delegate toolBarDidTapUndo:self];
@@ -319,19 +343,6 @@
return _avatarImageView;
}
//- (UIButton *)settingsButtonInternal {
// if (!_settingsButtonInternal) {
// _settingsButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
// _settingsButtonInternal.layer.cornerRadius = 16;
// _settingsButtonInternal.layer.masksToBounds = YES;
// _settingsButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
// [_settingsButtonInternal setTitle:@"⚙︎" forState:UIControlStateNormal]; // 齿
// [_settingsButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
// [_settingsButtonInternal addTarget:self action:@selector(onSettings) forControlEvents:UIControlEventTouchUpInside];
// }
// return _settingsButtonInternal;
//}
- (UIButton *)globeButtonInternal {
if (!_globeButtonInternal) {
_globeButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
@@ -419,7 +430,7 @@
- (void)kb_updateRightControlsConstraints {
[self.avatarImageView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self).offset(-12);
make.centerY.equalTo(self).offset(5);
make.centerY.equalTo(self).offset(0);
make.width.height.mas_equalTo(kKBAvatarSize);
}];
[self.undoButtonInternal mas_remakeConstraints:^(MASConstraintMaker *make) {
@@ -438,6 +449,10 @@
[self kb_updateUndoVisibilityAnimated:YES];
}
- (void)kb_skinDidChange {
[self kb_applyTheme];
}
- (void)kb_updateUndoVisibilityAnimated:(BOOL)animated {
BOOL visible = [KBBackspaceUndoManager shared].hasUndo;
if (self.kbUndoVisible == visible) { return; }

View File

@@ -0,0 +1,42 @@
Feb 5 20:30:09 macbookpro com.apple.dt.xcodebuild[56551] <Error>: Unable to deliver request ({
"developer_dir" = "/Applications/Xcode.app/Contents/Developer";
request = "set_developer_dir";
}) because we are not connected to CoreSimulatorService.
Feb 5 20:30:22 macbookpro com.apple.dt.xcodebuild[56567] <Error>: Unable to deliver request ({
"developer_dir" = "/Applications/Xcode.app/Contents/Developer";
request = "set_developer_dir";
}) because we are not connected to CoreSimulatorService.
Feb 5 20:31:06 macbookpro com.apple.dt.xcodebuild[56684] <Error>: Unable to deliver request ({
"developer_dir" = "/Applications/Xcode.app/Contents/Developer";
request = "set_developer_dir";
}) because we are not connected to CoreSimulatorService.
Feb 5 20:31:06 macbookpro com.apple.dt.xcodebuild[56684] <Warning>: Unable to discover any Simulator runtimes. Developer Directory is /Applications/Xcode.app/Contents/Developer.
Feb 5 20:31:06 macbookpro com.apple.dt.xcodebuild[56684] <Error>: Could not kickstart simdiskimaged; SimDiskImageManager services will not be available: Error Domain=NSPOSIXErrorDomain Code=53 "Software caused connection abort" UserInfo={NSLocalizedDescription=Error returned in reply from CoreSimulatorService: Connection invalid}
Feb 5 20:31:06 macbookpro com.apple.dt.xcodebuild[56684] <Error>: simdiskimaged returned error (invalid), marking disconnected.
Feb 5 20:31:06 macbookpro com.apple.dt.xcodebuild[56684] <Error>: Could not get list of trusted mount directories: Error Domain=com.apple.CoreSimulator.SimError Code=410 "The service used to manage runtime disk images (simdiskimaged) crashed or is not responding" UserInfo={NSLocalizedDescription=The service used to manage runtime disk images (simdiskimaged) crashed or is not responding}
Feb 5 20:31:06 macbookpro com.apple.dt.xcodebuild[56684] <Error>: simdiskimaged returned error (invalid), marking disconnected.
Feb 5 20:31:06 macbookpro com.apple.dt.xcodebuild[56684] <Error>: Unable to deliver request ({
request = "notification_subscription";
"set_path" = "/Users/mac/Library/Developer/CoreSimulator/Devices";
}) because we are not connected to CoreSimulatorService.
Feb 5 20:31:06 macbookpro com.apple.dt.xcodebuild[56684] <Error>: Unable to deliver request ({
request = "notification_subscription";
"set_path" = "/Users/mac/Library/Developer/CoreSimulator/Devices";
}) because we are not connected to CoreSimulatorService.
Feb 5 20:31:55 macbookpro com.apple.dt.xcodebuild[56806] <Error>: Unable to deliver request ({
"developer_dir" = "/Applications/Xcode.app/Contents/Developer";
request = "set_developer_dir";
}) because we are not connected to CoreSimulatorService.
Feb 5 20:31:55 macbookpro com.apple.dt.xcodebuild[56806] <Warning>: Unable to discover any Simulator runtimes. Developer Directory is /Applications/Xcode.app/Contents/Developer.
Feb 5 20:31:55 macbookpro com.apple.dt.xcodebuild[56806] <Error>: Could not kickstart simdiskimaged; SimDiskImageManager services will not be available: Error Domain=NSPOSIXErrorDomain Code=53 "Software caused connection abort" UserInfo={NSLocalizedDescription=Error returned in reply from CoreSimulatorService: Connection invalid}
Feb 5 20:31:55 macbookpro com.apple.dt.xcodebuild[56806] <Error>: simdiskimaged returned error (invalid), marking disconnected.
Feb 5 20:31:55 macbookpro com.apple.dt.xcodebuild[56806] <Error>: Could not get list of trusted mount directories: Error Domain=com.apple.CoreSimulator.SimError Code=410 "The service used to manage runtime disk images (simdiskimaged) crashed or is not responding" UserInfo={NSLocalizedDescription=The service used to manage runtime disk images (simdiskimaged) crashed or is not responding}
Feb 5 20:31:55 macbookpro com.apple.dt.xcodebuild[56806] <Error>: simdiskimaged returned error (invalid), marking disconnected.
Feb 5 20:31:55 macbookpro com.apple.dt.xcodebuild[56806] <Error>: Unable to deliver request ({
request = "notification_subscription";
"set_path" = "/Users/mac/Library/Developer/CoreSimulator/Devices";
}) because we are not connected to CoreSimulatorService.
Feb 5 20:31:55 macbookpro com.apple.dt.xcodebuild[56806] <Error>: Unable to deliver request ({
request = "notification_subscription";
"set_path" = "/Users/mac/Library/Developer/CoreSimulator/Devices";
}) because we are not connected to CoreSimulatorService.

13
Podfile
View File

@@ -6,7 +6,8 @@ target 'keyBoard' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
pod 'AFNetworking','4.0.1'
# pod 'AFNetworking','4.0.1'
pod 'AFNetworking', :git => 'https://github.com/gzx543097079/AppStorePrivacyInfo.git', :tag => 'AFNetworking-4.0.1.1'
pod 'Bugly', :configurations => ['Release']
pod 'DZNEmptyDataSet', '1.8.1'
pod 'FLAnimatedImage', '~> 1.0.17'
@@ -17,7 +18,8 @@ target 'keyBoard' do
pod 'LookinServer', :configurations => ['Debug']
pod 'LYEmptyView', '~> 1.3.1'
pod 'Masonry', '1.1.0'
pod 'MBProgressHUD', '1.2.0'
pod 'MBProgressHUD',:git => 'https://github.com/jdg/MBProgressHUD.git', :commit => '18c442d57398cee5ef57f852df10fc5ff65f0763'
# pod 'MBProgressHUD', '1.2.0'
pod 'MJExtension', '3.4.2'
pod 'MJRefresh', '3.7.9'
pod 'SDWebImage', '5.21.1'
@@ -28,11 +30,14 @@ target 'CustomKeyboard' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
pod 'AFNetworking','4.0.1'
# pod 'AFNetworking','4.0.1'
pod 'AFNetworking', :git => 'https://github.com/gzx543097079/AppStorePrivacyInfo.git', :tag => 'AFNetworking-4.0.1.1'
pod 'SDWebImage', '5.21.1'
pod 'Masonry', '1.1.0'
pod 'MBProgressHUD', '1.2.0'
# pod 'MBProgressHUD', '1.2.0'
pod 'MBProgressHUD',:git => 'https://github.com/jdg/MBProgressHUD.git', :commit => '18c442d57398cee5ef57f852df10fc5ff65f0763'
pod 'MJExtension', '3.4.2'
pod 'DZNEmptyDataSet', '1.8.1'
pod 'SSZipArchive', '~> 2.4.3'

View File

@@ -40,7 +40,7 @@ PODS:
- SSZipArchive (2.4.3)
DEPENDENCIES:
- AFNetworking (= 4.0.1)
- AFNetworking (from `https://github.com/gzx543097079/AppStorePrivacyInfo.git`, tag `AFNetworking-4.0.1.1`)
- Bugly
- DZNEmptyDataSet (= 1.8.1)
- FLAnimatedImage (~> 1.0.17)
@@ -51,7 +51,7 @@ DEPENDENCIES:
- LSTPopView (~> 0.3.10)
- LYEmptyView (~> 1.3.1)
- Masonry (= 1.1.0)
- MBProgressHUD (= 1.2.0)
- MBProgressHUD (from `https://github.com/jdg/MBProgressHUD.git`, commit `18c442d57398cee5ef57f852df10fc5ff65f0763`)
- MJExtension (= 3.4.2)
- MJRefresh (= 3.7.9)
- SDWebImage (= 5.21.1)
@@ -59,7 +59,6 @@ DEPENDENCIES:
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
- AFNetworking
- Bugly
- DZNEmptyDataSet
- FLAnimatedImage
@@ -71,14 +70,29 @@ SPEC REPOS:
- LSTTimer
- LYEmptyView
- Masonry
- MBProgressHUD
- MJExtension
- MJRefresh
- SDWebImage
- SSZipArchive
EXTERNAL SOURCES:
AFNetworking:
:git: https://github.com/gzx543097079/AppStorePrivacyInfo.git
:tag: AFNetworking-4.0.1.1
MBProgressHUD:
:commit: 18c442d57398cee5ef57f852df10fc5ff65f0763
:git: https://github.com/jdg/MBProgressHUD.git
CHECKOUT OPTIONS:
AFNetworking:
:git: https://github.com/gzx543097079/AppStorePrivacyInfo.git
:tag: AFNetworking-4.0.1.1
MBProgressHUD:
:commit: 18c442d57398cee5ef57f852df10fc5ff65f0763
:git: https://github.com/jdg/MBProgressHUD.git
SPEC CHECKSUMS:
AFNetworking: 3bd23d814e976cd148d7d44c3ab78017b744cd58
AFNetworking: 5dc31d980b5c60e63d2852bdd75252c3fd85a6d0
Bugly: 217ac2ce5f0f2626d43dbaa4f70764c953a26a31
DZNEmptyDataSet: 9525833b9e68ac21c30253e1d3d7076cc828eaa7
FLAnimatedImage: bbf914596368867157cc71b38a8ec834b3eeb32b
@@ -90,12 +104,12 @@ SPEC CHECKSUMS:
LSTTimer: caf8f02ff366ca175cf4c1778d26c166183c1b6f
LYEmptyView: b6d418cfa38b78df0cf243f9a9c25ccbdc399922
Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
MBProgressHUD: 3ee5efcc380f6a79a7cc9b363dd669c5e1ae7406
MBProgressHUD: 1b0fb447e80a0fda94808180750e8b78a07b3cd2
MJExtension: e97d164cb411aa9795cf576093a1fa208b4a8dd8
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
SDWebImage: f29024626962457f3470184232766516dee8dfea
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
PODFILE CHECKSUM: 890d1710715c017d7364a19c871e9bdf0d685fbf
PODFILE CHECKSUM: f79dfcf9dc400fbf6dc41f60d4b726fee361d430
COCOAPODS: 1.16.2

View File

@@ -227,7 +227,7 @@ static void AFNetworkReachabilityReleaseCallback(const void *info) {
SCNetworkReachabilitySetCallback(self.networkReachability, AFNetworkReachabilityCallback, &context);
SCNetworkReachabilityScheduleWithRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0),^{
SCNetworkReachabilityFlags flags;
if (SCNetworkReachabilityGetFlags(self.networkReachability, &flags)) {
AFPostReachabilityStatusChange(flags, callback);

View File

@@ -278,7 +278,7 @@ forHTTPHeaderField:(NSString *)field;
#pragma mark -
/**
The `AFMultipartFormData` protocol defines the methods supported by the parameter in the block argument of `AFHTTPRequestSerializer -multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:`.
The `AFMultipartFormData` protocol defines the methods supported by the parameter in the block argument of `AFHTTPRequestSerializer -multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:error:`.
*/
@protocol AFMultipartFormData

View File

@@ -368,10 +368,8 @@ forHTTPHeaderField:(NSString *)field
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
mutableRequest.HTTPMethod = method;
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
for (NSString *keyPath in self.mutableObservedChangedKeyPaths) {
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];

View File

@@ -316,10 +316,10 @@ NS_ASSUME_NONNULL_BEGIN
@param block A block object to be executed when a connection level authentication challenge has occurred. The block returns the disposition of the authentication challenge, and takes three arguments: the session, the authentication challenge, and a pointer to the credential that should be used to resolve the challenge.
@warning Implementing a session authentication challenge handler yourself totally bypasses AFNetworking's security policy defined in `AFSecurityPolicy`. Make sure you fully understand the implications before implementing a custom session authentication challenge handler. If you do not want to bypass AFNetworking's security policy, use `setTaskDidReceiveAuthenticationChallengeBlock:` instead.
@warning Implementing a session authentication challenge handler yourself totally bypasses AFNetworking's security policy defined in `AFSecurityPolicy`. Make sure you fully understand the implications before implementing a custom session authentication challenge handler. If you do not want to bypass AFNetworking's security policy, use `-setAuthenticationChallengeHandler:` instead.
@see -securityPolicy
@see -setTaskDidReceiveAuthenticationChallengeBlock:
@see -setAuthenticationChallengeHandler:
*/
- (void)setSessionDidReceiveAuthenticationChallengeBlock:(nullable NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * _Nullable __autoreleasing * _Nullable credential))block;
@@ -416,7 +416,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)setDataTaskWillCacheResponseBlock:(nullable NSCachedURLResponse * (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSCachedURLResponse *proposedResponse))block;
/**
Sets a block to be executed once all messages enqueued for a session have been delivered, as handled by the `NSURLSessionDataDelegate` method `URLSessionDidFinishEventsForBackgroundURLSession:`.
Sets a block to be executed once all messages enqueued for a session have been delivered, as handled by the `NSURLSessionDelegate` method `URLSessionDidFinishEventsForBackgroundURLSession:`.
@param block A block object to be executed once all messages enqueued for a session have been delivered. The block has no return value and takes a single argument: the session.
*/

Some files were not shown because too many files have changed in this diff Show More