Compare commits
59 Commits
dev_st_重构2
...
dev_st
| Author | SHA1 | Date | |
|---|---|---|---|
| e5472ebd6e | |||
| 8a778a6fdc | |||
| 2e95a0072a | |||
| 72142b0b71 | |||
| 0af7428353 | |||
| c1ace5f53e | |||
| 9fb2e2e694 | |||
| 6327f31f11 | |||
| cbcf8c4197 | |||
| e03287605c | |||
| 987391953a | |||
| 442d56decd | |||
| fb74fbed1c | |||
| 33a04186fb | |||
| bb74a330db | |||
| 3c18579a83 | |||
| d25dd38959 | |||
| eaf512be7f | |||
| d8a84dc478 | |||
| 8cc484edcb | |||
| a61f505f70 | |||
| 8316d42fb3 | |||
| cb0b8a0aee | |||
| fe08f8d54a | |||
| 7029209a4d | |||
| c42ccfbcdf | |||
| f2184cf9c6 | |||
| e7567909bc | |||
| 2d02e05956 | |||
| 973577c6eb | |||
| 5c0cf2b435 | |||
| fd5de4f197 | |||
| f9da0c40e5 | |||
| b1f1ddec7e | |||
| f30b1d7640 | |||
| 2a122d27a9 | |||
| 72069cc737 | |||
| 6786a76f41 | |||
| 361ccc12d6 | |||
| e5e059cf24 | |||
| 7adccd60c5 | |||
| 4c16ae1736 | |||
| a0c5afc75d | |||
| 4a26502c41 | |||
| b86801636a | |||
| bcc8981c06 | |||
| 211f30d793 | |||
| 494efb745e | |||
| 53c406c984 | |||
| 2aa5fa8d09 | |||
| 152c7052b4 | |||
| 2505de0f24 | |||
| fb6db0649c | |||
| a68fb9657f | |||
| 04cfc35485 | |||
| d79a1d15bc | |||
| 6e62394feb | |||
| 781e557e80 | |||
| da4649101e |
@@ -7,7 +7,11 @@
|
|||||||
"Bash(plutil:*)",
|
"Bash(plutil:*)",
|
||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(ls:*)",
|
"Bash(ls:*)",
|
||||||
"Bash(wc:*)"
|
"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
15
.gitignore
vendored
Normal 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/
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<string>kbkeyboardAppExtension</string>
|
<string>kbkeyboardAppExtension</string>
|
||||||
</array>
|
</array>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>需要使用麦克风进行语音输入</string>
|
<string>Microphone access is required for voice input and speech transcription.</string>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionAttributes</key>
|
<key>NSExtensionAttributes</key>
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
#import "KBKeyboardSubscriptionView.h"
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
#import "KBLocalizationManager.h"
|
#import "KBLocalizationManager.h"
|
||||||
#import "KBSkinManager.h"
|
#import "KBSkinManager.h"
|
||||||
|
#import "KBSkinInstallBridge.h"
|
||||||
#import "KBSuggestionEngine.h"
|
#import "KBSuggestionEngine.h"
|
||||||
|
#import "KBKeyboardLayoutResolver.h"
|
||||||
#import <SDWebImage/SDWebImage.h>
|
#import <SDWebImage/SDWebImage.h>
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@@ -48,6 +50,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
BOOL _kb_didTriggerLoginDeepLinkOnce;
|
BOOL _kb_didTriggerLoginDeepLinkOnce;
|
||||||
|
NSString *_kb_lastLoadedProfileId; // 记录上次加载的 profileId
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
BOOL _kb_debugDidCountAlive;
|
BOOL _kb_debugDidCountAlive;
|
||||||
#endif
|
#endif
|
||||||
@@ -93,10 +96,28 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
}
|
}
|
||||||
[self kb_applyTheme];
|
[self kb_applyTheme];
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
// 语言变化时,重建键盘 UI(保证“App 语言=键盘语言”,并支持 App 内切换语言后键盘即时刷新)
|
||||||
|
self.kb_localizationObserverToken = [[NSNotificationCenter defaultCenter]
|
||||||
|
addObserverForName:KBLocalizationDidChangeNotification
|
||||||
|
object:nil
|
||||||
|
queue:[NSOperationQueue mainQueue]
|
||||||
|
usingBlock:^(__unused NSNotification *_Nonnull note) {
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self kb_reloadUIForLocalizationChange];
|
||||||
|
}];
|
||||||
[self kb_applyTheme];
|
[self kb_applyTheme];
|
||||||
[self kb_registerDarwinSkinInstallObserver];
|
[self kb_registerDarwinSkinInstallObserver];
|
||||||
[self kb_consumePendingShopSkin];
|
[self kb_consumePendingShopSkin];
|
||||||
[self kb_applyDefaultSkinIfNeeded];
|
[self kb_applyDefaultSkinIfNeeded];
|
||||||
|
|
||||||
|
[self kb_startObservingAppGroupChanges];
|
||||||
|
|
||||||
|
// 监听 App Group 配置变化,动态切换键盘布局
|
||||||
|
[self kb_checkAndApplyLayoutIfNeeded];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)didReceiveMemoryWarning {
|
- (void)didReceiveMemoryWarning {
|
||||||
@@ -198,6 +219,9 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
if (self.kb_defaultGradientLayer) {
|
if (self.kb_defaultGradientLayer) {
|
||||||
self.kb_defaultGradientLayer.frame = self.bgImageView.bounds;
|
self.kb_defaultGradientLayer.frame = self.bgImageView.bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 每次布局时检查是否需要切换键盘布局
|
||||||
|
[self kb_checkAndApplyLayoutIfNeeded];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewWillTransitionToSize:(CGSize)size
|
- (void)viewWillTransitionToSize:(CGSize)size
|
||||||
@@ -228,6 +252,12 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
removeObserver:self.kb_skinObserverToken];
|
removeObserver:self.kb_skinObserverToken];
|
||||||
self.kb_skinObserverToken = nil;
|
self.kb_skinObserverToken = nil;
|
||||||
}
|
}
|
||||||
|
if (self.kb_localizationObserverToken) {
|
||||||
|
[[NSNotificationCenter defaultCenter]
|
||||||
|
removeObserver:self.kb_localizationObserverToken];
|
||||||
|
self.kb_localizationObserverToken = nil;
|
||||||
|
}
|
||||||
|
[self kb_stopObservingAppGroupChanges];
|
||||||
[self kb_unregisterDarwinSkinInstallObserver];
|
[self kb_unregisterDarwinSkinInstallObserver];
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if (_kb_debugDidCountAlive) {
|
if (_kb_debugDidCountAlive) {
|
||||||
@@ -238,4 +268,113 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - Localization
|
||||||
|
|
||||||
|
- (void)kb_reloadUIForLocalizationChange {
|
||||||
|
if (![NSThread isMainThread]) {
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[weakSelf kb_reloadUIForLocalizationChange];
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录当前面板状态,重建后尽量恢复。
|
||||||
|
KBKeyboardPanelMode targetMode = self.kb_panelMode;
|
||||||
|
// 强制下次布局刷新:即使 profileId 未变,也需要让新建的主视图应用一次当前 profile。
|
||||||
|
_kb_lastLoadedProfileId = nil;
|
||||||
|
|
||||||
|
// 主键盘/面板里有大量静态文案(init 时设置),语言变化后需要重建才能刷新。
|
||||||
|
if (_keyBoardMainView) {
|
||||||
|
[_keyBoardMainView removeFromSuperview];
|
||||||
|
_keyBoardMainView = nil;
|
||||||
|
}
|
||||||
|
self.keyBoardMainHeightConstraint = nil;
|
||||||
|
|
||||||
|
if (_functionView) {
|
||||||
|
[_functionView removeFromSuperview];
|
||||||
|
_functionView = nil;
|
||||||
|
}
|
||||||
|
if (_subscriptionView) {
|
||||||
|
[_subscriptionView removeFromSuperview];
|
||||||
|
_subscriptionView = nil;
|
||||||
|
}
|
||||||
|
if (_chatPanelView) {
|
||||||
|
[_chatPanelView removeFromSuperview];
|
||||||
|
_chatPanelView = nil;
|
||||||
|
}
|
||||||
|
self.chatPanelVisible = NO;
|
||||||
|
self.chatPanelHeightConstraint = nil;
|
||||||
|
|
||||||
|
// 强制触发面板刷新:先回到 Main,再切回目标面板(避免 kb_setPanelMode 直接 return)。
|
||||||
|
self.kb_panelMode = KBKeyboardPanelModeMain;
|
||||||
|
[self kb_setPanelMode:targetMode animated:NO];
|
||||||
|
// 语言变化后,键盘布局/profile 也可能需要同步更新(未手动选择键盘配置时会随 App 语言变化)
|
||||||
|
[self kb_checkAndApplyLayoutIfNeeded];
|
||||||
|
[KBHUD setContainerView:self.view];
|
||||||
|
[self kb_applyTheme];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Layout Switching
|
||||||
|
|
||||||
|
- (void)kb_checkAndApplyLayoutIfNeeded {
|
||||||
|
NSString *currentProfileId = [[KBKeyboardLayoutResolver sharedResolver] currentProfileId];
|
||||||
|
if (currentProfileId.length == 0) {
|
||||||
|
currentProfileId = @"en_US_qwerty";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([currentProfileId isEqualToString:_kb_lastLoadedProfileId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"[KeyboardViewController] Detected profileId change: %@ -> %@", _kb_lastLoadedProfileId, currentProfileId);
|
||||||
|
_kb_lastLoadedProfileId = currentProfileId;
|
||||||
|
|
||||||
|
if (self.keyBoardMainView && [self.keyBoardMainView respondsToSelector:@selector(reloadLayoutWithProfileId:)]) {
|
||||||
|
[self.keyBoardMainView performSelector:@selector(reloadLayoutWithProfileId:) withObject:currentProfileId];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *suggestionEngine = [[KBKeyboardLayoutResolver sharedResolver] suggestionEngineForProfileId:currentProfileId];
|
||||||
|
if (suggestionEngine.length > 0) {
|
||||||
|
[self kb_updateSuggestionEngineType:suggestionEngine];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *languageCode = [[KBKeyboardLayoutResolver sharedResolver] currentLanguageCode];
|
||||||
|
if (languageCode.length > 0) {
|
||||||
|
NSLog(@"[KeyboardViewController] Reloading skin icon map for language: %@", languageCode);
|
||||||
|
[KBSkinInstallBridge reloadCurrentSkinIconMapForLanguageCode:languageCode];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_updateSuggestionEngineType:(NSString *)engineType {
|
||||||
|
NSLog(@"[KeyboardViewController] Switching suggestion engine to: %@", engineType);
|
||||||
|
[[KBSuggestionEngine shared] setEngineTypeFromString:engineType];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - App Group KVO
|
||||||
|
|
||||||
|
- (void)kb_startObservingAppGroupChanges {
|
||||||
|
NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
|
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
self.kb_appGroupObserverToken = [[NSNotificationCenter defaultCenter]
|
||||||
|
addObserverForName:NSUserDefaultsDidChangeNotification
|
||||||
|
object:appGroup
|
||||||
|
queue:[NSOperationQueue mainQueue]
|
||||||
|
usingBlock:^(__unused NSNotification *_Nonnull note) {
|
||||||
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||||
|
if (!strongSelf) { return; }
|
||||||
|
[strongSelf kb_checkAndApplyLayoutIfNeeded];
|
||||||
|
}];
|
||||||
|
|
||||||
|
NSLog(@"[KeyboardViewController] Started observing App Group changes");
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_stopObservingAppGroupChanges {
|
||||||
|
if (self.kb_appGroupObserverToken) {
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self.kb_appGroupObserverToken];
|
||||||
|
self.kb_appGroupObserverToken = nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
#import "KBChatMessage.h"
|
#import "KBChatMessage.h"
|
||||||
#import "KBChatPanelView.h"
|
#import "KBChatPanelView.h"
|
||||||
#import "KBFullAccessManager.h"
|
#import "KBFullAccessManager.h"
|
||||||
#import "KBHostAppLauncher.h"
|
#import "../Utils/KBExtensionAppLauncher.h"
|
||||||
#import "KBInputBufferManager.h"
|
#import "KBInputBufferManager.h"
|
||||||
#import "KBNetworkManager.h"
|
#import "KBNetworkManager.h"
|
||||||
#import "KBVM.h"
|
#import "KBVM.h"
|
||||||
@@ -106,7 +106,7 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (trim.length == 0) {
|
if (trim.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"请输入内容")];
|
[KBHUD showInfo:KBLocalized(@"Please enter content")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[self kb_sendChatText:trim];
|
[self kb_sendChatText:trim];
|
||||||
@@ -118,7 +118,9 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
if (text.length == 0) {
|
if (text.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NSLog(@"[KB] 发送消息: %@", text);
|
#if DEBUG
|
||||||
|
NSLog(@"[KB] 发送消息 len=%lu", (unsigned long)text.length);
|
||||||
|
#endif
|
||||||
|
|
||||||
KBChatMessage *outgoing = [KBChatMessage userMessageWithText:text];
|
KBChatMessage *outgoing = [KBChatMessage userMessageWithText:text];
|
||||||
outgoing.avatarURL = [self kb_sharedUserAvatarURL];
|
outgoing.avatarURL = [self kb_sharedUserAvatarURL];
|
||||||
@@ -126,7 +128,7 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
[self kb_prefetchAvatarForMessage:outgoing];
|
[self kb_prefetchAvatarForMessage:outgoing];
|
||||||
|
|
||||||
if (![[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view]) {
|
if (![[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view]) {
|
||||||
[KBHUD showInfo:KBLocalized(@"请开启完全访问后使用")];
|
[KBHUD showInfo:KBLocalized(@"Please enable Full Access to continue")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,12 +259,12 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
if (mockPath.length > 0) {
|
if (mockPath.length > 0) {
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.35 * NSEC_PER_SEC)),
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.35 * NSEC_PER_SEC)),
|
||||||
dispatch_get_main_queue(), ^{
|
dispatch_get_main_queue(), ^{
|
||||||
NSString *displayText = KBLocalized(@"语音回复");
|
NSString *displayText = KBLocalized(@"Voice reply");
|
||||||
KBChatMessage *incoming =
|
KBChatMessage *incoming =
|
||||||
[KBChatMessage messageWithText:displayText
|
[KBChatMessage messageWithText:displayText
|
||||||
outgoing:NO
|
outgoing:NO
|
||||||
audioFilePath:mockPath];
|
audioFilePath:mockPath];
|
||||||
incoming.displayName = KBLocalized(@"AI助手");
|
incoming.displayName = KBLocalized(@"AI Assistant");
|
||||||
[self kb_appendChatMessage:incoming];
|
[self kb_appendChatMessage:incoming];
|
||||||
[self kb_playChatAudioAtPath:mockPath];
|
[self kb_playChatAudioAtPath:mockPath];
|
||||||
});
|
});
|
||||||
@@ -282,7 +284,7 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
NSString *tip = error.localizedDescription
|
NSString *tip = error.localizedDescription
|
||||||
?: KBLocalized(@"请求失败");
|
?: KBLocalized(@"Request failed");
|
||||||
[KBHUD showInfo:tip];
|
[KBHUD showInfo:tip];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -302,7 +304,7 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
initWithBase64EncodedString:audioBase64
|
initWithBase64EncodedString:audioBase64
|
||||||
options:0];
|
options:0];
|
||||||
if (data.length == 0) {
|
if (data.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"音频数据解析失败")];
|
[KBHUD showInfo:KBLocalized(@"Failed to parse audio data")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[self kb_handleChatAudioData:data
|
[self kb_handleChatAudioData:data
|
||||||
@@ -310,7 +312,7 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
displayText:displayText];
|
displayText:displayText];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[KBHUD showInfo:KBLocalized(@"未获取到音频文件")];
|
[KBHUD showInfo:KBLocalized(@"No audio file received")];
|
||||||
});
|
});
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
@@ -350,7 +352,7 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
[self.chatPanelView
|
[self.chatPanelView
|
||||||
kb_removeLoadingAssistantMessage];
|
kb_removeLoadingAssistantMessage];
|
||||||
[KBHUD showInfo:response.message
|
[KBHUD showInfo:response.message
|
||||||
?: KBLocalized(@"请求失败")];
|
?: KBLocalized(@"Request failed")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +362,7 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
if (response.data.aiResponse.length == 0) {
|
if (response.data.aiResponse.length == 0) {
|
||||||
[self.chatPanelView
|
[self.chatPanelView
|
||||||
kb_removeLoadingAssistantMessage];
|
kb_removeLoadingAssistantMessage];
|
||||||
[KBHUD showInfo:KBLocalized(@"未获取到回复内容")];
|
[KBHUD showInfo:KBLocalized(@"No reply content received")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,13 +450,13 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
[KBHUD showInfo:response.errorMessage
|
[KBHUD showInfo:response.errorMessage
|
||||||
?: KBLocalized(@"下载失败")];
|
?: KBLocalized(@"Download failed")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.audioData ||
|
if (!response.audioData ||
|
||||||
response.audioData.length == 0) {
|
response.audioData.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"未获取到音频数据")];
|
[KBHUD showInfo:KBLocalized(@"No audio data received")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,7 +475,7 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
fileExtension:(NSString *)extension
|
fileExtension:(NSString *)extension
|
||||||
displayText:(NSString *)displayText {
|
displayText:(NSString *)displayText {
|
||||||
if (data.length == 0) {
|
if (data.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"音频数据为空")];
|
[KBHUD showInfo:KBLocalized(@"Audio data is empty")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NSString *ext = extension.length > 0 ? extension : @"m4a";
|
NSString *ext = extension.length > 0 ? extension : @"m4a";
|
||||||
@@ -485,14 +487,14 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
NSString *filePath =
|
NSString *filePath =
|
||||||
[NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
|
[NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
|
||||||
if (![data writeToFile:filePath atomically:YES]) {
|
if (![data writeToFile:filePath atomically:YES]) {
|
||||||
[KBHUD showInfo:KBLocalized(@"音频保存失败")];
|
[KBHUD showInfo:KBLocalized(@"Failed to save audio")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NSString *text =
|
NSString *text =
|
||||||
displayText.length > 0 ? displayText : KBLocalized(@"语音消息");
|
displayText.length > 0 ? displayText : KBLocalized(@"Voice message");
|
||||||
KBChatMessage *incoming =
|
KBChatMessage *incoming =
|
||||||
[KBChatMessage messageWithText:text outgoing:NO audioFilePath:filePath];
|
[KBChatMessage messageWithText:text outgoing:NO audioFilePath:filePath];
|
||||||
incoming.displayName = KBLocalized(@"AI助手");
|
incoming.displayName = KBLocalized(@"AI Assistant");
|
||||||
[self kb_appendChatMessage:incoming];
|
[self kb_appendChatMessage:incoming];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,7 +594,7 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
}
|
}
|
||||||
NSURL *url = [NSURL fileURLWithPath:path];
|
NSURL *url = [NSURL fileURLWithPath:path];
|
||||||
if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
|
if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
|
||||||
[KBHUD showInfo:KBLocalized(@"音频文件不存在")];
|
[KBHUD showInfo:KBLocalized(@"Audio file does not exist")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,7 +624,7 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
AVAudioPlayer *player =
|
AVAudioPlayer *player =
|
||||||
[[AVAudioPlayer alloc] initWithContentsOfURL:url error:&playerError];
|
[[AVAudioPlayer alloc] initWithContentsOfURL:url error:&playerError];
|
||||||
if (playerError || !player) {
|
if (playerError || !player) {
|
||||||
[KBHUD showInfo:KBLocalized(@"音频播放失败")];
|
[KBHUD showInfo:KBLocalized(@"Audio playback failed")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.chatAudioPlayer = player;
|
self.chatAudioPlayer = player;
|
||||||
@@ -662,7 +664,7 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
if (playerError || !player) {
|
if (playerError || !player) {
|
||||||
NSLog(@"[Keyboard] 音频播放器初始化失败: %@",
|
NSLog(@"[Keyboard] 音频播放器初始化失败: %@",
|
||||||
playerError.localizedDescription);
|
playerError.localizedDescription);
|
||||||
[KBHUD showInfo:KBLocalized(@"音频播放失败")];
|
[KBHUD showInfo:KBLocalized(@"Audio playback failed")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,11 +704,21 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
[NSString stringWithFormat:@"%@://recharge?src=keyboard&vipType=svip",
|
[NSString stringWithFormat:@"%@://recharge?src=keyboard&vipType=svip",
|
||||||
KB_APP_SCHEME];
|
KB_APP_SCHEME];
|
||||||
NSURL *scheme = [NSURL URLWithString:urlString];
|
NSURL *scheme = [NSURL URLWithString:urlString];
|
||||||
BOOL success = [KBHostAppLauncher openHostAppURL:scheme
|
NSString *ulString = [NSString stringWithFormat:@"%@?src=keyboard&vipType=svip", KB_UL_RECHARGE];
|
||||||
fromResponder:self.view];
|
NSURL *ul = [NSURL URLWithString:ulString];
|
||||||
if (!success) {
|
__weak typeof(self) weakSelf = self;
|
||||||
[KBHUD showInfo:KBLocalized(@"Please open the App to finish purchase")];
|
[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
|
@end
|
||||||
|
|||||||
@@ -12,12 +12,11 @@
|
|||||||
#import "KBBackspaceUndoManager.h"
|
#import "KBBackspaceUndoManager.h"
|
||||||
#import "KBFullAccessManager.h"
|
#import "KBFullAccessManager.h"
|
||||||
#import "KBFunctionView.h"
|
#import "KBFunctionView.h"
|
||||||
#import "KBHostAppLauncher.h"
|
#import "../Utils/KBExtensionAppLauncher.h"
|
||||||
#import "KBInputBufferManager.h"
|
#import "KBInputBufferManager.h"
|
||||||
#import "KBKey.h"
|
#import "KBKey.h"
|
||||||
#import "KBKeyboardSubscriptionProduct.h"
|
#import "KBKeyboardSubscriptionProduct.h"
|
||||||
#import "KBKeyboardSubscriptionView.h"
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
#import "KBSettingView.h"
|
|
||||||
#import "KBChatMessage.h"
|
#import "KBChatMessage.h"
|
||||||
#import "KBChatPanelView.h"
|
#import "KBChatPanelView.h"
|
||||||
#import "KBChatLimitPopView.h"
|
#import "KBChatLimitPopView.h"
|
||||||
@@ -79,7 +78,6 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
*keyBoardMainView; // 功能面板视图(点击工具栏第0个时显示)
|
*keyBoardMainView; // 功能面板视图(点击工具栏第0个时显示)
|
||||||
@property(nonatomic, strong)
|
@property(nonatomic, strong)
|
||||||
KBFunctionView *functionView; // 功能面板视图(点击工具栏第0个时显示)
|
KBFunctionView *functionView; // 功能面板视图(点击工具栏第0个时显示)
|
||||||
@property(nonatomic, strong) KBSettingView *settingView; // 设置页
|
|
||||||
@property(nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
|
@property(nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
|
||||||
@property(nonatomic, strong) KBChatPanelView *chatPanelView;
|
@property(nonatomic, strong) KBChatPanelView *chatPanelView;
|
||||||
@property(nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
|
@property(nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
|
||||||
@@ -369,13 +367,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
if (context.length == 0) {
|
if (context.length == 0) {
|
||||||
return @"";
|
return @"";
|
||||||
}
|
}
|
||||||
static NSCharacterSet *letters = nil;
|
NSCharacterSet *letters = [self kb_allowedSuggestionCharacterSet];
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
letters = [NSCharacterSet
|
|
||||||
characterSetWithCharactersInString:
|
|
||||||
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
|
|
||||||
});
|
|
||||||
|
|
||||||
NSInteger idx = (NSInteger)context.length - 1;
|
NSInteger idx = (NSInteger)context.length - 1;
|
||||||
while (idx >= 0) {
|
while (idx >= 0) {
|
||||||
@@ -396,13 +388,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
if (text.length == 0) {
|
if (text.length == 0) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
static NSCharacterSet *letters = nil;
|
NSCharacterSet *letters = [self kb_allowedSuggestionCharacterSet];
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
letters = [NSCharacterSet
|
|
||||||
characterSetWithCharactersInString:
|
|
||||||
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
|
|
||||||
});
|
|
||||||
for (NSUInteger i = 0; i < text.length; i++) {
|
for (NSUInteger i = 0; i < text.length; i++) {
|
||||||
if (![letters characterIsMember:[text characterAtIndex:i]]) {
|
if (![letters characterIsMember:[text characterAtIndex:i]]) {
|
||||||
return NO;
|
return NO;
|
||||||
@@ -411,6 +397,62 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
return YES;
|
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 {
|
- (void)kb_updateSuggestionsForCurrentWord {
|
||||||
NSString *prefix = self.currentWord ?: @"";
|
NSString *prefix = self.currentWord ?: @"";
|
||||||
if (prefix.length == 0) {
|
if (prefix.length == 0) {
|
||||||
@@ -495,68 +537,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示/隐藏设置页(高度与 keyBoardMainView 一致),右侧滑入/滑出
|
|
||||||
- (void)showSettingView:(BOOL)show {
|
|
||||||
if (show) {
|
|
||||||
[self showChatPanel:NO];
|
|
||||||
[[KBMaiPointReporter sharedReporter]
|
|
||||||
reportPageExposureWithEventName:@"enter_keyboard_settings"
|
|
||||||
pageId:@"keyboard_settings"
|
|
||||||
extra:nil
|
|
||||||
completion:nil];
|
|
||||||
KBSettingView *settingView = self.settingView;
|
|
||||||
if (!settingView.superview) {
|
|
||||||
settingView.hidden = YES;
|
|
||||||
[self.contentView addSubview:settingView];
|
|
||||||
[settingView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.edges.equalTo(self.contentView);
|
|
||||||
}];
|
|
||||||
[settingView.backButton addTarget:self
|
|
||||||
action:@selector(onTapSettingsBack)
|
|
||||||
forControlEvents:UIControlEventTouchUpInside];
|
|
||||||
}
|
|
||||||
[self.contentView bringSubviewToFront:settingView];
|
|
||||||
// 以 keyBoardMainView 的实际宽度为准,避免首次添加时 self.view 宽度尚未计算
|
|
||||||
[self.contentView layoutIfNeeded];
|
|
||||||
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
|
|
||||||
if (w <= 0) {
|
|
||||||
w = CGRectGetWidth(self.contentView.bounds);
|
|
||||||
}
|
|
||||||
if (w <= 0) {
|
|
||||||
w = [self kb_portraitWidth];
|
|
||||||
}
|
|
||||||
settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
|
||||||
settingView.hidden = NO;
|
|
||||||
[UIView animateWithDuration:0.25
|
|
||||||
delay:0
|
|
||||||
options:UIViewAnimationOptionCurveEaseOut
|
|
||||||
animations:^{
|
|
||||||
settingView.transform = CGAffineTransformIdentity;
|
|
||||||
}
|
|
||||||
completion:nil];
|
|
||||||
} else {
|
|
||||||
KBSettingView *settingView = self.settingView;
|
|
||||||
if (!settingView.superview || settingView.hidden)
|
|
||||||
return;
|
|
||||||
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
|
|
||||||
if (w <= 0) {
|
|
||||||
w = CGRectGetWidth(self.contentView.bounds);
|
|
||||||
}
|
|
||||||
if (w <= 0) {
|
|
||||||
w = [self kb_portraitWidth];
|
|
||||||
}
|
|
||||||
[UIView animateWithDuration:0.22
|
|
||||||
delay:0
|
|
||||||
options:UIViewAnimationOptionCurveEaseIn
|
|
||||||
animations:^{
|
|
||||||
settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
|
||||||
}
|
|
||||||
completion:^(BOOL finished) {
|
|
||||||
settingView.hidden = YES;
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 显示/隐藏聊天面板(覆盖整个键盘区域)
|
/// 显示/隐藏聊天面板(覆盖整个键盘区域)
|
||||||
- (void)showChatPanel:(BOOL)show {
|
- (void)showChatPanel:(BOOL)show {
|
||||||
if (show == self.chatPanelVisible) {
|
if (show == self.chatPanelVisible) {
|
||||||
@@ -575,7 +555,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
_functionView.hidden = YES;
|
_functionView.hidden = YES;
|
||||||
}
|
}
|
||||||
[self hideSubscriptionPanel];
|
[self hideSubscriptionPanel];
|
||||||
[self showSettingView:NO];
|
|
||||||
[UIView animateWithDuration:0.2
|
[UIView animateWithDuration:0.2
|
||||||
delay:0
|
delay:0
|
||||||
options:UIViewAnimationOptionCurveEaseOut
|
options:UIViewAnimationOptionCurveEaseOut
|
||||||
@@ -704,17 +683,13 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
[_subscriptionView removeFromSuperview];
|
[_subscriptionView removeFromSuperview];
|
||||||
_subscriptionView = nil;
|
_subscriptionView = nil;
|
||||||
}
|
}
|
||||||
if (_settingView) {
|
|
||||||
[_settingView removeFromSuperview];
|
|
||||||
_settingView = nil;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)showSubscriptionPanel {
|
- (void)showSubscriptionPanel {
|
||||||
// 1) 先判断权限:未开启“完全访问”则走引导逻辑
|
// 1) 先判断权限:未开启“完全访问”则走引导逻辑
|
||||||
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
||||||
// 未开启完全访问:保持原有引导路径
|
// 未开启完全访问:保持原有引导路径
|
||||||
// [KBHUD showInfo:KBLocalized(@"处理中…")];
|
// [KBHUD showInfo:KBLocalized(@"Processing...")];
|
||||||
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view];
|
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -722,11 +697,15 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
// 2) 权限没问题,再判断是否登录:未登录 -> 直接拉起主 App,由主 App
|
// 2) 权限没问题,再判断是否登录:未登录 -> 直接拉起主 App,由主 App
|
||||||
// 负责完成登录
|
// 负责完成登录
|
||||||
if (!KBAuthManager.shared.isLoggedIn) {
|
if (!KBAuthManager.shared.isLoggedIn) {
|
||||||
NSString *schemeStr =
|
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard", KB_UL_LOGIN]];
|
||||||
[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
|
NSURL *scheme =
|
||||||
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
[NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME]];
|
||||||
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
__weak typeof(self) weakSelf = self;
|
||||||
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
[KBExtensionAppLauncher openPrimaryURL:ul
|
||||||
|
fallbackURL:scheme
|
||||||
|
usingInputController:self
|
||||||
|
source:(self.view ?: (UIResponder *)weakSelf)
|
||||||
|
completion:nil];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[[KBMaiPointReporter sharedReporter]
|
[[KBMaiPointReporter sharedReporter]
|
||||||
@@ -867,16 +846,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
[self showChatPanel:NO];
|
[self showChatPanel:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView {
|
|
||||||
[[KBMaiPointReporter sharedReporter]
|
|
||||||
reportClickWithEventName:@"click_keyboard_settings_btn"
|
|
||||||
pageId:@"keyboard_main_panel"
|
|
||||||
elementId:@"settings_btn"
|
|
||||||
extra:nil
|
|
||||||
completion:nil];
|
|
||||||
[self showSettingView:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView
|
||||||
didSelectEmoji:(NSString *)emoji {
|
didSelectEmoji:(NSString *)emoji {
|
||||||
if (emoji.length == 0) {
|
if (emoji.length == 0) {
|
||||||
@@ -899,17 +868,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
[self kb_scheduleContextRefreshResetSuppression:YES];
|
[self kb_scheduleContextRefreshResetSuppression:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)keyBoardMainViewDidTapEmojiSearch:
|
|
||||||
(KBKeyBoardMainView *)keyBoardMainView {
|
|
||||||
// [[KBMaiPointReporter sharedReporter]
|
|
||||||
// reportClickWithEventName:@"click_keyboard_emoji_search_btn"
|
|
||||||
// pageId:@"keyboard_main_panel"
|
|
||||||
// elementId:@"emoji_search_btn"
|
|
||||||
// extra:nil
|
|
||||||
// completion:nil];
|
|
||||||
[KBHUD showInfo:KBLocalized(@"Search coming soon")];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView
|
||||||
didSelectSuggestion:(NSString *)suggestion {
|
didSelectSuggestion:(NSString *)suggestion {
|
||||||
if (suggestion.length == 0) {
|
if (suggestion.length == 0) {
|
||||||
@@ -955,11 +913,15 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
extra:@{@"action" : @"login_or_recharge"}
|
extra:@{@"action" : @"login_or_recharge"}
|
||||||
completion:nil];
|
completion:nil];
|
||||||
if (!KBAuthManager.shared.isLoggedIn) {
|
if (!KBAuthManager.shared.isLoggedIn) {
|
||||||
NSString *schemeStr =
|
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard", KB_UL_LOGIN]];
|
||||||
[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
|
NSURL *scheme =
|
||||||
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
[NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME]];
|
||||||
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
__weak typeof(self) weakSelf = self;
|
||||||
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
[KBExtensionAppLauncher openPrimaryURL:ul
|
||||||
|
fallbackURL:scheme
|
||||||
|
usingInputController:self
|
||||||
|
source:(self.view ?: (UIResponder *)weakSelf)
|
||||||
|
completion:nil];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NSString *schemeStr =
|
NSString *schemeStr =
|
||||||
@@ -969,13 +931,20 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
// if (!ul && !scheme) { return; }
|
// if (!ul && !scheme) { return; }
|
||||||
//
|
//
|
||||||
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
||||||
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
__weak typeof(self) weakSelf = self;
|
||||||
|
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard", KB_UL_RECHARGE]];
|
||||||
if (!ok) {
|
[KBExtensionAppLauncher openPrimaryURL:ul
|
||||||
// 失败兜底:给个文案提示
|
fallbackURL:scheme
|
||||||
// 比如:请回到桌面手动打开 XXX App 进行设置/充值
|
usingInputController:self
|
||||||
[KBHUD showInfo:@"请回到桌面手动打开App进行充值"];
|
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 {
|
- (void)functionViewDidRequestSubscription:(KBFunctionView *)functionView {
|
||||||
@@ -1050,7 +1019,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
[rawText stringByTrimmingCharactersInSet:
|
[rawText stringByTrimmingCharactersInSet:
|
||||||
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
if (trim.length == 0) {
|
if (trim.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"请输入内容")];
|
[KBHUD showInfo:KBLocalized(@"Please enter content")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[self kb_sendChatText:trim];
|
[self kb_sendChatText:trim];
|
||||||
@@ -1070,7 +1039,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
[self kb_prefetchAvatarForMessage:outgoing];
|
[self kb_prefetchAvatarForMessage:outgoing];
|
||||||
|
|
||||||
if (![[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view]) {
|
if (![[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view]) {
|
||||||
[KBHUD showInfo:KBLocalized(@"请开启完全访问后使用")];
|
[KBHUD showInfo:KBLocalized(@"Please enable Full Access to continue")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1199,12 +1168,12 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
if (mockPath.length > 0) {
|
if (mockPath.length > 0) {
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.35 * NSEC_PER_SEC)),
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.35 * NSEC_PER_SEC)),
|
||||||
dispatch_get_main_queue(), ^{
|
dispatch_get_main_queue(), ^{
|
||||||
NSString *displayText = KBLocalized(@"语音回复");
|
NSString *displayText = KBLocalized(@"Voice reply");
|
||||||
KBChatMessage *incoming =
|
KBChatMessage *incoming =
|
||||||
[KBChatMessage messageWithText:displayText
|
[KBChatMessage messageWithText:displayText
|
||||||
outgoing:NO
|
outgoing:NO
|
||||||
audioFilePath:mockPath];
|
audioFilePath:mockPath];
|
||||||
incoming.displayName = KBLocalized(@"AI助手");
|
incoming.displayName = KBLocalized(@"AI Assistant");
|
||||||
[self kb_appendChatMessage:incoming];
|
[self kb_appendChatMessage:incoming];
|
||||||
[self kb_playChatAudioAtPath:mockPath];
|
[self kb_playChatAudioAtPath:mockPath];
|
||||||
});
|
});
|
||||||
@@ -1224,7 +1193,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
NSString *tip = error.localizedDescription
|
NSString *tip = error.localizedDescription
|
||||||
?: KBLocalized(@"请求失败");
|
?: KBLocalized(@"Request failed");
|
||||||
[KBHUD showInfo:tip];
|
[KBHUD showInfo:tip];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1244,7 +1213,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
initWithBase64EncodedString:audioBase64
|
initWithBase64EncodedString:audioBase64
|
||||||
options:0];
|
options:0];
|
||||||
if (data.length == 0) {
|
if (data.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"音频数据解析失败")];
|
[KBHUD showInfo:KBLocalized(@"Failed to parse audio data")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[self kb_handleChatAudioData:data
|
[self kb_handleChatAudioData:data
|
||||||
@@ -1252,7 +1221,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
displayText:displayText];
|
displayText:displayText];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[KBHUD showInfo:KBLocalized(@"未获取到音频文件")];
|
[KBHUD showInfo:KBLocalized(@"No audio file received")];
|
||||||
});
|
});
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
@@ -1285,7 +1254,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
}
|
}
|
||||||
NSLog(@"[KB] ❌ 请求失败: %@", response.message);
|
NSLog(@"[KB] ❌ 请求失败: %@", response.message);
|
||||||
[self.chatPanelView kb_removeLoadingAssistantMessage];
|
[self.chatPanelView kb_removeLoadingAssistantMessage];
|
||||||
[KBHUD showInfo:response.message ?: KBLocalized(@"请求失败")];
|
[KBHUD showInfo:response.message ?: KBLocalized(@"Request failed")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1293,7 +1262,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
|
|
||||||
if (response.data.aiResponse.length == 0) {
|
if (response.data.aiResponse.length == 0) {
|
||||||
[self.chatPanelView kb_removeLoadingAssistantMessage];
|
[self.chatPanelView kb_removeLoadingAssistantMessage];
|
||||||
[KBHUD showInfo:KBLocalized(@"未获取到回复内容")];
|
[KBHUD showInfo:KBLocalized(@"No reply content received")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1361,12 +1330,12 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
if (!self) return;
|
if (!self) return;
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
[KBHUD showInfo:response.errorMessage ?: KBLocalized(@"下载失败")];
|
[KBHUD showInfo:response.errorMessage ?: KBLocalized(@"Download failed")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.audioData || response.audioData.length == 0) {
|
if (!response.audioData || response.audioData.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"未获取到音频数据")];
|
[KBHUD showInfo:KBLocalized(@"No audio data received")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1385,7 +1354,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
fileExtension:(NSString *)extension
|
fileExtension:(NSString *)extension
|
||||||
displayText:(NSString *)displayText {
|
displayText:(NSString *)displayText {
|
||||||
if (data.length == 0) {
|
if (data.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"音频数据为空")];
|
[KBHUD showInfo:KBLocalized(@"Audio data is empty")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NSString *ext = extension.length > 0 ? extension : @"m4a";
|
NSString *ext = extension.length > 0 ? extension : @"m4a";
|
||||||
@@ -1397,15 +1366,15 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
NSString *filePath =
|
NSString *filePath =
|
||||||
[NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
|
[NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
|
||||||
if (![data writeToFile:filePath atomically:YES]) {
|
if (![data writeToFile:filePath atomically:YES]) {
|
||||||
[KBHUD showInfo:KBLocalized(@"音频保存失败")];
|
[KBHUD showInfo:KBLocalized(@"Failed to save audio")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NSString *text = displayText.length > 0 ? displayText : KBLocalized(@"语音消息");
|
NSString *text = displayText.length > 0 ? displayText : KBLocalized(@"Voice message");
|
||||||
KBChatMessage *incoming =
|
KBChatMessage *incoming =
|
||||||
[KBChatMessage messageWithText:text
|
[KBChatMessage messageWithText:text
|
||||||
outgoing:NO
|
outgoing:NO
|
||||||
audioFilePath:filePath];
|
audioFilePath:filePath];
|
||||||
incoming.displayName = KBLocalized(@"AI助手");
|
incoming.displayName = KBLocalized(@"AI Assistant");
|
||||||
[self kb_appendChatMessage:incoming];
|
[self kb_appendChatMessage:incoming];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1507,7 +1476,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
}
|
}
|
||||||
NSURL *url = [NSURL fileURLWithPath:path];
|
NSURL *url = [NSURL fileURLWithPath:path];
|
||||||
if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
|
if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
|
||||||
[KBHUD showInfo:KBLocalized(@"音频文件不存在")];
|
[KBHUD showInfo:KBLocalized(@"Audio file does not exist")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1537,7 +1506,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
AVAudioPlayer *player =
|
AVAudioPlayer *player =
|
||||||
[[AVAudioPlayer alloc] initWithContentsOfURL:url error:&playerError];
|
[[AVAudioPlayer alloc] initWithContentsOfURL:url error:&playerError];
|
||||||
if (playerError || !player) {
|
if (playerError || !player) {
|
||||||
[KBHUD showInfo:KBLocalized(@"音频播放失败")];
|
[KBHUD showInfo:KBLocalized(@"Audio playback failed")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.chatAudioPlayer = player;
|
self.chatAudioPlayer = player;
|
||||||
@@ -1575,7 +1544,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithData:audioData error:&playerError];
|
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithData:audioData error:&playerError];
|
||||||
if (playerError || !player) {
|
if (playerError || !player) {
|
||||||
NSLog(@"[Keyboard] 音频播放器初始化失败: %@", playerError.localizedDescription);
|
NSLog(@"[Keyboard] 音频播放器初始化失败: %@", playerError.localizedDescription);
|
||||||
[KBHUD showInfo:KBLocalized(@"音频播放失败")];
|
[KBHUD showInfo:KBLocalized(@"Audio playback failed")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1616,6 +1585,31 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
[self kb_openRechargeForProduct:product];
|
[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 - KBChatLimitPopViewDelegate
|
#pragma mark - KBChatLimitPopViewDelegate
|
||||||
|
|
||||||
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view {
|
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view {
|
||||||
@@ -1628,11 +1622,20 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
[NSString stringWithFormat:@"%@://recharge?src=keyboard&vipType=svip",
|
[NSString stringWithFormat:@"%@://recharge?src=keyboard&vipType=svip",
|
||||||
KB_APP_SCHEME];
|
KB_APP_SCHEME];
|
||||||
NSURL *scheme = [NSURL URLWithString:urlString];
|
NSURL *scheme = [NSURL URLWithString:urlString];
|
||||||
BOOL success = [KBHostAppLauncher openHostAppURL:scheme
|
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard&vipType=svip", KB_UL_RECHARGE]];
|
||||||
fromResponder:self.view];
|
__weak typeof(self) weakSelf = self;
|
||||||
if (!success) {
|
[KBExtensionAppLauncher openPrimaryURL:ul
|
||||||
[KBHUD showInfo:KBLocalized(@"Please open the App to finish purchase")];
|
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")];
|
||||||
|
});
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - lazy
|
#pragma mark - lazy
|
||||||
@@ -1652,13 +1655,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
return _functionView;
|
return _functionView;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (KBSettingView *)settingView {
|
|
||||||
if (!_settingView) {
|
|
||||||
_settingView = [[KBSettingView alloc] init];
|
|
||||||
}
|
|
||||||
return _settingView;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (KBChatPanelView *)chatPanelView {
|
- (KBChatPanelView *)chatPanelView {
|
||||||
if (!_chatPanelView) {
|
if (!_chatPanelView) {
|
||||||
NSLog(@"[Keyboard] ⚠️ chatPanelView 被创建!");
|
NSLog(@"[Keyboard] ⚠️ chatPanelView 被创建!");
|
||||||
@@ -1709,11 +1705,21 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
NSString *urlString = [NSString
|
NSString *urlString = [NSString
|
||||||
stringWithFormat:@"%@://recharge?src=keyboard&%@", KB_APP_SCHEME, query];
|
stringWithFormat:@"%@://recharge?src=keyboard&%@", KB_APP_SCHEME, query];
|
||||||
NSURL *scheme = [NSURL URLWithString:urlString];
|
NSURL *scheme = [NSURL URLWithString:urlString];
|
||||||
BOOL success = [KBHostAppLauncher openHostAppURL:scheme
|
NSString *ulString = [NSString stringWithFormat:@"%@?src=keyboard&%@", KB_UL_RECHARGE, query];
|
||||||
fromResponder:self.view];
|
NSURL *ul = [NSURL URLWithString:ulString];
|
||||||
if (!success) {
|
__weak typeof(self) weakSelf = self;
|
||||||
[KBHUD showInfo:KBLocalized(@"Please open the App to finish purchase")];
|
[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 {
|
+ (NSString *)kb_urlEncodedString:(NSString *)value {
|
||||||
@@ -1728,16 +1734,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
?: @"";
|
?: @"";
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onTapSettingsBack {
|
|
||||||
[[KBMaiPointReporter sharedReporter]
|
|
||||||
reportClickWithEventName:@"click_keyboard_settings_back_btn"
|
|
||||||
pageId:@"keyboard_settings"
|
|
||||||
elementId:@"back_btn"
|
|
||||||
extra:nil
|
|
||||||
completion:nil];
|
|
||||||
[self showSettingView:NO];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)dealloc {
|
- (void)dealloc {
|
||||||
if (self.kb_fullAccessObserverToken) {
|
if (self.kb_fullAccessObserverToken) {
|
||||||
[[NSNotificationCenter defaultCenter]
|
[[NSNotificationCenter defaultCenter]
|
||||||
@@ -2084,15 +2080,14 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
NSLog(@"[Keyboard] skin request failed: %@",
|
NSLog(@"[Keyboard] skin request failed: %@",
|
||||||
error);
|
error);
|
||||||
[KBHUD
|
[KBHUD
|
||||||
showInfo:
|
showInfo:KBLocalized(
|
||||||
KBLocalized(
|
@"Theme resource preparation failed, please try again later")];
|
||||||
@"皮肤资源准备失败,请稍后再试")];
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[weakSelf kb_applyTheme];
|
[weakSelf kb_applyTheme];
|
||||||
[KBHUD showInfo:KBLocalized(
|
[KBHUD showInfo:KBLocalized(
|
||||||
@"皮肤已更新,立即体验吧")];
|
@"Theme updated, try it now")];
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,11 @@
|
|||||||
#import "KBChatPanelView.h"
|
#import "KBChatPanelView.h"
|
||||||
#import "KBFunctionView.h"
|
#import "KBFunctionView.h"
|
||||||
#import "KBFullAccessManager.h"
|
#import "KBFullAccessManager.h"
|
||||||
#import "KBHostAppLauncher.h"
|
#import "../Utils/KBExtensionAppLauncher.h"
|
||||||
#import "KBInputBufferManager.h"
|
#import "KBInputBufferManager.h"
|
||||||
#import "KBKey.h"
|
#import "KBKey.h"
|
||||||
#import "KBKeyBoardMainView.h"
|
#import "KBKeyBoardMainView.h"
|
||||||
#import "KBKeyboardSubscriptionView.h"
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
#import "KBSettingView.h"
|
|
||||||
#import "Masonry.h"
|
#import "Masonry.h"
|
||||||
#import <SDWebImage/SDWebImage.h>
|
#import <SDWebImage/SDWebImage.h>
|
||||||
#import <AVFoundation/AVAudioPlayer.h>
|
#import <AVFoundation/AVAudioPlayer.h>
|
||||||
@@ -54,14 +53,23 @@
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (mode == KBKeyboardPanelModeFunction && !islogin) {
|
if (mode == KBKeyboardPanelModeFunction && !islogin) {
|
||||||
[KBHUD showInfo:KBLocalized(@"请先登录后使用AI功能")];
|
[KBHUD showInfo:KBLocalized(@"Please sign in before using AI features")];
|
||||||
NSString *schemeStr =
|
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard", KB_UL_LOGIN]];
|
||||||
[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
|
NSURL *scheme =
|
||||||
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
[NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME]];
|
||||||
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
__weak typeof(self) weakSelf = self;
|
||||||
if (!ok) {
|
[KBExtensionAppLauncher openPrimaryURL:ul
|
||||||
[KBHUD showInfo:KBLocalized(@"请回到桌面手动打开App登录")];
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +80,6 @@
|
|||||||
|
|
||||||
// 1) 先收起所有面板(再展开目标面板),避免互相调用导致漏关/层级错乱
|
// 1) 先收起所有面板(再展开目标面板),避免互相调用导致漏关/层级错乱
|
||||||
[self kb_setSubscriptionPanelVisible:NO animated:animated];
|
[self kb_setSubscriptionPanelVisible:NO animated:animated];
|
||||||
[self kb_setSettingViewVisible:NO animated:animated];
|
|
||||||
[self kb_setChatPanelVisible:NO animated:animated];
|
[self kb_setChatPanelVisible:NO animated:animated];
|
||||||
[self kb_setFunctionPanelVisible:NO];
|
[self kb_setFunctionPanelVisible:NO];
|
||||||
|
|
||||||
@@ -84,9 +91,6 @@
|
|||||||
case KBKeyboardPanelModeChat:
|
case KBKeyboardPanelModeChat:
|
||||||
[self kb_setChatPanelVisible:YES animated:animated];
|
[self kb_setChatPanelVisible:YES animated:animated];
|
||||||
break;
|
break;
|
||||||
case KBKeyboardPanelModeSettings:
|
|
||||||
[self kb_setSettingViewVisible:YES animated:animated];
|
|
||||||
break;
|
|
||||||
case KBKeyboardPanelModeSubscription:
|
case KBKeyboardPanelModeSubscription:
|
||||||
[self kb_setSubscriptionPanelVisible:YES animated:animated];
|
[self kb_setSubscriptionPanelVisible:YES animated:animated];
|
||||||
break;
|
break;
|
||||||
@@ -109,12 +113,6 @@
|
|||||||
pageId:@"keyboard_main_panel"
|
pageId:@"keyboard_main_panel"
|
||||||
extra:nil
|
extra:nil
|
||||||
completion:nil];
|
completion:nil];
|
||||||
} else if (mode == KBKeyboardPanelModeSettings) {
|
|
||||||
[[KBMaiPointReporter sharedReporter]
|
|
||||||
reportPageExposureWithEventName:@"enter_keyboard_settings"
|
|
||||||
pageId:@"keyboard_settings"
|
|
||||||
extra:nil
|
|
||||||
completion:nil];
|
|
||||||
} else if (mode == KBKeyboardPanelModeSubscription) {
|
} else if (mode == KBKeyboardPanelModeSubscription) {
|
||||||
[[KBMaiPointReporter sharedReporter]
|
[[KBMaiPointReporter sharedReporter]
|
||||||
reportPageExposureWithEventName:@"enter_keyboard_subscription_panel"
|
reportPageExposureWithEventName:@"enter_keyboard_subscription_panel"
|
||||||
@@ -126,8 +124,6 @@
|
|||||||
// 4) 层级:保证当前面板在最上层
|
// 4) 层级:保证当前面板在最上层
|
||||||
if (mode == KBKeyboardPanelModeSubscription) {
|
if (mode == KBKeyboardPanelModeSubscription) {
|
||||||
[self.contentView bringSubviewToFront:self.subscriptionView];
|
[self.contentView bringSubviewToFront:self.subscriptionView];
|
||||||
} else if (mode == KBKeyboardPanelModeSettings) {
|
|
||||||
[self.contentView bringSubviewToFront:self.settingView];
|
|
||||||
} else if (mode == KBKeyboardPanelModeChat) {
|
} else if (mode == KBKeyboardPanelModeChat) {
|
||||||
[self.contentView bringSubviewToFront:self.chatPanelView];
|
[self.contentView bringSubviewToFront:self.chatPanelView];
|
||||||
} else if (mode == KBKeyboardPanelModeFunction) {
|
} else if (mode == KBKeyboardPanelModeFunction) {
|
||||||
@@ -148,17 +144,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 对外兼容:显示/隐藏设置页(高度与 keyBoardMainView 一致),右侧滑入/滑出
|
|
||||||
- (void)showSettingView:(BOOL)show {
|
|
||||||
if (show) {
|
|
||||||
[self kb_setPanelMode:KBKeyboardPanelModeSettings animated:YES];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (self.kb_panelMode == KBKeyboardPanelModeSettings) {
|
|
||||||
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 对外兼容:显示/隐藏聊天面板(覆盖整个键盘区域)
|
/// 对外兼容:显示/隐藏聊天面板(覆盖整个键盘区域)
|
||||||
- (void)showChatPanel:(BOOL)show {
|
- (void)showChatPanel:(BOOL)show {
|
||||||
if (show) {
|
if (show) {
|
||||||
@@ -232,74 +217,6 @@
|
|||||||
[self kb_updateKeyboardLayoutIfNeeded];
|
[self kb_updateKeyboardLayoutIfNeeded];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)kb_setSettingViewVisible:(BOOL)visible animated:(BOOL)animated {
|
|
||||||
if (visible) {
|
|
||||||
KBSettingView *settingView = self.settingView;
|
|
||||||
if (!settingView.superview) {
|
|
||||||
settingView.hidden = YES;
|
|
||||||
[self.contentView addSubview:settingView];
|
|
||||||
[settingView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.edges.equalTo(self.contentView);
|
|
||||||
}];
|
|
||||||
[settingView.backButton addTarget:self
|
|
||||||
action:@selector(onTapSettingsBack)
|
|
||||||
forControlEvents:UIControlEventTouchUpInside];
|
|
||||||
}
|
|
||||||
[self.contentView bringSubviewToFront:settingView];
|
|
||||||
// 以 keyBoardMainView 的实际宽度为准,避免首次添加时 self.view 宽度尚未计算
|
|
||||||
[self.contentView layoutIfNeeded];
|
|
||||||
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
|
|
||||||
if (w <= 0) {
|
|
||||||
w = CGRectGetWidth(self.contentView.bounds);
|
|
||||||
}
|
|
||||||
if (w <= 0) {
|
|
||||||
w = [self kb_portraitWidth];
|
|
||||||
}
|
|
||||||
settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
|
||||||
settingView.hidden = NO;
|
|
||||||
if (animated) {
|
|
||||||
[UIView animateWithDuration:0.25
|
|
||||||
delay:0
|
|
||||||
options:UIViewAnimationOptionCurveEaseOut
|
|
||||||
animations:^{
|
|
||||||
settingView.transform = CGAffineTransformIdentity;
|
|
||||||
}
|
|
||||||
completion:nil];
|
|
||||||
} else {
|
|
||||||
settingView.transform = CGAffineTransformIdentity;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
KBSettingView *settingView = _settingView;
|
|
||||||
if (!settingView) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!settingView.superview || settingView.hidden) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
|
|
||||||
if (w <= 0) {
|
|
||||||
w = CGRectGetWidth(self.contentView.bounds);
|
|
||||||
}
|
|
||||||
if (w <= 0) {
|
|
||||||
w = [self kb_portraitWidth];
|
|
||||||
}
|
|
||||||
if (animated) {
|
|
||||||
[UIView animateWithDuration:0.22
|
|
||||||
delay:0
|
|
||||||
options:UIViewAnimationOptionCurveEaseIn
|
|
||||||
animations:^{
|
|
||||||
settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
|
||||||
}
|
|
||||||
completion:^(BOOL finished) {
|
|
||||||
settingView.hidden = YES;
|
|
||||||
}];
|
|
||||||
} else {
|
|
||||||
settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
|
||||||
settingView.hidden = YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)kb_setSubscriptionPanelVisible:(BOOL)visible animated:(BOOL)animated {
|
- (void)kb_setSubscriptionPanelVisible:(BOOL)visible animated:(BOOL)animated {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
KBKeyboardSubscriptionView *panel = self.subscriptionView;
|
KBKeyboardSubscriptionView *panel = self.subscriptionView;
|
||||||
@@ -469,10 +386,6 @@
|
|||||||
[_subscriptionView removeFromSuperview];
|
[_subscriptionView removeFromSuperview];
|
||||||
_subscriptionView = nil;
|
_subscriptionView = nil;
|
||||||
}
|
}
|
||||||
if (_settingView) {
|
|
||||||
[_settingView removeFromSuperview];
|
|
||||||
_settingView = nil;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - KBKeyBoardMainViewDelegate
|
// MARK: - KBKeyBoardMainViewDelegate
|
||||||
@@ -555,16 +468,6 @@
|
|||||||
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
|
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView {
|
|
||||||
[[KBMaiPointReporter sharedReporter]
|
|
||||||
reportClickWithEventName:@"click_keyboard_settings_btn"
|
|
||||||
pageId:@"keyboard_main_panel"
|
|
||||||
elementId:@"settings_btn"
|
|
||||||
extra:nil
|
|
||||||
completion:nil];
|
|
||||||
[self kb_setPanelMode:KBKeyboardPanelModeSettings animated:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView
|
||||||
didSelectEmoji:(NSString *)emoji {
|
didSelectEmoji:(NSString *)emoji {
|
||||||
if (emoji.length == 0) {
|
if (emoji.length == 0) {
|
||||||
@@ -587,17 +490,6 @@
|
|||||||
[self kb_scheduleContextRefreshResetSuppression:YES];
|
[self kb_scheduleContextRefreshResetSuppression:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)keyBoardMainViewDidTapEmojiSearch:
|
|
||||||
(KBKeyBoardMainView *)keyBoardMainView {
|
|
||||||
// [[KBMaiPointReporter sharedReporter]
|
|
||||||
// reportClickWithEventName:@"click_keyboard_emoji_search_btn"
|
|
||||||
// pageId:@"keyboard_main_panel"
|
|
||||||
// elementId:@"emoji_search_btn"
|
|
||||||
// extra:nil
|
|
||||||
// completion:nil];
|
|
||||||
[KBHUD showInfo:KBLocalized(@"Search coming soon")];
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - KBFunctionViewDelegate
|
// MARK: - KBFunctionViewDelegate
|
||||||
|
|
||||||
- (void)functionView:(KBFunctionView *)functionView
|
- (void)functionView:(KBFunctionView *)functionView
|
||||||
@@ -617,40 +509,37 @@
|
|||||||
extra:@{@"action" : @"login_or_recharge"}
|
extra:@{@"action" : @"login_or_recharge"}
|
||||||
completion:nil];
|
completion:nil];
|
||||||
if (!KBAuthManager.shared.isLoggedIn) {
|
if (!KBAuthManager.shared.isLoggedIn) {
|
||||||
NSString *schemeStr =
|
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard", KB_UL_LOGIN]];
|
||||||
[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
|
NSURL *scheme =
|
||||||
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
[NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME]];
|
||||||
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
__weak typeof(self) weakSelf = self;
|
||||||
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
[KBExtensionAppLauncher openPrimaryURL:ul
|
||||||
|
fallbackURL:scheme
|
||||||
|
usingInputController:self
|
||||||
|
source:(self.view ?: (UIResponder *)weakSelf)
|
||||||
|
completion:nil];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NSString *schemeStr =
|
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard", KB_UL_RECHARGE]];
|
||||||
[NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME];
|
NSURL *scheme =
|
||||||
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
[NSURL URLWithString:[NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME]];
|
||||||
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
__weak typeof(self) weakSelf = self;
|
||||||
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
[KBExtensionAppLauncher openPrimaryURL:ul
|
||||||
|
fallbackURL:scheme
|
||||||
if (!ok) {
|
usingInputController:self
|
||||||
// 失败兜底:给个文案提示
|
source:(self.view ?: (UIResponder *)weakSelf)
|
||||||
// 比如:请回到桌面手动打开 XXX App 进行设置/充值
|
completion:^(BOOL success) {
|
||||||
[KBHUD showInfo:@"请回到桌面手动打开App进行充值"];
|
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 {
|
- (void)functionViewDidRequestSubscription:(KBFunctionView *)functionView {
|
||||||
[self showSubscriptionPanel];
|
[self showSubscriptionPanel];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Actions
|
|
||||||
|
|
||||||
- (void)onTapSettingsBack {
|
|
||||||
[[KBMaiPointReporter sharedReporter]
|
|
||||||
reportClickWithEventName:@"click_keyboard_settings_back_btn"
|
|
||||||
pageId:@"keyboard_settings"
|
|
||||||
elementId:@"back_btn"
|
|
||||||
extra:nil
|
|
||||||
completion:nil];
|
|
||||||
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
@class KBFunctionView;
|
@class KBFunctionView;
|
||||||
@class KBKeyBoardMainView;
|
@class KBKeyBoardMainView;
|
||||||
@class KBKeyboardSubscriptionView;
|
@class KBKeyboardSubscriptionView;
|
||||||
@class KBSettingView;
|
|
||||||
@class KBSuggestionEngine;
|
@class KBSuggestionEngine;
|
||||||
|
|
||||||
@protocol KBChatLimitPopViewDelegate;
|
@protocol KBChatLimitPopViewDelegate;
|
||||||
@@ -28,7 +27,6 @@ typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
|
|||||||
KBKeyboardPanelModeMain = 0,
|
KBKeyboardPanelModeMain = 0,
|
||||||
KBKeyboardPanelModeFunction,
|
KBKeyboardPanelModeFunction,
|
||||||
KBKeyboardPanelModeChat,
|
KBKeyboardPanelModeChat,
|
||||||
KBKeyboardPanelModeSettings,
|
|
||||||
KBKeyboardPanelModeSubscription,
|
KBKeyboardPanelModeSubscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,7 +40,6 @@ typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
|
|||||||
UIView *_contentView;
|
UIView *_contentView;
|
||||||
KBKeyBoardMainView *_keyBoardMainView;
|
KBKeyBoardMainView *_keyBoardMainView;
|
||||||
KBFunctionView *_functionView;
|
KBFunctionView *_functionView;
|
||||||
KBSettingView *_settingView;
|
|
||||||
UIImageView *_bgImageView;
|
UIImageView *_bgImageView;
|
||||||
KBChatPanelView *_chatPanelView;
|
KBChatPanelView *_chatPanelView;
|
||||||
KBKeyboardSubscriptionView *_subscriptionView;
|
KBKeyboardSubscriptionView *_subscriptionView;
|
||||||
@@ -68,6 +65,7 @@ typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
|
|||||||
NSString *_chatPanelBaselineText;
|
NSString *_chatPanelBaselineText;
|
||||||
id _kb_fullAccessObserverToken;
|
id _kb_fullAccessObserverToken;
|
||||||
id _kb_skinObserverToken;
|
id _kb_skinObserverToken;
|
||||||
|
id _kb_localizationObserverToken;
|
||||||
KBKeyboardPanelMode _kb_panelMode;
|
KBKeyboardPanelMode _kb_panelMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +76,6 @@ typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
|
|||||||
*keyBoardMainView; // 功能面板视图(点击工具栏第0个时显示)
|
*keyBoardMainView; // 功能面板视图(点击工具栏第0个时显示)
|
||||||
@property(nonatomic, strong)
|
@property(nonatomic, strong)
|
||||||
KBFunctionView *functionView; // 功能面板视图(点击工具栏第0个时显示)
|
KBFunctionView *functionView; // 功能面板视图(点击工具栏第0个时显示)
|
||||||
@property(nonatomic, strong) KBSettingView *settingView; // 设置页
|
|
||||||
@property(nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
|
@property(nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
|
||||||
@property(nonatomic, strong) KBChatPanelView *chatPanelView;
|
@property(nonatomic, strong) KBChatPanelView *chatPanelView;
|
||||||
@property(nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
|
@property(nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
|
||||||
@@ -104,7 +101,9 @@ typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
|
|||||||
@property(nonatomic, copy) NSString *chatPanelBaselineText; // 打开聊天面板时宿主输入框已有的文本
|
@property(nonatomic, copy) NSString *chatPanelBaselineText; // 打开聊天面板时宿主输入框已有的文本
|
||||||
@property(nonatomic, strong, nullable) id kb_fullAccessObserverToken;
|
@property(nonatomic, strong, nullable) id kb_fullAccessObserverToken;
|
||||||
@property(nonatomic, strong, nullable) id kb_skinObserverToken;
|
@property(nonatomic, strong, nullable) id kb_skinObserverToken;
|
||||||
|
@property(nonatomic, strong, nullable) id kb_localizationObserverToken;
|
||||||
@property(nonatomic, assign) KBKeyboardPanelMode kb_panelMode;
|
@property(nonatomic, assign) KBKeyboardPanelMode kb_panelMode;
|
||||||
|
@property(nonatomic, strong, nullable) id kb_appGroupObserverToken;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -116,7 +115,6 @@ typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
|
|||||||
|
|
||||||
// Panels
|
// Panels
|
||||||
- (void)showFunctionPanel:(BOOL)show;
|
- (void)showFunctionPanel:(BOOL)show;
|
||||||
- (void)showSettingView:(BOOL)show;
|
|
||||||
- (void)showChatPanel:(BOOL)show;
|
- (void)showChatPanel:(BOOL)show;
|
||||||
- (void)showSubscriptionPanel;
|
- (void)showSubscriptionPanel;
|
||||||
- (void)hideSubscriptionPanel;
|
- (void)hideSubscriptionPanel;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
#import "KBAuthManager.h"
|
#import "KBAuthManager.h"
|
||||||
#import "KBFullAccessManager.h"
|
#import "KBFullAccessManager.h"
|
||||||
#import "KBHostAppLauncher.h"
|
#import "../Utils/KBExtensionAppLauncher.h"
|
||||||
#import "KBKeyboardSubscriptionProduct.h"
|
#import "KBKeyboardSubscriptionProduct.h"
|
||||||
#import "KBKeyboardSubscriptionView.h"
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
|
|
||||||
@@ -19,18 +19,22 @@
|
|||||||
// 1) 先判断权限:未开启“完全访问”则走引导逻辑
|
// 1) 先判断权限:未开启“完全访问”则走引导逻辑
|
||||||
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
||||||
// 未开启完全访问:保持原有引导路径
|
// 未开启完全访问:保持原有引导路径
|
||||||
// [KBHUD showInfo:KBLocalized(@"处理中…")];
|
// [KBHUD showInfo:KBLocalized(@"Processing...")];
|
||||||
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view];
|
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 点击充值要先判断是否登录
|
// 点击充值要先判断是否登录
|
||||||
// 2) 权限没问题,再判断是否登录:未登录 -> 直接拉起主 App,由主 App 负责完成登录
|
// 2) 权限没问题,再判断是否登录:未登录 -> 直接拉起主 App,由主 App 负责完成登录
|
||||||
if (!KBAuthManager.shared.isLoggedIn) {
|
if (!KBAuthManager.shared.isLoggedIn) {
|
||||||
NSString *schemeStr =
|
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard", KB_UL_LOGIN]];
|
||||||
[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
|
NSURL *scheme =
|
||||||
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
[NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME]];
|
||||||
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
__weak typeof(self) weakSelf = self;
|
||||||
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
[KBExtensionAppLauncher openPrimaryURL:ul
|
||||||
|
fallbackURL:scheme
|
||||||
|
usingInputController:self
|
||||||
|
source:(self.view ?: (UIResponder *)weakSelf)
|
||||||
|
completion:nil];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[self kb_setPanelMode:KBKeyboardPanelModeSubscription animated:YES];
|
[self kb_setPanelMode:KBKeyboardPanelModeSubscription animated:YES];
|
||||||
@@ -72,6 +76,31 @@
|
|||||||
[self kb_openRechargeForProduct:product];
|
[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
|
#pragma mark - Actions
|
||||||
|
|
||||||
- (void)kb_openRechargeForProduct:(KBKeyboardSubscriptionProduct *)product {
|
- (void)kb_openRechargeForProduct:(KBKeyboardSubscriptionProduct *)product {
|
||||||
@@ -96,10 +125,21 @@
|
|||||||
NSString *urlString = [NSString
|
NSString *urlString = [NSString
|
||||||
stringWithFormat:@"%@://recharge?src=keyboard&%@", KB_APP_SCHEME, query];
|
stringWithFormat:@"%@://recharge?src=keyboard&%@", KB_APP_SCHEME, query];
|
||||||
NSURL *scheme = [NSURL URLWithString:urlString];
|
NSURL *scheme = [NSURL URLWithString:urlString];
|
||||||
BOOL success = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
NSString *ulString = [NSString stringWithFormat:@"%@?src=keyboard&%@", KB_UL_RECHARGE, query];
|
||||||
if (!success) {
|
NSURL *ul = [NSURL URLWithString:ulString];
|
||||||
[KBHUD showInfo:KBLocalized(@"Please open the App to finish purchase")];
|
__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 {
|
+ (NSString *)kb_urlEncodedString:(NSString *)value {
|
||||||
|
|||||||
@@ -58,13 +58,7 @@
|
|||||||
if (context.length == 0) {
|
if (context.length == 0) {
|
||||||
return @"";
|
return @"";
|
||||||
}
|
}
|
||||||
static NSCharacterSet *letters = nil;
|
NSCharacterSet *letters = [self kb_allowedSuggestionCharacterSet];
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
letters = [NSCharacterSet
|
|
||||||
characterSetWithCharactersInString:
|
|
||||||
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
|
|
||||||
});
|
|
||||||
|
|
||||||
NSInteger idx = (NSInteger)context.length - 1;
|
NSInteger idx = (NSInteger)context.length - 1;
|
||||||
while (idx >= 0) {
|
while (idx >= 0) {
|
||||||
@@ -85,13 +79,7 @@
|
|||||||
if (text.length == 0) {
|
if (text.length == 0) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
static NSCharacterSet *letters = nil;
|
NSCharacterSet *letters = [self kb_allowedSuggestionCharacterSet];
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
letters = [NSCharacterSet
|
|
||||||
characterSetWithCharactersInString:
|
|
||||||
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
|
|
||||||
});
|
|
||||||
for (NSUInteger i = 0; i < text.length; i++) {
|
for (NSUInteger i = 0; i < text.length; i++) {
|
||||||
if (![letters characterIsMember:[text characterAtIndex:i]]) {
|
if (![letters characterIsMember:[text characterAtIndex:i]]) {
|
||||||
return NO;
|
return NO;
|
||||||
@@ -100,6 +88,62 @@
|
|||||||
return YES;
|
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 {
|
- (void)kb_updateSuggestionsForCurrentWord {
|
||||||
NSString *prefix = self.currentWord ?: @"";
|
NSString *prefix = self.currentWord ?: @"";
|
||||||
if (prefix.length == 0) {
|
if (prefix.length == 0) {
|
||||||
@@ -175,4 +219,3 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -166,9 +166,8 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
[self kb_logSkinDiagnosticsWithTheme:t backgroundImage:img];
|
[self kb_logSkinDiagnosticsWithTheme:t backgroundImage:img];
|
||||||
self.bgImageView.image = img;
|
self.bgImageView.image = img;
|
||||||
|
|
||||||
// 触发键区按主题重绘
|
// 皮肤资源可能被“重新下载”,即使 skinId 未变也需要刷新按键图标。
|
||||||
if (themeChanged &&
|
if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
|
||||||
[self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
|
|
||||||
// method declared in KBKeyBoardMainView.h
|
// method declared in KBKeyBoardMainView.h
|
||||||
#pragma clang diagnostic push
|
#pragma clang diagnostic push
|
||||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||||
@@ -177,7 +176,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
}
|
}
|
||||||
// 注意:这里不能直接访问 self.functionView,否则会导致功能面板提前创建,占用内存。
|
// 注意:这里不能直接访问 self.functionView,否则会导致功能面板提前创建,占用内存。
|
||||||
KBFunctionView *functionView = [self kb_functionViewIfCreated];
|
KBFunctionView *functionView = [self kb_functionViewIfCreated];
|
||||||
if (themeChanged && functionView &&
|
if (functionView &&
|
||||||
[functionView respondsToSelector:@selector(kb_applyTheme)]) {
|
[functionView respondsToSelector:@selector(kb_applyTheme)]) {
|
||||||
#pragma clang diagnostic push
|
#pragma clang diagnostic push
|
||||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||||
@@ -323,15 +322,14 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
NSLog(@"[Keyboard] skin request failed: %@",
|
NSLog(@"[Keyboard] skin request failed: %@",
|
||||||
error);
|
error);
|
||||||
[KBHUD
|
[KBHUD
|
||||||
showInfo:
|
showInfo:KBLocalized(
|
||||||
KBLocalized(
|
@"Theme resource preparation failed, please try again later")];
|
||||||
@"皮肤资源准备失败,请稍后再试")];
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[weakSelf kb_applyTheme];
|
[weakSelf kb_applyTheme];
|
||||||
[KBHUD showInfo:KBLocalized(
|
[KBHUD showInfo:KBLocalized(
|
||||||
@"皮肤已更新,立即体验吧")];
|
@"Theme updated, try it now")];
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +349,6 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NSString *targetId = [self kb_defaultSkinIdForCurrentStyle];
|
NSString *targetId = [self kb_defaultSkinIdForCurrentStyle];
|
||||||
NSString *targetZip = [self kb_defaultSkinZipNameForCurrentStyle];
|
|
||||||
if (currentId.length > 0 && [currentId isEqualToString:targetId]) {
|
if (currentId.length > 0 && [currentId isEqualToString:targetId]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -360,17 +357,12 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
if ([KBSkinInstallBridge applyInstalledSkinWithId:targetId error:&applyError]) {
|
if ([KBSkinInstallBridge applyInstalledSkinWithId:targetId error:&applyError]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 默认皮肤 zip 仅由主 App 持有并解压。扩展侧不再尝试从自身 bundle 解压。
|
||||||
[KBSkinInstallBridge publishBundleSkinRequestWithId:targetId
|
// 若主 App 尚未安装对应默认皮肤,这里仅保留当前主题,避免“找不到 zip”报错。
|
||||||
name:targetId
|
if (applyError) {
|
||||||
zipName:targetZip
|
NSLog(@"[Keyboard] default skin %@ not installed in AppGroup yet: %@",
|
||||||
iconShortNames:nil];
|
targetId, applyError);
|
||||||
[KBSkinInstallBridge
|
}
|
||||||
consumePendingRequestFromBundle:NSBundle.mainBundle
|
|
||||||
completion:^(__unused BOOL success,
|
|
||||||
__unused NSError *_Nullable error) {
|
|
||||||
// 已通过通知触发主题刷新,这里无需额外处理
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
#import "KBFunctionView.h"
|
#import "KBFunctionView.h"
|
||||||
#import "KBKeyBoardMainView.h"
|
#import "KBKeyBoardMainView.h"
|
||||||
#import "KBKeyboardSubscriptionView.h"
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
#import "KBSettingView.h"
|
|
||||||
#import "Masonry.h"
|
#import "Masonry.h"
|
||||||
|
|
||||||
@implementation KeyboardViewController (UI)
|
@implementation KeyboardViewController (UI)
|
||||||
@@ -115,13 +114,6 @@
|
|||||||
return _functionView;
|
return _functionView;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (KBSettingView *)settingView {
|
|
||||||
if (!_settingView) {
|
|
||||||
_settingView = [[KBSettingView alloc] init];
|
|
||||||
}
|
|
||||||
return _settingView;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (KBChatPanelView *)chatPanelView {
|
- (KBChatPanelView *)chatPanelView {
|
||||||
if (!_chatPanelView) {
|
if (!_chatPanelView) {
|
||||||
NSLog(@"[Keyboard] ⚠️ chatPanelView 被创建!");
|
NSLog(@"[Keyboard] ⚠️ chatPanelView 被创建!");
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ FOUNDATION_EXPORT NSString * const KBEmojiRecentsDidChangeNotification;
|
|||||||
/// 更新当前语言对应的分类标题。
|
/// 更新当前语言对应的分类标题。
|
||||||
- (void)refreshLocalizedTitles;
|
- (void)refreshLocalizedTitles;
|
||||||
|
|
||||||
|
/// 释放大块缓存(emoji 分类与索引),下次访问会重新加载。
|
||||||
|
- (void)purgeLargeCaches;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -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 {
|
- (void)onLocalizationChanged:(__unused NSNotification *)note {
|
||||||
[self refreshLocalizedTitles];
|
[self refreshLocalizedTitles];
|
||||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBEmojiRecentsDidChangeNotification object:nil];
|
[[NSNotificationCenter defaultCenter] postNotificationName:KBEmojiRecentsDidChangeNotification object:nil];
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
// KBFullAccessManager.m
|
// KBFullAccessManager.m
|
||||||
//
|
//
|
||||||
// 统一封装“允许完全访问”检测:
|
// 统一封装“允许完全访问”检测:
|
||||||
// 1) 首选:反射调用 UIInputViewController 的 hasFullAccess(避免直接引用私有 API 标识)
|
// 1) 直接使用 UIInputViewController.hasFullAccess(公开 API)
|
||||||
// 2) 兜底:无法判断时返回 Unknown(上层可按需降级为 Denied 并提示)
|
// 2) 兜底:无法判断时返回 Unknown(上层可按需降级为 Denied 并提示)
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "KBFullAccessManager.h"
|
#import "KBFullAccessManager.h"
|
||||||
#import <objc/message.h>
|
|
||||||
#if __has_include("KBNetworkManager.h")
|
#if __has_include("KBNetworkManager.h")
|
||||||
#import "KBNetworkManager.h"
|
#import "KBNetworkManager.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -62,7 +61,10 @@ NSNotificationName const KBFullAccessChangedNotification = @"KBFullAccessChanged
|
|||||||
Class guideCls = NSClassFromString(@"KBFullAccessGuideView");
|
Class guideCls = NSClassFromString(@"KBFullAccessGuideView");
|
||||||
if (guideCls && [guideCls respondsToSelector:NSSelectorFromString(@"showInView:")]) {
|
if (guideCls && [guideCls respondsToSelector:NSSelectorFromString(@"showInView:")]) {
|
||||||
SEL sel = 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
|
#endif
|
||||||
return NO;
|
return NO;
|
||||||
@@ -74,13 +76,9 @@ NSNotificationName const KBFullAccessChangedNotification = @"KBFullAccessChanged
|
|||||||
- (KBFullAccessState)p_detectFullAccessState {
|
- (KBFullAccessState)p_detectFullAccessState {
|
||||||
UIInputViewController *ivc = self.ivc;
|
UIInputViewController *ivc = self.ivc;
|
||||||
if (!ivc) return KBFullAccessStateUnknown;
|
if (!ivc) return KBFullAccessStateUnknown;
|
||||||
|
if ([ivc respondsToSelector:@selector(hasFullAccess)]) {
|
||||||
SEL sel = NSSelectorFromString(@"hasFullAccess");
|
return ivc.hasFullAccess ? KBFullAccessStateGranted : KBFullAccessStateDenied;
|
||||||
if ([ivc respondsToSelector:sel]) {
|
|
||||||
BOOL granted = ((BOOL (*)(id, SEL))objc_msgSend)(ivc, sel);
|
|
||||||
return granted ? KBFullAccessStateGranted : KBFullAccessStateDenied;
|
|
||||||
}
|
}
|
||||||
// 无法判断时标记 Unknown(上层可按需处理为未开启)
|
|
||||||
return KBFullAccessStateUnknown;
|
return KBFullAccessStateUnknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
37
CustomKeyboard/Manager/KBKeyboardLayoutResolver.h
Normal file
37
CustomKeyboard/Manager/KBKeyboardLayoutResolver.h
Normal 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
|
||||||
106
CustomKeyboard/Manager/KBKeyboardLayoutResolver.m
Normal file
106
CustomKeyboard/Manager/KBKeyboardLayoutResolver.m
Normal 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
|
||||||
@@ -7,9 +7,22 @@
|
|||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
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).
|
/// Simple local suggestion engine (prefix match + lightweight ranking).
|
||||||
@interface KBSuggestionEngine : NSObject
|
@interface KBSuggestionEngine : NSObject
|
||||||
|
|
||||||
|
@property (nonatomic, assign) KBSuggestionEngineType engineType;
|
||||||
|
|
||||||
+ (instancetype)shared;
|
+ (instancetype)shared;
|
||||||
|
|
||||||
/// Returns suggestions for prefix (lowercase expected), limited by count.
|
/// 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.
|
/// Record a selection to slightly boost ranking next time.
|
||||||
- (void)recordSelection:(NSString *)word;
|
- (void)recordSelection:(NSString *)word;
|
||||||
|
|
||||||
|
/// 设置联想引擎类型(根据 profileId 的 suggestionEngine 字段)
|
||||||
|
- (void)setEngineTypeFromString:(NSString *)engineTypeString;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -10,6 +10,14 @@
|
|||||||
@property (nonatomic, copy) NSArray<NSString *> *words;
|
@property (nonatomic, copy) NSArray<NSString *> *words;
|
||||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *selectionCounts;
|
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *selectionCounts;
|
||||||
@property (nonatomic, strong) NSSet<NSString *> *priorityWords;
|
@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
|
@end
|
||||||
|
|
||||||
@implementation KBSuggestionEngine
|
@implementation KBSuggestionEngine
|
||||||
@@ -25,49 +33,51 @@
|
|||||||
|
|
||||||
- (instancetype)init {
|
- (instancetype)init {
|
||||||
if (self = [super init]) {
|
if (self = [super init]) {
|
||||||
|
_engineType = KBSuggestionEngineTypeLatin;
|
||||||
_selectionCounts = [NSMutableDictionary dictionary];
|
_selectionCounts = [NSMutableDictionary dictionary];
|
||||||
NSArray<NSString *> *defaults = [self.class kb_defaultWords];
|
NSArray<NSString *> *defaults = [self.class kb_defaultWords];
|
||||||
_priorityWords = [NSSet setWithArray:defaults];
|
_priorityWords = [NSSet setWithArray:defaults];
|
||||||
_words = [self kb_loadWords];
|
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSArray<NSString *> *)suggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
|
- (NSArray<NSString *> *)suggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
|
||||||
if (prefix.length == 0 || limit == 0) { return @[]; }
|
if (prefix.length == 0 || limit == 0) { return @[]; }
|
||||||
NSString *lower = prefix.lowercaseString;
|
// 为过滤留出候选空间,避免过滤后数量过少。
|
||||||
NSMutableArray<NSString *> *matches = [NSMutableArray array];
|
NSUInteger fetchLimit = limit;
|
||||||
|
if (fetchLimit < 80) {
|
||||||
for (NSString *word in self.words) {
|
fetchLimit = MIN((NSUInteger)80, MAX(fetchLimit * 4, fetchLimit));
|
||||||
if ([word hasPrefix:lower]) {
|
|
||||||
[matches addObject:word];
|
|
||||||
if (matches.count >= limit * 3) {
|
|
||||||
// Avoid scanning too many matches for long lists.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
NSArray<NSString *> *raw = nil;
|
||||||
|
|
||||||
if (matches.count == 0) { return @[]; }
|
switch (self.engineType) {
|
||||||
|
case KBSuggestionEngineTypeEnglish:
|
||||||
[matches sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
|
raw = [self kb_englishSuggestionsForPrefix:prefix limit:fetchLimit];
|
||||||
NSInteger ca = self.selectionCounts[a].integerValue;
|
break;
|
||||||
NSInteger cb = self.selectionCounts[b].integerValue;
|
case KBSuggestionEngineTypeSpanish:
|
||||||
if (ca != cb) {
|
raw = [self kb_spanishSuggestionsForPrefix:prefix limit:fetchLimit];
|
||||||
return (cb > ca) ? NSOrderedAscending : NSOrderedDescending;
|
break;
|
||||||
}
|
case KBSuggestionEngineTypePortuguese:
|
||||||
BOOL pa = [self.priorityWords containsObject:a];
|
raw = [self kb_portugueseSuggestionsForPrefix:prefix limit:fetchLimit];
|
||||||
BOOL pb = [self.priorityWords containsObject:b];
|
break;
|
||||||
if (pa != pb) {
|
case KBSuggestionEngineTypeIndonesian:
|
||||||
return pa ? NSOrderedAscending : NSOrderedDescending;
|
raw = [self kb_indonesianSuggestionsForPrefix:prefix limit:fetchLimit];
|
||||||
}
|
break;
|
||||||
return [a compare:b];
|
case KBSuggestionEngineTypePinyinTraditional:
|
||||||
}];
|
raw = [self kb_traditionalPinyinSuggestionsForPrefix:prefix limit:fetchLimit];
|
||||||
|
break;
|
||||||
if (matches.count > limit) {
|
case KBSuggestionEngineTypePinyinSimplified:
|
||||||
return [matches subarrayWithRange:NSMakeRange(0, limit)];
|
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 {
|
- (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
|
@end
|
||||||
|
|||||||
@@ -73,7 +73,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@end
|
@end
|
||||||
|
|
||||||
@interface KBKeyboardLayout : NSObject
|
@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 *> *rows;
|
||||||
|
@property (nonatomic, strong, nullable) NSArray<KBKeyboardRowConfig *> *shiftRows;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface KBKeyboardLayoutConfig : NSObject
|
@interface KBKeyboardLayoutConfig : NSObject
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#import "KBConfig.h"
|
#import "KBConfig.h"
|
||||||
|
|
||||||
static NSString * const kKBKeyboardLayoutConfigFileName = @"kb_keyboard_layout_config";
|
static NSString * const kKBKeyboardLayoutConfigFileName = @"kb_keyboard_layout_config";
|
||||||
|
static NSString * const kKBKeyboardLayoutI18nFileName = @"kb_keyboard_layouts_i18n";
|
||||||
|
|
||||||
@implementation KBKeyboardLayoutMetrics
|
@implementation KBKeyboardLayoutMetrics
|
||||||
@end
|
@end
|
||||||
@@ -81,7 +82,7 @@ static NSString * const kKBKeyboardLayoutConfigFileName = @"kb_keyboard_layout_c
|
|||||||
@implementation KBKeyboardLayout
|
@implementation KBKeyboardLayout
|
||||||
|
|
||||||
+ (NSDictionary *)mj_objectClassInArray {
|
+ (NSDictionary *)mj_objectClassInArray {
|
||||||
return @{ @"rows": [KBKeyboardRowConfig class] };
|
return @{ @"rows": [KBKeyboardRowConfig class], @"shiftRows": [KBKeyboardRowConfig class] };
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -92,13 +93,110 @@ static NSString * const kKBKeyboardLayoutConfigFileName = @"kb_keyboard_layout_c
|
|||||||
static KBKeyboardLayoutConfig *config = nil;
|
static KBKeyboardLayoutConfig *config = nil;
|
||||||
static dispatch_once_t onceToken;
|
static dispatch_once_t onceToken;
|
||||||
dispatch_once(&onceToken, ^{
|
dispatch_once(&onceToken, ^{
|
||||||
NSString *path = [[NSBundle mainBundle] pathForResource:kKBKeyboardLayoutConfigFileName ofType:@"json"];
|
config = [[KBKeyboardLayoutConfig alloc] init];
|
||||||
NSData *data = path.length ? [NSData dataWithContentsOfFile:path] : nil;
|
[config kb_loadMainConfig];
|
||||||
config = data ? [KBKeyboardLayoutConfig configFromJSONData:data] : nil;
|
[config kb_loadI18nConfig];
|
||||||
});
|
});
|
||||||
return config;
|
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 {
|
+ (instancetype)configFromJSONData:(NSData *)data {
|
||||||
if (data.length == 0) { return nil; }
|
if (data.length == 0) { return nil; }
|
||||||
NSError *error = nil;
|
NSError *error = nil;
|
||||||
|
|||||||
@@ -225,7 +225,10 @@ static NSString * const kKBStreamSplitToken = @"<SPLIT>";
|
|||||||
}
|
}
|
||||||
if (payload.length > 0) {
|
if (payload.length > 0) {
|
||||||
if (self.loggingEnabled) {
|
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;
|
NSString *llmText = nil;
|
||||||
if ([self processLLMChunkPayload:payload output:&llmText]) {
|
if ([self processLLMChunkPayload:payload output:&llmText]) {
|
||||||
@@ -278,7 +281,10 @@ static NSString * const kKBStreamSplitToken = @"<SPLIT>";
|
|||||||
}
|
}
|
||||||
if (payload.length > 0) {
|
if (payload.length > 0) {
|
||||||
if (self.loggingEnabled) {
|
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;
|
NSString *delta = nil;
|
||||||
if ((NSInteger)payload.length >= self.deliveredCharCount) {
|
if ((NSInteger)payload.length >= self.deliveredCharCount) {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "NetworkStreamHandler.h"
|
#import "NetworkStreamHandler.h"
|
||||||
|
#import <Security/Security.h>
|
||||||
|
#import "KBLocalizationManager.h"
|
||||||
|
|
||||||
@interface NetworkStreamHandler ()
|
@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:@"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:@"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:@"keep-alive" forHTTPHeaderField:@"Connection"];
|
||||||
[request setValue:@"1" forHTTPHeaderField:@"Upgrade-Insecure-Requests"];
|
[request setValue:@"1" forHTTPHeaderField:@"Upgrade-Insecure-Requests"];
|
||||||
|
|
||||||
@@ -243,8 +249,26 @@ didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
|
|||||||
|
|
||||||
// 处理 SSL 认证挑战
|
// 处理 SSL 认证挑战
|
||||||
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
|
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
|
||||||
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
|
SecTrustRef trust = challenge.protectionSpace.serverTrust;
|
||||||
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
|
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 {
|
} else {
|
||||||
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
|
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,16 @@
|
|||||||
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||||
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
#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
|
#ifndef KB_URL_BRIDGE_ENABLE
|
||||||
|
#if DEBUG
|
||||||
#define KB_URL_BRIDGE_ENABLE 1
|
#define KB_URL_BRIDGE_ENABLE 1
|
||||||
|
#else
|
||||||
|
#define KB_URL_BRIDGE_ENABLE 1
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,19 @@
|
|||||||
<array/>
|
<array/>
|
||||||
<key>NSPrivacyCollectedDataTypes</key>
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
<array>
|
<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>
|
<dict>
|
||||||
<key>NSPrivacyCollectedDataType</key>
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
<string>NSPrivacyCollectedDataTypeOtherUserContent</string>
|
<string>NSPrivacyCollectedDataTypeOtherUserContent</string>
|
||||||
@@ -20,6 +33,18 @@
|
|||||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</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>
|
</array>
|
||||||
<key>NSPrivacyAccessedAPITypes</key>
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
<array>
|
<array>
|
||||||
@@ -31,6 +56,14 @@
|
|||||||
<string>CA92.1</string>
|
<string>CA92.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryActiveKeyboards</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>3EC4.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||||
|
|||||||
262
CustomKeyboard/Resource/KBSkinIconMap_es.strings
Normal file
262
CustomKeyboard/Resource/KBSkinIconMap_es.strings
Normal 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";
|
||||||
250
CustomKeyboard/Resource/KBSkinIconMap_id.strings
Normal file
250
CustomKeyboard/Resource/KBSkinIconMap_id.strings
Normal 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";
|
||||||
250
CustomKeyboard/Resource/KBSkinIconMap_pt.strings
Normal file
250
CustomKeyboard/Resource/KBSkinIconMap_pt.strings
Normal 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";
|
||||||
330
CustomKeyboard/Resource/KBSkinIconMap_zh_hant.strings
Normal file
330
CustomKeyboard/Resource/KBSkinIconMap_zh_hant.strings
Normal 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";
|
||||||
345
CustomKeyboard/Resource/bopomofo_to_chinese.json
Normal file
345
CustomKeyboard/Resource/bopomofo_to_chinese.json
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
{
|
||||||
|
"__comment": "注音符号映射表:注音组合 -> 繁体字候选词列表",
|
||||||
|
"__comment_symbols": "聲母: ㄅㄆㄇㄈㄉㄊㄋㄌㄍㄎㄏㄐㄑㄒㄓㄔㄕㄖㄗㄘㄙ",
|
||||||
|
"__comment_vowels": "韻母: ㄚㄛㄜㄝㄞㄟㄠㄡㄢㄣㄤㄥㄦㄧㄨㄩ",
|
||||||
|
"__comment_tones": "聲調: ˊ(二聲) ˇ(三聲) ˋ(四聲) ˙(輕聲), 無符號為一聲",
|
||||||
|
"mappings": {
|
||||||
|
"ㄅㄚ": ["八", "巴", "吧", "爸", "拔", "罷", "霸", "扒", "叭", "芭", "疤", "粑"],
|
||||||
|
"ㄅㄞ": ["白", "百", "拜", "敗", "柏", "擺", "佰", "佰"],
|
||||||
|
"ㄅㄢ": ["班", "般", "板", "版", "半", "伴", "扮", "拌", "瓣", "頒", "斑", "搬"],
|
||||||
|
"ㄅㄤ": ["幫", "邦", "榜", "膀", "綁", "棒", "磅", "邦"],
|
||||||
|
"ㄅㄠ": ["包", "保", "報", "寶", "抱", "暴", "爆", "薄", "豹", "飽", "堡", "刨", "苞", "胞", "雹"],
|
||||||
|
"ㄅㄟ": ["北", "被", "背", "備", "悲", "杯", "碑", "輩", "倍", "貝", "臂"],
|
||||||
|
"ㄅㄣ": ["本", "奔", "笨", "盆", "賁"],
|
||||||
|
"ㄅㄥ": ["崩", "繃", "蹦", "泵", "甭"],
|
||||||
|
"ㄅㄧ": ["比", "必", "筆", "畢", "避", "閉", "鼻", "彼", "碧", "壁", "弊", "臂", "秘", "辟", "逼", "幣", "庇", "痹", "匕"],
|
||||||
|
"ㄅㄧㄝ": ["別", "憋", "癟", "鱉"],
|
||||||
|
"ㄅㄧㄢ": ["變", "便", "邊", "編", "辯", "遍", "鞭", "辨", "扁", "貶", "匾", "蝙"],
|
||||||
|
"ㄅㄧㄠ": ["表", "標", "彪", "錶", "鏢", "錶", "裱", "婊"],
|
||||||
|
"ㄅㄧㄣ": ["賓", "彬", "斌", "瀕", "濱", "殯", "鬢"],
|
||||||
|
"ㄅㄧㄥ": ["病", "並", "冰", "兵", "餅", "柄", "秉", "稟", "炳", "稟"],
|
||||||
|
"ㄅㄛ": ["波", "博", "播", "伯", "薄", "泊", "柏", "勃", "搏", "撥", "剝", "脖", "卜", "玻", "柏"],
|
||||||
|
"ㄅㄨ": ["不", "步", "部", "布", "補", "捕", "簿", "卜", "怖", "哺", "埠", "簿"],
|
||||||
|
"ㄆㄚ": ["趴", "啪", "葩", "扒"],
|
||||||
|
"ㄆㄞ": ["排", "拍", "牌", "派", "徘", "湃", "俳"],
|
||||||
|
"ㄆㄢ": ["判", "盤", "盼", "攀", "畔", "胖", "叛", "潘", "磐", "蹣", "拚", "泮"],
|
||||||
|
"ㄆㄤ": ["旁", "胖", "龐", "膀", "磅", "彷", "螃", "乓"],
|
||||||
|
"ㄆㄠ": ["跑", "炮", "泡", "拋", "刨", "袍", "咆", "庖", "匏"],
|
||||||
|
"ㄆㄟ": ["配", "陪", "培", "賠", "佩", "沛", "裴", "胚", "霈"],
|
||||||
|
"ㄆㄣ": ["噴", "盆"],
|
||||||
|
"ㄆㄥ": ["朋", "碰", "彭", "棚", "蓬", "鵬", "捧", "烹", "澎", "怦", "砰", "堋"],
|
||||||
|
"ㄆㄧ": ["皮", "批", "披", "匹", "疲", "僻", "脾", "劈", "琵", "毗", "啤", "坯", "譬", "霹", "屁", "闢", "紕", "闢"],
|
||||||
|
"ㄆㄧㄝ": ["撇", "瞥", "苤"],
|
||||||
|
"ㄆㄧㄢ": ["片", "便", "騙", "偏", "篇", "翩", "扁", "諞", "騙"],
|
||||||
|
"ㄆㄧㄠ": ["票", "飄", "漂", "瓢", "嫖", "縹", "驃", "飄"],
|
||||||
|
"ㄆㄧㄣ": ["品", "貧", "頻", "聘", "拼", "拚", "嬪"],
|
||||||
|
"ㄆㄧㄥ": ["平", "評", "憑", "瓶", "萍", "屏", "蘋", "坪", "秤", "娉", "馮", "萍"],
|
||||||
|
"ㄆㄛ": ["破", "迫", "婆", "頗", "坡", "潑", "泊", "魄", "粕", "朴", "珀", "叵", "鄱"],
|
||||||
|
"ㄆㄨ": ["普", "鋪", "樸", "譜", "浦", "葡", "蒲", "僕", "撲", "圃", "濮", "璞", "噗", "莆"],
|
||||||
|
"ㄇㄚ": ["媽", "馬", "麻", "罵", "嘛", "螞", "碼", "瑪", "抹", "摩", "螞"],
|
||||||
|
"ㄇㄞ": ["買", "賣", "麥", "埋", "邁", "脈", "霾", "賣"],
|
||||||
|
"ㄇㄢ": ["滿", "慢", "曼", "漫", "蠻", "瞞", "饅", "蔓", "謾", "墁", "幔", "曼"],
|
||||||
|
"ㄇㄤ": ["忙", "盲", "茫", "芒", "莽", "氓", "硭", "邙"],
|
||||||
|
"ㄇㄠ": ["貓", "毛", "矛", "茅", "茂", "冒", "帽", "貌", "貿", "卯", "錨", "耄", "髦", "瑁", "懋", "卯"],
|
||||||
|
"ㄇㄟ": ["沒", "美", "妹", "每", "梅", "媒", "煤", "眉", "霉", "魅", "玫", "枚", "寐", "昧", "媚", "湄", "鎂", "糜", "梅"],
|
||||||
|
"ㄇㄣ": ["們", "門", "悶", "燜", "捫", "悶"],
|
||||||
|
"ㄇㄥ": ["夢", "孟", "猛", "蒙", "盟", "萌", "朦", "檬", "懵", "礞", "甍", "萌"],
|
||||||
|
"ㄇㄧ": ["米", "密", "迷", "蜜", "祕", "眯", "靡", "糜", "彌", "覓", "冪", "泌", "祕", "謎"],
|
||||||
|
"ㄇㄧㄝ": ["滅", "蔑", "篾", "乜", "咩"],
|
||||||
|
"ㄇㄧㄢ": ["面", "免", "棉", "眠", "綿", "勉", "緬", "冕", "娩", "湎", "眄", "冕"],
|
||||||
|
"ㄇㄧㄠ": ["描", "秒", "妙", "廟", "苗", "瞄", "渺", "淼", "緲", "藐", "喵"],
|
||||||
|
"ㄇㄧㄣ": ["民", "敏", "名", "皿", "閔", "抿", "泯", "憫", "閔", "愍"],
|
||||||
|
"ㄇㄧㄥ": ["名", "明", "命", "鳴", "銘", "冥", "茗", "溟", "瞑", "螟", "銘"],
|
||||||
|
"ㄇㄛ": ["麼", "摸", "磨", "摩", "魔", "膜", "默", "墨", "抹", "末", "莫", "漠", "寞", "陌", "謨", "茉", "驀", "歿", "麼"],
|
||||||
|
"ㄇㄡ": ["某", "謀", "牟", "眸", "繆", "鍪", "哞"],
|
||||||
|
"ㄇㄨ": ["目", "母", "木", "幕", "牧", "慕", "墓", "暮", "穆", "睦", "沐", "募", "姆", "拇", "牡", "畝", "慕"],
|
||||||
|
"ㄈㄚ": ["發", "法", "罰", "乏", "伐", "閥", "筏", "佳", "髮", "法"],
|
||||||
|
"ㄈㄢ": ["反", "飯", "煩", "繁", "範", "犯", "泛", "番", "翻", "凡", "帆", "返", "販", "礬", "釩", "蕃"],
|
||||||
|
"ㄈㄤ": ["方", "放", "房", "防", "訪", "仿", "芳", "坊", "妨", "紡", "舫", "肪", "仿"],
|
||||||
|
"ㄈㄟ": ["非", "飛", "費", "肥", "廢", "匪", "誹", "啡", "菲", "沸", "翡", "吠", "肺", "狒", "妃"],
|
||||||
|
"ㄈㄣ": ["分", "份", "粉", "奮", "憤", "紛", "芬", "墳", "焚", "氛", "糞", "吩", "汾"],
|
||||||
|
"ㄈㄥ": ["風", "封", "豐", "峰", "鋒", "蜂", "瘋", "逢", "縫", "鳳", "奉", "諷", "楓", "烽", "豐", "峰"],
|
||||||
|
"ㄈㄛ": ["佛", "彿"],
|
||||||
|
"ㄈㄡ": ["否", "縫", "缶"],
|
||||||
|
"ㄈㄨ": ["父", "夫", "付", "服", "福", "府", "負", "富", "復", "副", "婦", "撫", "附", "幅", "浮", "腐", "符", "弗", "腹", "輻", "敷", "氟", "芙", "敷", "伏", "扶", "俘", "袱", "芙", "斧", "脯", "腑", "滏", "蚨", "跗", "馥"],
|
||||||
|
"ㄉㄚ": ["大", "打", "答", "達", "搭", "塔", "瘩", "妲", "怛", "耷"],
|
||||||
|
"ㄉㄞ": ["大", "代", "帶", "待", "袋", "戴", "呆", "貸", "逮", "怠", "殆", "黛", "岱", "迨"],
|
||||||
|
"ㄉㄢ": ["但", "單", "擔", "膽", "丹", "淡", "蛋", "誕", "彈", "旦", "氮", "耽", "憚", "殫", "瘅", "眈"],
|
||||||
|
"ㄉㄤ": ["當", "黨", "檔", "擋", "蕩", "宕", "檔", "璫", "璫"],
|
||||||
|
"ㄉㄠ": ["到", "道", "導", "刀", "倒", "島", "盜", "悼", "搗", "禱", "蹈", "叨", "忉", "氘"],
|
||||||
|
"ㄉㄜ": ["的", "得", "德", "底", "德"],
|
||||||
|
"ㄉㄥ": ["等", "燈", "登", "鄧", "瞪", "凳", "蹬", "噔", "嶝"],
|
||||||
|
"ㄉㄧ": ["的", "地", "第", "低", "底", "敵", "弟", "帝", "抵", "遞", "迪", "滴", "堤", "笛", "締", "嫡", "詆", "邸", "砥", "睇", "鏑"],
|
||||||
|
"ㄉㄧㄝ": ["爹", "跌", "叠", "蝶", "碟", "諜", "迭", "帖", "耋", "牒", "瓞", "鰈"],
|
||||||
|
"ㄉㄧㄢ": ["點", "電", "店", "典", "墊", "澱", "殿", "顛", "滇", "碘", "巔", "癲", "惦", "奠", "甸", "阽"],
|
||||||
|
"ㄉㄧㄠ": ["調", "掉", "吊", "雕", "刁", "釣", "凋", "碉", "貂", "雕"],
|
||||||
|
"ㄉㄧㄥ": ["定", "訂", "頂", "丁", "釘", "盯", "叮", "鼎", "叮", "丁", "町"],
|
||||||
|
"ㄉㄨ": ["讀", "都", "度", "獨", "毒", "渡", "杜", "肚", "堵", "賭", "鍍", "督", "篤", "嘟", "睹", "妒", "芏"],
|
||||||
|
"ㄉㄨㄢ": ["段", "斷", "短", "鍛", "緞", "端", "椴", "煅"],
|
||||||
|
"ㄉㄨㄟ": ["對", "隊", "堆", "兌", "懟", "憝"],
|
||||||
|
"ㄉㄨㄣ": ["頓", "噸", "盾", "蹲", "敦", "墩", "燉", "鈍", "囤", "遁", "燉"],
|
||||||
|
"ㄉㄨㄛ": ["多", "度", "奪", "躲", "朵", "墮", "舵", "跺", "惰", "哆", "垛", "躲", "踱", "剁", "咄"],
|
||||||
|
"ㄊㄚ": ["他", "她", "它", "塔", "踏", "拓", "榻", "獺", "撻", "闒", "遢", "遢"],
|
||||||
|
"ㄊㄞ": ["太", "台", "臺", "態", "泰", "抬", "胎", "鮐", "薹", "駘", "炱", "邰", "苔", "颱"],
|
||||||
|
"ㄊㄢ": ["談", "探", "彈", "壇", "攤", "貪", "嘆", "潭", "坦", "毯", "痰", "檀", "譚", "忐", "袒", "郯", "澹", "覃", "忐", "曇", "忐"],
|
||||||
|
"ㄊㄤ": ["堂", "唐", "糖", "躺", "趟", "湯", "燙", "塘", "膛", "棠", "搪", "螳", "鏜", "鐋", "耥", "鏜"],
|
||||||
|
"ㄊㄠ": ["套", "逃", "桃", "陶", "討", "濤", "掏", "滔", "萄", "淘", "燾", "絳", "叨", "洮", "啕", "饕"],
|
||||||
|
"ㄊㄜ": ["特", "忒", "慝", "鋱", "忒"],
|
||||||
|
"ㄊㄥ": ["疼", "騰", "藤", "滕", "謄", "疼", "滕"],
|
||||||
|
"ㄊㄧ": ["提", "題", "體", "替", "踢", "梯", "剔", "蹄", "啼", "惕", "涕", "銻", "倜", "悌", "嚏", "醍", "緹"],
|
||||||
|
"ㄊㄧㄝ": ["鐵", "貼", "帖", "萜", "帖", "餮"],
|
||||||
|
"ㄊㄧㄢ": ["天", "田", "填", "甜", "添", "恬", "腆", "殄", "忝", "闐", "祆", "忝"],
|
||||||
|
"ㄊㄧㄠ": ["條", "跳", "調", "挑", "眺", "佻", "祧", "銚", "髫", "鰷", "調", "眺"],
|
||||||
|
"ㄊㄧㄥ": ["聽", "停", "庭", "挺", "廳", "廷", "亭", "婷", "艇", "汀", "蜓", "霆", "鋌", "莛", "汀"],
|
||||||
|
"ㄊㄨ": ["圖", "土", "突", "途", "吐", "兔", "屠", "徒", "凸", "禿", "荼", "釷", "菟", "兔"],
|
||||||
|
"ㄊㄨㄢ": ["團", "摶", "彖", "湍", "摶"],
|
||||||
|
"ㄊㄨㄟ": ["推", "退", "腿", "蛻", "頹", "褪", "忒"],
|
||||||
|
"ㄊㄨㄣ": ["吞", "屯", "臀", "囤", "褪", "豚", "吞"],
|
||||||
|
"ㄊㄨㄛ": ["脫", "托", "拖", "妥", "拓", "唾", "陀", "沱", "坨", "駝", "鴕", "橐", "砣", "佗", "跎", "坨", "酡"],
|
||||||
|
"ㄋㄚ": ["那", "拿", "哪", "納", "吶", "娜", "鈉", "衲", "鎿"],
|
||||||
|
"ㄋㄞ": ["奶", "耐", "乃", "奈", "氖", "萘", "鼐", "氖"],
|
||||||
|
"ㄋㄢ": ["南", "難", "男", "喃", "楠", "赧", "囝", "囡"],
|
||||||
|
"ㄋㄤ": ["囊", "囔", "餿"],
|
||||||
|
"ㄋㄠ": ["腦", "惱", "鬧", "撓", "淖", "鐃", "橈", "鬧", "鬧"],
|
||||||
|
"ㄋㄜ": ["呢", "訥"],
|
||||||
|
"ㄋㄟ": ["內", "那", "餒"],
|
||||||
|
"ㄋㄣ": ["嫩", "恁"],
|
||||||
|
"ㄋㄥ": ["能"],
|
||||||
|
"ㄋㄧ": ["你", "妳", "呢", "泥", "尼", "擬", "逆", "妮", "霓", "倪", "匿", "溺", "膩", "旎", "昵", "妮"],
|
||||||
|
"ㄋㄧㄝ": ["捏", "聶", "孽", "躡", "鎳", "囁", "臬", "涅", "孽"],
|
||||||
|
"ㄋㄧㄢ": ["年", "念", "黏", "碾", "捻", "撚", "蔦", "念", "唸"],
|
||||||
|
"ㄋㄧㄤ": ["娘", "釀", "釀"],
|
||||||
|
"ㄋㄧㄠ": ["鳥", "尿", "裊", "嬲", "蔦", "鳥"],
|
||||||
|
"ㄋㄧㄣ": ["您"],
|
||||||
|
"ㄋㄧㄥ": ["寧", "凝", "擰", "檸", "獰", "嚀", "甯", "寧"],
|
||||||
|
"ㄋㄧㄡ": ["牛", "紐", "扭", "鈕", "妞", "拗", "妞"],
|
||||||
|
"ㄋㄨ": ["女", "努", "怒", "奴", "弩", "胬", "弩"],
|
||||||
|
"ㄋㄨㄢ": ["暖"],
|
||||||
|
"ㄋㄨㄣ": ["嫩", "恁"],
|
||||||
|
"ㄋㄨㄛ": ["挪", "諾", "懦", "糯", "喏", "懦"],
|
||||||
|
"ㄌㄚ": ["拉", "啦", "蠟", "辣", "臘", "喇", "落", "啦", "邋"],
|
||||||
|
"ㄌㄞ": ["來", "賴", "萊", "徠", "賚", "賴", "睞"],
|
||||||
|
"ㄌㄢ": ["藍", "蘭", "攔", "籃", "懶", "爛", "濫", "覽", "欄", "瀾", "嵐", "襤", "懶", "讕"],
|
||||||
|
"ㄌㄤ": ["浪", "郎", "狼", "廊", "朗", "琅", "螂", "朗", "郎", "閬"],
|
||||||
|
"ㄌㄠ": ["老", "勞", "落", "牢", "撈", "澇", "絡", "姥", "佬", "潦", "澇", "癆"],
|
||||||
|
"ㄌㄜ": ["了", "樂", "勒", "肋", "勒", "肋"],
|
||||||
|
"ㄌㄟ": ["累", "類", "淚", "雷", "勒", "壘", "蕾", "磊", "擂", "鐳", "儡", "勒", "擂"],
|
||||||
|
"ㄌㄥ": ["冷", "愣", "楞", "冷"],
|
||||||
|
"ㄌㄧ": ["裡", "力", "理", "利", "立", "離", "例", "歷", "李", "禮", "麗", "勵", "梨", "厘", "莉", "犁", "黎", "璃", "狸", "漓", "罹", "驪", "鱧", "吏", "栗", "俐", "荔", "痢", "裡", "裏", "裡", "吏", "戾", "蠡", "蜊", "悝", "喱"],
|
||||||
|
"ㄌㄧㄚ": ["倆"],
|
||||||
|
"ㄌㄧㄝ": ["列", "烈", "獵", "裂", "劣", "咧", "冽", "捩", "躐", "冽", "洌"],
|
||||||
|
"ㄌㄧㄢ": ["連", "聯", "臉", "練", "蓮", "戀", "煉", "廉", "憐", "漣", "鐮", "斂", "璉", "斂", "斂"],
|
||||||
|
"ㄌㄧㄤ": ["兩", "亮", "量", "良", "涼", "梁", "糧", "樑", "諒", "晾", "踉", "靚", "倆", "倆", "粱", "量"],
|
||||||
|
"ㄌㄧㄠ": ["了", "料", "聊", "療", "遼", "撩", "僚", "燎", "繚", "潦", "寥", "嘹", "撩", "鐐", "獠"],
|
||||||
|
"ㄌㄧㄝ": ["列", "烈", "獵", "裂", "劣", "咧", "冽", "捩", "躐", "獵", "獵"],
|
||||||
|
"ㄌㄧㄣ": ["林", "臨", "鄰", "淋", "琳", "霖", "鱗", "麟", "遴", "藺", "吝", "躪", "琳", "淋"],
|
||||||
|
"ㄌㄧㄥ": ["領", "零", "靈", "令", "另", "玲", "鈴", "陵", "嶺", "凌", "菱", "羚", "翎", "聆", "伶", "拎", "凌", "鈴", "鈴"],
|
||||||
|
"ㄌㄧㄡ": ["六", "流", "留", "劉", "柳", "溜", "琉", "榴", "硫", "鎏", "鷚", "溜", "溜", "鎦"],
|
||||||
|
"ㄌㄨ": ["路", "錄", "陸", "綠", "露", "旅", "律", "慮", "呂", "履", "侶", "屢", "濾", "氯", "廬", "爐", "蘆", "盧", "顱", "魯", "擼", "祿", "麓", "碌", "陸", "輅", "輅"],
|
||||||
|
"ㄌㄨㄢ": ["亂", "卵", "巒", "鑾", "鸞", "欒", "鸞", "鑾"],
|
||||||
|
"ㄌㄨㄣ": ["論", "輪", "倫", "侖", "綸", "淪", "論", "論"],
|
||||||
|
"ㄌㄨㄛ": ["落", "羅", "洛", "絡", "邏", "鑼", "籮", "駱", "裸", "螺", "蘿", "摞", "囉", "羅", "邏"],
|
||||||
|
"ㄍㄚ": ["嘎", "噶", "軋", "噶"],
|
||||||
|
"ㄍㄞ": ["改", "該", "蓋", "概", "溉", "丐", "芥", "鈣", "蓋", "蓋"],
|
||||||
|
"ㄍㄢ": ["幹", "感", "敢", "甘", "肝", "趕", "桿", "乾", "贛", "柑", "竿", "尴", "擀", "乾", "乾"],
|
||||||
|
"ㄍㄤ": ["剛", "鋼", "港", "崗", "綱", "岡", "缸", "槓", "扛", "剛", "崗"],
|
||||||
|
"ㄍㄠ": ["高", "告", "搞", "稿", "糕", "鎬", "膏", "篙", "稿", "稿"],
|
||||||
|
"ㄍㄜ": ["個", "各", "歌", "格", "哥", "割", "革", "隔", "閣", "葛", "戈", "擱", "鴿", "胳", "骼", "個", "個"],
|
||||||
|
"ㄍㄟ": ["給"],
|
||||||
|
"ㄍㄣ": ["跟", "根", "亙", "艮", "跟"],
|
||||||
|
"ㄍㄥ": ["更", "耕", "庚", "羹", "耿", "梗", "更", "耕"],
|
||||||
|
"ㄍㄨ": ["古", "故", "顧", "骨", "谷", "股", "鼓", "固", "孤", "姑", "辜", "沽", "咕", "估", "谷", "谷"],
|
||||||
|
"ㄍㄨㄚ": ["掛", "瓜", "刮", "寡", "呱", "褂", "掛", "掛"],
|
||||||
|
"ㄍㄨㄞ": ["怪", "乖", "拐", "乖"],
|
||||||
|
"ㄍㄨㄢ": ["關", "觀", "管", "官", "館", "慣", "灌", "冠", "罐", "貫", "棺", "倌", "觀", "關"],
|
||||||
|
"ㄍㄨㄤ": ["光", "廣", "逛", "胱", "光", "光"],
|
||||||
|
"ㄍㄨㄟ": ["貴", "規", "歸", "鬼", "軌", "櫃", "桂", "跪", "龜", "瑰", "詭", "閨", "圭", "桂", "歸"],
|
||||||
|
"ㄍㄨㄣ": ["滾", "棍", "滾"],
|
||||||
|
"ㄍㄨㄛ": ["過", "國", "果", "鍋", "郭", "裹", "渦", "過", "過"],
|
||||||
|
"ㄎㄚ": ["卡", "咖", "喀", "咔", "卡"],
|
||||||
|
"ㄎㄞ": ["開", "凱", "楷", "慨", "愷", "鎧", "鍇", "開", "凱"],
|
||||||
|
"ㄎㄢ": ["看", "砍", "坎", "勘", "刊", "堪", "瞰", "龕", "看", "看"],
|
||||||
|
"ㄎㄤ": ["康", "抗", "扛", "亢", "糠", "慷", "伉", "康", "康"],
|
||||||
|
"ㄎㄠ": ["考", "靠", "烤", "拷", "栲", "犒", "考", "考"],
|
||||||
|
"ㄎㄜ": ["可", "客", "科", "刻", "課", "顆", "克", "渴", "柯", "棵", "磕", "咳", "殼", "坷", "可", "可"],
|
||||||
|
"ㄎㄣ": ["肯", "懇", "啃", "齦", "肯"],
|
||||||
|
"ㄎㄥ": ["坑", "吭", "鏗", "坑"],
|
||||||
|
"ㄎㄨ": ["苦", "哭", "庫", "酷", "枯", "窟", "骷", "苦", "苦"],
|
||||||
|
"ㄎㄨㄚ": ["跨", "誇", "垮", "挎", "胯", "跨", "跨"],
|
||||||
|
"ㄎㄨㄞ": ["快", "塊", "筷", "儈", "膾", "快", "快"],
|
||||||
|
"ㄎㄨㄢ": ["寬", "款", "寬"],
|
||||||
|
"ㄎㄨㄤ": ["況", "礦", "狂", "框", "曠", "眶", "筐", "匡", "誑", "況", "況"],
|
||||||
|
"ㄎㄨㄟ": ["虧", "愧", "潰", "窺", "葵", "魁", "饋", "匱", "睽", "聵", "虧", "虧"],
|
||||||
|
"ㄎㄨㄣ": ["困", "昆", "坤", "捆", "琨", "鯤", "困", "困"],
|
||||||
|
"ㄎㄨㄛ": ["擴", "括", "闊", "廓", "擴", "擴"],
|
||||||
|
"ㄏㄚ": ["哈", "蛤", "哈"],
|
||||||
|
"ㄏㄞ": ["還", "海", "害", "孩", "嗨", "亥", "骸", "氦", "海", "海"],
|
||||||
|
"ㄏㄢ": ["漢", "寒", "汗", "喊", "韓", "旱", "憾", "悍", "翰", "涵", "酣", "憨", "漢", "漢"],
|
||||||
|
"ㄏㄤ": ["行", "航", "杭", "巷", "夯", "吭", "行", "行"],
|
||||||
|
"ㄏㄠ": ["好", "號", "豪", "毫", "浩", "耗", "郝", "蒿", "嚎", "壕", "濠", "好", "好"],
|
||||||
|
"ㄏㄜ": ["和", "合", "河", "何", "核", "賀", "喝", "赫", "褐", "鶴", "荷", "盒", "禾", "嚇", "呵", "和", "和"],
|
||||||
|
"ㄏㄟ": ["黑", "嘿", "黑"],
|
||||||
|
"ㄏㄣ": ["很", "狠", "恨", "痕", "很", "很"],
|
||||||
|
"ㄏㄥ": ["橫", "恆", "衡", "亨", "哼", "橫", "橫"],
|
||||||
|
"ㄏㄨ": ["湖", "呼", "戶", "虎", "護", "互", "忽", "胡", "壺", "狐", "糊", "弧", "蝴", "乎", "滬", "戶", "戶"],
|
||||||
|
"ㄏㄨㄚ": ["話", "花", "化", "華", "畫", "劃", "滑", "嘩", "樺", "驊", "花", "花"],
|
||||||
|
"ㄏㄨㄞ": ["壞", "懷", "槐", "徊", "壞", "壞"],
|
||||||
|
"ㄏㄨㄢ": ["還", "換", "環", "歡", "緩", "患", "喚", "幻", "煥", "桓", "宦", "渙", "瘓", "歡", "歡"],
|
||||||
|
"ㄏㄨㄤ": ["黃", "皇", "荒", "慌", "煌", "晃", "謊", "凰", "惶", "簧", "恍", "黃", "黃"],
|
||||||
|
"ㄏㄨㄟ": ["會", "回", "灰", "輝", "惠", "慧", "繪", "匯", "毀", "悔", "晦", "賄", "穢", "會", "會"],
|
||||||
|
"ㄏㄨㄣ": ["婚", "魂", "混", "渾", "昏", "葷", "餛", "婚", "婚"],
|
||||||
|
"ㄏㄨㄛ": ["活", "火", "或", "夥", "獲", "貨", "禍", "惑", "霍", "豁", "鍬", "鑊", "活", "活"],
|
||||||
|
"ㄐㄧ": ["幾", "機", "己", "記", "計", "集", "基", "際", "極", "擊", "激", "其", "及", "級", "即", "急", "季", "跡", "技", "績", "輯", "籍", "擠", "吉", "雞", "奇", "肌", "饑", "譏", "磯", "姬", "嫉", "棘", "寂", "冀", "驥", "己", "己"],
|
||||||
|
"ㄐㄧㄚ": ["家", "加", "價", "假", "架", "佳", "甲", "駕", "嘉", "稼", "嫁", "夾", "頰", "戛", "枷", "家", "家"],
|
||||||
|
"ㄐㄧㄢ": ["見", "間", "建", "件", "簡", "檢", "堅", "健", "漸", "劍", "鍵", "尖", "肩", "艦", "鑒", "剪", "撿", "踐", "賤", "箭", "澗", "濺", "薦", "餞", "諫", "見", "見"],
|
||||||
|
"ㄐㄧㄤ": ["將", "江", "強", "講", "降", "獎", "疆", "匠", "蔣", "漿", "僵", "薑", "絳", "將", "將"],
|
||||||
|
"ㄐㄧㄠ": ["叫", "教", "腳", "角", "交", "覺", "較", "焦", "膠", "驕", "澆", "攪", "椒", "嬌", "郊", "蕉", "矯", "絞", "僥", "佼", "叫", "叫"],
|
||||||
|
"ㄐㄧㄝ": ["接", "節", "街", "結", "解", "姐", "介", "界", "借", "傑", "潔", "截", "揭", "劫", "捷", "睫", "竭", "桔", "戒", "芥", "藉", "拮", "接", "接"],
|
||||||
|
"ㄐㄧㄣ": ["進", "金", "近", "今", "緊", "盡", "僅", "勁", "錦", "津", "筋", "巾", "斤", "禁", "襟", "瑾", "進", "進"],
|
||||||
|
"ㄐㄧㄥ": ["經", "精", "景", "警", "靜", "境", "競", "淨", "鏡", "徑", "驚", "京", "晶", "睛", "莖", "荊", "兢", "涇", "憬", "經", "經"],
|
||||||
|
"ㄐㄧㄡ": ["就", "九", "久", "酒", "舊", "救", "究", "糾", "舅", "揪", "韭", "灸", "玖", "臼", "就", "就"],
|
||||||
|
"ㄐㄩ": ["句", "具", "據", "局", "舉", "巨", "聚", "居", "距", "懼", "劇", "鋸", "矩", "拒", "俱", "菊", "橘", "颶", "踞", "遽", "句", "句"],
|
||||||
|
"ㄐㄩㄢ": ["卷", "捐", "圈", "眷", "倦", "娟", "雋", "涓", "鐫", "卷", "卷"],
|
||||||
|
"ㄐㄩㄝ": ["決", "覺", "絕", "角", "爵", "掘", "倔", "厥", "譎", "獗", "矍", "嚼", "決", "決"],
|
||||||
|
"ㄐㄩㄣ": ["軍", "君", "均", "俊", "菌", "竣", "鈞", "峻", "雋", "軍", "軍"],
|
||||||
|
"ㄑㄧ": ["起", "其", "氣", "期", "七", "奇", "妻", "棋", "齊", "旗", "企", "啟", "器", "棄", "汽", "祈", "騎", "豈", "漆", "契", "砌", "琪", "淇", "岐", "祁", "崎", "祺", "臍", "訖", "磧", "起", "起"],
|
||||||
|
"ㄑㄧㄚ": ["恰", "洽", "卡", "掐", "髂", "袷", "恰", "恰"],
|
||||||
|
"ㄑㄧㄢ": ["前", "錢", "千", "簽", "遷", "淺", "欠", "牽", "潛", "鉛", "謙", "乾", "嵌", "譴", "倩", "槍", "嗆", "薔", "牆", "強", "搶", "腔", "羌", "嬙", "檣", "鏘", "鏹", "前", "前"],
|
||||||
|
"ㄑㄧㄠ": ["橋", "瞧", "巧", "敲", "俏", "殼", "竅", "喬", "翹", "峭", "撬", "憔", "譙", "樵", "橋", "橋"],
|
||||||
|
"ㄑㄧㄝ": ["切", "且", "茄", "怯", "竊", "妾", "愜", "鍥", "伽", "切", "切"],
|
||||||
|
"ㄑㄧㄣ": ["親", "琴", "勤", "侵", "秦", "欽", "禽", "寢", "沁", "芹", "擒", "噙", "覃", "親", "親"],
|
||||||
|
"ㄑㄧㄥ": ["情", "請", "清", "青", "輕", "慶", "傾", "頃", "晴", "擎", "卿", "氫", "罄", "磬", "蜻", "鯖", "綮", "情", "情"],
|
||||||
|
"ㄑㄩ": ["去", "取", "曲", "區", "趣", "娶", "渠", "屈", "驅", "蛆", "軀", "祛", "瞿", "蛐", "麴", "衢", "去", "去"],
|
||||||
|
"ㄑㄩㄢ": ["全", "權", "圈", "泉", "拳", "犬", "勸", "券", "詮", "痊", "銓", "蜷", "顴", "全", "全"],
|
||||||
|
"ㄑㄩㄝ": ["確", "卻", "缺", "雀", "鵲", "闕", "瘸", "榷", "愨", "確", "確"],
|
||||||
|
"ㄑㄩㄣ": ["群", "裙", "逡", "群", "群"],
|
||||||
|
"ㄒㄧ": ["西", "系", "息", "希", "席", "習", "細", "喜", "戲", "洗", "惜", "稀", "溪", "錫", "析", "膝", "襲", "昔", "熙", "夕", "兮", "悉", "熄", "嬉", "汐", "犀", "烯", "曦", "奚", "唏", "淅", "嘻", "樨", "蠡", "璽", "徙", "隙", "餼", "覡", "西", "西"],
|
||||||
|
"ㄒㄧㄚ": ["下", "夏", "嚇", "廈", "峽", "蝦", "瞎", "霞", "轄", "俠", "暇", "遐", "瑕", "匣", "黠", "硤", "罅", "下", "下"],
|
||||||
|
"ㄒㄧㄢ": ["先", "現", "線", "限", "縣", "顯", "險", "鮮", "獻", "賢", "閒", "仙", "鹹", "羨", "陷", "憲", "餡", "掀", "纖", "閑", "涎", "嫻", "銜", "冼", "燹", "蜆", "筧", "薟", "躚", "先", "先"],
|
||||||
|
"ㄒㄧㄤ": ["想", "向", "相", "鄉", "香", "響", "享", "像", "象", "項", "巷", "降", "箱", "祥", "湘", "詳", "翔", "襄", "鑲", "廂", "驤", "薌", "餉", "緗", "嚮", "想", "想"],
|
||||||
|
"ㄒㄧㄠ": ["小", "笑", "效", "消", "校", "銷", "曉", "蕭", "肖", "削", "孝", "宵", "硝", "霄", "淆", "嘯", "驍", "梟", "瀟", "簫", "筱", "嘵", "蟰", "小", "小"],
|
||||||
|
"ㄒㄧㄝ": ["些", "寫", "謝", "協", "鞋", "血", "歇", "斜", "脅", "諧", "攜", "洩", "卸", "懈", "蟹", "邪", "械", "屑", "偕", "褻", "榭", "廨", "瀣", "薤", "躞", "頡", "擷", "些", "些"],
|
||||||
|
"ㄒㄧㄣ": ["新", "心", "信", "辛", "欣", "薪", "馨", "鑫", "芯", "鋅", "昕", "忻", "歆", "鐔", "囟", "新", "新"],
|
||||||
|
"ㄒㄧㄥ": ["行", "星", "形", "性", "姓", "興", "刑", "型", "幸", "杏", "腥", "猩", "邢", "悻", "滎", "餳", "行", "行"],
|
||||||
|
"ㄒㄩ": ["須", "需", "許", "續", "序", "徐", "虛", "緒", "蓄", "敘", "旭", "恤", "墟", "絮", "婿", "栩", "戌", "詡", "洫", "溆", "酗", "糈", "勖", "昫", "盱", "蓿", "須", "須"],
|
||||||
|
"ㄒㄩㄢ": ["選", "宣", "懸", "旋", "玄", "軒", "喧", "炫", "渲", "萱", "漩", "璇", "癬", "煊", "諼", "鋗", "選", "選"],
|
||||||
|
"ㄒㄩㄝ": ["學", "雪", "血", "穴", "謔", "噱", "鱈", "學", "學"],
|
||||||
|
"ㄒㄩㄣ": ["訊", "迅", "尋", "巡", "訓", "詢", "循", "旬", "熏", "勳", "薰", "潯", "馴", "汛", "遜", "殉", "徇", "巽", "塤", "曛", "窯", "鱘", "訊", "訊"],
|
||||||
|
"ㄓㄚ": ["炸", "紮", "查", "渣", "扎", "眨", "柵", "詐", "乍", "榨", "吒", "砟", "蚱", "齇", "鮓", "醡", "炸", "炸"],
|
||||||
|
"ㄓㄞ": ["債", "寨", "齋", "摘", "窄", "翟", "瘵", "齋", "齋"],
|
||||||
|
"ㄓㄢ": ["站", "展", "戰", "佔", "斬", "瞻", "沾", "詹", "盞", "嶄", "湛", "綻", "輾", "搌", "旃", "站", "站"],
|
||||||
|
"ㄓㄤ": ["長", "張", "章", "掌", "丈", "帳", "仗", "脹", "障", "彰", "漳", "璋", "嶂", "幛", "瘴", "鄣", "張", "張"],
|
||||||
|
"ㄓㄠ": ["找", "照", "招", "朝", "趙", "兆", "罩", "肇", "詔", "沼", "爪", "召", "昭", "嘲", "濯", "櫂", "笊", "招", "招"],
|
||||||
|
"ㄓㄜ": ["這", "著", "者", "折", "哲", "蔗", "遮", "轍", "浙", "褶", "蟄", "鷓", "謫", "輒", "晢", "蜇", "這", "這"],
|
||||||
|
"ㄓㄣ": ["真", "針", "鎮", "陣", "珍", "震", "振", "診", "枕", "斟", "甄", "臻", "疹", "砧", "貞", "偵", "軫", "縝", "榛", "楨", "賑", "禎", "畛", "圳", "蓁", "真", "真"],
|
||||||
|
"ㄓㄥ": ["正", "政", "整", "爭", "證", "鄭", "征", "蒸", "掙", "睜", "錚", "崢", "箏", "怔", "拯", "鉦", "幀", "諍", "癥", "正", "正"],
|
||||||
|
"ㄓㄨ": ["主", "住", "注", "著", "助", "築", "逐", "祝", "豬", "珠", "朱", "諸", "竹", "株", "燭", "矚", "駐", "鑄", "煮", "拄", "囑", "佇", "杼", "渚", "瀦", "躅", "櫫", "褚", "苧", "洙", "麈", "瘃", "主", "主"],
|
||||||
|
"ㄓㄨㄚ": ["抓", "爪", "抓"],
|
||||||
|
"ㄓㄨㄞ": ["轉", "拽", "轉"],
|
||||||
|
"ㄓㄨㄢ": ["專", "轉", "傳", "賺", "磚", "撰", "篆", "饌", "顓", "專", "專"],
|
||||||
|
"ㄓㄨㄤ": ["裝", "狀", "莊", "撞", "壯", "幢", "妝", "樁", "裝", "裝"],
|
||||||
|
"ㄓㄨㄟ": ["追", "墜", "綴", "贅", "縋", "惴", "騅", "追", "追"],
|
||||||
|
"ㄓㄨㄣ": ["準", "諄", "肫", "窀", "準", "準"],
|
||||||
|
"ㄓㄨㄛ": ["著", "桌", "捉", "卓", "濁", "灼", "酌", "拙", "琢", "茁", "擢", "倬", "涿", "浞", "禚", "斫", "桌", "桌"],
|
||||||
|
"ㄔㄚ": ["查", "茶", "差", "插", "察", "剎", "叉", "岔", "詫", "差", "差"],
|
||||||
|
"ㄔㄞ": ["差", "拆", "柴", "豺", "差"],
|
||||||
|
"ㄔㄢ": ["產", "纏", "禪", "蟬", "鏟", "闡", "顫", "摻", "潺", "產", "產"],
|
||||||
|
"ㄔㄤ": ["長", "常", "場", "唱", "廠", "昌", "倡", "嘗", "腸", "暢", "償", "長", "長"],
|
||||||
|
"ㄔㄠ": ["超", "朝", "潮", "吵", "炒", "抄", "鈔", "巢", "嘲", "超", "超"],
|
||||||
|
"ㄔㄜ": ["車", "徹", "撤", "扯", "澈", "車", "車"],
|
||||||
|
"ㄔㄣ": ["陳", "晨", "沉", "趁", "襯", "臣", "塵", "辰", "忱", "陳", "陳"],
|
||||||
|
"ㄔㄥ": ["成", "城", "程", "稱", "承", "誠", "乘", "撐", "橙", "呈", "懲", "成", "成"],
|
||||||
|
"ㄔㄨ": ["出", "處", "初", "除", "書", "楚", "觸", "儲", "廚", "畜", "鋤", "出", "出"],
|
||||||
|
"ㄔㄨㄞ": ["揣", "踹", "揣"],
|
||||||
|
"ㄔㄨㄢ": ["傳", "穿", "船", "川", "串", "喘", "釧", "傳", "傳"],
|
||||||
|
"ㄔㄨㄤ": ["床", "窗", "創", "闖", "幢", "床", "床"],
|
||||||
|
"ㄔㄨㄟ": ["吹", "垂", "錘", "捶", "炊", "吹", "吹"],
|
||||||
|
"ㄔㄨㄣ": ["春", "純", "唇", "淳", "醇", "春", "春"],
|
||||||
|
"ㄔㄨㄛ": ["戳", "綽", "輟", "齪", "戳"],
|
||||||
|
"ㄕㄚ": ["殺", "沙", "紗", "傻", "啥", "煞", "莎", "杉", "剎", "砂", "痧", "裟", "鎩", "霎", "殺", "殺"],
|
||||||
|
"ㄕㄞ": ["曬", "篩", "色", "曬", "曬"],
|
||||||
|
"ㄕㄢ": ["山", "善", "閃", "衫", "扇", "杉", "刪", "珊", "柵", "膳", "擅", "贍", "汕", "潸", "姍", "煽", "跚", "訕", "疝", "鱔", "山", "山"],
|
||||||
|
"ㄕㄤ": ["上", "商", "傷", "尚", "賞", "裳", "熵", "觴", "殤", "垧", "上", "上"],
|
||||||
|
"ㄕㄠ": ["少", "燒", "紹", "稍", "勺", "哨", "韶", "捎", "梢", "芍", "苕", "蛸", "筲", "少", "少"],
|
||||||
|
"ㄕㄜ": ["社", "設", "射", "蛇", "舌", "捨", "涉", "赦", "攝", "奢", "賒", "麝", "懾", "灄", "社", "社"],
|
||||||
|
"ㄕㄣ": ["身", "深", "神", "什", "申", "伸", "審", "慎", "腎", "滲", "沈", "參", "甚", "嬸", "砷", "莘", "哂", "瀋", "糝", "身", "身"],
|
||||||
|
"ㄕㄥ": ["生", "聲", "勝", "升", "省", "聖", "盛", "剩", "繩", "笙", "甥", "晟", "生", "生"],
|
||||||
|
"ㄕㄨ": ["書", "數", "樹", "輸", "術", "述", "叔", "屬", "暑", "署", "鼠", "束", "疏", "舒", "淑", "梳", "抒", "殊", "蔬", "孰", "贖", "熟", "恕", "庶", "墅", "俞", "澍", "紓", "倏", "毹", "書", "書"],
|
||||||
|
"ㄕㄨㄚ": ["刷", "耍", "唰", "刷", "刷"],
|
||||||
|
"ㄕㄨㄞ": ["帥", "率", "摔", "甩", "蟀", "帥", "帥"],
|
||||||
|
"ㄕㄨㄢ": ["栓", "拴", "閂", "涮", "栓", "栓"],
|
||||||
|
"ㄕㄨㄤ": ["雙", "爽", "霜", "孀", "雙", "雙"],
|
||||||
|
"ㄕㄨㄟ": ["水", "說", "稅", "睡", "誰", "水", "水"],
|
||||||
|
"ㄕㄨㄣ": ["順", "瞬", "舜", "吮", "順", "順"],
|
||||||
|
"ㄕㄨㄛ": ["說", "數", "碩", "朔", "爍", "鑠", "蒴", "搠", "說", "說"],
|
||||||
|
"ㄖㄢ": ["然", "燃", "染", "冉", "髯", "蚺", "然", "然"],
|
||||||
|
"ㄖㄤ": ["讓", "嚷", "壤", "攘", "穰", "瓤", "讓", "讓"],
|
||||||
|
"ㄖㄠ": ["擾", "繞", "饒", "嬈", "橈", "蕘", "擾", "擾"],
|
||||||
|
"ㄖㄜ": ["熱", "惹", "喏", "熱", "熱"],
|
||||||
|
"ㄖㄣ": ["人", "認", "任", "仁", "忍", "刃", "韌", "紉", "妊", "葚", "稔", "人", "人"],
|
||||||
|
"ㄖㄥ": ["仍", "扔", "仍", "仍"],
|
||||||
|
"ㄖㄨ": ["如", "入", "儒", "乳", "辱", "孺", "茹", "蠕", "嚅", "濡", "縟", "洳", "如", "如"],
|
||||||
|
"ㄖㄨㄢ": ["軟", "阮", "軟", "軟"],
|
||||||
|
"ㄖㄨㄟ": ["瑞", "銳", "蕊", "芮", "蚋", "枘", "瑞", "瑞"],
|
||||||
|
"ㄖㄨㄣ": ["潤", "閏", "潤", "潤"],
|
||||||
|
"ㄖㄨㄛ": ["若", "弱", "偌", "箬", "蒻", "若", "若"],
|
||||||
|
"ㄗㄚ": ["雜", "砸", "咂", "拶", "雜", "雜"],
|
||||||
|
"ㄗㄞ": ["在", "再", "載", "災", "宰", "栽", "崽", "哉", "在", "在"],
|
||||||
|
"ㄗㄢ": ["咱", "讚", "暫", "拶", "昝", "簪", "糌", "咱", "咱"],
|
||||||
|
"ㄗㄤ": ["藏", "臟", "葬", "臧", "奘", "駔", "臟", "臟"],
|
||||||
|
"ㄗㄠ": ["早", "造", "遭", "燥", "澡", "藻", "棗", "躁", "鑿", "蚤", "皁", "竈", "早", "早"],
|
||||||
|
"ㄗㄜ": ["則", "責", "擇", "澤", "側", "仄", "迮", "幘", "賾", "箦", "則", "則"],
|
||||||
|
"ㄗㄟ": ["賊", "賊", "賊"],
|
||||||
|
"ㄗㄣ": ["怎", "譖", "怎", "怎"],
|
||||||
|
"ㄗㄥ": ["增", "贈", "憎", "甑", "繒", "罾", "增", "增"],
|
||||||
|
"ㄗㄨ": ["租", "族", "組", "阻", "卒", "俎", "詛", "菹", "祖", "祖"],
|
||||||
|
"ㄗㄨㄢ": ["鑽", "纂", "攢", "繵", "躜", "鑽", "鑽"],
|
||||||
|
"ㄗㄨㄟ": ["最", "罪", "嘴", "醉", "蕞", "最", "最"],
|
||||||
|
"ㄗㄨㄣ": ["尊", "遵", "樽", "撙", "尊", "尊"],
|
||||||
|
"ㄗㄨㄛ": ["做", "作", "座", "左", "昨", "佐", "琢", "撮", "唑", "嘬", "怍", "祚", "胙", "做", "做"],
|
||||||
|
"ㄘㄚ": ["擦", "嚓", "擦", "擦"],
|
||||||
|
"ㄘㄞ": ["才", "材", "才", "財", "采", "彩", "菜", "猜", "裁", "踩", "才", "才"],
|
||||||
|
"ㄘㄢ": ["參", "餐", "殘", "慘", "燦", "蠶", "參", "參"],
|
||||||
|
"ㄘㄤ": ["藏", "倉", "蒼", "艙", "藏", "藏"],
|
||||||
|
"ㄘㄠ": ["草", "操", "曹", "糙", "槽", "草", "草"],
|
||||||
|
"ㄘㄜ": ["策", "測", "側", "廁", "冊", "策", "策"],
|
||||||
|
"ㄘㄥ": ["層", "曾", "蹭", "層", "層"],
|
||||||
|
"ㄘㄨ": ["粗", "促", "醋", "簇", "猝", "粗", "粗"],
|
||||||
|
"ㄘㄨㄢ": ["竄", "攢", "篡", "竄", "竄"],
|
||||||
|
"ㄘㄨㄟ": ["催", "脆", "翠", "粹", "崔", "淬", "萃", "催", "催"],
|
||||||
|
"ㄘㄨㄣ": ["村", "存", "寸", "磋", "村", "村"],
|
||||||
|
"ㄘㄨㄛ": ["錯", "措", "搓", "磋", "挫", "錯", "錯"],
|
||||||
|
"ㄙㄚ": ["撒", "灑", "薩", "卅", "颯", "撒", "撒"],
|
||||||
|
"ㄙㄞ": ["賽", "塞", "腮", "鰓", "噻", "賽", "賽"],
|
||||||
|
"ㄙㄢ": ["三", "散", "傘", "參", "霰", "三", "三"],
|
||||||
|
"ㄙㄤ": ["喪", "桑", "嗓", "顙", "搡", "喪", "喪"],
|
||||||
|
"ㄙㄠ": ["掃", "嫂", "騷", "搔", "瘙", "繅", "掃", "掃"],
|
||||||
|
"ㄙㄜ": ["色", "塞", "瑟", "澀", "嗇", "穡", "色", "色"],
|
||||||
|
"ㄙㄣ": ["森", "森", "森"],
|
||||||
|
"ㄙㄥ": ["僧", "僧", "僧"],
|
||||||
|
"ㄙㄨ": ["速", "素", "蘇", "訴", "俗", "塑", "溯", "宿", "粟", "夙", "簌", "愫", "嗉", "謖", "速", "速"],
|
||||||
|
"ㄙㄨㄢ": ["算", "酸", "蒜", "狻", "算", "算"],
|
||||||
|
"ㄙㄨㄟ": ["隨", "歲", "雖", "碎", "遂", "穗", "隧", "髓", "祟", "綏", "邃", "燧", "謁", "隨", "隨"],
|
||||||
|
"ㄙㄨㄣ": ["損", "孫", "筍", "遜", "榫", "蓀", "猻", "損", "損"],
|
||||||
|
"ㄙㄨㄛ": ["所", "鎖", "索", "縮", "瑣", "嗦", "唆", "梭", "嗩", "娑", "蓑", "所", "所"],
|
||||||
|
"ㄧㄚ": ["呀", "壓", "牙", "亞", "雅", "鴨", "押", "芽", "涯", "訝", "崖", "啞", "衙", "軋", "蚜", "睚", "痖", "呀", "呀"],
|
||||||
|
"ㄧㄞ": ["涯", "崖", "睚", "涯"],
|
||||||
|
"ㄧㄢ": ["言", "研", "眼", "嚴", "演", "驗", "煙", "顏", "鹽", "延", "沿", "燕", "宴", "炎", "掩", "衍", "岩", "艷", "雁", "焰", "厭", "彥", "諺", "堰", "硯", "嫣", "閻", "焉", "淹", "偃", "儼", "兗", "讌", "讞", "筵", "蜓", "鼴", "罨", "剡", "鄢", "閆", "滟", "妍", "琰", "罳", "言", "言"],
|
||||||
|
"ㄧㄤ": ["樣", "陽", "洋", "養", "央", "揚", "羊", "氧", "仰", "癢", "漾", "殃", "秧", "恙", "颺", "煬", "佯", "瘍", "鞅", "樣", "樣"],
|
||||||
|
"ㄧㄠ": ["要", "藥", "搖", "遙", "腰", "邀", "耀", "瑤", "姚", "咬", "堯", "鑰", "謠", "夭", "妖", "窯", "杳", "舀", "徭", "珧", "軺", "銚", "鰩", "么", "瘧", "要", "要"],
|
||||||
|
"ㄧㄝ": ["也", "業", "夜", "葉", "爺", "野", "液", "謁", "頁", "邪", "掖", "曳", "腋", "噎", "鄴", "曄", "燁", "鐺", "也", "也"],
|
||||||
|
"ㄧㄣ": ["因", "音", "引", "銀", "印", "飲", "隱", "陰", "吟", "尹", "殷", "茵", "蔭", "垠", "夤", "齦", "湮", "氤", "胤", "鄞", "喑", "洇", "狺", "因", "因"],
|
||||||
|
"ㄧㄥ": ["應", "英", "營", "迎", "影", "贏", "硬", "映", "盈", "穎", "瑩", "鷹", "嬰", "櫻", "瀛", "蠅", "嬴", "罌", "縈", "楹", "熒", "螢", "瀅", "瓔", "鸚", "膺", "瀠", "應", "應"],
|
||||||
|
"ㄨㄚ": ["挖", "哇", "蛙", "瓦", "娃", "襪", "凹", "媧", "佤", "腽", "挖", "挖"],
|
||||||
|
"ㄨㄞ": ["外", "歪", "崴", "外", "外"],
|
||||||
|
"ㄨㄢ": ["完", "晚", "玩", "碗", "彎", "灣", "丸", "婉", "腕", "惋", "宛", "蜿", "豌", "莞", "綰", "剜", "完", "完"],
|
||||||
|
"ㄨㄤ": ["王", "往", "忘", "亡", "望", "網", "旺", "汪", "妄", "罔", "惘", "輞", "尪", "王", "王"],
|
||||||
|
"ㄨㄟ": ["為", "位", "未", "委", "圍", "唯", "威", "偉", "危", "尾", "微", "維", "違", "胃", "餵", "味", "慰", "魏", "衛", "畏", "萎", "偽", "娓", "惟", "巍", "緯", "煒", "韋", "薇", "帷", "渭", "猬", "闈", "洧", "沩", "為", "為"],
|
||||||
|
"ㄨㄣ": ["問", "文", "聞", "溫", "穩", "紋", "吻", "蚊", "雯", "紊", "刎", "璺", "問", "問"],
|
||||||
|
"ㄨㄥ": ["翁", "嗡", "甕", "蓊", "翁", "翁"],
|
||||||
|
"ㄩㄢ": ["元", "原", "員", "圓", "院", "源", "遠", "願", "緣", "園", "怨", "冤", "援", "袁", "淵", "猿", "轅", "媛", "垣", "沅", "塬", "圜", "鴛", "鳶", "螈", "爰", "瑗", "掾", "元", "元"],
|
||||||
|
"ㄩㄝ": ["月", "約", "越", "樂", "曰", "閱", "躍", "悅", "岳", "粵", "鑰", "櫟", "鉞", "瀹", "龠", "刖", "軏", "月", "月"],
|
||||||
|
"ㄩㄣ": ["雲", "運", "員", "韻", "勻", "允", "孕", "蘊", "暈", "隕", "耘", "紜", "慍", "殞", "惲", "醞", "狁", "鄖", "雲", "雲"],
|
||||||
|
"ㄦ": ["二", "兒", "耳", "而", "爾", "餌", "洱", "貳", "兒", "兒"]
|
||||||
|
}
|
||||||
|
}
|
||||||
48267
CustomKeyboard/Resource/english_words.json
Normal file
48267
CustomKeyboard/Resource/english_words.json
Normal file
File diff suppressed because it is too large
Load Diff
50005
CustomKeyboard/Resource/indonesian_words.json
Normal file
50005
CustomKeyboard/Resource/indonesian_words.json
Normal file
File diff suppressed because it is too large
Load Diff
80
CustomKeyboard/Resource/kb_diacritics_map.json
Normal file
80
CustomKeyboard/Resource/kb_diacritics_map.json
Normal 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", "ü", "ǖ", "ǘ", "ǚ", "ǜ"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -189,9 +189,321 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"__comment": "字母第二行 asdfghjkl",
|
"__comment": "字母第二行 asdfghjkl",
|
||||||
"align": "center",
|
"align": "left",
|
||||||
"__comment_align": "对齐方式:left/center",
|
"__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)",
|
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset)",
|
||||||
"insetRight": 0,
|
"insetRight": 0,
|
||||||
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset)",
|
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset)",
|
||||||
@@ -409,6 +721,228 @@
|
|||||||
"__comment_items": "本行按键列表;letter:x/digit:x/sym:x 或 keyDefs 中的 id"
|
"__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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1654
CustomKeyboard/Resource/kb_keyboard_layouts_i18n.json
Normal file
1654
CustomKeyboard/Resource/kb_keyboard_layouts_i18n.json
Normal file
File diff suppressed because it is too large
Load Diff
405
CustomKeyboard/Resource/pinyin_to_traditional.json
Normal file
405
CustomKeyboard/Resource/pinyin_to_traditional.json
Normal 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": ["做", "作", "座", "左", "昨", "佐", "琢", "撮", "唑", "嘬", "怍", "祚", "胙"]
|
||||||
|
}
|
||||||
|
}
|
||||||
42269
CustomKeyboard/Resource/portuguese_words.json
Normal file
42269
CustomKeyboard/Resource/portuguese_words.json
Normal file
File diff suppressed because it is too large
Load Diff
54839
CustomKeyboard/Resource/spanish_words.json
Normal file
54839
CustomKeyboard/Resource/spanish_words.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -392,6 +392,7 @@ typedef NS_ENUM(NSInteger, KBClearPhase) {
|
|||||||
[[KBInputBufferManager shared] clearPendingClearSnapshot];
|
[[KBInputBufferManager shared] clearPendingClearSnapshot];
|
||||||
[[KBInputBufferManager shared] commitLiveToManual];
|
[[KBInputBufferManager shared] commitLiveToManual];
|
||||||
}
|
}
|
||||||
|
[self kb_refreshSuggestionsAfterLongPressClear:shouldClear];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Clear Label
|
#pragma mark - Clear Label
|
||||||
@@ -499,6 +500,7 @@ typedef NS_ENUM(NSInteger, KBClearPhase) {
|
|||||||
#pragma mark - Clear
|
#pragma mark - Clear
|
||||||
|
|
||||||
- (void)kb_clearAllInput {
|
- (void)kb_clearAllInput {
|
||||||
|
[self kb_clearCurrentWordIfPossible];
|
||||||
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
if (ivc) {
|
if (ivc) {
|
||||||
@@ -659,4 +661,36 @@ typedef NS_ENUM(NSInteger, KBClearPhase) {
|
|||||||
#endif
|
#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
|
@end
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// KBExtensionAppLauncher.h
|
// KBExtensionAppLauncher.h
|
||||||
// CustomKeyboard
|
// CustomKeyboard
|
||||||
//
|
//
|
||||||
// 封装:在键盘扩展中拉起主 App(Scheme / Universal Link + 响应链兜底)。
|
// 封装:在键盘扩展中拉起主 App(Scheme / Universal Link)。
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
@@ -12,23 +12,24 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@interface KBExtensionAppLauncher : NSObject
|
@interface KBExtensionAppLauncher : NSObject
|
||||||
|
|
||||||
/// 通用入口:优先尝试 primaryURL,失败后尝试 fallbackURL,
|
/// 通用入口:优先尝试 primaryURL,失败后尝试 fallbackURL,
|
||||||
/// 两者都失败时再通过响应链(openURL:)做兜底。
|
/// 均通过 `extensionContext openURL` 发起跳转(避免使用扩展禁用 API/响应链绕行)。
|
||||||
|
/// 若开启 `KB_URL_BRIDGE_ENABLE=1`,会在两次 `extensionContext openURL` 均失败时,
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - primaryURL: 第一优先尝试的 URL(可为 Scheme 或 UL)
|
/// - primaryURL: 第一优先尝试的 URL(可为 Scheme 或 UL)
|
||||||
/// - fallbackURL: 失败时的备用 URL(可为 nil)
|
/// - fallbackURL: 失败时的备用 URL(可为 nil)
|
||||||
/// - ivc: 当前的 UIInputViewController(用于 extensionContext openURL)
|
/// - ivc: 当前的 UIInputViewController(用于 extensionContext openURL)
|
||||||
/// - source: 兜底时用作起点的 responder(通常传 self 或 self.view)
|
/// - source: 作为响应链兜底的起点(可为 nil)
|
||||||
/// - completion: 最终是否“看起来已成功发起”打开动作(不保证一定跳转到 App)
|
/// - completion: 最终是否“看起来已成功发起”打开动作(不保证一定跳转到 App)
|
||||||
+ (void)openPrimaryURL:(NSURL * _Nullable)primaryURL
|
+ (void)openPrimaryURL:(NSURL * _Nullable)primaryURL
|
||||||
fallbackURL:(NSURL * _Nullable)fallbackURL
|
fallbackURL:(NSURL * _Nullable)fallbackURL
|
||||||
usingInputController:(UIInputViewController *)ivc
|
usingInputController:(UIInputViewController *)ivc
|
||||||
source:(UIResponder *)source
|
source:(UIResponder * _Nullable)source
|
||||||
completion:(void (^ _Nullable)(BOOL success))completion;
|
completion:(void (^ _Nullable)(BOOL success))completion;
|
||||||
|
|
||||||
/// 简化版:只针对单一 Scheme 做尝试 + 响应链兜底。
|
/// 简化版:只针对单一 Scheme 做尝试。
|
||||||
+ (void)openScheme:(NSURL *)scheme
|
+ (void)openScheme:(NSURL *)scheme
|
||||||
usingInputController:(UIInputViewController *)ivc
|
usingInputController:(UIInputViewController *)ivc
|
||||||
source:(UIResponder *)source
|
source:(UIResponder * _Nullable)source
|
||||||
completion:(void (^ _Nullable)(BOOL success))completion;
|
completion:(void (^ _Nullable)(BOOL success))completion;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -4,15 +4,86 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "KBExtensionAppLauncher.h"
|
#import "KBExtensionAppLauncher.h"
|
||||||
|
|
||||||
|
#if KB_URL_BRIDGE_ENABLE
|
||||||
#import <objc/message.h>
|
#import <objc/message.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
@implementation KBExtensionAppLauncher
|
@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
|
+ (void)openPrimaryURL:(NSURL * _Nullable)primaryURL
|
||||||
fallbackURL:(NSURL * _Nullable)fallbackURL
|
fallbackURL:(NSURL * _Nullable)fallbackURL
|
||||||
usingInputController:(UIInputViewController *)ivc
|
usingInputController:(UIInputViewController *)ivc
|
||||||
source:(UIResponder *)source
|
source:(UIResponder * _Nullable)source
|
||||||
completion:(void (^ _Nullable)(BOOL success))completion {
|
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 (!ivc || (!primaryURL && !fallbackURL)) {
|
||||||
if (completion) { completion(NO); }
|
if (completion) { completion(NO); }
|
||||||
return;
|
return;
|
||||||
@@ -48,19 +119,37 @@
|
|||||||
finish(YES);
|
finish(YES);
|
||||||
return;
|
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 {
|
} else {
|
||||||
BOOL bridged = [self p_bridgeFirst:first second:nil from:source];
|
#if KB_URL_BRIDGE_ENABLE
|
||||||
finish(bridged);
|
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
|
+ (void)openScheme:(NSURL *)scheme
|
||||||
usingInputController:(UIInputViewController *)ivc
|
usingInputController:(UIInputViewController *)ivc
|
||||||
source:(UIResponder *)source
|
source:(UIResponder * _Nullable)source
|
||||||
completion:(void (^ _Nullable)(BOOL success))completion {
|
completion:(void (^ _Nullable)(BOOL success))completion {
|
||||||
[self openPrimaryURL:scheme
|
[self openPrimaryURL:scheme
|
||||||
fallbackURL:nil
|
fallbackURL:nil
|
||||||
@@ -69,53 +158,4 @@
|
|||||||
completion:completion];
|
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
|
@end
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
if (completion) {
|
if (completion) {
|
||||||
KBChatResponse *response = [[KBChatResponse alloc] init];
|
KBChatResponse *response = [[KBChatResponse alloc] init];
|
||||||
response.success = NO;
|
response.success = NO;
|
||||||
response.message = @"内容为空";
|
response.message = KBLocalized(@"Content is empty");
|
||||||
completion(response);
|
completion(response);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
if (completion) {
|
if (completion) {
|
||||||
KBAudioResponse *response = [[KBAudioResponse alloc] init];
|
KBAudioResponse *response = [[KBAudioResponse alloc] init];
|
||||||
response.success = NO;
|
response.success = NO;
|
||||||
response.errorMessage = @"audioId 为空";
|
response.errorMessage = KBLocalized(@"audioId is empty");
|
||||||
completion(response);
|
completion(response);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
// 达到最大重试次数
|
// 达到最大重试次数
|
||||||
KBAudioResponse *failResponse = [[KBAudioResponse alloc] init];
|
KBAudioResponse *failResponse = [[KBAudioResponse alloc] init];
|
||||||
failResponse.success = NO;
|
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);
|
if (completion) completion(failResponse);
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
if (completion) {
|
if (completion) {
|
||||||
KBAudioResponse *response = [[KBAudioResponse alloc] init];
|
KBAudioResponse *response = [[KBAudioResponse alloc] init];
|
||||||
response.success = NO;
|
response.success = NO;
|
||||||
response.errorMessage = @"URL 为空";
|
response.errorMessage = KBLocalized(@"URL is empty");
|
||||||
completion(response);
|
completion(response);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
|
|
||||||
if (error || !data || data.length == 0) {
|
if (error || !data || data.length == 0) {
|
||||||
audioResponse.success = NO;
|
audioResponse.success = NO;
|
||||||
audioResponse.errorMessage = error.localizedDescription ?: @"下载失败";
|
audioResponse.errorMessage = error.localizedDescription ?: KBLocalized(@"Download failed");
|
||||||
if (completion) completion(audioResponse);
|
if (completion) completion(audioResponse);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@optional
|
@optional
|
||||||
- (void)subscriptionViewDidTapClose:(KBKeyboardSubscriptionView *)view;
|
- (void)subscriptionViewDidTapClose:(KBKeyboardSubscriptionView *)view;
|
||||||
- (void)subscriptionView:(KBKeyboardSubscriptionView *)view didTapPurchaseForProduct:(KBKeyboardSubscriptionProduct *)product;
|
- (void)subscriptionView:(KBKeyboardSubscriptionView *)view didTapPurchaseForProduct:(KBKeyboardSubscriptionProduct *)product;
|
||||||
|
- (void)subscriptionViewDidTapAgreement:(KBKeyboardSubscriptionView *)view;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
/// 键盘内的订阅弹层
|
/// 键盘内的订阅弹层
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ static id KBKeyboardSubscriptionSanitizeJSON(id obj) {
|
|||||||
|
|
||||||
- (void)setupFeatureItems {
|
- (void)setupFeatureItems {
|
||||||
NSArray *titles = @[
|
NSArray *titles = @[
|
||||||
KBLocalized(@"Wireless Sub-ai\nDialogue"),
|
KBLocalized(@"Wireless Sub-ai Dialogue"),
|
||||||
KBLocalized(@"Personalized\nKeyboard"),
|
KBLocalized(@"Personalized\nKeyboard"),
|
||||||
KBLocalized(@"Chat\nPersona"),
|
KBLocalized(@"Chat\nPersona"),
|
||||||
KBLocalized(@"Emotional\nCounseling")
|
KBLocalized(@"Emotional\nCounseling")
|
||||||
@@ -192,7 +192,11 @@ static id KBKeyboardSubscriptionSanitizeJSON(id obj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)onTapAgreement {
|
- (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
|
#pragma mark - Data
|
||||||
@@ -200,7 +204,7 @@ static id KBKeyboardSubscriptionSanitizeJSON(id obj) {
|
|||||||
- (void)fetchProducts {
|
- (void)fetchProducts {
|
||||||
if (self.isLoading) { return; }
|
if (self.isLoading) { return; }
|
||||||
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
||||||
[KBHUD showInfo:KBLocalized(@"Enable Full Access to continue")];
|
[KBHUD showInfo:KBLocalized(@"Please enable Full Access to continue")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.loading = YES;
|
self.loading = YES;
|
||||||
@@ -405,7 +409,7 @@ static id KBKeyboardSubscriptionSanitizeJSON(id obj) {
|
|||||||
- (UILabel *)agreementLabel {
|
- (UILabel *)agreementLabel {
|
||||||
if (!_agreementLabel) {
|
if (!_agreementLabel) {
|
||||||
_agreementLabel = [[UILabel alloc] init];
|
_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.font = [UIFont systemFontOfSize:11];
|
||||||
_agreementLabel.textColor = [UIColor colorWithHex:0x4A4A4A];
|
_agreementLabel.textColor = [UIColor colorWithHex:0x4A4A4A];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ static const NSUInteger kKBChatMessageLimit = 10;
|
|||||||
|
|
||||||
// 创建 AI 消息
|
// 创建 AI 消息
|
||||||
KBChatMessage *msg = [KBChatMessage assistantMessageWithText:text audioId:audioId];
|
KBChatMessage *msg = [KBChatMessage assistantMessageWithText:text audioId:audioId];
|
||||||
msg.displayName = KBLocalized(@"AI助手");
|
msg.displayName = KBLocalized(@"AI Assistant");
|
||||||
NSLog(@"[Panel] 创建 AI 消息,needsTypewriter: %d", msg.needsTypewriterEffect);
|
NSLog(@"[Panel] 创建 AI 消息,needsTypewriter: %d", msg.needsTypewriterEffect);
|
||||||
|
|
||||||
// 使用批量更新,避免界面跳动
|
// 使用批量更新,避免界面跳动
|
||||||
@@ -323,7 +323,7 @@ static const NSUInteger kKBChatMessageLimit = 10;
|
|||||||
_titleLabel.textColor =
|
_titleLabel.textColor =
|
||||||
[UIColor kb_dynamicColorWithLightColor:[UIColor colorWithHex:0x1B1F1A]
|
[UIColor kb_dynamicColorWithLightColor:[UIColor colorWithHex:0x1B1F1A]
|
||||||
darkColor:[UIColor whiteColor]];
|
darkColor:[UIColor whiteColor]];
|
||||||
_titleLabel.text = KBLocalized(@"AI对话");
|
_titleLabel.text = KBLocalized(@"AI Chat");
|
||||||
}
|
}
|
||||||
return _titleLabel;
|
return _titleLabel;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@protocol KBEmojiPanelViewDelegate <NSObject>
|
@protocol KBEmojiPanelViewDelegate <NSObject>
|
||||||
- (void)emojiPanelView:(KBEmojiPanelView *)panel didSelectEmoji:(NSString *)emoji;
|
- (void)emojiPanelView:(KBEmojiPanelView *)panel didSelectEmoji:(NSString *)emoji;
|
||||||
- (void)emojiPanelViewDidRequestClose:(KBEmojiPanelView *)panel;
|
- (void)emojiPanelViewDidRequestClose:(KBEmojiPanelView *)panel;
|
||||||
- (void)emojiPanelViewDidTapSearch:(KBEmojiPanelView *)panel;
|
|
||||||
@optional
|
@optional
|
||||||
- (void)emojiPanelViewDidTapDelete:(KBEmojiPanelView *)panel;
|
- (void)emojiPanelViewDidTapDelete:(KBEmojiPanelView *)panel;
|
||||||
@end
|
@end
|
||||||
@@ -30,6 +29,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
/// 高亮指定分类
|
/// 高亮指定分类
|
||||||
- (void)selectCategoryAtIndex:(NSInteger)index;
|
- (void)selectCategoryAtIndex:(NSInteger)index;
|
||||||
|
|
||||||
|
/// 释放 emoji 数据缓存(隐藏面板时可用)
|
||||||
|
- (void)purgeEmojiCache;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
@property (nonatomic, strong) UIButton *backButton;
|
@property (nonatomic, strong) UIButton *backButton;
|
||||||
@property (nonatomic, strong) UICollectionView *collectionView;
|
@property (nonatomic, strong) UICollectionView *collectionView;
|
||||||
@property (nonatomic, strong) KBEmojiBottomBarView *bottomBar;
|
@property (nonatomic, strong) KBEmojiBottomBarView *bottomBar;
|
||||||
//@property (nonatomic, strong) UIButton *searchButton;
|
|
||||||
@property (nonatomic, strong) NSArray<UIButton *> *tabButtons;
|
@property (nonatomic, strong) NSArray<UIButton *> *tabButtons;
|
||||||
@property (nonatomic, strong) KBEmojiDataProvider *dataProvider;
|
@property (nonatomic, strong) KBEmojiDataProvider *dataProvider;
|
||||||
@property (nonatomic, copy) NSArray<KBEmojiCategory *> *categories;
|
@property (nonatomic, copy) NSArray<KBEmojiCategory *> *categories;
|
||||||
@@ -100,14 +99,6 @@
|
|||||||
[self addSubview:self.bottomBar];
|
[self addSubview:self.bottomBar];
|
||||||
[self.bottomBar.deleteButton addTarget:self action:@selector(onDelete) forControlEvents:UIControlEventTouchUpInside];
|
[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) {
|
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.equalTo(self.mas_left).offset(12);
|
make.left.equalTo(self.mas_left).offset(12);
|
||||||
make.right.equalTo(self.mas_right).offset(-12);
|
make.right.equalTo(self.mas_right).offset(-12);
|
||||||
@@ -185,6 +176,15 @@
|
|||||||
[self updateSelectionToIndex:preserved];
|
[self updateSelectionToIndex:preserved];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)purgeEmojiCache {
|
||||||
|
[self.dataProvider purgeLargeCaches];
|
||||||
|
self.categories = @[];
|
||||||
|
self.currentIndex = NSNotFound;
|
||||||
|
self.titleLabel.text = @"";
|
||||||
|
[self rebuildTabButtons];
|
||||||
|
[self.collectionView reloadData];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)rebuildTabButtons {
|
- (void)rebuildTabButtons {
|
||||||
UIStackView *stackView = self.bottomBar.tabStackView;
|
UIStackView *stackView = self.bottomBar.tabStackView;
|
||||||
if (!stackView) { return; }
|
if (!stackView) { return; }
|
||||||
@@ -260,12 +260,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onSearch {
|
|
||||||
if ([self.delegate respondsToSelector:@selector(emojiPanelViewDidTapSearch:)]) {
|
|
||||||
[self.delegate emojiPanelViewDidTapSearch:self];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)onDelete {
|
- (void)onDelete {
|
||||||
if ([self.delegate respondsToSelector:@selector(emojiPanelViewDidTapDelete:)]) {
|
if ([self.delegate respondsToSelector:@selector(emojiPanelViewDidTapDelete:)]) {
|
||||||
[self.delegate emojiPanelViewDidTapDelete:self];
|
[self.delegate emojiPanelViewDidTapDelete:self];
|
||||||
@@ -294,7 +288,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)onLocalizationChanged {
|
- (void)onLocalizationChanged {
|
||||||
// [self.searchButton setTitle:KBLocalized(@"Search") forState:UIControlStateNormal];
|
|
||||||
[self reloadData];
|
[self reloadData];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,8 +298,6 @@
|
|||||||
self.backgroundColor = bg;
|
self.backgroundColor = bg;
|
||||||
self.collectionView.backgroundColor = [UIColor clearColor];
|
self.collectionView.backgroundColor = [UIColor clearColor];
|
||||||
self.titleLabel.textColor = theme.keyTextColor ?: [UIColor whiteColor];
|
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.tabNormalColor = [UIColor colorWithWhite:1 alpha:0.08];
|
||||||
self.tabSelectedColor = theme.accentColor ?: [UIColor colorWithWhite:1 alpha:0.25];
|
self.tabSelectedColor = theme.accentColor ?: [UIColor colorWithWhite:1 alpha:0.25];
|
||||||
[self updateTabHighlightStates];
|
[self updateTabHighlightStates];
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
self.audioLabel.textColor = textColor;
|
self.audioLabel.textColor = textColor;
|
||||||
self.audioIconView.tintColor = textColor;
|
self.audioIconView.tintColor = textColor;
|
||||||
self.audioLabel.text =
|
self.audioLabel.text =
|
||||||
(message.text.length > 0) ? message.text : KBLocalized(@"语音回复");
|
(message.text.length > 0) ? message.text : KBLocalized(@"Voice reply");
|
||||||
self.messageLabel.hidden = audioMessage;
|
self.messageLabel.hidden = audioMessage;
|
||||||
self.audioIconView.hidden = !audioMessage;
|
self.audioIconView.hidden = !audioMessage;
|
||||||
self.audioLabel.hidden = !audioMessage;
|
self.audioLabel.hidden = !audioMessage;
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
self.nameLabel.hidden = outgoing;
|
self.nameLabel.hidden = outgoing;
|
||||||
self.nameLabel.textColor = nameColor;
|
self.nameLabel.textColor = nameColor;
|
||||||
self.nameLabel.text =
|
self.nameLabel.text =
|
||||||
(message.displayName.length > 0) ? message.displayName : KBLocalized(@"AI助手");
|
(message.displayName.length > 0) ? message.displayName : KBLocalized(@"AI Assistant");
|
||||||
|
|
||||||
// 处理 loading 状态
|
// 处理 loading 状态
|
||||||
if (message.isLoading && !outgoing) {
|
if (message.isLoading && !outgoing) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#import "Masonry.h"
|
#import "Masonry.h"
|
||||||
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
|
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
|
||||||
#import "KBHUD.h"
|
#import "KBHUD.h"
|
||||||
#import "KBHostAppLauncher.h"
|
#import "../Utils/KBExtensionAppLauncher.h"
|
||||||
|
|
||||||
@interface KBFullAccessGuideView ()
|
@interface KBFullAccessGuideView ()
|
||||||
@property (nonatomic, strong) UIControl *backdrop;
|
@property (nonatomic, strong) UIControl *backdrop;
|
||||||
@@ -159,18 +159,34 @@
|
|||||||
// 工具方法已提取到 KBResponderUtils.h
|
// 工具方法已提取到 KBResponderUtils.h
|
||||||
// 打开主 App,引导用户去系统设置开启完全访问:通过宿主 UIApplication + 自定义 Scheme 拉起。
|
// 打开主 App,引导用户去系统设置开启完全访问:通过宿主 UIApplication + 自定义 Scheme 拉起。
|
||||||
- (void)onTapGoEnable {
|
- (void)onTapGoEnable {
|
||||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
UIInputViewController *ivc = self.ivc ?: KBFindInputViewController(self);
|
||||||
// 找不到键盘控制器也可以尝试从自身 responder 链出发
|
if (!ivc) {
|
||||||
UIResponder *start = ivc.view ?: (UIResponder *)self;
|
NSString *showInfo = [NSString stringWithFormat:KBLocalized(@"Follow: Settings -> General -> Keyboard -> Keyboards -> %@ -> Allow Full Access"), AppName];
|
||||||
|
|
||||||
// 自定义 Scheme(AppDelegate 中处理 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];
|
|
||||||
[KBHUD showInfo:showInfo];
|
[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
|
@end
|
||||||
|
|||||||
@@ -60,7 +60,6 @@
|
|||||||
// if (!_placeholderLabelInternal) {
|
// if (!_placeholderLabelInternal) {
|
||||||
// _placeholderLabelInternal = [[UILabel alloc] init];
|
// _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.textColor = [UIColor colorWithRed:0.20 green:0.64 blue:0.54 alpha:1.0];
|
||||||
// _placeholderLabelInternal.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
// _placeholderLabelInternal.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||||||
// }
|
// }
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
#import "KBFunctionPasteView.h"
|
#import "KBFunctionPasteView.h"
|
||||||
#import "KBFunctionTagCell.h"
|
#import "KBFunctionTagCell.h"
|
||||||
#import "KBFunctionTagListView.h"
|
#import "KBFunctionTagListView.h"
|
||||||
#import "KBHostAppLauncher.h"
|
#import "KBExtensionAppLauncher.h"
|
||||||
#import "KBInputBufferManager.h"
|
#import "KBInputBufferManager.h"
|
||||||
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
|
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
|
||||||
#import "KBSignUtils.h"
|
#import "KBSignUtils.h"
|
||||||
@@ -142,76 +142,26 @@
|
|||||||
|
|
||||||
/// 整体背景色:暗黑 #323232,浅色 #D0D3DA
|
/// 整体背景色:暗黑 #323232,浅色 #D0D3DA
|
||||||
+ (UIColor *)kb_backgroundColor {
|
+ (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];
|
return [UIColor colorWithHex:0xD0D3DA];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cell 背景色:暗黑 #707070,浅色 白色90%透明度
|
/// Cell 背景色:暗黑 #707070,浅色 白色90%透明度
|
||||||
+ (UIColor *)kb_cellBackgroundColor {
|
+ (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];
|
return [UIColor colorWithWhite:1 alpha:0.9];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cell 文字颜色:暗黑 #FFFFFF,浅色 #1B1F1A
|
/// Cell 文字颜色:暗黑 #FFFFFF,浅色 #1B1F1A
|
||||||
+ (UIColor *)kb_cellTextColor {
|
+ (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];
|
return [UIColor colorWithHex:0x1B1F1A];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear 按钮文字颜色:暗黑白色,浅色黑色
|
/// Clear 按钮文字颜色:暗黑白色,浅色黑色
|
||||||
+ (UIColor *)kb_clearButtonTextColor {
|
+ (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];
|
return [UIColor blackColor];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除按钮背景色:暗黑 #707070,浅色 #B9BDC8
|
/// 删除按钮背景色:暗黑 #707070,浅色 #B9BDC8
|
||||||
+ (UIColor *)kb_deleteButtonBackgroundColor {
|
+ (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];
|
return [UIColor colorWithHex:0xB9BDC8];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,13 +277,8 @@
|
|||||||
// self.itemsInternal = @[KBLocalized(@"Warm hearted man"),
|
// self.itemsInternal = @[KBLocalized(@"Warm hearted man"),
|
||||||
// KBLocalized(@"Warm2 hearted man"),
|
// KBLocalized(@"Warm2 hearted man"),
|
||||||
// KBLocalized(@"Warm3 hearted man"),
|
// KBLocalized(@"Warm3 hearted man"),
|
||||||
// KBLocalized(@"撩女生啊u发顺丰大师傅"),
|
|
||||||
// KBLocalized(@"Warm = man"),
|
// KBLocalized(@"Warm = man"),
|
||||||
// KBLocalized(@"Warm hearted man"),
|
// KBLocalized(@"Warm hearted man"),
|
||||||
// KBLocalized(@"一枚暖男发放"),
|
|
||||||
// KBLocalized(@"聊天搭子"),
|
|
||||||
// KBLocalized(@"表达爱意"),
|
|
||||||
// KBLocalized(@"更多话术")];
|
|
||||||
// [self.tagListView setItems:self.itemsInternal];
|
// [self.tagListView setItems:self.itemsInternal];
|
||||||
//}
|
//}
|
||||||
|
|
||||||
@@ -505,7 +450,10 @@
|
|||||||
if (event.data.length == 0) {
|
if (event.data.length == 0) {
|
||||||
return;
|
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];
|
NSData *jsonData = [event.data dataUsingEncoding:NSUTF8StringEncoding];
|
||||||
if (!jsonData) {
|
if (!jsonData) {
|
||||||
return;
|
return;
|
||||||
@@ -563,7 +511,7 @@
|
|||||||
}
|
}
|
||||||
BOOL shouldShowError = (error != nil);
|
BOOL shouldShowError = (error != nil);
|
||||||
if (shouldShowError) {
|
if (shouldShowError) {
|
||||||
[KBHUD showInfo:error.localizedDescription ?: KBLocalized(@"拉取失败")];
|
[KBHUD showInfo:error.localizedDescription ?: KBLocalized(@"Fetch failed")];
|
||||||
}
|
}
|
||||||
if (self.streamOverlay) {
|
if (self.streamOverlay) {
|
||||||
[self.streamOverlay finish];
|
[self.streamOverlay finish];
|
||||||
@@ -580,7 +528,7 @@
|
|||||||
BOOL needSubscriptionGuide = (code == KBBizCodeQuotaExhausted);
|
BOOL needSubscriptionGuide = (code == KBBizCodeQuotaExhausted);
|
||||||
NSString *msg = KBBizMessageFromJSONObject(payload);
|
NSString *msg = KBBizMessageFromJSONObject(payload);
|
||||||
if (msg.length == 0) {
|
if (msg.length == 0) {
|
||||||
msg = KBLocalized(@"拉取失败");
|
msg = KBLocalized(@"Fetch failed");
|
||||||
}
|
}
|
||||||
NSError *bizError =
|
NSError *bizError =
|
||||||
[NSError errorWithDomain:@"KBStreamBizError"
|
[NSError errorWithDomain:@"KBStreamBizError"
|
||||||
@@ -768,7 +716,7 @@
|
|||||||
// 1) 先判断权限:未开启“完全访问”则走引导逻辑
|
// 1) 先判断权限:未开启“完全访问”则走引导逻辑
|
||||||
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
||||||
// 未开启完全访问:保持原有引导路径
|
// 未开启完全访问:保持原有引导路径
|
||||||
[KBHUD showInfo:KBLocalized(@"处理中…")];
|
[KBHUD showInfo:KBLocalized(@"Processing...")];
|
||||||
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self];
|
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -778,12 +726,27 @@
|
|||||||
if (!KBAuthManager.shared.isLoggedIn) {
|
if (!KBAuthManager.shared.isLoggedIn) {
|
||||||
|
|
||||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
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 =
|
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=keyboard", KB_UL_LOGIN]];
|
||||||
[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
|
NSURL *scheme =
|
||||||
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
[NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME]];
|
||||||
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
__weak typeof(self) weakSelf = self;
|
||||||
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:ivc.view];
|
[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;
|
return;
|
||||||
// if (!ivc) return;
|
// if (!ivc) return;
|
||||||
// NSString *encodedTitle = [title
|
// NSString *encodedTitle = [title
|
||||||
@@ -813,7 +776,6 @@
|
|||||||
// [ivc dismissKeyboard];
|
// [ivc dismissKeyboard];
|
||||||
// BOOL ok = [KBHostAppLauncher openHostAppURL:scheme
|
// BOOL ok = [KBHostAppLauncher openHostAppURL:scheme
|
||||||
// fromResponder:start]; if (!ok) {
|
// fromResponder:start]; if (!ok) {
|
||||||
// [KBHUD showInfo:KBLocalized(@"请切换到主App完成登录")];
|
|
||||||
// }else{
|
// }else{
|
||||||
//
|
//
|
||||||
// }
|
// }
|
||||||
@@ -861,7 +823,7 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
[KBHUD showInfo:KBLocalized(@"处理中…")];
|
[KBHUD showInfo:KBLocalized(@"Processing...")];
|
||||||
|
|
||||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||||
if (!ivc)
|
if (!ivc)
|
||||||
@@ -881,37 +843,34 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer,
|
|||||||
if (!ul)
|
if (!ul)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
dispatch_after(
|
NSURL *scheme = [NSURL
|
||||||
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)),
|
URLWithString:
|
||||||
dispatch_get_main_queue(), ^{
|
[NSString stringWithFormat:
|
||||||
// 先尝试通过 extensionContext 打开 UL
|
@"%@://login?src=functionView&index=%ld&title=%@",
|
||||||
[ivc.extensionContext
|
KB_APP_SCHEME, (long)indexPath.item, encodedTitle]];
|
||||||
openURL:ul
|
|
||||||
completionHandler:^(BOOL ok) {
|
__weak typeof(self) weakSelf = self;
|
||||||
if (ok) {
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)),
|
||||||
return;
|
dispatch_get_main_queue(), ^{
|
||||||
}
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
// UL 失败时,再通过宿主 UIApplication + 自定义 Scheme 兜底
|
if (!self) {
|
||||||
NSURL *scheme = [NSURL
|
return;
|
||||||
URLWithString:
|
}
|
||||||
[NSString
|
[KBExtensionAppLauncher
|
||||||
stringWithFormat:
|
openPrimaryURL:ul
|
||||||
@"%@@//login?src=functionView&index=%ld&title=%@",
|
fallbackURL:scheme
|
||||||
KB_APP_SCHEME, (long)indexPath.item,
|
usingInputController:ivc
|
||||||
encodedTitle]];
|
source:(ivc.view ?: (UIResponder *)self)
|
||||||
UIResponder *start = ivc.view ?: (UIResponder *)self;
|
completion:^(BOOL success) {
|
||||||
BOOL ok2 = [KBHostAppLauncher openHostAppURL:scheme
|
if (success) {
|
||||||
fromResponder:start];
|
return;
|
||||||
if (!ok2) {
|
}
|
||||||
// 两条路都失败:大概率未开完全访问或宿主拦截。统一交由 Manager
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
// 引导。
|
[[KBFullAccessManager shared]
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
ensureFullAccessOrGuideInView:self];
|
||||||
[[KBFullAccessManager shared]
|
});
|
||||||
ensureFullAccessOrGuideInView:self];
|
}];
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Button Actions
|
#pragma mark - Button Actions
|
||||||
|
|||||||
@@ -21,17 +21,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
/// 需求:当 index == 0 时,由外部(KeyboardViewController)决定是否切换到功能面板
|
/// 需求:当 index == 0 时,由外部(KeyboardViewController)决定是否切换到功能面板
|
||||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index;
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index;
|
||||||
|
|
||||||
/// 点击了右侧设置按钮
|
|
||||||
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView;
|
|
||||||
/// 点击了撤销删除按钮
|
/// 点击了撤销删除按钮
|
||||||
- (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView;
|
- (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView;
|
||||||
|
|
||||||
/// emoji 视图里选择了一个表情
|
/// emoji 视图里选择了一个表情
|
||||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectEmoji:(NSString *)emoji;
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectEmoji:(NSString *)emoji;
|
||||||
|
|
||||||
/// emoji 面板点击搜索
|
|
||||||
- (void)keyBoardMainViewDidTapEmojiSearch:(KBKeyBoardMainView *)keyBoardMainView;
|
|
||||||
|
|
||||||
/// 选择了联想词
|
/// 选择了联想词
|
||||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion;
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion;
|
||||||
@end
|
@end
|
||||||
@@ -45,6 +40,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
/// 更新联想候选
|
/// 更新联想候选
|
||||||
- (void)kb_setSuggestions:(NSArray<NSString *> *)suggestions;
|
- (void)kb_setSuggestions:(NSArray<NSString *> *)suggestions;
|
||||||
|
|
||||||
|
/// 根据 profileId 重新加载键盘布局
|
||||||
|
- (void)reloadLayoutWithProfileId:(NSString *)profileId;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
[self addSubview:self.suggestionBar];
|
[self addSubview:self.suggestionBar];
|
||||||
|
|
||||||
// 键盘区域(高度按照设计值做等比缩放,避免不同机型上按键被压缩/拉伸)
|
// 键盘区域(高度按照设计值做等比缩放,避免不同机型上按键被压缩/拉伸)
|
||||||
CGFloat keyboardAreaHeight = KBFit(200.0f);
|
CGFloat keyboardAreaHeight = KBFit(215.0f);
|
||||||
KBKeyboardLayoutConfig *layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
|
KBKeyboardLayoutConfig *layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
|
||||||
if (layoutConfig) {
|
if (layoutConfig) {
|
||||||
CGFloat configHeight = [layoutConfig keyboardAreaScaledHeight];
|
CGFloat configHeight = [layoutConfig keyboardAreaScaledHeight];
|
||||||
@@ -66,14 +66,7 @@
|
|||||||
make.bottom.equalTo(self.mas_bottom).offset(-bottomInset);
|
make.bottom.equalTo(self.mas_bottom).offset(-bottomInset);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
self.emojiView = [[KBEmojiPanelView alloc] init];
|
// emoji 面板改为按需创建
|
||||||
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);
|
|
||||||
}];
|
|
||||||
|
|
||||||
// [self.topBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
// [self.topBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
// make.left.right.equalTo(self);
|
// make.left.right.equalTo(self);
|
||||||
@@ -116,9 +109,10 @@
|
|||||||
if (self.emojiPanelVisible == visible) return;
|
if (self.emojiPanelVisible == visible) return;
|
||||||
self.emojiPanelVisible = visible;
|
self.emojiPanelVisible = visible;
|
||||||
if (visible) {
|
if (visible) {
|
||||||
[self.emojiView reloadData];
|
KBEmojiPanelView *emojiView = [self emojiView];
|
||||||
self.emojiView.hidden = NO;
|
[emojiView reloadData];
|
||||||
[self bringSubviewToFront:self.emojiView];
|
emojiView.hidden = NO;
|
||||||
|
[self bringSubviewToFront:emojiView];
|
||||||
} else {
|
} else {
|
||||||
self.keyboardView.hidden = NO;
|
self.keyboardView.hidden = NO;
|
||||||
self.topBar.hidden = NO;
|
self.topBar.hidden = NO;
|
||||||
@@ -126,19 +120,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
void (^changes)(void) = ^{
|
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.keyboardView.alpha = visible ? 0.0 : 1.0;
|
||||||
self.topBar.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);
|
self.suggestionBar.alpha = visible ? 0.0 : ([self kb_shouldShowSuggestions] ? 1.0 : 0.0);
|
||||||
};
|
};
|
||||||
void (^completion)(BOOL) = ^(BOOL finished) {
|
void (^completion)(BOOL) = ^(BOOL finished) {
|
||||||
self.emojiView.hidden = !visible;
|
if (self.emojiView) {
|
||||||
|
self.emojiView.hidden = !visible;
|
||||||
|
}
|
||||||
self.keyboardView.hidden = visible;
|
self.keyboardView.hidden = visible;
|
||||||
self.topBar.hidden = visible;
|
self.topBar.hidden = visible;
|
||||||
if (visible) {
|
if (visible) {
|
||||||
self.suggestionBar.hidden = YES;
|
self.suggestionBar.hidden = YES;
|
||||||
} else {
|
} else {
|
||||||
self.suggestionBar.hidden = ![self kb_shouldShowSuggestions];
|
self.suggestionBar.hidden = ![self kb_shouldShowSuggestions];
|
||||||
|
if (self.emojiView) {
|
||||||
|
[self.emojiView purgeEmojiCache];
|
||||||
|
[self.emojiView removeFromSuperview];
|
||||||
|
self.emojiView = nil;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -154,6 +157,22 @@
|
|||||||
[self setEmojiPanelVisible:!self.emojiPanelVisible animated:YES];
|
[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
|
#pragma mark - KBToolBarDelegate
|
||||||
|
|
||||||
@@ -164,12 +183,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)toolBarDidTapSettings:(KBToolBar *)toolBar {
|
|
||||||
if ([self.delegate respondsToSelector:@selector(keyBoardMainViewDidTapSettings:)]) {
|
|
||||||
[self.delegate keyBoardMainViewDidTapSettings:self];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)toolBarDidTapUndo:(KBToolBar *)toolBar {
|
- (void)toolBarDidTapUndo:(KBToolBar *)toolBar {
|
||||||
if ([self.delegate respondsToSelector:@selector(keyBoardMainViewDidTapUndo:)]) {
|
if ([self.delegate respondsToSelector:@selector(keyBoardMainViewDidTapUndo:)]) {
|
||||||
[self.delegate keyBoardMainViewDidTapUndo:self];
|
[self.delegate keyBoardMainViewDidTapUndo:self];
|
||||||
@@ -242,12 +255,6 @@
|
|||||||
[self setEmojiPanelVisible:NO animated:YES];
|
[self setEmojiPanelVisible:NO animated:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)emojiPanelViewDidTapSearch:(KBEmojiPanelView *)panel {
|
|
||||||
if ([self.delegate respondsToSelector:@selector(keyBoardMainViewDidTapEmojiSearch:)]) {
|
|
||||||
[self.delegate keyBoardMainViewDidTapEmojiSearch:self];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)emojiPanelViewDidTapDelete:(KBEmojiPanelView *)panel {
|
- (void)emojiPanelViewDidTapDelete:(KBEmojiPanelView *)panel {
|
||||||
if ([self.delegate respondsToSelector:@selector(keyBoardMainView:didTapKey:)]) {
|
if ([self.delegate respondsToSelector:@selector(keyBoardMainView:didTapKey:)]) {
|
||||||
KBKey *backspace = [KBKey keyWithTitle:@"" type:KBKeyTypeBackspace];
|
KBKey *backspace = [KBKey keyWithTitle:@"" type:KBKeyTypeBackspace];
|
||||||
@@ -293,10 +300,7 @@
|
|||||||
|
|
||||||
- (BOOL)kb_shouldShowSuggestions {
|
- (BOOL)kb_shouldShowSuggestions {
|
||||||
if (self.emojiPanelVisible) { return NO; }
|
if (self.emojiPanelVisible) { return NO; }
|
||||||
if (![KBBackspaceUndoManager shared].hasUndo && self.suggestionBarHasItems) {
|
return self.suggestionBarHasItems;
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
return NO;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)kb_applySuggestionVisibility {
|
- (void)kb_applySuggestionVisibility {
|
||||||
@@ -305,4 +309,13 @@
|
|||||||
self.suggestionBar.alpha = shouldShow ? 1.0 : 0.0;
|
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
|
@end
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
27
CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.h
Normal file
27
CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.h
Normal 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
|
||||||
26
CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.m
Normal file
26
CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.m
Normal 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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
28
CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.h
Normal file
28
CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.h
Normal 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
|
||||||
186
CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.m
Normal file
186
CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.m
Normal 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
|
||||||
42
CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.h
Normal file
42
CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.h
Normal 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
|
||||||
245
CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.m
Normal file
245
CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.m
Normal 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
|
||||||
28
CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.h
Normal file
28
CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.h
Normal 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
|
||||||
291
CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.m
Normal file
291
CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.m
Normal 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/ABC、Emoji、Send 给定尺寸,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/ABC、Emoji、Send 指定宽度,
|
||||||
|
// 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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
33
CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.h
Normal file
33
CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.h
Normal 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
|
||||||
330
CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.m
Normal file
330
CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.m
Normal 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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -27,7 +27,9 @@ typedef NS_ENUM(NSInteger, KBKeyboardLayoutStyle) {
|
|||||||
@property (nonatomic, assign, getter=isShiftOn) BOOL shiftOn; // 大小写状态
|
@property (nonatomic, assign, getter=isShiftOn) BOOL shiftOn; // 大小写状态
|
||||||
// 在数字布局中,是否显示“更多符号”(#+=)页
|
// 在数字布局中,是否显示“更多符号”(#+=)页
|
||||||
@property (nonatomic, assign) BOOL symbolsMoreOn;
|
@property (nonatomic, assign) BOOL symbolsMoreOn;
|
||||||
|
@property (nonatomic, copy) NSString *currentLayoutJsonId; // 当前使用的布局 JSON ID
|
||||||
|
|
||||||
- (void)reloadKeys; // 当布局样式/大小写变化时调用
|
- (void)reloadKeys; // 当布局样式/大小写变化时调用
|
||||||
|
- (void)reloadLayoutWithProfileId:(NSString *)profileId; // 根据 profileId 重新加载布局
|
||||||
|
|
||||||
@end
|
@end
|
||||||
853
CustomKeyboard/View/KBKeyboardView/KBKeyboardView.m
Normal file
853
CustomKeyboard/View/KBKeyboardView/KBKeyboardView.m
Normal 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
|
||||||
@@ -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
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -8,15 +8,37 @@
|
|||||||
#import "KBSkinManager.h"
|
#import "KBSkinManager.h"
|
||||||
|
|
||||||
@interface KBSuggestionBarView ()
|
@interface KBSuggestionBarView ()
|
||||||
@property (nonatomic, strong) UIScrollView *scrollView;
|
|
||||||
@property (nonatomic, strong) UIStackView *stackView;
|
@property (nonatomic, strong) UIStackView *stackView;
|
||||||
|
@property (nonatomic, strong) UIView *topLineView;
|
||||||
|
@property (nonatomic, strong) UIView *bottomLineView;
|
||||||
@property (nonatomic, copy) NSArray<NSString *> *items;
|
@property (nonatomic, copy) NSArray<NSString *> *items;
|
||||||
@property (nonatomic, strong) UIColor *pillColor;
|
|
||||||
@property (nonatomic, strong) UIColor *textColor;
|
@property (nonatomic, strong) UIColor *textColor;
|
||||||
|
@property (nonatomic, strong) UIColor *separatorColor;
|
||||||
|
@property (nonatomic, strong) UIColor *highlightColor;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KBSuggestionBarView
|
@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 {
|
- (instancetype)initWithFrame:(CGRect)frame {
|
||||||
if (self = [super initWithFrame:frame]) {
|
if (self = [super initWithFrame:frame]) {
|
||||||
self.backgroundColor = [UIColor clearColor];
|
self.backgroundColor = [UIColor clearColor];
|
||||||
@@ -26,15 +48,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)setupUI {
|
- (void)setupUI {
|
||||||
[self addSubview:self.scrollView];
|
[self addSubview:self.stackView];
|
||||||
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.stackView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.edges.equalTo(self);
|
make.left.right.equalTo(self);
|
||||||
|
make.top.equalTo(self);
|
||||||
|
make.bottom.equalTo(self);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
[self.scrollView addSubview:self.stackView];
|
[self addSubview:self.topLineView];
|
||||||
[self.stackView mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.topLineView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.edges.equalTo(self.scrollView).insets(UIEdgeInsetsMake(0, 8, 0, 8));
|
make.left.right.top.equalTo(self);
|
||||||
make.height.equalTo(self.scrollView);
|
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];
|
[self applyTheme:[KBSkinManager shared].current];
|
||||||
@@ -48,35 +78,75 @@
|
|||||||
[view removeFromSuperview];
|
[view removeFromSuperview];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (NSString *item in self.items) {
|
NSUInteger count = self.items.count;
|
||||||
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
|
for (NSUInteger idx = 0; idx < count; idx++) {
|
||||||
btn.layer.cornerRadius = 12.0;
|
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.layer.masksToBounds = YES;
|
||||||
btn.backgroundColor = self.pillColor ?: [UIColor colorWithWhite:1 alpha:0.9];
|
btn.adjustsImageWhenHighlighted = NO;
|
||||||
btn.titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
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 setTitle:item forState:UIControlStateNormal];
|
||||||
[btn setTitleColor:self.textColor ?: [UIColor blackColor] forState:UIControlStateNormal];
|
[btn setTitleColor:self.textColor ?: UIColor.blackColor forState:UIControlStateNormal];
|
||||||
btn.contentEdgeInsets = UIEdgeInsetsMake(4, 10, 4, 10);
|
if (self.highlightColor) {
|
||||||
|
[btn setBackgroundImage:[self kb_imageWithColor:self.highlightColor] forState:UIControlStateHighlighted];
|
||||||
|
}
|
||||||
[btn addTarget:self action:@selector(onTapSuggestion:) forControlEvents:UIControlEventTouchUpInside];
|
[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);
|
self.hidden = (self.items.count == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)applyTheme:(KBSkinTheme *)theme {
|
- (void)applyTheme:(KBSkinTheme *)theme {
|
||||||
UIColor *bg = theme.keyBackground ?: [UIColor whiteColor];
|
UIColor *barBg = theme.keyboardBackground ?: [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||||
UIColor *text = theme.keyTextColor ?: [UIColor blackColor];
|
UIColor *text = theme.keyTextColor ?: UIColor.blackColor;
|
||||||
UIColor *barBg = [UIColor colorWithHex:0xD1D3DB];
|
UIColor *highlight = theme.keyHighlightBackground ?: [UIColor colorWithWhite:0.85 alpha:1.0];
|
||||||
self.backgroundColor = barBg;
|
self.backgroundColor = barBg;
|
||||||
self.pillColor = bg;
|
|
||||||
self.textColor = text;
|
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) {
|
for (UIView *view in self.stackView.arrangedSubviews) {
|
||||||
if (![view isKindOfClass:[UIButton class]]) { continue; }
|
UIButton *btn = (UIButton *)[view viewWithTag:[self kb_buttonTag]];
|
||||||
UIButton *btn = (UIButton *)view;
|
if ([btn isKindOfClass:UIButton.class]) {
|
||||||
btn.backgroundColor = self.pillColor;
|
[btn setTitleColor:self.textColor ?: UIColor.blackColor forState:UIControlStateNormal];
|
||||||
[btn setTitleColor:self.textColor 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
|
#pragma mark - Lazy
|
||||||
|
|
||||||
- (UIScrollView *)scrollView {
|
|
||||||
if (!_scrollView) {
|
|
||||||
_scrollView = [[UIScrollView alloc] init];
|
|
||||||
_scrollView.showsHorizontalScrollIndicator = NO;
|
|
||||||
_scrollView.alwaysBounceHorizontal = YES;
|
|
||||||
}
|
|
||||||
return _scrollView;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UIStackView *)stackView {
|
- (UIStackView *)stackView {
|
||||||
if (!_stackView) {
|
if (!_stackView) {
|
||||||
_stackView = [[UIStackView alloc] init];
|
_stackView = [[UIStackView alloc] init];
|
||||||
_stackView.axis = UILayoutConstraintAxisHorizontal;
|
_stackView.axis = UILayoutConstraintAxisHorizontal;
|
||||||
_stackView.alignment = UIStackViewAlignmentCenter;
|
_stackView.alignment = UIStackViewAlignmentFill;
|
||||||
_stackView.spacing = 8.0;
|
_stackView.distribution = UIStackViewDistributionFillEqually;
|
||||||
|
_stackView.spacing = 0.0;
|
||||||
|
_stackView.layoutMarginsRelativeArrangement = YES;
|
||||||
|
_stackView.layoutMargins = UIEdgeInsetsMake(0, 8, 0, 8);
|
||||||
}
|
}
|
||||||
return _stackView;
|
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
|
@end
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@optional
|
@optional
|
||||||
/// 左侧功能按钮点击(index 从 0 开始)
|
/// 左侧功能按钮点击(index 从 0 开始)
|
||||||
- (void)toolBar:(KBToolBar *)toolBar didTapActionAtIndex:(NSInteger)index;
|
- (void)toolBar:(KBToolBar *)toolBar didTapActionAtIndex:(NSInteger)index;
|
||||||
/// 右侧设置按钮点击
|
|
||||||
- (void)toolBarDidTapSettings:(KBToolBar *)toolBar;
|
|
||||||
/// 右侧撤销删除按钮点击
|
/// 右侧撤销删除按钮点击
|
||||||
- (void)toolBarDidTapUndo:(KBToolBar *)toolBar;
|
- (void)toolBarDidTapUndo:(KBToolBar *)toolBar;
|
||||||
@end
|
@end
|
||||||
@@ -31,7 +29,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
|
|
||||||
/// 暴露按钮以便外部定制(只读;首次访问时懒加载创建)
|
/// 暴露按钮以便外部定制(只读;首次访问时懒加载创建)
|
||||||
@property (nonatomic, strong, readonly) NSArray<UIButton *> *leftButtons;
|
@property (nonatomic, strong, readonly) NSArray<UIButton *> *leftButtons;
|
||||||
@property (nonatomic, strong, readonly) UIButton *settingsButton;
|
|
||||||
@property (nonatomic, strong, readonly) UIButton *undoButton;
|
@property (nonatomic, strong, readonly) UIButton *undoButton;
|
||||||
|
|
||||||
/// 应用皮肤(更新 AI 按钮背景图)
|
/// 应用皮肤(更新 AI 按钮背景图)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
@interface KBToolBar ()
|
@interface KBToolBar ()
|
||||||
@property (nonatomic, strong) UIView *leftContainer;
|
@property (nonatomic, strong) UIView *leftContainer;
|
||||||
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
|
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
|
||||||
//@property (nonatomic, strong) UIButton *settingsButtonInternal;
|
|
||||||
@property (nonatomic, strong) UIButton *globeButtonInternal; // 可选:系统“切换输入法”键
|
@property (nonatomic, strong) UIButton *globeButtonInternal; // 可选:系统“切换输入法”键
|
||||||
@property (nonatomic, strong) UIImageView *avatarImageView; // 右侧头像(AppGroup persona_cover.jpg)
|
@property (nonatomic, strong) UIImageView *avatarImageView; // 右侧头像(AppGroup persona_cover.jpg)
|
||||||
@property (nonatomic, strong) UIButton *undoButtonInternal; // 右侧撤销删除
|
@property (nonatomic, strong) UIButton *undoButtonInternal; // 右侧撤销删除
|
||||||
@@ -42,6 +41,10 @@
|
|||||||
selector:@selector(kb_undoStateChanged)
|
selector:@selector(kb_undoStateChanged)
|
||||||
name:KBBackspaceUndoStateDidChangeNotification
|
name:KBBackspaceUndoStateDidChangeNotification
|
||||||
object:nil];
|
object:nil];
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(kb_skinDidChange)
|
||||||
|
name:KBSkinDidChangeNotification
|
||||||
|
object:nil];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -61,10 +64,6 @@
|
|||||||
return self.undoButtonInternal;
|
return self.undoButtonInternal;
|
||||||
}
|
}
|
||||||
|
|
||||||
//- (UIButton *)settingsButton {
|
|
||||||
// return self.settingsButtonInternal;
|
|
||||||
//}
|
|
||||||
|
|
||||||
- (void)setLeftButtonTitles:(NSArray<NSString *> *)leftButtonTitles {
|
- (void)setLeftButtonTitles:(NSArray<NSString *> *)leftButtonTitles {
|
||||||
_leftButtonTitles = [leftButtonTitles copy];
|
_leftButtonTitles = [leftButtonTitles copy];
|
||||||
// Update titles if buttons already exist
|
// Update titles if buttons already exist
|
||||||
@@ -80,18 +79,10 @@
|
|||||||
|
|
||||||
- (void)setupUI {
|
- (void)setupUI {
|
||||||
[self addSubview:self.leftContainer];
|
[self addSubview:self.leftContainer];
|
||||||
// [self addSubview:self.settingsButtonInternal];
|
|
||||||
[self addSubview:self.globeButtonInternal];
|
[self addSubview:self.globeButtonInternal];
|
||||||
[self addSubview:self.undoButtonInternal];
|
[self addSubview:self.undoButtonInternal];
|
||||||
[self addSubview:self.avatarImageView];
|
[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) {
|
[self.globeButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.equalTo(self.mas_left).offset(12);
|
make.left.equalTo(self.mas_left).offset(12);
|
||||||
@@ -183,9 +174,14 @@
|
|||||||
KBSkinManager *skinManager = [KBSkinManager shared];
|
KBSkinManager *skinManager = [KBSkinManager shared];
|
||||||
UIImage *icon = [skinManager iconImageForKeyIdentifier:kKBAIKeyIdentifier caseVariant:0];
|
UIImage *icon = [skinManager iconImageForKeyIdentifier:kKBAIKeyIdentifier caseVariant:0];
|
||||||
NSString *skinId = skinManager.current.skinId ?: @"";
|
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"];
|
icon = [UIImage imageNamed:@"ai_key_icon"];
|
||||||
|
NSLog(@"[KBToolBar] fallback to bundled ai_key_icon: %@", icon ? @"有" : @"nil");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (icon) {
|
if (icon) {
|
||||||
@@ -304,12 +300,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onSettings {
|
|
||||||
if ([self.delegate respondsToSelector:@selector(toolBarDidTapSettings:)]) {
|
|
||||||
[self.delegate toolBarDidTapSettings:self];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)onUndo {
|
- (void)onUndo {
|
||||||
if ([self.delegate respondsToSelector:@selector(toolBarDidTapUndo:)]) {
|
if ([self.delegate respondsToSelector:@selector(toolBarDidTapUndo:)]) {
|
||||||
[self.delegate toolBarDidTapUndo:self];
|
[self.delegate toolBarDidTapUndo:self];
|
||||||
@@ -353,19 +343,6 @@
|
|||||||
return _avatarImageView;
|
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 {
|
- (UIButton *)globeButtonInternal {
|
||||||
if (!_globeButtonInternal) {
|
if (!_globeButtonInternal) {
|
||||||
_globeButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
|
_globeButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||||
@@ -472,6 +449,10 @@
|
|||||||
[self kb_updateUndoVisibilityAnimated:YES];
|
[self kb_updateUndoVisibilityAnimated:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)kb_skinDidChange {
|
||||||
|
[self kb_applyTheme];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)kb_updateUndoVisibilityAnimated:(BOOL)animated {
|
- (void)kb_updateUndoVisibilityAnimated:(BOOL)animated {
|
||||||
BOOL visible = [KBBackspaceUndoManager shared].hasUndo;
|
BOOL visible = [KBBackspaceUndoManager shared].hasUndo;
|
||||||
if (self.kbUndoVisible == visible) { return; }
|
if (self.kbUndoVisible == visible) { return; }
|
||||||
|
|||||||
13
Podfile
13
Podfile
@@ -6,7 +6,8 @@ target 'keyBoard' do
|
|||||||
# Comment the next line if you don't want to use dynamic frameworks
|
# Comment the next line if you don't want to use dynamic frameworks
|
||||||
use_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 'Bugly', :configurations => ['Release']
|
||||||
pod 'DZNEmptyDataSet', '1.8.1'
|
pod 'DZNEmptyDataSet', '1.8.1'
|
||||||
pod 'FLAnimatedImage', '~> 1.0.17'
|
pod 'FLAnimatedImage', '~> 1.0.17'
|
||||||
@@ -17,7 +18,8 @@ target 'keyBoard' do
|
|||||||
pod 'LookinServer', :configurations => ['Debug']
|
pod 'LookinServer', :configurations => ['Debug']
|
||||||
pod 'LYEmptyView', '~> 1.3.1'
|
pod 'LYEmptyView', '~> 1.3.1'
|
||||||
pod 'Masonry', '1.1.0'
|
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 'MJExtension', '3.4.2'
|
||||||
pod 'MJRefresh', '3.7.9'
|
pod 'MJRefresh', '3.7.9'
|
||||||
pod 'SDWebImage', '5.21.1'
|
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
|
# Comment the next line if you don't want to use dynamic frameworks
|
||||||
use_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 'SDWebImage', '5.21.1'
|
||||||
|
|
||||||
pod 'Masonry', '1.1.0'
|
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 'MJExtension', '3.4.2'
|
||||||
pod 'DZNEmptyDataSet', '1.8.1'
|
pod 'DZNEmptyDataSet', '1.8.1'
|
||||||
pod 'SSZipArchive', '~> 2.4.3'
|
pod 'SSZipArchive', '~> 2.4.3'
|
||||||
|
|||||||
28
Podfile.lock
28
Podfile.lock
@@ -40,7 +40,7 @@ PODS:
|
|||||||
- SSZipArchive (2.4.3)
|
- SSZipArchive (2.4.3)
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- AFNetworking (= 4.0.1)
|
- AFNetworking (from `https://github.com/gzx543097079/AppStorePrivacyInfo.git`, tag `AFNetworking-4.0.1.1`)
|
||||||
- Bugly
|
- Bugly
|
||||||
- DZNEmptyDataSet (= 1.8.1)
|
- DZNEmptyDataSet (= 1.8.1)
|
||||||
- FLAnimatedImage (~> 1.0.17)
|
- FLAnimatedImage (~> 1.0.17)
|
||||||
@@ -51,7 +51,7 @@ DEPENDENCIES:
|
|||||||
- LSTPopView (~> 0.3.10)
|
- LSTPopView (~> 0.3.10)
|
||||||
- LYEmptyView (~> 1.3.1)
|
- LYEmptyView (~> 1.3.1)
|
||||||
- Masonry (= 1.1.0)
|
- Masonry (= 1.1.0)
|
||||||
- MBProgressHUD (= 1.2.0)
|
- MBProgressHUD (from `https://github.com/jdg/MBProgressHUD.git`, commit `18c442d57398cee5ef57f852df10fc5ff65f0763`)
|
||||||
- MJExtension (= 3.4.2)
|
- MJExtension (= 3.4.2)
|
||||||
- MJRefresh (= 3.7.9)
|
- MJRefresh (= 3.7.9)
|
||||||
- SDWebImage (= 5.21.1)
|
- SDWebImage (= 5.21.1)
|
||||||
@@ -59,7 +59,6 @@ DEPENDENCIES:
|
|||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
https://github.com/CocoaPods/Specs.git:
|
https://github.com/CocoaPods/Specs.git:
|
||||||
- AFNetworking
|
|
||||||
- Bugly
|
- Bugly
|
||||||
- DZNEmptyDataSet
|
- DZNEmptyDataSet
|
||||||
- FLAnimatedImage
|
- FLAnimatedImage
|
||||||
@@ -71,14 +70,29 @@ SPEC REPOS:
|
|||||||
- LSTTimer
|
- LSTTimer
|
||||||
- LYEmptyView
|
- LYEmptyView
|
||||||
- Masonry
|
- Masonry
|
||||||
- MBProgressHUD
|
|
||||||
- MJExtension
|
- MJExtension
|
||||||
- MJRefresh
|
- MJRefresh
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SSZipArchive
|
- 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:
|
SPEC CHECKSUMS:
|
||||||
AFNetworking: 3bd23d814e976cd148d7d44c3ab78017b744cd58
|
AFNetworking: 5dc31d980b5c60e63d2852bdd75252c3fd85a6d0
|
||||||
Bugly: 217ac2ce5f0f2626d43dbaa4f70764c953a26a31
|
Bugly: 217ac2ce5f0f2626d43dbaa4f70764c953a26a31
|
||||||
DZNEmptyDataSet: 9525833b9e68ac21c30253e1d3d7076cc828eaa7
|
DZNEmptyDataSet: 9525833b9e68ac21c30253e1d3d7076cc828eaa7
|
||||||
FLAnimatedImage: bbf914596368867157cc71b38a8ec834b3eeb32b
|
FLAnimatedImage: bbf914596368867157cc71b38a8ec834b3eeb32b
|
||||||
@@ -90,12 +104,12 @@ SPEC CHECKSUMS:
|
|||||||
LSTTimer: caf8f02ff366ca175cf4c1778d26c166183c1b6f
|
LSTTimer: caf8f02ff366ca175cf4c1778d26c166183c1b6f
|
||||||
LYEmptyView: b6d418cfa38b78df0cf243f9a9c25ccbdc399922
|
LYEmptyView: b6d418cfa38b78df0cf243f9a9c25ccbdc399922
|
||||||
Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
|
Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
|
||||||
MBProgressHUD: 3ee5efcc380f6a79a7cc9b363dd669c5e1ae7406
|
MBProgressHUD: 1b0fb447e80a0fda94808180750e8b78a07b3cd2
|
||||||
MJExtension: e97d164cb411aa9795cf576093a1fa208b4a8dd8
|
MJExtension: e97d164cb411aa9795cf576093a1fa208b4a8dd8
|
||||||
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
|
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
|
||||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||||
|
|
||||||
PODFILE CHECKSUM: 890d1710715c017d7364a19c871e9bdf0d685fbf
|
PODFILE CHECKSUM: f79dfcf9dc400fbf6dc41f60d4b726fee361d430
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ static void AFNetworkReachabilityReleaseCallback(const void *info) {
|
|||||||
SCNetworkReachabilitySetCallback(self.networkReachability, AFNetworkReachabilityCallback, &context);
|
SCNetworkReachabilitySetCallback(self.networkReachability, AFNetworkReachabilityCallback, &context);
|
||||||
SCNetworkReachabilityScheduleWithRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
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;
|
SCNetworkReachabilityFlags flags;
|
||||||
if (SCNetworkReachabilityGetFlags(self.networkReachability, &flags)) {
|
if (SCNetworkReachabilityGetFlags(self.networkReachability, &flags)) {
|
||||||
AFPostReachabilityStatusChange(flags, callback);
|
AFPostReachabilityStatusChange(flags, callback);
|
||||||
@@ -278,7 +278,7 @@ forHTTPHeaderField:(NSString *)field;
|
|||||||
#pragma mark -
|
#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
|
@protocol AFMultipartFormData
|
||||||
|
|
||||||
@@ -368,10 +368,8 @@ forHTTPHeaderField:(NSString *)field
|
|||||||
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
|
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
|
||||||
mutableRequest.HTTPMethod = method;
|
mutableRequest.HTTPMethod = method;
|
||||||
|
|
||||||
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
|
for (NSString *keyPath in self.mutableObservedChangedKeyPaths) {
|
||||||
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
|
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
|
||||||
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
|
mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
|
||||||
@@ -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.
|
@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 -securityPolicy
|
||||||
@see -setTaskDidReceiveAuthenticationChallengeBlock:
|
@see -setAuthenticationChallengeHandler:
|
||||||
*/
|
*/
|
||||||
- (void)setSessionDidReceiveAuthenticationChallengeBlock:(nullable NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * _Nullable __autoreleasing * _Nullable credential))block;
|
- (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;
|
- (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.
|
@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.
|
||||||
*/
|
*/
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>logFormatVersion</key>
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
<integer>11</integer>
|
<array/>
|
||||||
<key>logs</key>
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
<dict/>
|
<array/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user