Compare commits
12 Commits
1c9013bede
...
dev_st_重构2
| Author | SHA1 | Date | |
|---|---|---|---|
| 47291934a2 | |||
| e619f48f93 | |||
| f55a70681c | |||
| cb86f7c32c | |||
| 40ef964b8c | |||
| 4269fde923 | |||
| c3e037e070 | |||
| a711be4c4d | |||
| 69bd2b2af9 | |||
| 82222afd76 | |||
| 92ca5c6180 | |||
| 851c0d9531 |
@@ -2,7 +2,12 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"WebSearch",
|
"WebSearch",
|
||||||
"Bash(git checkout:*)"
|
"Bash(git checkout:*)",
|
||||||
|
"Bash(xcodebuild:*)",
|
||||||
|
"Bash(plutil:*)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(ls:*)",
|
||||||
|
"Bash(wc:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,13 +93,25 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
NSString *trim =
|
NSString *trim =
|
||||||
[rawText stringByTrimmingCharactersInSet:
|
[rawText stringByTrimmingCharactersInSet:
|
||||||
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
|
NSString *textToClear = rawText;
|
||||||
|
if (trim.length == 0) {
|
||||||
|
// 兼容「先输入再打开聊天面板」场景:
|
||||||
|
// 此时新增文本为空,但当前输入框已有可发送内容,应该允许直接发送。
|
||||||
|
NSString *fullTrim =
|
||||||
|
[fullText stringByTrimmingCharactersInSet:
|
||||||
|
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
|
if (fullTrim.length > 0) {
|
||||||
|
trim = fullTrim;
|
||||||
|
textToClear = fullText;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (trim.length == 0) {
|
if (trim.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"请输入内容")];
|
[KBHUD showInfo:KBLocalized(@"请输入内容")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[self kb_sendChatText:trim];
|
[self kb_sendChatText:trim];
|
||||||
// 只清除新增的文本,保留基线文本
|
// 默认只清新增文本;若命中兜底则清当前全文,避免“已发送但输入框残留”。
|
||||||
[self kb_clearHostInputForText:rawText];
|
[self kb_clearHostInputForText:textToClear];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)kb_sendChatText:(NSString *)text {
|
- (void)kb_sendChatText:(NSString *)text {
|
||||||
@@ -359,6 +371,9 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
audioId:response.data.audioId];
|
audioId:response.data.audioId];
|
||||||
NSLog(@"[KB] AI 消息添加完成");
|
NSLog(@"[KB] AI 消息添加完成");
|
||||||
|
|
||||||
|
// 通知主 App 刷新对应 persona 的聊天记录
|
||||||
|
[self kb_notifyMainAppChatUpdatedWithCompanionId:companionId];
|
||||||
|
|
||||||
// 如果有 audioId,开始预加载音频
|
// 如果有 audioId,开始预加载音频
|
||||||
if (response.data.audioId.length > 0) {
|
if (response.data.audioId.length > 0) {
|
||||||
[self kb_preloadAudioWithAudioId:
|
[self kb_preloadAudioWithAudioId:
|
||||||
@@ -659,6 +674,22 @@ static const NSUInteger kKBChatMessageLimit = 6;
|
|||||||
NSLog(@"[Keyboard] 开始播放音频,时长: %.2f秒", player.duration);
|
NSLog(@"[Keyboard] 开始播放音频,时长: %.2f秒", player.duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - Notify Main App
|
||||||
|
|
||||||
|
/// 通知主 App 刷新对应 persona 的聊天记录
|
||||||
|
- (void)kb_notifyMainAppChatUpdatedWithCompanionId:(NSInteger)companionId {
|
||||||
|
NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
|
[ud setInteger:companionId forKey:AppGroup_ChatUpdatedCompanionId];
|
||||||
|
[ud synchronize];
|
||||||
|
|
||||||
|
CFNotificationCenterPostNotification(
|
||||||
|
CFNotificationCenterGetDarwinNotifyCenter(),
|
||||||
|
(__bridge CFStringRef)kKBDarwinChatUpdated,
|
||||||
|
NULL, NULL, true);
|
||||||
|
|
||||||
|
NSLog(@"[KB] 已通知主 App 刷新 companionId=%ld 的聊天记录", (long)companionId);
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - KBChatLimitPopViewDelegate
|
#pragma mark - KBChatLimitPopViewDelegate
|
||||||
|
|
||||||
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view {
|
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#import "KBChatMessage.h"
|
#import "KBChatMessage.h"
|
||||||
#import "KBChatPanelView.h"
|
#import "KBChatPanelView.h"
|
||||||
#import "KBFunctionView.h"
|
#import "KBFunctionView.h"
|
||||||
|
#import "KBFullAccessManager.h"
|
||||||
#import "KBHostAppLauncher.h"
|
#import "KBHostAppLauncher.h"
|
||||||
#import "KBInputBufferManager.h"
|
#import "KBInputBufferManager.h"
|
||||||
#import "KBKey.h"
|
#import "KBKey.h"
|
||||||
@@ -32,6 +33,38 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
KBKeyboardPanelMode fromMode = self.kb_panelMode;
|
KBKeyboardPanelMode fromMode = self.kb_panelMode;
|
||||||
|
|
||||||
|
// AI 入口先判完全访问:未开启时仅展示引导,不再继续登录态判断。
|
||||||
|
if (mode == KBKeyboardPanelModeFunction &&
|
||||||
|
![[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未登录时,不要提前写入面板状态,避免 mode 被错误卡在 Function 导致后续点击无响应。
|
||||||
|
BOOL islogin = YES;
|
||||||
|
if (mode == KBKeyboardPanelModeFunction) {
|
||||||
|
[[KBAuthManager shared] reloadFromKeychain];
|
||||||
|
islogin = KBAuthManager.shared.isLoggedIn;
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
if (mode == KBKeyboardPanelModeFunction) {
|
||||||
|
NSString *token = [KBAuthManager shared].current.accessToken ?: @"";
|
||||||
|
NSLog(@"[AuthTrace][Ext] tapAI mode=%ld isLoggedIn=%d tokenLen=%lu",
|
||||||
|
(long)mode, islogin, (unsigned long)token.length);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (mode == KBKeyboardPanelModeFunction && !islogin) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"请先登录后使用AI功能")];
|
||||||
|
NSString *schemeStr =
|
||||||
|
[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
|
||||||
|
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
||||||
|
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
||||||
|
if (!ok) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"请回到桌面手动打开App登录")];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.kb_panelMode = mode;
|
self.kb_panelMode = mode;
|
||||||
|
|
||||||
// 主键盘视图是基础承载:确保存在(键盘隐藏后会被释放)
|
// 主键盘视图是基础承载:确保存在(键盘隐藏后会被释放)
|
||||||
@@ -44,31 +77,23 @@
|
|||||||
[self kb_setFunctionPanelVisible:NO];
|
[self kb_setFunctionPanelVisible:NO];
|
||||||
|
|
||||||
// 2) 再展开目标面板
|
// 2) 再展开目标面板
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case KBKeyboardPanelModeFunction:
|
case KBKeyboardPanelModeFunction:
|
||||||
if (!KBAuthManager.shared.isLoggedIn) {
|
[self kb_setFunctionPanelVisible:YES];
|
||||||
NSString *schemeStr =
|
break;
|
||||||
[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
|
case KBKeyboardPanelModeChat:
|
||||||
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
[self kb_setChatPanelVisible:YES animated:animated];
|
||||||
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
break;
|
||||||
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
case KBKeyboardPanelModeSettings:
|
||||||
return;
|
[self kb_setSettingViewVisible:YES animated:animated];
|
||||||
}
|
break;
|
||||||
[self kb_setFunctionPanelVisible:YES];
|
case KBKeyboardPanelModeSubscription:
|
||||||
break;
|
[self kb_setSubscriptionPanelVisible:YES animated:animated];
|
||||||
case KBKeyboardPanelModeChat:
|
break;
|
||||||
[self kb_setChatPanelVisible:YES animated:animated];
|
case KBKeyboardPanelModeMain:
|
||||||
break;
|
default:
|
||||||
case KBKeyboardPanelModeSettings:
|
break;
|
||||||
[self kb_setSettingViewVisible:YES animated:animated];
|
}
|
||||||
break;
|
|
||||||
case KBKeyboardPanelModeSubscription:
|
|
||||||
[self kb_setSubscriptionPanelVisible:YES animated:animated];
|
|
||||||
break;
|
|
||||||
case KBKeyboardPanelModeMain:
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) 事件埋点:保持原逻辑(仅功能面板/主面板会互相曝光)
|
// 3) 事件埋点:保持原逻辑(仅功能面板/主面板会互相曝光)
|
||||||
if (mode == KBKeyboardPanelModeFunction) {
|
if (mode == KBKeyboardPanelModeFunction) {
|
||||||
|
|||||||
44
CustomKeyboard/PrivacyInfo.xcprivacy
Normal file
44
CustomKeyboard/PrivacyInfo.xcprivacy
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeOtherUserContent</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>CA92.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>C617.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -67,6 +67,8 @@ static CGFloat const kKBItemSpace = 4;
|
|||||||
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return kKBItemSpace; }
|
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return kKBItemSpace; }
|
||||||
|
|
||||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
// 有cell正在loading时,不允许点击其他cell
|
||||||
|
if (self.loadingIndexes.count > 0) { return; }
|
||||||
KBTagItemModel *model = (indexPath.item < self.items.count) ? self.items[indexPath.item] : [KBTagItemModel new];
|
KBTagItemModel *model = (indexPath.item < self.items.count) ? self.items[indexPath.item] : [KBTagItemModel new];
|
||||||
NSInteger personaId = 0;
|
NSInteger personaId = 0;
|
||||||
if ([model isKindOfClass:KBTagItemModel.class]) {
|
if ([model isKindOfClass:KBTagItemModel.class]) {
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
|
|
||||||
|
|
||||||
#define API_LOGOUT @"/user/logout" // 退出登录
|
#define API_LOGOUT @"/user/logout" // 退出登录
|
||||||
|
#define API_USER_CANCEL_ACCOUNT @"/user/cancelAccount" // 注销账户
|
||||||
|
#define API_CANCEL_ACCOUNT_WARNING @"/keyboardWarningMessage/byLocale" // 按locale查询注销提示信息
|
||||||
|
|
||||||
#define API_UPDATA_INFO @"/user/updateInfo" // 更新用户
|
#define API_UPDATA_INFO @"/user/updateInfo" // 更新用户
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,12 @@
|
|||||||
/// 用户头像 URL(主 App 写入,键盘扩展读取)
|
/// 用户头像 URL(主 App 写入,键盘扩展读取)
|
||||||
#define AppGroup_UserAvatarURL @"AppGroup_UserAvatarURL"
|
#define AppGroup_UserAvatarURL @"AppGroup_UserAvatarURL"
|
||||||
|
|
||||||
|
/// 键盘扩展聊天更新的 companionId(键盘写入,主 App 读取后刷新对应聊天记录)
|
||||||
|
#define AppGroup_ChatUpdatedCompanionId @"AppGroup_ChatUpdatedCompanionId"
|
||||||
|
|
||||||
|
/// Darwin 跨进程通知:键盘扩展发送聊天消息后通知主 App 刷新
|
||||||
|
#define kKBDarwinChatUpdated @"com.loveKey.nyx.chat.updated"
|
||||||
|
|
||||||
/// 皮肤图标加载模式:
|
/// 皮肤图标加载模式:
|
||||||
/// 0 = 使用本地 Assets 图片名(key_icons 的 value 写成图片名,例如 "kb_q_melon")
|
/// 0 = 使用本地 Assets 图片名(key_icons 的 value 写成图片名,例如 "kb_q_melon")
|
||||||
/// 1 = 使用远程 Zip 皮肤包(skinJSON 中提供 zip_url;key_icons 的 value 写成 Zip 内图标文件名,例如 "key_a")
|
/// 1 = 使用远程 Zip 皮肤包(skinJSON 中提供 zip_url;key_icons 的 value 写成 Zip 内图标文件名,例如 "key_a")
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
#if __has_include("KBNetworkManager.h")
|
#if __has_include("KBNetworkManager.h")
|
||||||
// 远程下载(http/https)
|
// 远程下载(http/https)
|
||||||
NSLog(@"🌐[SkinBridge] will GET zip: %@", zipURL);
|
NSLog(@"🌐[SkinBridge] will GET zip: %@", zipURL);
|
||||||
[KBHUD showWithStatus:@"正在下载..."];
|
[KBHUD showWithStatus:KBLocalized(@"Downloading...")];
|
||||||
[[KBNetworkManager shared] GETData:zipURL parameters:nil headers:nil completion:^(NSData *data, NSURLResponse *response, NSError *error) {
|
[[KBNetworkManager shared] GETData:zipURL parameters:nil headers:nil completion:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||||
NSLog(@"🌐[SkinBridge] GET finished id=%@ error=%@", skinId, error);
|
NSLog(@"🌐[SkinBridge] GET finished id=%@ error=%@", skinId, error);
|
||||||
if (error || data.length == 0) {
|
if (error || data.length == 0) {
|
||||||
|
|||||||
@@ -99,10 +99,22 @@ static NSString * const kKBSessionInstallFlagKey = @"KBSession.installInitialize
|
|||||||
// 先写 Keychain(统一走 KBAuthManager)
|
// 先写 Keychain(统一走 KBAuthManager)
|
||||||
NSString *token = user.token;
|
NSString *token = user.token;
|
||||||
if (token.length > 0) {
|
if (token.length > 0) {
|
||||||
[[KBAuthManager shared] saveAccessToken:token
|
BOOL saveOK = [[KBAuthManager shared] saveAccessToken:token
|
||||||
refreshToken:nil
|
refreshToken:nil
|
||||||
expiryDate:nil
|
expiryDate:nil
|
||||||
userIdentifier:nil];
|
userIdentifier:nil];
|
||||||
|
#if DEBUG
|
||||||
|
NSLog(@"[AuthTrace][App] saveAccessToken tokenLen=%lu saveOK=%d",
|
||||||
|
(unsigned long)token.length, saveOK);
|
||||||
|
#endif
|
||||||
|
// 主动 reload 一次,便于立即验证当前进程读到的登录态
|
||||||
|
[[KBAuthManager shared] reloadFromKeychain];
|
||||||
|
#if DEBUG
|
||||||
|
NSString *savedToken = [KBAuthManager shared].current.accessToken ?: @"";
|
||||||
|
NSLog(@"[AuthTrace][App] reloadAfterSave isLoggedIn=%d tokenLen=%lu",
|
||||||
|
[KBAuthManager shared].isLoggedIn,
|
||||||
|
(unsigned long)savedToken.length);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// 再缓存用户信息,供 App/键盘使用
|
// 再缓存用户信息,供 App/键盘使用
|
||||||
|
|||||||
@@ -172,6 +172,11 @@
|
|||||||
"Delete" = "Delete";
|
"Delete" = "Delete";
|
||||||
"Points\nMall" = "Points\nMall";
|
"Points\nMall" = "Points\nMall";
|
||||||
"Log Out" = "Log Out";
|
"Log Out" = "Log Out";
|
||||||
|
"Cancel Account" = "Cancel Account";
|
||||||
|
"After cancellation, your account will be deactivated and local login data will be cleared. Continue?" = "After cancellation, your account will be deactivated and local login data will be cleared. Continue?";
|
||||||
|
"Please enter your password" = "Please enter your password";
|
||||||
|
"Cancel Account Notice" = "Cancel Account Notice";
|
||||||
|
"Confirm Cancel Account" = "Confirm Cancel Account";
|
||||||
"Ranking List" = "Ranking List";
|
"Ranking List" = "Ranking List";
|
||||||
"Persona circle" = "Persona circle";
|
"Persona circle" = "Persona circle";
|
||||||
"Clear" = "Clear";
|
"Clear" = "Clear";
|
||||||
@@ -207,3 +212,4 @@
|
|||||||
"Purchase pending approval." = "Purchase pending approval.";
|
"Purchase pending approval." = "Purchase pending approval.";
|
||||||
"Unable to obtain transaction payload." = "Unable to obtain transaction payload.";
|
"Unable to obtain transaction payload." = "Unable to obtain transaction payload.";
|
||||||
"Resume Purchase" = "Resume Purchase";
|
"Resume Purchase" = "Resume Purchase";
|
||||||
|
"Downloading..." = "Downloading...";
|
||||||
|
|||||||
@@ -172,6 +172,11 @@
|
|||||||
"Delete" = "删除";
|
"Delete" = "删除";
|
||||||
"Points\nMall" = "积分\n商城";
|
"Points\nMall" = "积分\n商城";
|
||||||
"Log Out" = "退出";
|
"Log Out" = "退出";
|
||||||
|
"Cancel Account" = "注销账户";
|
||||||
|
"After cancellation, your account will be deactivated and local login data will be cleared. Continue?" = "注销后账号将被停用,并清除本地登录数据,是否继续?";
|
||||||
|
"Please enter your password" = "请输入密码";
|
||||||
|
"Cancel Account Notice" = "注销账户须知";
|
||||||
|
"Confirm Cancel Account" = "确认注销";
|
||||||
"Ranking List" = "排行榜";
|
"Ranking List" = "排行榜";
|
||||||
"Persona circle" = "圈子";
|
"Persona circle" = "圈子";
|
||||||
"Clear" = "立刻清空";
|
"Clear" = "立刻清空";
|
||||||
@@ -209,3 +214,4 @@
|
|||||||
"Purchase pending approval." = "购买等待确认";
|
"Purchase pending approval." = "购买等待确认";
|
||||||
"Unable to obtain transaction payload." = "无法获取交易凭据";
|
"Unable to obtain transaction payload." = "无法获取交易凭据";
|
||||||
"Resume Purchase" = "恢复购买";
|
"Resume Purchase" = "恢复购买";
|
||||||
|
"Downloading..." = "正在下载...";
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
1
_spm/checkouts/swift-collections
Submodule
1
_spm/checkouts/swift-collections
Submodule
Submodule _spm/checkouts/swift-collections added at 7b847a3b70
@@ -16,7 +16,6 @@
|
|||||||
040B62142F4BF2560099DEAC /* KeyboardViewController+Subscription.m in Sources */ = {isa = PBXBuildFile; fileRef = 040B620A2F4BF2560099DEAC /* KeyboardViewController+Subscription.m */; };
|
040B62142F4BF2560099DEAC /* KeyboardViewController+Subscription.m in Sources */ = {isa = PBXBuildFile; fileRef = 040B620A2F4BF2560099DEAC /* KeyboardViewController+Subscription.m */; };
|
||||||
040B62162F4BF2560099DEAC /* KeyboardViewController+Suggestions.m in Sources */ = {isa = PBXBuildFile; fileRef = 040B620B2F4BF2560099DEAC /* KeyboardViewController+Suggestions.m */; };
|
040B62162F4BF2560099DEAC /* KeyboardViewController+Suggestions.m in Sources */ = {isa = PBXBuildFile; fileRef = 040B620B2F4BF2560099DEAC /* KeyboardViewController+Suggestions.m */; };
|
||||||
041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */ = {isa = PBXBuildFile; fileRef = 041007D12ECE012000D203BB /* KBSkinIconMap.strings */; };
|
041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */ = {isa = PBXBuildFile; fileRef = 041007D12ECE012000D203BB /* KBSkinIconMap.strings */; };
|
||||||
041007D42ECE012500D203BB /* 002.zip in Resources */ = {isa = PBXBuildFile; fileRef = 041007D32ECE012500D203BB /* 002.zip */; };
|
|
||||||
04122F5D2EC5E5A900EF7AB3 /* KBLoginVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F5B2EC5E5A900EF7AB3 /* KBLoginVM.m */; };
|
04122F5D2EC5E5A900EF7AB3 /* KBLoginVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F5B2EC5E5A900EF7AB3 /* KBLoginVM.m */; };
|
||||||
04122F622EC5F41D00EF7AB3 /* KBUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F612EC5F41D00EF7AB3 /* KBUser.m */; };
|
04122F622EC5F41D00EF7AB3 /* KBUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F612EC5F41D00EF7AB3 /* KBUser.m */; };
|
||||||
04122F7E2EC5FC5500EF7AB3 /* KBJfPayCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F7C2EC5FC5500EF7AB3 /* KBJfPayCell.m */; };
|
04122F7E2EC5FC5500EF7AB3 /* KBJfPayCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F7C2EC5FC5500EF7AB3 /* KBJfPayCell.m */; };
|
||||||
@@ -35,7 +34,6 @@
|
|||||||
04286A032ECB0A1600CE730C /* KBSexSelVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04286A022ECB0A1600CE730C /* KBSexSelVC.m */; };
|
04286A032ECB0A1600CE730C /* KBSexSelVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04286A022ECB0A1600CE730C /* KBSexSelVC.m */; };
|
||||||
04286A062ECC81B200CE730C /* KBSkinService.m in Sources */ = {isa = PBXBuildFile; fileRef = 04286A052ECC81B200CE730C /* KBSkinService.m */; };
|
04286A062ECC81B200CE730C /* KBSkinService.m in Sources */ = {isa = PBXBuildFile; fileRef = 04286A052ECC81B200CE730C /* KBSkinService.m */; };
|
||||||
04286A0B2ECD88B400CE730C /* KeyboardAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 04286A0A2ECD88B400CE730C /* KeyboardAssets.xcassets */; };
|
04286A0B2ECD88B400CE730C /* KeyboardAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 04286A0A2ECD88B400CE730C /* KeyboardAssets.xcassets */; };
|
||||||
04286A0F2ECDA71B00CE730C /* 001.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04286A0E2ECDA71B00CE730C /* 001.zip */; };
|
|
||||||
04286A132ECDEBF900CE730C /* KBSkinIconMap.strings in Resources */ = {isa = PBXBuildFile; fileRef = 041007D12ECE012000D203BB /* KBSkinIconMap.strings */; };
|
04286A132ECDEBF900CE730C /* KBSkinIconMap.strings in Resources */ = {isa = PBXBuildFile; fileRef = 041007D12ECE012000D203BB /* KBSkinIconMap.strings */; };
|
||||||
043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EAE12EAF940F0089C901 /* KBPermissionViewController.m */; };
|
043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EAE12EAF940F0089C901 /* KBPermissionViewController.m */; };
|
||||||
0450AA742EF013D000B6AF06 /* KBEmojiCollectionCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 0450AA732EF013D000B6AF06 /* KBEmojiCollectionCell.m */; };
|
0450AA742EF013D000B6AF06 /* KBEmojiCollectionCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 0450AA732EF013D000B6AF06 /* KBEmojiCollectionCell.m */; };
|
||||||
@@ -60,7 +58,7 @@
|
|||||||
0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */; };
|
0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */; };
|
||||||
0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; };
|
0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; };
|
||||||
0459D1B82EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; };
|
0459D1B82EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; };
|
||||||
0460866B2F18D75500757C95 /* ai_test.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0460866A2F18D75500757C95 /* ai_test.m4a */; };
|
045ED5212F52AF9200131114 /* KBCancelAccountVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 045ED5202F52AF9200131114 /* KBCancelAccountVC.m */; };
|
||||||
046086752F191CC700757C95 /* AI技术分析.txt in Resources */ = {isa = PBXBuildFile; fileRef = 046086742F191CC700757C95 /* AI技术分析.txt */; };
|
046086752F191CC700757C95 /* AI技术分析.txt in Resources */ = {isa = PBXBuildFile; fileRef = 046086742F191CC700757C95 /* AI技术分析.txt */; };
|
||||||
0460869A2F19238500757C95 /* KBAiWaveformView.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086992F19238500757C95 /* KBAiWaveformView.m */; };
|
0460869A2F19238500757C95 /* KBAiWaveformView.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086992F19238500757C95 /* KBAiWaveformView.m */; };
|
||||||
0460869C2F19238500757C95 /* KBAiRecordButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086972F19238500757C95 /* KBAiRecordButton.m */; };
|
0460869C2F19238500757C95 /* KBAiRecordButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086972F19238500757C95 /* KBAiRecordButton.m */; };
|
||||||
@@ -87,7 +85,6 @@
|
|||||||
04791F952ED48028004E8522 /* KBFeedBackVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F942ED48028004E8522 /* KBFeedBackVC.m */; };
|
04791F952ED48028004E8522 /* KBFeedBackVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F942ED48028004E8522 /* KBFeedBackVC.m */; };
|
||||||
04791F982ED49CE7004E8522 /* KBFont.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F972ED49CE7004E8522 /* KBFont.m */; };
|
04791F982ED49CE7004E8522 /* KBFont.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F972ED49CE7004E8522 /* KBFont.m */; };
|
||||||
04791F992ED49CE7004E8522 /* KBFont.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F972ED49CE7004E8522 /* KBFont.m */; };
|
04791F992ED49CE7004E8522 /* KBFont.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F972ED49CE7004E8522 /* KBFont.m */; };
|
||||||
04791FF72ED5B985004E8522 /* Christmas.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04791FF62ED5B985004E8522 /* Christmas.zip */; };
|
|
||||||
04791FFC2ED71D17004E8522 /* UIColor+Extension.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95E42EB220B5007BD342 /* UIColor+Extension.m */; };
|
04791FFC2ED71D17004E8522 /* UIColor+Extension.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95E42EB220B5007BD342 /* UIColor+Extension.m */; };
|
||||||
04791FFF2ED830FA004E8522 /* KBKeyboardMaskView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791FFE2ED830FA004E8522 /* KBKeyboardMaskView.m */; };
|
04791FFF2ED830FA004E8522 /* KBKeyboardMaskView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791FFE2ED830FA004E8522 /* KBKeyboardMaskView.m */; };
|
||||||
047920072ED86ABC004E8522 /* kb_guide_keyboard.gif in Resources */ = {isa = PBXBuildFile; fileRef = 047920062ED86ABC004E8522 /* kb_guide_keyboard.gif */; };
|
047920072ED86ABC004E8522 /* kb_guide_keyboard.gif in Resources */ = {isa = PBXBuildFile; fileRef = 047920062ED86ABC004E8522 /* kb_guide_keyboard.gif */; };
|
||||||
@@ -233,6 +230,8 @@
|
|||||||
04E0B2022F300002002CA5A0 /* KBVoiceRecordManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0B2012F300002002CA5A0 /* KBVoiceRecordManager.m */; };
|
04E0B2022F300002002CA5A0 /* KBVoiceRecordManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0B2012F300002002CA5A0 /* KBVoiceRecordManager.m */; };
|
||||||
04E161832F10E6470022C23B /* normal_hei_them.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04E161812F10E6470022C23B /* normal_hei_them.zip */; };
|
04E161832F10E6470022C23B /* normal_hei_them.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04E161812F10E6470022C23B /* normal_hei_them.zip */; };
|
||||||
04E161842F10E6470022C23B /* normal_them.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04E161822F10E6470022C23B /* normal_them.zip */; };
|
04E161842F10E6470022C23B /* normal_them.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04E161822F10E6470022C23B /* normal_them.zip */; };
|
||||||
|
04E2277D2F516EBD001A8F14 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 04E2277C2F516EBD001A8F14 /* PrivacyInfo.xcprivacy */; };
|
||||||
|
04E2277F2F516ED3001A8F14 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 04E2277E2F516ED3001A8F14 /* PrivacyInfo.xcprivacy */; };
|
||||||
04F4C0AA2F32274000E8F08C /* KBPayMainVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04F4C0A92F32274000E8F08C /* KBPayMainVC.m */; };
|
04F4C0AA2F32274000E8F08C /* KBPayMainVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04F4C0A92F32274000E8F08C /* KBPayMainVC.m */; };
|
||||||
04F4C0AD2F32288600E8F08C /* KBPaySvipVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04F4C0AC2F32288600E8F08C /* KBPaySvipVC.m */; };
|
04F4C0AD2F32288600E8F08C /* KBPaySvipVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04F4C0AC2F32288600E8F08C /* KBPaySvipVC.m */; };
|
||||||
04F4C0B02F322EF200E8F08C /* PagingViewTableHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04F4C0AF2F322EF200E8F08C /* PagingViewTableHeaderView.m */; };
|
04F4C0B02F322EF200E8F08C /* PagingViewTableHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04F4C0AF2F322EF200E8F08C /* PagingViewTableHeaderView.m */; };
|
||||||
@@ -347,7 +346,6 @@
|
|||||||
040B620C2F4BF2560099DEAC /* KeyboardViewController+Theme.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "KeyboardViewController+Theme.m"; sourceTree = "<group>"; };
|
040B620C2F4BF2560099DEAC /* KeyboardViewController+Theme.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "KeyboardViewController+Theme.m"; sourceTree = "<group>"; };
|
||||||
040B620D2F4BF2560099DEAC /* KeyboardViewController+UI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "KeyboardViewController+UI.m"; sourceTree = "<group>"; };
|
040B620D2F4BF2560099DEAC /* KeyboardViewController+UI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "KeyboardViewController+UI.m"; sourceTree = "<group>"; };
|
||||||
041007D12ECE012000D203BB /* KBSkinIconMap.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = KBSkinIconMap.strings; sourceTree = "<group>"; };
|
041007D12ECE012000D203BB /* KBSkinIconMap.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = KBSkinIconMap.strings; sourceTree = "<group>"; };
|
||||||
041007D32ECE012500D203BB /* 002.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = 002.zip; sourceTree = "<group>"; };
|
|
||||||
04122F592EC5D40000EF7AB3 /* KBAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAPI.h; sourceTree = "<group>"; };
|
04122F592EC5D40000EF7AB3 /* KBAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAPI.h; sourceTree = "<group>"; };
|
||||||
04122F5A2EC5E5A900EF7AB3 /* KBLoginVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBLoginVM.h; sourceTree = "<group>"; };
|
04122F5A2EC5E5A900EF7AB3 /* KBLoginVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBLoginVM.h; sourceTree = "<group>"; };
|
||||||
04122F5B2EC5E5A900EF7AB3 /* KBLoginVM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBLoginVM.m; sourceTree = "<group>"; };
|
04122F5B2EC5E5A900EF7AB3 /* KBLoginVM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBLoginVM.m; sourceTree = "<group>"; };
|
||||||
@@ -382,7 +380,6 @@
|
|||||||
04286A042ECC81B200CE730C /* KBSkinService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinService.h; sourceTree = "<group>"; };
|
04286A042ECC81B200CE730C /* KBSkinService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinService.h; sourceTree = "<group>"; };
|
||||||
04286A052ECC81B200CE730C /* KBSkinService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinService.m; sourceTree = "<group>"; };
|
04286A052ECC81B200CE730C /* KBSkinService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinService.m; sourceTree = "<group>"; };
|
||||||
04286A0A2ECD88B400CE730C /* KeyboardAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = KeyboardAssets.xcassets; sourceTree = "<group>"; };
|
04286A0A2ECD88B400CE730C /* KeyboardAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = KeyboardAssets.xcassets; sourceTree = "<group>"; };
|
||||||
04286A0E2ECDA71B00CE730C /* 001.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = 001.zip; sourceTree = "<group>"; };
|
|
||||||
0450AA722EF013D000B6AF06 /* KBEmojiCollectionCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBEmojiCollectionCell.h; sourceTree = "<group>"; };
|
0450AA722EF013D000B6AF06 /* KBEmojiCollectionCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBEmojiCollectionCell.h; sourceTree = "<group>"; };
|
||||||
0450AA732EF013D000B6AF06 /* KBEmojiCollectionCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBEmojiCollectionCell.m; sourceTree = "<group>"; };
|
0450AA732EF013D000B6AF06 /* KBEmojiCollectionCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBEmojiCollectionCell.m; sourceTree = "<group>"; };
|
||||||
0450AAE02EF03D5100B6AF06 /* keyBoard-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "keyBoard-Bridging-Header.h"; sourceTree = "<group>"; };
|
0450AAE02EF03D5100B6AF06 /* keyBoard-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "keyBoard-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
@@ -408,7 +405,8 @@
|
|||||||
0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinCenterVC.m; sourceTree = "<group>"; };
|
0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinCenterVC.m; sourceTree = "<group>"; };
|
||||||
0459D1B52EBA287900F2D189 /* KBSkinManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinManager.h; sourceTree = "<group>"; };
|
0459D1B52EBA287900F2D189 /* KBSkinManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinManager.h; sourceTree = "<group>"; };
|
||||||
0459D1B62EBA287900F2D189 /* KBSkinManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinManager.m; sourceTree = "<group>"; };
|
0459D1B62EBA287900F2D189 /* KBSkinManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinManager.m; sourceTree = "<group>"; };
|
||||||
0460866A2F18D75500757C95 /* ai_test.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = ai_test.m4a; sourceTree = "<group>"; };
|
045ED51F2F52AF9200131114 /* KBCancelAccountVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBCancelAccountVC.h; sourceTree = "<group>"; };
|
||||||
|
045ED5202F52AF9200131114 /* KBCancelAccountVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBCancelAccountVC.m; sourceTree = "<group>"; };
|
||||||
046086742F191CC700757C95 /* AI技术分析.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "AI技术分析.txt"; sourceTree = "<group>"; };
|
046086742F191CC700757C95 /* AI技术分析.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "AI技术分析.txt"; sourceTree = "<group>"; };
|
||||||
046086962F19238500757C95 /* KBAiRecordButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAiRecordButton.h; sourceTree = "<group>"; };
|
046086962F19238500757C95 /* KBAiRecordButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAiRecordButton.h; sourceTree = "<group>"; };
|
||||||
046086972F19238500757C95 /* KBAiRecordButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAiRecordButton.m; sourceTree = "<group>"; };
|
046086972F19238500757C95 /* KBAiRecordButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAiRecordButton.m; sourceTree = "<group>"; };
|
||||||
@@ -455,7 +453,6 @@
|
|||||||
04791F942ED48028004E8522 /* KBFeedBackVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFeedBackVC.m; sourceTree = "<group>"; };
|
04791F942ED48028004E8522 /* KBFeedBackVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFeedBackVC.m; sourceTree = "<group>"; };
|
||||||
04791F962ED49CE7004E8522 /* KBFont.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFont.h; sourceTree = "<group>"; };
|
04791F962ED49CE7004E8522 /* KBFont.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFont.h; sourceTree = "<group>"; };
|
||||||
04791F972ED49CE7004E8522 /* KBFont.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFont.m; sourceTree = "<group>"; };
|
04791F972ED49CE7004E8522 /* KBFont.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFont.m; sourceTree = "<group>"; };
|
||||||
04791FF62ED5B985004E8522 /* Christmas.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = Christmas.zip; sourceTree = "<group>"; };
|
|
||||||
04791FFD2ED830FA004E8522 /* KBKeyboardMaskView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardMaskView.h; sourceTree = "<group>"; };
|
04791FFD2ED830FA004E8522 /* KBKeyboardMaskView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardMaskView.h; sourceTree = "<group>"; };
|
||||||
04791FFE2ED830FA004E8522 /* KBKeyboardMaskView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardMaskView.m; sourceTree = "<group>"; };
|
04791FFE2ED830FA004E8522 /* KBKeyboardMaskView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardMaskView.m; sourceTree = "<group>"; };
|
||||||
047920062ED86ABC004E8522 /* kb_guide_keyboard.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = kb_guide_keyboard.gif; sourceTree = "<group>"; };
|
047920062ED86ABC004E8522 /* kb_guide_keyboard.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = kb_guide_keyboard.gif; sourceTree = "<group>"; };
|
||||||
@@ -719,6 +716,8 @@
|
|||||||
04E0B2012F300002002CA5A0 /* KBVoiceRecordManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBVoiceRecordManager.m; sourceTree = "<group>"; };
|
04E0B2012F300002002CA5A0 /* KBVoiceRecordManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBVoiceRecordManager.m; sourceTree = "<group>"; };
|
||||||
04E161812F10E6470022C23B /* normal_hei_them.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = normal_hei_them.zip; sourceTree = "<group>"; };
|
04E161812F10E6470022C23B /* normal_hei_them.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = normal_hei_them.zip; sourceTree = "<group>"; };
|
||||||
04E161822F10E6470022C23B /* normal_them.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = normal_them.zip; sourceTree = "<group>"; };
|
04E161822F10E6470022C23B /* normal_them.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = normal_them.zip; sourceTree = "<group>"; };
|
||||||
|
04E2277C2F516EBD001A8F14 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
|
04E2277E2F516ED3001A8F14 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
04F4C0A82F32274000E8F08C /* KBPayMainVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPayMainVC.h; sourceTree = "<group>"; };
|
04F4C0A82F32274000E8F08C /* KBPayMainVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPayMainVC.h; sourceTree = "<group>"; };
|
||||||
04F4C0A92F32274000E8F08C /* KBPayMainVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPayMainVC.m; sourceTree = "<group>"; };
|
04F4C0A92F32274000E8F08C /* KBPayMainVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPayMainVC.m; sourceTree = "<group>"; };
|
||||||
04F4C0AB2F32288600E8F08C /* KBPaySvipVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPaySvipVC.h; sourceTree = "<group>"; };
|
04F4C0AB2F32288600E8F08C /* KBPaySvipVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPaySvipVC.h; sourceTree = "<group>"; };
|
||||||
@@ -909,15 +908,12 @@
|
|||||||
041007D02ECE010100D203BB /* Resource */ = {
|
041007D02ECE010100D203BB /* Resource */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
0460866A2F18D75500757C95 /* ai_test.m4a */,
|
|
||||||
04E161812F10E6470022C23B /* normal_hei_them.zip */,
|
04E161812F10E6470022C23B /* normal_hei_them.zip */,
|
||||||
04E161822F10E6470022C23B /* normal_them.zip */,
|
04E161822F10E6470022C23B /* normal_them.zip */,
|
||||||
A1B2C3EC2F20000000000001 /* kb_words.txt */,
|
A1B2C3EC2F20000000000001 /* kb_words.txt */,
|
||||||
A1B2C3F02F20000000000002 /* kb_keyboard_layout_config.json */,
|
A1B2C3F02F20000000000002 /* kb_keyboard_layout_config.json */,
|
||||||
0498BDF42EEC50EE006CC1D5 /* emoji_categories.json */,
|
0498BDF42EEC50EE006CC1D5 /* emoji_categories.json */,
|
||||||
041007D12ECE012000D203BB /* KBSkinIconMap.strings */,
|
041007D12ECE012000D203BB /* KBSkinIconMap.strings */,
|
||||||
041007D32ECE012500D203BB /* 002.zip */,
|
|
||||||
04791FF62ED5B985004E8522 /* Christmas.zip */,
|
|
||||||
);
|
);
|
||||||
path = Resource;
|
path = Resource;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1263,7 +1259,6 @@
|
|||||||
0479200A2ED87CEE004E8522 /* permiss_video.mp4 */,
|
0479200A2ED87CEE004E8522 /* permiss_video.mp4 */,
|
||||||
047920102ED98E7D004E8522 /* permiss_video_2.mp4 */,
|
047920102ED98E7D004E8522 /* permiss_video_2.mp4 */,
|
||||||
047920062ED86ABC004E8522 /* kb_guide_keyboard.gif */,
|
047920062ED86ABC004E8522 /* kb_guide_keyboard.gif */,
|
||||||
04286A0E2ECDA71B00CE730C /* 001.zip */,
|
|
||||||
);
|
);
|
||||||
path = Resource;
|
path = Resource;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1568,6 +1563,7 @@
|
|||||||
04C6EAB92EAF86530089C901 /* keyBoard */ = {
|
04C6EAB92EAF86530089C901 /* keyBoard */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
04E2277C2F516EBD001A8F14 /* PrivacyInfo.xcprivacy */,
|
||||||
04FC95F52EB33B52007BD342 /* keyBoard.entitlements */,
|
04FC95F52EB33B52007BD342 /* keyBoard.entitlements */,
|
||||||
04FC95BF2EB1E3B1007BD342 /* Class */,
|
04FC95BF2EB1E3B1007BD342 /* Class */,
|
||||||
04C6EAE32EAF942E0089C901 /* VC */,
|
04C6EAE32EAF942E0089C901 /* VC */,
|
||||||
@@ -1588,6 +1584,7 @@
|
|||||||
04C6EAD72EAF870B0089C901 /* CustomKeyboard */ = {
|
04C6EAD72EAF870B0089C901 /* CustomKeyboard */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
04E2277E2F516ED3001A8F14 /* PrivacyInfo.xcprivacy */,
|
||||||
0419C9632F2C7630002E86D3 /* VM */,
|
0419C9632F2C7630002E86D3 /* VM */,
|
||||||
041007D02ECE010100D203BB /* Resource */,
|
041007D02ECE010100D203BB /* Resource */,
|
||||||
0477BD942EBAFF4E0055D639 /* Utils */,
|
0477BD942EBAFF4E0055D639 /* Utils */,
|
||||||
@@ -1848,6 +1845,8 @@
|
|||||||
A1F0C1A52F1234567890ABCD /* KBConsumptionRecordVC.m */,
|
A1F0C1A52F1234567890ABCD /* KBConsumptionRecordVC.m */,
|
||||||
049FB2212EC311F900FAB05D /* KBPersonInfoVC.h */,
|
049FB2212EC311F900FAB05D /* KBPersonInfoVC.h */,
|
||||||
049FB2222EC311F900FAB05D /* KBPersonInfoVC.m */,
|
049FB2222EC311F900FAB05D /* KBPersonInfoVC.m */,
|
||||||
|
045ED51F2F52AF9200131114 /* KBCancelAccountVC.h */,
|
||||||
|
045ED5202F52AF9200131114 /* KBCancelAccountVC.m */,
|
||||||
04791F902ED48010004E8522 /* KBNoticeVC.h */,
|
04791F902ED48010004E8522 /* KBNoticeVC.h */,
|
||||||
04791F912ED48010004E8522 /* KBNoticeVC.m */,
|
04791F912ED48010004E8522 /* KBNoticeVC.m */,
|
||||||
04791F932ED48028004E8522 /* KBFeedBackVC.h */,
|
04791F932ED48028004E8522 /* KBFeedBackVC.h */,
|
||||||
@@ -2254,13 +2253,11 @@
|
|||||||
04E161832F10E6470022C23B /* normal_hei_them.zip in Resources */,
|
04E161832F10E6470022C23B /* normal_hei_them.zip in Resources */,
|
||||||
04E161842F10E6470022C23B /* normal_them.zip in Resources */,
|
04E161842F10E6470022C23B /* normal_them.zip in Resources */,
|
||||||
04A9FE202EB893F10020DB6D /* Localizable.strings in Resources */,
|
04A9FE202EB893F10020DB6D /* Localizable.strings in Resources */,
|
||||||
0460866B2F18D75500757C95 /* ai_test.m4a in Resources */,
|
|
||||||
041007D42ECE012500D203BB /* 002.zip in Resources */,
|
|
||||||
041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */,
|
041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */,
|
||||||
|
04E2277F2F516ED3001A8F14 /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
A1B2C3ED2F20000000000001 /* kb_words.txt in Resources */,
|
A1B2C3ED2F20000000000001 /* kb_words.txt in Resources */,
|
||||||
A1B2C3F12F20000000000002 /* kb_keyboard_layout_config.json in Resources */,
|
A1B2C3F12F20000000000002 /* kb_keyboard_layout_config.json in Resources */,
|
||||||
0498BDF52EEC50EE006CC1D5 /* emoji_categories.json in Resources */,
|
0498BDF52EEC50EE006CC1D5 /* emoji_categories.json in Resources */,
|
||||||
04791FF72ED5B985004E8522 /* Christmas.zip in Resources */,
|
|
||||||
04286A0B2ECD88B400CE730C /* KeyboardAssets.xcassets in Resources */,
|
04286A0B2ECD88B400CE730C /* KeyboardAssets.xcassets in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -2269,9 +2266,9 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
04286A0F2ECDA71B00CE730C /* 001.zip in Resources */,
|
|
||||||
04E038D82F20BFFB002CA5A0 /* websocket-api.md in Resources */,
|
04E038D82F20BFFB002CA5A0 /* websocket-api.md in Resources */,
|
||||||
0479200B2ED87CEE004E8522 /* permiss_video.mp4 in Resources */,
|
0479200B2ED87CEE004E8522 /* permiss_video.mp4 in Resources */,
|
||||||
|
04E2277D2F516EBD001A8F14 /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
04C6EABA2EAF86530089C901 /* Assets.xcassets in Resources */,
|
04C6EABA2EAF86530089C901 /* Assets.xcassets in Resources */,
|
||||||
04A9FE212EB893F10020DB6D /* Localizable.strings in Resources */,
|
04A9FE212EB893F10020DB6D /* Localizable.strings in Resources */,
|
||||||
047920072ED86ABC004E8522 /* kb_guide_keyboard.gif in Resources */,
|
047920072ED86ABC004E8522 /* kb_guide_keyboard.gif in Resources */,
|
||||||
@@ -2618,6 +2615,7 @@
|
|||||||
0498BDE42EEA885D006CC1D5 /* KBShopThemeModel.m in Sources */,
|
0498BDE42EEA885D006CC1D5 /* KBShopThemeModel.m in Sources */,
|
||||||
048908CD2EBE373500FABA60 /* KBSearchSectionHeader.m in Sources */,
|
048908CD2EBE373500FABA60 /* KBSearchSectionHeader.m in Sources */,
|
||||||
049FB2202EC30D2700FAB05D /* HomeRankDetailPopView.m in Sources */,
|
049FB2202EC30D2700FAB05D /* HomeRankDetailPopView.m in Sources */,
|
||||||
|
045ED5212F52AF9200131114 /* KBCancelAccountVC.m in Sources */,
|
||||||
048908CE2EBE373500FABA60 /* KBSkinCardCell.m in Sources */,
|
048908CE2EBE373500FABA60 /* KBSkinCardCell.m in Sources */,
|
||||||
048908CF2EBE373500FABA60 /* KBTagCell.m in Sources */,
|
048908CF2EBE373500FABA60 /* KBTagCell.m in Sources */,
|
||||||
0477BEA22EBCF0000055D639 /* KBTopImageButton.m in Sources */,
|
0477BEA22EBCF0000055D639 /* KBTopImageButton.m in Sources */,
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict/>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>BuildLocationStyle</key>
|
||||||
|
<string>UseAppPreferences</string>
|
||||||
|
<key>CustomBuildLocationType</key>
|
||||||
|
<string>RelativeToDerivedData</string>
|
||||||
|
<key>DerivedDataLocationStyle</key>
|
||||||
|
<string>Default</string>
|
||||||
|
<key>ShowSharedSchemesAutomaticallyEnabled</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
21
keyBoard/Assets.xcassets/Ohter/ai_placehode_icon.imageset/Contents.json
vendored
Normal file
21
keyBoard/Assets.xcassets/Ohter/ai_placehode_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "微信图片_20260226192149_128_935 (1).png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
keyBoard/Assets.xcassets/Ohter/ai_placehode_icon.imageset/微信图片_20260226192149_128_935 (1).png
vendored
Normal file
BIN
keyBoard/Assets.xcassets/Ohter/ai_placehode_icon.imageset/微信图片_20260226192149_128_935 (1).png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 414 KiB |
@@ -69,6 +69,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
/// 当前 Cell 不再是屏幕主显示页
|
/// 当前 Cell 不再是屏幕主显示页
|
||||||
- (void)onResignedCurrentPersonaCell;
|
- (void)onResignedCurrentPersonaCell;
|
||||||
|
|
||||||
|
/// 刷新聊天记录(重置分页状态,从第一页重新加载)
|
||||||
|
- (void)refreshChatHistory;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
|
|
||||||
// 设置 UI
|
// 设置 UI
|
||||||
[self.backgroundImageView sd_setImageWithURL:[NSURL URLWithString:persona.coverImageUrl]
|
[self.backgroundImageView sd_setImageWithURL:[NSURL URLWithString:persona.coverImageUrl]
|
||||||
placeholderImage:[UIImage imageNamed:@"placeholder_bg"]];
|
placeholderImage:[UIImage imageNamed:@"ai_placehode_icon"]];
|
||||||
[self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:persona.avatarUrl]
|
[self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:persona.avatarUrl]
|
||||||
placeholderImage:[UIImage imageNamed:@"placeholder_avatar"]];
|
placeholderImage:[UIImage imageNamed:@"placeholder_avatar"]];
|
||||||
self.nameLabel.text = persona.name;
|
self.nameLabel.text = persona.name;
|
||||||
@@ -256,7 +256,20 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
|||||||
if (self.hasLoadedData || self.isLoading) {
|
if (self.hasLoadedData || self.isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[self loadChatHistory];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)refreshChatHistory {
|
||||||
|
// 重置分页状态
|
||||||
|
self.currentPage = 1;
|
||||||
|
self.hasMoreHistory = YES;
|
||||||
|
self.hasLoadedData = NO;
|
||||||
|
self.isLoading = NO;
|
||||||
|
|
||||||
|
// 清空当前消息并重新加载
|
||||||
|
[self.messages removeAllObjects];
|
||||||
|
[self.chatView clearMessages];
|
||||||
[self loadChatHistory];
|
[self loadChatHistory];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)showKeyboard {
|
- (void)showKeyboard {
|
||||||
[self.textField becomeFirstResponder];
|
BOOL before = self.textField.isFirstResponder;
|
||||||
|
BOOL become = [self.textField becomeFirstResponder];
|
||||||
|
NSLog(@"[KBAICommentInputView] showKeyboard before=%d become=%d after=%d", before, become, self.textField.isFirstResponder);
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Actions
|
#pragma mark - Actions
|
||||||
@@ -129,11 +131,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
|
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
|
||||||
|
NSLog(@"[KBAICommentInputView] textFieldShouldBeginEditing");
|
||||||
[self updatePlaceholderVisibility];
|
[self updatePlaceholderVisibility];
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)textFieldDidBeginEditing:(UITextField *)textField {
|
||||||
|
NSLog(@"[KBAICommentInputView] textFieldDidBeginEditing firstResponder=%d", textField.isFirstResponder);
|
||||||
|
}
|
||||||
|
|
||||||
- (void)textFieldDidEndEditing:(UITextField *)textField {
|
- (void)textFieldDidEndEditing:(UITextField *)textField {
|
||||||
|
NSLog(@"[KBAICommentInputView] textFieldDidEndEditing");
|
||||||
[self updatePlaceholderVisibility];
|
[self updatePlaceholderVisibility];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,19 @@
|
|||||||
|
|
||||||
static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId";
|
static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId";
|
||||||
|
|
||||||
|
#pragma mark - Darwin Notification Callback (键盘扩展聊天更新)
|
||||||
|
|
||||||
|
static void KBChatUpdatedDarwinCallback(CFNotificationCenterRef center,
|
||||||
|
void *observer,
|
||||||
|
CFStringRef name,
|
||||||
|
const void *object,
|
||||||
|
CFDictionaryRef userInfo) {
|
||||||
|
KBAIHomeVC *self = (__bridge KBAIHomeVC *)observer;
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[self kb_handleChatUpdatedFromExtension];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Keyboard Gate
|
#pragma mark - Keyboard Gate
|
||||||
|
|
||||||
/// 查找当前 view 树里的 firstResponder
|
/// 查找当前 view 树里的 firstResponder
|
||||||
@@ -163,10 +176,21 @@ static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId";
|
|||||||
[self setupKeyboardNotifications];
|
[self setupKeyboardNotifications];
|
||||||
[self setupKeyboardDismissGesture];
|
[self setupKeyboardDismissGesture];
|
||||||
[self loadPersonas];
|
[self loadPersonas];
|
||||||
|
|
||||||
|
// 监听键盘扩展聊天更新的 Darwin 跨进程通知
|
||||||
|
CFNotificationCenterAddObserver(
|
||||||
|
CFNotificationCenterGetDarwinNotifyCenter(),
|
||||||
|
(__bridge const void *)(self),
|
||||||
|
KBChatUpdatedDarwinCallback,
|
||||||
|
(__bridge CFStringRef)kKBDarwinChatUpdated,
|
||||||
|
NULL,
|
||||||
|
CFNotificationSuspensionBehaviorDeliverImmediately);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewDidAppear:(BOOL)animated {
|
- (void)viewDidAppear:(BOOL)animated {
|
||||||
[super viewDidAppear:animated];
|
[super viewDidAppear:animated];
|
||||||
|
[self kb_syncTextInputStateIfNeeded];
|
||||||
|
[self kb_logInputLayoutWithTag:@"viewDidAppear"];
|
||||||
KBPersonaChatCell *cell = [self currentPersonaCell];
|
KBPersonaChatCell *cell = [self currentPersonaCell];
|
||||||
if (cell) {
|
if (cell) {
|
||||||
[cell onBecameCurrentPersonaCell];
|
[cell onBecameCurrentPersonaCell];
|
||||||
@@ -175,6 +199,17 @@ static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId";
|
|||||||
|
|
||||||
- (void)viewWillDisappear:(BOOL)animated {
|
- (void)viewWillDisappear:(BOOL)animated {
|
||||||
[super viewWillDisappear:animated];
|
[super viewWillDisappear:animated];
|
||||||
|
// 离开页面时结束编辑并重置底部输入状态,避免返回时出现 hidden/firstResponder 错位
|
||||||
|
[self.view endEditing:YES];
|
||||||
|
self.voiceInputKeyboardActive = NO;
|
||||||
|
self.currentKeyboardHeight = 0.0;
|
||||||
|
self.isTextInputMode = NO;
|
||||||
|
self.commentInputView.hidden = YES;
|
||||||
|
[self.commentInputBottomConstraint setOffset:100];
|
||||||
|
self.voiceInputBar.hidden = NO;
|
||||||
|
[self.voiceInputBarBottomConstraint setOffset:-self.baseInputBarBottomSpacing];
|
||||||
|
[self.view layoutIfNeeded];
|
||||||
|
[self kb_logInputLayoutWithTag:@"viewWillDisappear"];
|
||||||
for (NSIndexPath *indexPath in self.collectionView.indexPathsForVisibleItems) {
|
for (NSIndexPath *indexPath in self.collectionView.indexPathsForVisibleItems) {
|
||||||
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
||||||
if (cell) {
|
if (cell) {
|
||||||
@@ -263,16 +298,26 @@ static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId";
|
|||||||
self.isTextInputMode = YES;
|
self.isTextInputMode = YES;
|
||||||
self.voiceInputBar.hidden = YES;
|
self.voiceInputBar.hidden = YES;
|
||||||
self.commentInputView.hidden = NO;
|
self.commentInputView.hidden = NO;
|
||||||
|
[self kb_logInputLayoutWithTag:@"showTextInputView-beforeAdjust"];
|
||||||
|
// 键盘未弹起时先停在底部栏位置,随后由键盘通知动画抬起到键盘上方
|
||||||
|
// 键盘已弹起时(如返回页面)直接使用当前键盘高度对齐
|
||||||
|
CGFloat targetOffset = self.currentKeyboardHeight > 0.0 ? -self.currentKeyboardHeight : -self.baseInputBarBottomSpacing;
|
||||||
|
[self.commentInputBottomConstraint setOffset:targetOffset];
|
||||||
|
[self.view layoutIfNeeded];
|
||||||
|
[self kb_logInputLayoutWithTag:@"showTextInputView-afterAdjust"];
|
||||||
[self.commentInputView showKeyboard];
|
[self.commentInputView showKeyboard];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 隐藏文本输入视图
|
/// 隐藏文本输入视图
|
||||||
- (void)hideTextInputView {
|
- (void)hideTextInputView {
|
||||||
|
[self kb_logInputLayoutWithTag:@"hideTextInputView-before"];
|
||||||
self.isTextInputMode = NO;
|
self.isTextInputMode = NO;
|
||||||
[self.view endEditing:YES];
|
[self.view endEditing:YES];
|
||||||
[self.commentInputView clearText];
|
[self.commentInputView clearText];
|
||||||
self.commentInputView.hidden = YES;
|
self.commentInputView.hidden = YES;
|
||||||
|
[self.commentInputBottomConstraint setOffset:100];
|
||||||
self.voiceInputBar.hidden = NO;
|
self.voiceInputBar.hidden = NO;
|
||||||
|
[self kb_logInputLayoutWithTag:@"hideTextInputView-after"];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - 2:数据加载
|
#pragma mark - 2:数据加载
|
||||||
@@ -570,12 +615,32 @@ static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId";
|
|||||||
|
|
||||||
- (void)setupKeyboardNotifications {
|
- (void)setupKeyboardNotifications {
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
selector:@selector(handleKeyboardWillChangeFrame:)
|
selector:@selector(handleKeyboardNotification:)
|
||||||
name:UIKeyboardWillChangeFrameNotification
|
name:UIKeyboardWillChangeFrameNotification
|
||||||
object:nil];
|
object:nil];
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(handleKeyboardNotification:)
|
||||||
|
name:UIKeyboardDidChangeFrameNotification
|
||||||
|
object:nil];
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(handleKeyboardNotification:)
|
||||||
|
name:UIKeyboardWillShowNotification
|
||||||
|
object:nil];
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(handleKeyboardNotification:)
|
||||||
|
name:UIKeyboardDidShowNotification
|
||||||
|
object:nil];
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(handleKeyboardNotification:)
|
||||||
|
name:UIKeyboardWillHideNotification
|
||||||
|
object:nil];
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(handleKeyboardNotification:)
|
||||||
|
name:UIKeyboardDidHideNotification
|
||||||
|
object:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)handleKeyboardWillChangeFrame:(NSNotification *)notification {
|
- (void)handleKeyboardNotification:(NSNotification *)notification {
|
||||||
NSDictionary *userInfo = notification.userInfo;
|
NSDictionary *userInfo = notification.userInfo;
|
||||||
CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||||
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
||||||
@@ -583,17 +648,42 @@ static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId";
|
|||||||
|
|
||||||
CGRect convertedFrame = [self.view convertRect:endFrame fromView:nil];
|
CGRect convertedFrame = [self.view convertRect:endFrame fromView:nil];
|
||||||
CGFloat keyboardHeight = MAX(0.0, CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(convertedFrame));
|
CGFloat keyboardHeight = MAX(0.0, CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(convertedFrame));
|
||||||
|
BOOL shouldHandle = YES;
|
||||||
|
BOOL fromAllowedInput = [self kb_isKeyboardFromVoiceInputBar];
|
||||||
|
|
||||||
if (keyboardHeight > 0.0) {
|
if (keyboardHeight > 0.0) {
|
||||||
if (![self kb_isKeyboardFromVoiceInputBar]) {
|
// 文本输入模式优先跟随键盘,避免返回页面后 firstResponder 瞬时不稳定导致被误过滤
|
||||||
return;
|
if (!self.isTextInputMode && !fromAllowedInput) {
|
||||||
|
shouldHandle = NO;
|
||||||
}
|
}
|
||||||
self.voiceInputKeyboardActive = YES;
|
self.voiceInputKeyboardActive = YES;
|
||||||
} else {
|
} else {
|
||||||
if (!self.voiceInputKeyboardActive) {
|
if (!self.voiceInputKeyboardActive && !self.isTextInputMode) {
|
||||||
return;
|
shouldHandle = NO;
|
||||||
}
|
}
|
||||||
self.voiceInputKeyboardActive = NO;
|
self.voiceInputKeyboardActive = NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self kb_logKeyboardNotification:notification
|
||||||
|
keyboardHeight:keyboardHeight
|
||||||
|
convertedFrame:convertedFrame
|
||||||
|
fromAllowedInput:fromAllowedInput
|
||||||
|
shouldHandle:shouldHandle];
|
||||||
|
if (!shouldHandle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIView *firstResponder = [self kb_findFirstResponderInView:self.view];
|
||||||
|
BOOL firstInComment = firstResponder ? [firstResponder isDescendantOfView:self.commentInputView] : NO;
|
||||||
|
if (keyboardHeight > 0.0 && firstInComment && !self.isTextInputMode) {
|
||||||
|
// 防止出现「firstResponder 在 commentInput,但 textMode 仍为 NO」导致定位到底部
|
||||||
|
self.isTextInputMode = YES;
|
||||||
|
self.voiceInputBar.hidden = YES;
|
||||||
|
self.commentInputView.hidden = NO;
|
||||||
|
[self kb_logInputLayoutWithTag:@"keyboardSync-forceTextMode"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyboardHeight <= 0.0) {
|
||||||
// 键盘隐藏时,如果是文本输入模式,隐藏文本输入框并显示 VoiceInputBar
|
// 键盘隐藏时,如果是文本输入模式,隐藏文本输入框并显示 VoiceInputBar
|
||||||
if (self.isTextInputMode) {
|
if (self.isTextInputMode) {
|
||||||
[self hideTextInputView];
|
[self hideTextInputView];
|
||||||
@@ -625,7 +715,9 @@ static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId";
|
|||||||
animations:^{
|
animations:^{
|
||||||
[self.view layoutIfNeeded];
|
[self.view layoutIfNeeded];
|
||||||
}
|
}
|
||||||
completion:nil];
|
completion:^(BOOL finished) {
|
||||||
|
[self kb_logInputLayoutWithTag:@"handleKeyboardNotification-animComplete"];
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - 7:键盘收起
|
#pragma mark - 7:键盘收起
|
||||||
@@ -693,6 +785,36 @@ static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId";
|
|||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - 键盘扩展聊天更新处理
|
||||||
|
|
||||||
|
/// 收到键盘扩展的聊天更新通知后,刷新对应 persona 的聊天记录
|
||||||
|
- (void)kb_handleChatUpdatedFromExtension {
|
||||||
|
NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
|
NSInteger companionId = [ud integerForKey:AppGroup_ChatUpdatedCompanionId];
|
||||||
|
if (companionId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"[KBAIHomeVC] 收到键盘扩展聊天更新通知,companionId=%ld", (long)companionId);
|
||||||
|
|
||||||
|
// 查找对应 persona 的索引
|
||||||
|
NSInteger index = [self indexOfPersonaId:companionId];
|
||||||
|
if (index == NSNotFound) {
|
||||||
|
NSLog(@"[KBAIHomeVC] 未找到 companionId=%ld 对应的 persona", (long)companionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取对应的 cell 并刷新聊天记录
|
||||||
|
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];
|
||||||
|
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
||||||
|
if (cell) {
|
||||||
|
[cell refreshChatHistory];
|
||||||
|
NSLog(@"[KBAIHomeVC] 已触发 companionId=%ld 的聊天记录刷新", (long)companionId);
|
||||||
|
} else {
|
||||||
|
NSLog(@"[KBAIHomeVC] companionId=%ld 的 cell 不可见,下次显示时会自动加载", (long)companionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- (NSInteger)indexOfPersonaId:(NSInteger)personaId {
|
- (NSInteger)indexOfPersonaId:(NSInteger)personaId {
|
||||||
if (personaId <= 0) {
|
if (personaId <= 0) {
|
||||||
return NSNotFound;
|
return NSNotFound;
|
||||||
@@ -843,6 +965,7 @@ static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId";
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view {
|
- (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view {
|
||||||
|
NSLog(@"[KBAIHomeVC][Pay] chatLimitPopViewDidTapRecharge");
|
||||||
[self.chatLimitPopView dismiss];
|
[self.chatLimitPopView dismiss];
|
||||||
if (![KBUserSessionManager shared].isLoggedIn) {
|
if (![KBUserSessionManager shared].isLoggedIn) {
|
||||||
[[KBUserSessionManager shared] goLoginVC];
|
[[KBUserSessionManager shared] goLoginVC];
|
||||||
@@ -961,6 +1084,74 @@ static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId";
|
|||||||
[self showPersonaSidebar];
|
[self showPersonaSidebar];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - Debug Log
|
||||||
|
|
||||||
|
- (void)kb_syncTextInputStateIfNeeded {
|
||||||
|
UIView *firstResponder = [self kb_findFirstResponderInView:self.view];
|
||||||
|
BOOL firstInComment = firstResponder ? [firstResponder isDescendantOfView:self.commentInputView] : NO;
|
||||||
|
if (!firstInComment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.isTextInputMode = YES;
|
||||||
|
self.voiceInputBar.hidden = YES;
|
||||||
|
self.commentInputView.hidden = NO;
|
||||||
|
if (self.currentKeyboardHeight > 0.0) {
|
||||||
|
[self.commentInputBottomConstraint setOffset:-self.currentKeyboardHeight];
|
||||||
|
[self.voiceInputBarBottomConstraint setOffset:-MAX(self.currentKeyboardHeight - 5.0, self.baseInputBarBottomSpacing)];
|
||||||
|
} else {
|
||||||
|
[self.commentInputBottomConstraint setOffset:-self.baseInputBarBottomSpacing];
|
||||||
|
}
|
||||||
|
[self.view layoutIfNeeded];
|
||||||
|
[self kb_logInputLayoutWithTag:@"kb_syncTextInputStateIfNeeded"];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_logInputLayoutWithTag:(NSString *)tag {
|
||||||
|
[self.view layoutIfNeeded];
|
||||||
|
UIView *firstResponder = [self kb_findFirstResponderInView:self.view];
|
||||||
|
NSString *firstResponderInfo = firstResponder ? NSStringFromClass(firstResponder.class) : @"nil";
|
||||||
|
NSLog(@"[KBAIHomeVC][Layout][%@] textMode=%d voiceHidden=%d commentHidden=%d currentKeyboardHeight=%.2f viewH=%.2f safeBottom=%.2f commentFrame=%@ voiceFrame=%@ firstResponder=%@",
|
||||||
|
tag ?: @"-",
|
||||||
|
self.isTextInputMode,
|
||||||
|
self.voiceInputBar.hidden,
|
||||||
|
self.commentInputView.hidden,
|
||||||
|
self.currentKeyboardHeight,
|
||||||
|
CGRectGetHeight(self.view.bounds),
|
||||||
|
self.view.safeAreaInsets.bottom,
|
||||||
|
NSStringFromCGRect(self.commentInputView.frame),
|
||||||
|
NSStringFromCGRect(self.voiceInputBar.frame),
|
||||||
|
firstResponderInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_logKeyboardNotification:(NSNotification *)notification
|
||||||
|
keyboardHeight:(CGFloat)keyboardHeight
|
||||||
|
convertedFrame:(CGRect)convertedFrame
|
||||||
|
fromAllowedInput:(BOOL)fromAllowedInput
|
||||||
|
shouldHandle:(BOOL)shouldHandle {
|
||||||
|
NSDictionary *userInfo = notification.userInfo;
|
||||||
|
CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||||
|
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
||||||
|
NSInteger curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
|
||||||
|
UIView *firstResponder = [self kb_findFirstResponderInView:self.view];
|
||||||
|
NSString *firstResponderInfo = firstResponder ? NSStringFromClass(firstResponder.class) : @"nil";
|
||||||
|
BOOL firstInComment = firstResponder ? [firstResponder isDescendantOfView:self.commentInputView] : NO;
|
||||||
|
BOOL firstInVoice = firstResponder ? [firstResponder isDescendantOfView:self.voiceInputBar] : NO;
|
||||||
|
NSLog(@"[KBAIHomeVC][Keyboard][%@] shouldHandle=%d textMode=%d voiceActive=%d fromAllowed=%d firstInComment=%d firstInVoice=%d firstResponder=%@ endFrame=%@ converted=%@ keyboardHeight=%.2f duration=%.2f curve=%ld viewWindow=%@",
|
||||||
|
notification.name,
|
||||||
|
shouldHandle,
|
||||||
|
self.isTextInputMode,
|
||||||
|
self.voiceInputKeyboardActive,
|
||||||
|
fromAllowedInput,
|
||||||
|
firstInComment,
|
||||||
|
firstInVoice,
|
||||||
|
firstResponderInfo,
|
||||||
|
NSStringFromCGRect(endFrame),
|
||||||
|
NSStringFromCGRect(convertedFrame),
|
||||||
|
keyboardHeight,
|
||||||
|
duration,
|
||||||
|
(long)curve,
|
||||||
|
self.view.window ? @"YES" : @"NO");
|
||||||
|
}
|
||||||
|
|
||||||
- (void)showPersonaSidebar {
|
- (void)showPersonaSidebar {
|
||||||
if (!self.sidebarView) {
|
if (!self.sidebarView) {
|
||||||
CGFloat width = KB_SCREEN_WIDTH * 0.7;
|
CGFloat width = KB_SCREEN_WIDTH * 0.7;
|
||||||
@@ -1221,6 +1412,11 @@ static NSString * const KBAISelectedPersonaIdKey = @"KBAISelectedPersonaId";
|
|||||||
|
|
||||||
- (void)dealloc {
|
- (void)dealloc {
|
||||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||||
|
CFNotificationCenterRemoveObserver(
|
||||||
|
CFNotificationCenterGetDarwinNotifyCenter(),
|
||||||
|
(__bridge const void *)(self),
|
||||||
|
(__bridge CFStringRef)kKBDarwinChatUpdated,
|
||||||
|
NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// KBAiMainVC.h
|
|
||||||
// keyBoard
|
|
||||||
//
|
|
||||||
// Created by Mac on 2026/1/15.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
/// AI 语音陪伴聊天主界面
|
|
||||||
@interface KBAiMainVC : BaseViewController
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,809 +0,0 @@
|
|||||||
//
|
|
||||||
// KBAiMainVC.m
|
|
||||||
// keyBoard
|
|
||||||
//
|
|
||||||
// Created by Mac on 2026/1/15.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "KBAiMainVC.h"
|
|
||||||
#import "ConversationOrchestrator.h"
|
|
||||||
#import "AiVM.h"
|
|
||||||
#import "AudioSessionManager.h"
|
|
||||||
#import "DeepgramStreamingManager.h"
|
|
||||||
#import "KBAICommentView.h"
|
|
||||||
#import "KBChatTableView.h"
|
|
||||||
#import "KBAiRecordButton.h"
|
|
||||||
#import "KBHUD.h"
|
|
||||||
#import "KBChatLimitPopView.h"
|
|
||||||
#import "KBPayMainVC.h"
|
|
||||||
#import "LSTPopView.h"
|
|
||||||
#import "VoiceChatStreamingManager.h"
|
|
||||||
#import "KBUserSessionManager.h"
|
|
||||||
#import <AVFoundation/AVFoundation.h>
|
|
||||||
|
|
||||||
@interface KBAiMainVC () <KBAiRecordButtonDelegate,
|
|
||||||
VoiceChatStreamingManagerDelegate,
|
|
||||||
DeepgramStreamingManagerDelegate,
|
|
||||||
AVAudioPlayerDelegate,
|
|
||||||
KBChatLimitPopViewDelegate>
|
|
||||||
@property(nonatomic, weak) LSTPopView *popView;
|
|
||||||
@property(nonatomic, weak) LSTPopView *limitPopView;
|
|
||||||
|
|
||||||
// UI
|
|
||||||
@property(nonatomic, strong) KBChatTableView *chatView;
|
|
||||||
@property(nonatomic, strong) KBAiRecordButton *recordButton;
|
|
||||||
@property(nonatomic, strong) UILabel *statusLabel;
|
|
||||||
@property(nonatomic, strong) UILabel *transcriptLabel;
|
|
||||||
@property(nonatomic, strong) UIButton *commentButton;
|
|
||||||
@property(nonatomic, strong) KBAICommentView *commentView;
|
|
||||||
@property(nonatomic, strong) UIView *tabbarBackgroundView;
|
|
||||||
@property(nonatomic, strong) UIVisualEffectView *blurEffectView;
|
|
||||||
@property(nonatomic, strong) CAGradientLayer *gradientLayer;
|
|
||||||
@property(nonatomic, strong) UIImageView *personImageView;
|
|
||||||
|
|
||||||
// 核心模块
|
|
||||||
@property(nonatomic, strong) ConversationOrchestrator *orchestrator;
|
|
||||||
@property(nonatomic, strong) VoiceChatStreamingManager *streamingManager;
|
|
||||||
@property(nonatomic, strong) DeepgramStreamingManager *deepgramManager;
|
|
||||||
@property(nonatomic, strong) AiVM *aiVM;
|
|
||||||
@property(nonatomic, strong) AVAudioPlayer *aiAudioPlayer;
|
|
||||||
@property(nonatomic, strong) NSMutableData *voiceChatAudioBuffer;
|
|
||||||
|
|
||||||
// 文本跟踪
|
|
||||||
@property(nonatomic, strong) NSMutableString *assistantVisibleText;
|
|
||||||
@property(nonatomic, strong) NSMutableString *deepgramFullText;
|
|
||||||
|
|
||||||
// 日志节流
|
|
||||||
@property(nonatomic, assign) NSTimeInterval lastRMSLogTime;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation KBAiMainVC
|
|
||||||
|
|
||||||
#pragma mark - Lifecycle
|
|
||||||
|
|
||||||
- (void)viewDidLoad {
|
|
||||||
[super viewDidLoad];
|
|
||||||
|
|
||||||
// 让视图延伸到屏幕边缘(包括状态栏和导航栏下方)
|
|
||||||
self.edgesForExtendedLayout = UIRectEdgeAll;
|
|
||||||
self.extendedLayoutIncludesOpaqueBars = YES;
|
|
||||||
|
|
||||||
[self setupUI];
|
|
||||||
[self setupOrchestrator];
|
|
||||||
[self setupStreamingManager];
|
|
||||||
[self setupDeepgramManager];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)viewWillAppear:(BOOL)animated {
|
|
||||||
[super viewWillAppear:animated];
|
|
||||||
// TabBar 背景色由 BaseTabBarController 统一管理,这里不需要设置
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)viewWillDisappear:(BOOL)animated {
|
|
||||||
[super viewWillDisappear:animated];
|
|
||||||
|
|
||||||
// 页面消失时停止对话
|
|
||||||
[self.orchestrator stop];
|
|
||||||
[self.streamingManager disconnect];
|
|
||||||
[self.deepgramManager disconnect];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)viewDidLayoutSubviews {
|
|
||||||
[super viewDidLayoutSubviews];
|
|
||||||
|
|
||||||
// 只更新 mask 的 frame(mask 已在 setupUI 中创建)
|
|
||||||
if (self.blurEffectView.layer.mask) {
|
|
||||||
self.blurEffectView.layer.mask.frame = self.blurEffectView.bounds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - UI Setup
|
|
||||||
|
|
||||||
- (void)setupUI {
|
|
||||||
self.view.backgroundColor = [UIColor whiteColor];
|
|
||||||
self.title = @"AI 助手";
|
|
||||||
|
|
||||||
// 安全区域
|
|
||||||
UILayoutGuide *safeArea = self.view.safeAreaLayoutGuide;
|
|
||||||
|
|
||||||
// PersonImageView(背景图,最底层)
|
|
||||||
self.personImageView =
|
|
||||||
[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"person_icon"]];
|
|
||||||
[self.view addSubview:self.personImageView];
|
|
||||||
[self.personImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.left.right.top.bottom.equalTo(self.view);
|
|
||||||
}];
|
|
||||||
|
|
||||||
// TabBar 毛玻璃模糊背景(在 personImageView 之上)
|
|
||||||
self.tabbarBackgroundView = [[UIView alloc] init];
|
|
||||||
self.tabbarBackgroundView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
self.tabbarBackgroundView.clipsToBounds = YES;
|
|
||||||
[self.view addSubview:self.tabbarBackgroundView];
|
|
||||||
|
|
||||||
// 模糊效果
|
|
||||||
UIBlurEffect *blurEffect =
|
|
||||||
[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
|
||||||
self.blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
|
|
||||||
self.blurEffectView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
[self.tabbarBackgroundView addSubview:self.blurEffectView];
|
|
||||||
|
|
||||||
// 为 blurEffectView 创建透明度渐变
|
|
||||||
// mask(从底部到中间不透明,从中间到顶部透明)
|
|
||||||
CAGradientLayer *maskLayer = [CAGradientLayer layer];
|
|
||||||
maskLayer.startPoint = CGPointMake(0.5, 1); // 底部
|
|
||||||
maskLayer.endPoint = CGPointMake(0.5, 0); // 顶部
|
|
||||||
// 底部到中间保持不透明,从中间到顶部过渡透明
|
|
||||||
maskLayer.colors = @[
|
|
||||||
(__bridge id)[UIColor whiteColor].CGColor, // 底部:完全不透明
|
|
||||||
(__bridge id)[UIColor whiteColor].CGColor, // 中间:完全不透明
|
|
||||||
(__bridge id)[UIColor clearColor].CGColor // 顶部:完全透明
|
|
||||||
];
|
|
||||||
maskLayer.locations = @[ @(0.0), @(0.5), @(1.0) ];
|
|
||||||
self.blurEffectView.layer.mask = maskLayer;
|
|
||||||
|
|
||||||
// 状态标签
|
|
||||||
self.statusLabel = [[UILabel alloc] init];
|
|
||||||
self.statusLabel.text = @"按住按钮开始对话";
|
|
||||||
self.statusLabel.font = [UIFont systemFontOfSize:14];
|
|
||||||
self.statusLabel.textColor = [UIColor secondaryLabelColor];
|
|
||||||
self.statusLabel.textAlignment = NSTextAlignmentCenter;
|
|
||||||
self.statusLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
[self.view addSubview:self.statusLabel];
|
|
||||||
|
|
||||||
// 转写文本标签
|
|
||||||
self.transcriptLabel = [[UILabel alloc] init];
|
|
||||||
self.transcriptLabel.text = @"";
|
|
||||||
self.transcriptLabel.font = [UIFont systemFontOfSize:16];
|
|
||||||
self.transcriptLabel.textColor = [UIColor labelColor];
|
|
||||||
self.transcriptLabel.numberOfLines = 0;
|
|
||||||
self.transcriptLabel.textAlignment = NSTextAlignmentRight;
|
|
||||||
self.transcriptLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
[self.view addSubview:self.transcriptLabel];
|
|
||||||
|
|
||||||
// 聊天视图
|
|
||||||
self.chatView = [[KBChatTableView alloc] init];
|
|
||||||
self.chatView.backgroundColor = [UIColor clearColor];
|
|
||||||
self.chatView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
[self.view addSubview:self.chatView];
|
|
||||||
|
|
||||||
// 录音按钮
|
|
||||||
self.recordButton = [[KBAiRecordButton alloc] init];
|
|
||||||
self.recordButton.delegate = self;
|
|
||||||
self.recordButton.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
[self.view addSubview:self.recordButton];
|
|
||||||
|
|
||||||
// 评论按钮(聊天视图右侧居中)
|
|
||||||
self.commentButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
||||||
[self.commentButton setImage:[UIImage systemImageNamed:@"bubble.right.fill"]
|
|
||||||
forState:UIControlStateNormal];
|
|
||||||
self.commentButton.tintColor = [UIColor whiteColor];
|
|
||||||
self.commentButton.backgroundColor = [UIColor systemBlueColor];
|
|
||||||
self.commentButton.layer.cornerRadius = 25;
|
|
||||||
self.commentButton.layer.shadowColor = [UIColor blackColor].CGColor;
|
|
||||||
self.commentButton.layer.shadowOffset = CGSizeMake(0, 2);
|
|
||||||
self.commentButton.layer.shadowOpacity = 0.3;
|
|
||||||
self.commentButton.layer.shadowRadius = 4;
|
|
||||||
self.commentButton.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
[self.commentButton addTarget:self
|
|
||||||
action:@selector(showComment)
|
|
||||||
forControlEvents:UIControlEventTouchUpInside];
|
|
||||||
[self.view addSubview:self.commentButton];
|
|
||||||
|
|
||||||
// 布局约束 - 使用 Masonry
|
|
||||||
[self.tabbarBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.left.right.bottom.equalTo(self.view);
|
|
||||||
make.height.mas_equalTo(KBFit(238));
|
|
||||||
}];
|
|
||||||
|
|
||||||
[self.blurEffectView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.edges.equalTo(self.tabbarBackgroundView);
|
|
||||||
}];
|
|
||||||
|
|
||||||
[self.statusLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop).offset(8);
|
|
||||||
make.left.equalTo(self.view).offset(16);
|
|
||||||
make.right.equalTo(self.view).offset(-16);
|
|
||||||
// 设置固定高度,避免内容变化导致布局跳动
|
|
||||||
make.height.mas_equalTo(20); // 单行文本高度
|
|
||||||
}];
|
|
||||||
|
|
||||||
[self.transcriptLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.top.equalTo(self.statusLabel.mas_bottom).offset(8);
|
|
||||||
make.left.equalTo(self.view).offset(16);
|
|
||||||
make.right.equalTo(self.view).offset(-16);
|
|
||||||
// 设置固定高度,避免内容变化导致布局跳动
|
|
||||||
make.height.mas_equalTo(60); // 根据实际需要调整高度
|
|
||||||
}];
|
|
||||||
// 设置内容压缩阻力,避免被压缩
|
|
||||||
[self.transcriptLabel setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
|
|
||||||
forAxis:UILayoutConstraintAxisVertical];
|
|
||||||
|
|
||||||
[self.chatView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.left.right.equalTo(self.view);
|
|
||||||
make.bottom.equalTo(self.tabbarBackgroundView.mas_top).offset(-8);
|
|
||||||
make.top.equalTo(self.transcriptLabel.mas_bottom).offset(8);
|
|
||||||
// 设置最小高度,避免被压缩为 0
|
|
||||||
make.height.greaterThanOrEqualTo(@100).priority(MASLayoutPriorityDefaultHigh);
|
|
||||||
}];
|
|
||||||
// chatView 应该尽可能占据空间
|
|
||||||
[self.chatView setContentCompressionResistancePriority:UILayoutPriorityRequired
|
|
||||||
forAxis:UILayoutConstraintAxisVertical];
|
|
||||||
|
|
||||||
[self.recordButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.left.equalTo(self.view.mas_safeAreaLayoutGuideLeft).offset(20);
|
|
||||||
make.right.equalTo(self.view.mas_safeAreaLayoutGuideRight).offset(-20);
|
|
||||||
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-16);
|
|
||||||
make.height.mas_equalTo(50);
|
|
||||||
}];
|
|
||||||
|
|
||||||
[self.commentButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.right.equalTo(self.view.mas_safeAreaLayoutGuideRight).offset(-16);
|
|
||||||
make.centerY.equalTo(self.view);
|
|
||||||
make.width.height.mas_equalTo(50);
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Orchestrator Setup
|
|
||||||
|
|
||||||
- (void)setupOrchestrator {
|
|
||||||
self.orchestrator = [[ConversationOrchestrator alloc] init];
|
|
||||||
|
|
||||||
// 配置服务器地址
|
|
||||||
// 1. ASR 语音识别服务(WebSocket)
|
|
||||||
self.orchestrator.asrServerURL = @"ws://192.168.2.21:7529/ws/asr";
|
|
||||||
|
|
||||||
// 2. LLM 大语言模型服务(HTTP Stream)
|
|
||||||
self.orchestrator.llmServerURL = @"http://192.168.2.21:7529/api/chat/stream";
|
|
||||||
|
|
||||||
// 3. TTS 语音合成服务(HTTP)
|
|
||||||
self.orchestrator.ttsServerURL = @"http://192.168.2.21:7529/api/tts/stream";
|
|
||||||
|
|
||||||
__weak typeof(self) weakSelf = self;
|
|
||||||
|
|
||||||
// 状态变化回调
|
|
||||||
self.orchestrator.onStateChange = ^(ConversationState state) {
|
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
||||||
if (!strongSelf)
|
|
||||||
return;
|
|
||||||
|
|
||||||
[strongSelf updateStatusForState:state];
|
|
||||||
};
|
|
||||||
|
|
||||||
// 实时识别文本回调
|
|
||||||
self.orchestrator.onPartialText = ^(NSString *text) {
|
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
||||||
if (!strongSelf)
|
|
||||||
return;
|
|
||||||
|
|
||||||
strongSelf.statusLabel.text = text.length > 0 ? text : @"正在识别...";
|
|
||||||
};
|
|
||||||
|
|
||||||
// 用户最终文本回调
|
|
||||||
self.orchestrator.onUserFinalText = ^(NSString *text) {
|
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
||||||
if (!strongSelf)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (text.length > 0) {
|
|
||||||
[strongSelf.chatView addUserMessage:text];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// AI 可见文本回调(打字机效果)
|
|
||||||
self.orchestrator.onAssistantVisibleText = ^(NSString *text) {
|
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
||||||
if (!strongSelf)
|
|
||||||
return;
|
|
||||||
|
|
||||||
[strongSelf.chatView updateLastAssistantMessage:text];
|
|
||||||
};
|
|
||||||
|
|
||||||
// AI 完整回复回调
|
|
||||||
self.orchestrator.onAssistantFullText = ^(NSString *text) {
|
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
||||||
if (!strongSelf)
|
|
||||||
return;
|
|
||||||
|
|
||||||
[strongSelf.chatView updateLastAssistantMessage:text];
|
|
||||||
[strongSelf.chatView markLastAssistantMessageComplete];
|
|
||||||
};
|
|
||||||
|
|
||||||
// 音量更新回调
|
|
||||||
self.orchestrator.onVolumeUpdate = ^(float rms) {
|
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
||||||
if (!strongSelf)
|
|
||||||
return;
|
|
||||||
|
|
||||||
[strongSelf.recordButton updateVolumeRMS:rms];
|
|
||||||
};
|
|
||||||
|
|
||||||
// AI 开始说话
|
|
||||||
self.orchestrator.onSpeakingStart = ^{
|
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
||||||
if (!strongSelf)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// 添加空的 AI 消息占位
|
|
||||||
[strongSelf.chatView addAssistantMessage:@"" audioDuration:0 audioData:nil];
|
|
||||||
};
|
|
||||||
|
|
||||||
// AI 说话结束
|
|
||||||
self.orchestrator.onSpeakingEnd = ^{
|
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
||||||
if (!strongSelf)
|
|
||||||
return;
|
|
||||||
|
|
||||||
[strongSelf.chatView markLastAssistantMessageComplete];
|
|
||||||
};
|
|
||||||
|
|
||||||
// 错误回调
|
|
||||||
self.orchestrator.onError = ^(NSError *error) {
|
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
||||||
if (!strongSelf)
|
|
||||||
return;
|
|
||||||
|
|
||||||
[strongSelf showError:error];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Streaming Manager
|
|
||||||
|
|
||||||
- (void)setupStreamingManager {
|
|
||||||
self.streamingManager = [[VoiceChatStreamingManager alloc] init];
|
|
||||||
self.streamingManager.delegate = self;
|
|
||||||
self.streamingManager.serverURL = @"ws://192.168.2.21:7529/api/ws/chat";
|
|
||||||
self.assistantVisibleText = [[NSMutableString alloc] init];
|
|
||||||
self.voiceChatAudioBuffer = [[NSMutableData alloc] init];
|
|
||||||
self.lastRMSLogTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Deepgram Manager
|
|
||||||
|
|
||||||
- (void)setupDeepgramManager {
|
|
||||||
self.deepgramManager = [[DeepgramStreamingManager alloc] init];
|
|
||||||
self.deepgramManager.delegate = self;
|
|
||||||
self.deepgramManager.serverURL = @"wss://api.deepgram.com/v1/listen";
|
|
||||||
self.deepgramManager.apiKey = @"9c792eb63a65d644cbc95785155754cd1e84f8cf";
|
|
||||||
self.deepgramManager.language = @"en";
|
|
||||||
self.deepgramManager.model = @"nova-3";
|
|
||||||
self.deepgramManager.punctuate = YES;
|
|
||||||
self.deepgramManager.smartFormat = YES;
|
|
||||||
self.deepgramManager.interimResults = YES;
|
|
||||||
self.deepgramManager.encoding = @"linear16";
|
|
||||||
self.deepgramManager.sampleRate = 16000.0;
|
|
||||||
self.deepgramManager.channels = 1;
|
|
||||||
[self.deepgramManager prepareConnection];
|
|
||||||
|
|
||||||
self.deepgramFullText = [[NSMutableString alloc] init];
|
|
||||||
self.aiVM = [[AiVM alloc] init];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - 事件
|
|
||||||
- (void)showComment {
|
|
||||||
CGFloat customViewHeight = KB_SCREEN_HEIGHT * (0.8);
|
|
||||||
KBAICommentView *customView = [[KBAICommentView alloc]
|
|
||||||
initWithFrame:CGRectMake(0, 0, KB_SCREEN_WIDTH, customViewHeight)];
|
|
||||||
LSTPopView *popView =
|
|
||||||
[LSTPopView initWithCustomView:customView
|
|
||||||
parentView:nil
|
|
||||||
popStyle:LSTPopStyleSmoothFromBottom
|
|
||||||
dismissStyle:LSTDismissStyleSmoothToBottom];
|
|
||||||
self.popView = popView;
|
|
||||||
popView.priority = 1000;
|
|
||||||
popView.isAvoidKeyboard = false;
|
|
||||||
popView.hemStyle = LSTHemStyleBottom;
|
|
||||||
popView.dragStyle = LSTDragStyleY_Positive;
|
|
||||||
popView.dragDistance = customViewHeight * 0.5;
|
|
||||||
popView.sweepStyle = LSTSweepStyleY_Positive;
|
|
||||||
popView.swipeVelocity = 1600;
|
|
||||||
popView.sweepDismissStyle = LSTSweepDismissStyleSmooth;
|
|
||||||
|
|
||||||
[popView pop];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)showCommentDirectly {
|
|
||||||
if (self.commentView.superview) {
|
|
||||||
[self.view bringSubviewToFront:self.commentView];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CGFloat customViewHeight = KB_SCREEN_HEIGHT * (0.8);
|
|
||||||
KBAICommentView *customView =
|
|
||||||
[[KBAICommentView alloc] initWithFrame:CGRectZero];
|
|
||||||
customView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
[self.view addSubview:customView];
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
[customView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
|
||||||
[customView.trailingAnchor
|
|
||||||
constraintEqualToAnchor:self.view.trailingAnchor],
|
|
||||||
[customView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
|
|
||||||
[customView.heightAnchor constraintEqualToConstant:customViewHeight],
|
|
||||||
]];
|
|
||||||
self.commentView = customView;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - 次数用尽弹窗
|
|
||||||
|
|
||||||
- (void)showChatLimitPopWithMessage:(NSString *)message {
|
|
||||||
if (self.limitPopView) {
|
|
||||||
[self.limitPopView dismiss];
|
|
||||||
}
|
|
||||||
|
|
||||||
CGFloat width = 252.0;
|
|
||||||
CGFloat height = 252.0 + 18.0 + 53.0 + 18.0 + 28.0;
|
|
||||||
KBChatLimitPopView *content =
|
|
||||||
[[KBChatLimitPopView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
|
|
||||||
content.message = message;
|
|
||||||
content.delegate = self;
|
|
||||||
|
|
||||||
LSTPopView *popView =
|
|
||||||
[LSTPopView initWithCustomView:content
|
|
||||||
parentView:nil
|
|
||||||
popStyle:LSTPopStyleFade
|
|
||||||
dismissStyle:LSTDismissStyleFade];
|
|
||||||
popView.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
|
|
||||||
popView.hemStyle = LSTHemStyleCenter;
|
|
||||||
popView.isClickBgDismiss = YES;
|
|
||||||
popView.isAvoidKeyboard = NO;
|
|
||||||
self.limitPopView = popView;
|
|
||||||
[popView pop];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - KBChatLimitPopViewDelegate
|
|
||||||
|
|
||||||
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view {
|
|
||||||
[self.limitPopView dismiss];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view {
|
|
||||||
[self.limitPopView dismiss];
|
|
||||||
if (![KBUserSessionManager shared].isLoggedIn) {
|
|
||||||
[[KBUserSessionManager shared] goLoginVC];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
KBPayMainVC *vc = [[KBPayMainVC alloc] init];
|
|
||||||
vc.initialSelectedIndex = 1; // SVIP
|
|
||||||
[KB_CURRENT_NAV pushViewController:vc animated:true];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - UI Updates
|
|
||||||
|
|
||||||
- (void)updateStatusForState:(ConversationState)state {
|
|
||||||
switch (state) {
|
|
||||||
case ConversationStateIdle:
|
|
||||||
self.statusLabel.text = @"按住按钮开始对话";
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ConversationStateListening:
|
|
||||||
self.statusLabel.text = @"正在聆听...";
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateRecording;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ConversationStateRecognizing:
|
|
||||||
self.statusLabel.text = @"正在识别...";
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ConversationStateThinking:
|
|
||||||
self.statusLabel.text = @"AI 正在思考...";
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ConversationStateSpeaking:
|
|
||||||
self.statusLabel.text = @"AI 正在回复...";
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)showError:(NSError *)error {
|
|
||||||
UIAlertController *alert =
|
|
||||||
[UIAlertController alertControllerWithTitle:@"错误"
|
|
||||||
message:error.localizedDescription
|
|
||||||
preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
[alert addAction:[UIAlertAction actionWithTitle:@"确定"
|
|
||||||
style:UIAlertActionStyleDefault
|
|
||||||
handler:nil]];
|
|
||||||
[self presentViewController:alert animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - KBAiRecordButtonDelegate
|
|
||||||
|
|
||||||
- (void)recordButtonDidBeginPress:(KBAiRecordButton *)button {
|
|
||||||
NSLog(@"[KBAiMainVC] Record button began press");
|
|
||||||
|
|
||||||
// 停止正在播放的音频
|
|
||||||
[self.chatView stopPlayingAudio];
|
|
||||||
|
|
||||||
NSString *token = [[KBUserSessionManager shared] accessToken] ?: @"";
|
|
||||||
if (token.length == 0) {
|
|
||||||
[[KBUserSessionManager shared] goLoginVC];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.statusLabel.text = @"正在连接...";
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateRecording;
|
|
||||||
[self.deepgramFullText setString:@""];
|
|
||||||
self.transcriptLabel.text = @"";
|
|
||||||
[self.deepgramManager start];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)recordButtonDidEndPress:(KBAiRecordButton *)button {
|
|
||||||
NSLog(@"[KBAiMainVC] Record button end press");
|
|
||||||
[self.deepgramManager stopAndFinalize];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)recordButtonDidCancelPress:(KBAiRecordButton *)button {
|
|
||||||
NSLog(@"[KBAiMainVC] Record button cancel press");
|
|
||||||
[self.deepgramManager cancel];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - VoiceChatStreamingManagerDelegate
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidConnect {
|
|
||||||
self.statusLabel.text = @"已连接,准备中...";
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidDisconnect:(NSError *_Nullable)error {
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|
||||||
if (error) {
|
|
||||||
[self showError:error];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidStartSession:(NSString *)sessionId {
|
|
||||||
self.statusLabel.text = @"正在聆听...";
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateRecording;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidStartTurn:(NSInteger)turnIndex {
|
|
||||||
self.statusLabel.text = @"正在聆听...";
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateRecording;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidReceiveEagerEndOfTurnWithTranscript:(NSString *)text
|
|
||||||
confidence:(double)confidence {
|
|
||||||
self.statusLabel.text = @"准备响应...";
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidResumeTurn {
|
|
||||||
self.statusLabel.text = @"正在聆听...";
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidUpdateRMS:(float)rms {
|
|
||||||
[self.recordButton updateVolumeRMS:rms];
|
|
||||||
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
|
|
||||||
if (now - self.lastRMSLogTime >= 1.0) {
|
|
||||||
self.lastRMSLogTime = now;
|
|
||||||
NSLog(@"[KBAiMainVC] RMS: %.3f", rms);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidReceiveInterimTranscript:(NSString *)text {
|
|
||||||
self.statusLabel.text = @"正在识别...";
|
|
||||||
if (text.length > 0) {
|
|
||||||
self.transcriptLabel.text = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidReceiveFinalTranscript:(NSString *)text {
|
|
||||||
if (text.length > 0) {
|
|
||||||
self.transcriptLabel.text = @"";
|
|
||||||
[self.chatView addUserMessage:text];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidReceiveLLMStart {
|
|
||||||
self.statusLabel.text = @"AI 正在思考...";
|
|
||||||
[self.assistantVisibleText setString:@""];
|
|
||||||
[self.chatView addAssistantMessage:@"" audioDuration:0 audioData:nil];
|
|
||||||
[self.voiceChatAudioBuffer setLength:0];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidReceiveLLMToken:(NSString *)token {
|
|
||||||
if (token.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
[self.assistantVisibleText appendString:token];
|
|
||||||
[self.chatView updateLastAssistantMessage:self.assistantVisibleText];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidReceiveAudioChunk:(NSData *)audioData {
|
|
||||||
if (audioData.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
[self.voiceChatAudioBuffer appendData:audioData];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidCompleteWithTranscript:(NSString *)transcript
|
|
||||||
aiResponse:(NSString *)aiResponse {
|
|
||||||
NSString *finalText = aiResponse.length > 0 ? aiResponse : self.assistantVisibleText;
|
|
||||||
if (aiResponse.length > 0) {
|
|
||||||
[self.assistantVisibleText setString:aiResponse];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算音频时长
|
|
||||||
NSTimeInterval duration = 0;
|
|
||||||
if (self.voiceChatAudioBuffer.length > 0) {
|
|
||||||
NSError *error = nil;
|
|
||||||
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithData:self.voiceChatAudioBuffer
|
|
||||||
error:&error];
|
|
||||||
if (!error && player) {
|
|
||||||
duration = player.duration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalText.length > 0) {
|
|
||||||
[self.chatView updateLastAssistantMessage:finalText];
|
|
||||||
[self.chatView markLastAssistantMessageComplete];
|
|
||||||
} else if (transcript.length > 0) {
|
|
||||||
[self.chatView addAssistantMessage:transcript
|
|
||||||
audioDuration:duration
|
|
||||||
audioData:self.voiceChatAudioBuffer.length > 0 ? self.voiceChatAudioBuffer : nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.voiceChatAudioBuffer.length > 0) {
|
|
||||||
[self playAiAudioData:self.voiceChatAudioBuffer];
|
|
||||||
[self.voiceChatAudioBuffer setLength:0];
|
|
||||||
}
|
|
||||||
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|
||||||
self.statusLabel.text = @"完成";
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)voiceChatStreamingManagerDidFail:(NSError *)error {
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|
||||||
[self showError:error];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - DeepgramStreamingManagerDelegate
|
|
||||||
|
|
||||||
- (void)deepgramStreamingManagerDidConnect {
|
|
||||||
self.statusLabel.text = @"已连接,准备中...";
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)deepgramStreamingManagerDidDisconnect:(NSError *_Nullable)error {
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|
||||||
if (error) {
|
|
||||||
[self showError:error];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)deepgramStreamingManagerDidUpdateRMS:(float)rms {
|
|
||||||
[self.recordButton updateVolumeRMS:rms];
|
|
||||||
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
|
|
||||||
if (now - self.lastRMSLogTime >= 1.0) {
|
|
||||||
self.lastRMSLogTime = now;
|
|
||||||
NSLog(@"[KBAiMainVC] RMS: %.3f", rms);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)deepgramStreamingManagerDidReceiveInterimTranscript:(NSString *)text {
|
|
||||||
self.statusLabel.text = @"正在识别...";
|
|
||||||
NSString *displayText = text ?: @"";
|
|
||||||
if (self.deepgramFullText.length > 0 && displayText.length > 0) {
|
|
||||||
displayText =
|
|
||||||
[NSString stringWithFormat:@"%@ %@", self.deepgramFullText, displayText];
|
|
||||||
} else if (self.deepgramFullText.length > 0) {
|
|
||||||
displayText = [self.deepgramFullText copy];
|
|
||||||
}
|
|
||||||
self.transcriptLabel.text = displayText;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)deepgramStreamingManagerDidReceiveFinalTranscript:(NSString *)text {
|
|
||||||
if (text.length > 0) {
|
|
||||||
if (self.deepgramFullText.length > 0) {
|
|
||||||
[self.deepgramFullText appendString:@" "];
|
|
||||||
}
|
|
||||||
[self.deepgramFullText appendString:text];
|
|
||||||
}
|
|
||||||
self.transcriptLabel.text = self.deepgramFullText;
|
|
||||||
self.statusLabel.text = @"识别完成";
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|
||||||
|
|
||||||
NSString *finalText = [self.deepgramFullText copy];
|
|
||||||
if (finalText.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加用户消息
|
|
||||||
[self.chatView addUserMessage:finalText];
|
|
||||||
|
|
||||||
__weak typeof(self) weakSelf = self;
|
|
||||||
[KBHUD showWithStatus:@"AI 思考中..."];
|
|
||||||
|
|
||||||
// 请求 chat/message 接口
|
|
||||||
[self.aiVM requestChatMessageWithContent:finalText
|
|
||||||
companionId:0
|
|
||||||
completion:^(KBAiMessageResponse *_Nullable response,
|
|
||||||
NSError *_Nullable error) {
|
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
||||||
if (!strongSelf) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[KBHUD dismiss];
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
[KBHUD showError:error.localizedDescription ?: @"请求失败"];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code == 50030) {
|
|
||||||
NSString *message = response.message ?: @"";
|
|
||||||
[strongSelf showChatLimitPopWithMessage:message];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response || !response.data) {
|
|
||||||
NSString *message = response.message ?: @"AI 回复为空";
|
|
||||||
[KBHUD showError:message];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 AI 回复文本
|
|
||||||
NSString *aiResponse = response.data.aiResponse ?: response.data.content ?: response.data.text ?: response.data.message ?: @"";
|
|
||||||
|
|
||||||
if (aiResponse.length == 0) {
|
|
||||||
[KBHUD showError:@"AI 回复为空"];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 audioId
|
|
||||||
NSString *audioId = response.data.audioId;
|
|
||||||
|
|
||||||
// 添加 AI 消息(带 audioId)
|
|
||||||
[strongSelf.chatView addAssistantMessage:aiResponse
|
|
||||||
audioId:audioId];
|
|
||||||
});
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)deepgramStreamingManagerDidFail:(NSError *)error {
|
|
||||||
self.recordButton.state = KBAiRecordButtonStateNormal;
|
|
||||||
[self showError:error];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Audio Playback
|
|
||||||
|
|
||||||
- (void)playAiAudioData:(NSData *)audioData {
|
|
||||||
if (audioData.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSError *sessionError = nil;
|
|
||||||
AudioSessionManager *audioSession = [AudioSessionManager sharedManager];
|
|
||||||
if (![audioSession configureForPlayback:&sessionError]) {
|
|
||||||
NSLog(@"[KBAiMainVC] Configure playback failed: %@",
|
|
||||||
sessionError.localizedDescription ?: @"");
|
|
||||||
}
|
|
||||||
if (![audioSession activateSession:&sessionError]) {
|
|
||||||
NSLog(@"[KBAiMainVC] Activate playback session failed: %@",
|
|
||||||
sessionError.localizedDescription ?: @"");
|
|
||||||
}
|
|
||||||
|
|
||||||
NSError *error = nil;
|
|
||||||
self.aiAudioPlayer = [[AVAudioPlayer alloc] initWithData:audioData
|
|
||||||
error:&error];
|
|
||||||
if (error || !self.aiAudioPlayer) {
|
|
||||||
NSLog(@"[KBAiMainVC] Audio player init failed: %@",
|
|
||||||
error.localizedDescription ?: @"");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.aiAudioPlayer.delegate = self;
|
|
||||||
[self.aiAudioPlayer prepareToPlay];
|
|
||||||
[self.aiAudioPlayer play];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - AVAudioPlayerDelegate
|
|
||||||
|
|
||||||
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player
|
|
||||||
successfully:(BOOL)flag {
|
|
||||||
[[AudioSessionManager sharedManager] deactivateSession];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
}
|
}
|
||||||
NSString *message = nil;
|
NSString *message = nil;
|
||||||
if (success) {
|
if (success) {
|
||||||
message = KBLocalized(@"已应用,切到键盘查看");
|
message = KBLocalized(@"Applied. Switch to the keyboard to view.");
|
||||||
} else if ([error.domain isEqualToString:KBSkinBridgeErrorDomain] &&
|
} else if ([error.domain isEqualToString:KBSkinBridgeErrorDomain] &&
|
||||||
error.code == KBSkinBridgeErrorContainerUnavailable) {
|
error.code == KBSkinBridgeErrorContainerUnavailable) {
|
||||||
message = KBLocalized(@"无法访问共享容器,应用皮肤失败");
|
message = KBLocalized(@"无法访问共享容器,应用皮肤失败");
|
||||||
|
|||||||
19
keyBoard/Class/Me/VC/KBCancelAccountVC.h
Normal file
19
keyBoard/Class/Me/VC/KBCancelAccountVC.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// KBCancelAccountVC.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// 注销账户页面
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "BaseViewController.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface KBCancelAccountVC : BaseViewController
|
||||||
|
|
||||||
|
/// 注销协议 HTML 内容(由外部传入或内部请求)
|
||||||
|
@property (nonatomic, copy, nullable) NSString *htmlContent;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
142
keyBoard/Class/Me/VC/KBCancelAccountVC.m
Normal file
142
keyBoard/Class/Me/VC/KBCancelAccountVC.m
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
//
|
||||||
|
// KBCancelAccountVC.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// 注销账户页面:顶部标题 + 中间 HTML 协议展示 + 底部注销按钮
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBCancelAccountVC.h"
|
||||||
|
#import <WebKit/WebKit.h>
|
||||||
|
#import <Masonry/Masonry.h>
|
||||||
|
#import "KBMyVM.h"
|
||||||
|
#import "KBAlert.h"
|
||||||
|
|
||||||
|
@interface KBCancelAccountVC ()
|
||||||
|
|
||||||
|
@property (nonatomic, strong) UILabel *titleLabel;
|
||||||
|
@property (nonatomic, strong) WKWebView *webView;
|
||||||
|
@property (nonatomic, strong) UIButton *cancelAccountBtn;
|
||||||
|
@property (nonatomic, strong) KBMyVM *myVM;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBCancelAccountVC
|
||||||
|
|
||||||
|
- (void)viewDidLoad {
|
||||||
|
[super viewDidLoad];
|
||||||
|
self.kb_titleLabel.text = KBLocalized(@"Cancel Account");
|
||||||
|
self.view.backgroundColor = [UIColor colorWithHex:0xFFFFFF];
|
||||||
|
|
||||||
|
[self.view addSubview:self.titleLabel];
|
||||||
|
[self.view addSubview:self.webView];
|
||||||
|
[self.view addSubview:self.cancelAccountBtn];
|
||||||
|
|
||||||
|
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT + 20);
|
||||||
|
make.left.equalTo(self.view).offset(16);
|
||||||
|
make.right.equalTo(self.view).offset(-16);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.cancelAccountBtn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(self.view).offset(16);
|
||||||
|
make.right.equalTo(self.view).offset(-16);
|
||||||
|
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-12);
|
||||||
|
make.height.mas_equalTo(56);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.webView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(self.titleLabel.mas_bottom).offset(16);
|
||||||
|
make.left.equalTo(self.view).offset(16);
|
||||||
|
make.right.equalTo(self.view).offset(-16);
|
||||||
|
make.bottom.equalTo(self.cancelAccountBtn.mas_top).offset(-16);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self fetchAgreement];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)fetchAgreement {
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
[self.myVM fetchCancelAccountWarningWithCompletion:^(NSString * _Nullable html, NSError * _Nullable error) {
|
||||||
|
if (html.length > 0) {
|
||||||
|
weakSelf.htmlContent = html;
|
||||||
|
}
|
||||||
|
[weakSelf loadHTMLContent];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)loadHTMLContent {
|
||||||
|
NSString *html = self.htmlContent ?: @"";
|
||||||
|
// 包裹一层基本样式,适配移动端
|
||||||
|
NSString *wrappedHTML = [NSString stringWithFormat:
|
||||||
|
@"<html><head>"
|
||||||
|
"<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'>"
|
||||||
|
"<style>"
|
||||||
|
"body { font-family: -apple-system, sans-serif; font-size: 15px; color: #333; "
|
||||||
|
"line-height: 1.6; padding: 0; margin: 0; word-wrap: break-word; }"
|
||||||
|
"</style></head><body>%@</body></html>", html];
|
||||||
|
[self.webView loadHTMLString:wrappedHTML baseURL:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Actions
|
||||||
|
|
||||||
|
- (void)onTapCancelAccount {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_cancel_account_confirm_btn"
|
||||||
|
pageId:@"cancel_account"
|
||||||
|
elementId:@"cancel_account_confirm_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
KBWeakSelf;
|
||||||
|
|
||||||
|
[KBAlert confirmTitle:KBLocalized(@"Cancel Account") message:KBLocalized(@"After cancellation, your account will be deactivated and local login data will be cleared. Continue?") ok:KBLocalized(@"Confirm") cancel:KBLocalized(@"Cancel") completion:^(BOOL ok) {
|
||||||
|
if (!ok) { return; }
|
||||||
|
[weakSelf.myVM cancelAccountWithCompletion:nil];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Lazy
|
||||||
|
|
||||||
|
- (UILabel *)titleLabel {
|
||||||
|
if (!_titleLabel) {
|
||||||
|
_titleLabel = [UILabel new];
|
||||||
|
_titleLabel.text = KBLocalized(@"Cancel Account Notice");
|
||||||
|
_titleLabel.textColor = [UIColor colorWithHex:KBBlackValue];
|
||||||
|
_titleLabel.font = [KBFont bold:20];
|
||||||
|
_titleLabel.numberOfLines = 0;
|
||||||
|
}
|
||||||
|
return _titleLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (WKWebView *)webView {
|
||||||
|
if (!_webView) {
|
||||||
|
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
|
||||||
|
_webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config];
|
||||||
|
_webView.backgroundColor = UIColor.whiteColor;
|
||||||
|
_webView.scrollView.showsVerticalScrollIndicator = YES;
|
||||||
|
_webView.layer.cornerRadius = 12;
|
||||||
|
_webView.layer.masksToBounds = YES;
|
||||||
|
}
|
||||||
|
return _webView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIButton *)cancelAccountBtn {
|
||||||
|
if (!_cancelAccountBtn) {
|
||||||
|
_cancelAccountBtn = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||||
|
[_cancelAccountBtn setTitle:KBLocalized(@"Confirm Cancel Account") forState:UIControlStateNormal];
|
||||||
|
[_cancelAccountBtn setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
|
||||||
|
_cancelAccountBtn.titleLabel.font = [KBFont medium:16];
|
||||||
|
_cancelAccountBtn.backgroundColor = [UIColor colorWithHex:0xFF0000];
|
||||||
|
_cancelAccountBtn.layer.cornerRadius = 12;
|
||||||
|
_cancelAccountBtn.layer.masksToBounds = YES;
|
||||||
|
[_cancelAccountBtn addTarget:self action:@selector(onTapCancelAccount) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
}
|
||||||
|
return _cancelAccountBtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBMyVM *)myVM {
|
||||||
|
if (!_myVM) {
|
||||||
|
_myVM = [[KBMyVM alloc] init];
|
||||||
|
}
|
||||||
|
return _myVM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -14,6 +14,8 @@
|
|||||||
#import "KBChangeNicknamePopView.h"
|
#import "KBChangeNicknamePopView.h"
|
||||||
#import "KBGenderPickerPopView.h"
|
#import "KBGenderPickerPopView.h"
|
||||||
#import "KBMyVM.h"
|
#import "KBMyVM.h"
|
||||||
|
#import "KBAlert.h"
|
||||||
|
#import "KBCancelAccountVC.h"
|
||||||
@interface KBPersonInfoVC () <UITableViewDelegate, UITableViewDataSource, PHPickerViewControllerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate>
|
@interface KBPersonInfoVC () <UITableViewDelegate, UITableViewDataSource, PHPickerViewControllerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate>
|
||||||
|
|
||||||
// 列表
|
// 列表
|
||||||
@@ -25,7 +27,7 @@
|
|||||||
@property (nonatomic, strong) UIButton *editBadge; // 头像右下角的小铅笔
|
@property (nonatomic, strong) UIButton *editBadge; // 头像右下角的小铅笔
|
||||||
@property (nonatomic, strong) UILabel *modifyLabel; // “Modify” 文案
|
@property (nonatomic, strong) UILabel *modifyLabel; // “Modify” 文案
|
||||||
|
|
||||||
// 底部退出按钮(固定在屏幕底部)
|
// 底部退出登录按钮
|
||||||
@property (nonatomic, strong) UIButton *logoutBtn;
|
@property (nonatomic, strong) UIButton *logoutBtn;
|
||||||
|
|
||||||
// 数据
|
// 数据
|
||||||
@@ -64,7 +66,7 @@
|
|||||||
// 表头
|
// 表头
|
||||||
self.tableView.tableHeaderView = self.headerView;
|
self.tableView.tableHeaderView = self.headerView;
|
||||||
|
|
||||||
// 底部退出按钮固定在屏幕底部
|
// 底部退出登录按钮
|
||||||
[self.view addSubview:self.logoutBtn];
|
[self.view addSubview:self.logoutBtn];
|
||||||
[self.logoutBtn mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.logoutBtn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.equalTo(self.view).offset(16);
|
make.left.equalTo(self.view).offset(16);
|
||||||
@@ -116,14 +118,18 @@
|
|||||||
|
|
||||||
#pragma mark - UITableView
|
#pragma mark - UITableView
|
||||||
|
|
||||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; }
|
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 2; }
|
||||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.items.count; }
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||||
|
return section == 0 ? self.items.count : 1;
|
||||||
|
}
|
||||||
|
|
||||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
return 56.0;
|
return 56.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { return 12.0; }
|
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
|
||||||
|
return section == 0 ? 12.0 : 15.0;
|
||||||
|
}
|
||||||
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { return [UIView new]; }
|
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { return [UIView new]; }
|
||||||
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { return 0.01; }
|
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { return 0.01; }
|
||||||
|
|
||||||
@@ -131,19 +137,34 @@
|
|||||||
static NSString *cid = @"KBPersonInfoItemCell";
|
static NSString *cid = @"KBPersonInfoItemCell";
|
||||||
KBPersonInfoItemCell *cell = [tableView dequeueReusableCellWithIdentifier:cid];
|
KBPersonInfoItemCell *cell = [tableView dequeueReusableCellWithIdentifier:cid];
|
||||||
if (!cell) { cell = [[KBPersonInfoItemCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cid]; }
|
if (!cell) { cell = [[KBPersonInfoItemCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cid]; }
|
||||||
NSDictionary *it = self.items[indexPath.row];
|
|
||||||
BOOL isTop = (indexPath.row == 0);
|
if (indexPath.section == 0) {
|
||||||
BOOL isBottom = (indexPath.row == self.items.count - 1);
|
NSDictionary *it = self.items[indexPath.row];
|
||||||
[cell configWithTitle:it[@"title"]
|
BOOL isTop = (indexPath.row == 0);
|
||||||
value:it[@"value"]
|
BOOL isBottom = (indexPath.row == self.items.count - 1);
|
||||||
showArrow:[it[@"arrow"] boolValue]
|
[cell configWithTitle:it[@"title"]
|
||||||
showCopy:[it[@"copy"] boolValue]
|
value:it[@"value"]
|
||||||
isTop:isTop
|
showArrow:[it[@"arrow"] boolValue]
|
||||||
isBottom:isBottom];
|
showCopy:[it[@"copy"] boolValue]
|
||||||
|
isTop:isTop
|
||||||
|
isBottom:isBottom];
|
||||||
|
} else {
|
||||||
|
[cell configWithTitle:KBLocalized(@"Cancel Account")
|
||||||
|
value:@""
|
||||||
|
showArrow:YES
|
||||||
|
showCopy:NO
|
||||||
|
isTop:YES
|
||||||
|
isBottom:YES];
|
||||||
|
}
|
||||||
return cell;
|
return cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
if (indexPath.section == 1) {
|
||||||
|
KBCancelAccountVC *vc = [[KBCancelAccountVC alloc] init];
|
||||||
|
[self.navigationController pushViewController:vc animated:YES];
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (indexPath.row == 0) {
|
if (indexPath.row == 0) {
|
||||||
// 昵称编辑 -> 弹窗
|
// 昵称编辑 -> 弹窗
|
||||||
CGFloat width = KB_SCREEN_WIDTH;
|
CGFloat width = KB_SCREEN_WIDTH;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#import "KBMyVM.h"
|
#import "KBMyVM.h"
|
||||||
#import "KBMyTheme.h"
|
#import "KBMyTheme.h"
|
||||||
#import "KBHUD.h"
|
#import "KBHUD.h"
|
||||||
|
#import "KBSkinManager.h"
|
||||||
|
|
||||||
static NSString * const kMySkinCellId = @"kMySkinCellId";
|
static NSString * const kMySkinCellId = @"kMySkinCellId";
|
||||||
|
|
||||||
@@ -254,6 +255,25 @@ static NSString * const kMySkinCellId = @"kMySkinCellId";
|
|||||||
return cell;
|
return cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
if (self.isEditingMode) {
|
||||||
|
KBMyTheme *theme = self.data[indexPath.item];
|
||||||
|
NSString *currentSkinId = [KBSkinManager shared].current.skinId;
|
||||||
|
// themeId 可能是 NSString 或 NSNumber,统一转字符串比较
|
||||||
|
NSString *themeIdStr = nil;
|
||||||
|
if ([theme.themeId isKindOfClass:[NSString class]]) {
|
||||||
|
themeIdStr = theme.themeId;
|
||||||
|
} else if ([theme.themeId respondsToSelector:@selector(stringValue)]) {
|
||||||
|
themeIdStr = [(id)theme.themeId stringValue];
|
||||||
|
}
|
||||||
|
if (currentSkinId.length > 0 && themeIdStr.length > 0 && [themeIdStr isEqualToString:currentSkinId]) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"The skin in use cannot be deleted")];
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
if (!self.isEditingMode) {
|
if (!self.isEditingMode) {
|
||||||
// 非编辑态:可在此进入详情,当前示例不处理
|
// 非编辑态:可在此进入详情,当前示例不处理
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ typedef void(^KBSubmitFeedbackCompletion)(BOOL success, NSError *_Nullable error
|
|||||||
typedef void(^KBMyPurchaseRecordCompletion)(NSArray<KBConsumptionRecord *> *_Nullable records, NSError *_Nullable error);
|
typedef void(^KBMyPurchaseRecordCompletion)(NSArray<KBConsumptionRecord *> *_Nullable records, NSError *_Nullable error);
|
||||||
typedef void(^KBMyInviteCodeCompletion)(KBInviteCodeModel *_Nullable inviteCode, NSError *_Nullable error);
|
typedef void(^KBMyInviteCodeCompletion)(KBInviteCodeModel *_Nullable inviteCode, NSError *_Nullable error);
|
||||||
typedef void(^KBMyCustomerMailCompletion)(NSString *_Nullable customerMail, NSError *_Nullable error);
|
typedef void(^KBMyCustomerMailCompletion)(NSString *_Nullable customerMail, NSError *_Nullable error);
|
||||||
|
typedef void(^KBCancelAccountCompletion)(BOOL success, NSError *_Nullable error);
|
||||||
|
typedef void(^KBCancelAccountAgreementCompletion)(NSString *_Nullable html, NSError *_Nullable error);
|
||||||
|
|
||||||
@interface KBMyVM : NSObject
|
@interface KBMyVM : NSObject
|
||||||
|
|
||||||
@@ -77,6 +79,12 @@ typedef void(^KBMyCustomerMailCompletion)(NSString *_Nullable customerMail, NSEr
|
|||||||
|
|
||||||
/// 退出登录
|
/// 退出登录
|
||||||
- (void)logout;
|
- (void)logout;
|
||||||
|
|
||||||
|
/// 注销账号(/user/cancelAccount)
|
||||||
|
- (void)cancelAccountWithCompletion:(KBCancelAccountCompletion)completion;
|
||||||
|
|
||||||
|
/// 获取注销提示信息 HTML(GET /keyboardWarningMessage/byLocale)
|
||||||
|
- (void)fetchCancelAccountWarningWithCompletion:(KBCancelAccountAgreementCompletion)completion;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -457,18 +457,98 @@ NSString * const KBUserCharacterDeletedNotification = @"KBUserCharacterDeletedNo
|
|||||||
|
|
||||||
NSString *message = jsonOrData[KBMessage] ?: KBLocalized(@"Success");
|
NSString *message = jsonOrData[KBMessage] ?: KBLocalized(@"Success");
|
||||||
[KBHUD showSuccess:message];
|
[KBHUD showSuccess:message];
|
||||||
|
[self kb_clearLoginInfoAndRouteHome];
|
||||||
// 本地会话退出
|
|
||||||
[[KBUserSessionManager shared] logout];
|
|
||||||
|
|
||||||
// 回到登录 / 主界面
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
id<UIApplicationDelegate> appDelegate = UIApplication.sharedApplication.delegate;
|
|
||||||
if ([appDelegate respondsToSelector:@selector(toMainTabbarVC)]) {
|
|
||||||
AppDelegate *delegate = (AppDelegate *)appDelegate;
|
|
||||||
[delegate toMainTabbarVC];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)cancelAccountWithCompletion:(KBCancelAccountCompletion)completion {
|
||||||
|
[KBHUD show];
|
||||||
|
[[KBNetworkManager shared] POST:API_USER_CANCEL_ACCOUNT
|
||||||
|
jsonBody:nil
|
||||||
|
headers:nil
|
||||||
|
autoShowBusinessError:NO
|
||||||
|
completion:^(NSDictionary *jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||||||
|
[KBHUD dismiss];
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
NSString *msg = KBBizMessageFromJSONObject(jsonOrData) ?: error.localizedDescription ?: KBLocalized(@"Network error");
|
||||||
|
[KBHUD showInfo:msg];
|
||||||
|
if (completion) {
|
||||||
|
completion(NO, error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *message = jsonOrData[KBMessage] ?: KBLocalized(@"Success");
|
||||||
|
[KBHUD showSuccess:message];
|
||||||
|
|
||||||
|
[self kb_clearLoginInfoAndRouteHome];
|
||||||
|
if (completion) {
|
||||||
|
completion(YES, nil);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)fetchCancelAccountWarningWithCompletion:(KBCancelAccountAgreementCompletion)completion {
|
||||||
|
KBLanguageCode langCode = [KBLocalizationManager shared].currentLanguageCode;
|
||||||
|
NSString *locale;
|
||||||
|
if ([langCode isEqualToString:KBLanguageCodeSimplifiedChinese]) {
|
||||||
|
locale = @"zh-CN";
|
||||||
|
} else {
|
||||||
|
locale = @"en-US";
|
||||||
|
}
|
||||||
|
|
||||||
|
[[KBNetworkManager shared] GET:API_CANCEL_ACCOUNT_WARNING
|
||||||
|
parameters:@{@"locale": locale}
|
||||||
|
headers:nil
|
||||||
|
autoShowBusinessError:NO
|
||||||
|
completion:^(NSDictionary *jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||||||
|
if (error) {
|
||||||
|
if (completion) completion(nil, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
id dataObj = jsonOrData[KBData];
|
||||||
|
NSString *html = @"";
|
||||||
|
if ([dataObj isKindOfClass:[NSDictionary class]]) {
|
||||||
|
id content = dataObj[@"content"];
|
||||||
|
if ([content isKindOfClass:[NSString class]]) {
|
||||||
|
html = (NSString *)content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (completion) completion(html, nil);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Private
|
||||||
|
|
||||||
|
/// 清理本地登录态及关联缓存,并回到首页。
|
||||||
|
- (void)kb_clearLoginInfoAndRouteHome {
|
||||||
|
[[KBUserSessionManager shared] logout];
|
||||||
|
|
||||||
|
NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
|
NSArray<NSString *> *sharedKeys = @[
|
||||||
|
AppGroup_MyKbJson,
|
||||||
|
AppGroup_UserAvatarURL,
|
||||||
|
AppGroup_SubscriptionPrefillPayload,
|
||||||
|
AppGroup_ChatUpdatedCompanionId,
|
||||||
|
@"AppGroup_SelectedPersona"
|
||||||
|
];
|
||||||
|
for (NSString *key in sharedKeys) {
|
||||||
|
[sharedDefaults removeObjectForKey:key];
|
||||||
|
}
|
||||||
|
[sharedDefaults synchronize];
|
||||||
|
|
||||||
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||||
|
[defaults removeObjectForKey:@"KBAISelectedPersonaId"];
|
||||||
|
[defaults synchronize];
|
||||||
|
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
id<UIApplicationDelegate> appDelegate = UIApplication.sharedApplication.delegate;
|
||||||
|
if ([appDelegate respondsToSelector:@selector(toMainTabbarVC)]) {
|
||||||
|
AppDelegate *delegate = (AppDelegate *)appDelegate;
|
||||||
|
[delegate toMainTabbarVC];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
|
|||||||
NSDictionary<NSString *, NSString *> *signHeaders = [KBSignUtils signHeadersWithBodyParams:bodyParams];
|
NSDictionary<NSString *, NSString *> *signHeaders = [KBSignUtils signHeadersWithBodyParams:bodyParams];
|
||||||
NSMutableDictionary<NSString *, NSString *> *headers =
|
NSMutableDictionary<NSString *, NSString *> *headers =
|
||||||
[self.defaultHeaders mutableCopy] ?: [NSMutableDictionary dictionary];
|
[self.defaultHeaders mutableCopy] ?: [NSMutableDictionary dictionary];
|
||||||
|
// 每次请求动态更新 Accept-Language,确保语言切换后生效
|
||||||
|
headers[@"Accept-Language"] = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
|
||||||
[headers addEntriesFromDictionary:signHeaders ?: @{}];
|
[headers addEntriesFromDictionary:signHeaders ?: @{}];
|
||||||
self.defaultHeaders = headers;
|
self.defaultHeaders = headers;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ static NSString * const kKBVipReviewItemCellId = @"kKBVipReviewItemCellId";
|
|||||||
|
|
||||||
// 模拟数据
|
// 模拟数据
|
||||||
_data = @[
|
_data = @[
|
||||||
@{@"name":@"Sdsd666", @"text":@"I Highly Recommend This App. It Taught Me How To Chat"},
|
// @{@"name":@"Sdsd666", @"text":@"I Highly Recommend This App. It Taught Me How To Chat"},
|
||||||
@{@"name":@"Joyce", @"text":@"Great keyboard and AI features!"},
|
// @{@"name":@"Joyce", @"text":@"Great keyboard and AI features!"},
|
||||||
@{@"name":@"Luna", @"text":@"Amazing app, love it."},
|
// @{@"name":@"Luna", @"text":@"Amazing app, love it."},
|
||||||
@{@"name":@"Mark", @"text":@"Helps with chat and emotion."},
|
// @{@"name":@"Mark", @"text":@"Helps with chat and emotion."},
|
||||||
@{@"name":@"Alan", @"text":@"Useful personalized keyboard."},
|
// @{@"name":@"Alan", @"text":@"Useful personalized keyboard."},
|
||||||
@{@"name":@"Coco", @"text":@"Recommend to friends."},
|
// @{@"name":@"Coco", @"text":@"Recommend to friends."},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
|
|||||||
@@ -396,7 +396,7 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId";
|
|||||||
if (indexPath.section == 1) {
|
if (indexPath.section == 1) {
|
||||||
return CGSizeMake(w, KBFit(75 + 6));
|
return CGSizeMake(w, KBFit(75 + 6));
|
||||||
} else {
|
} else {
|
||||||
return CGSizeMake(w, 140);
|
return CGSizeMake(w, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -313,7 +313,7 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
|
|||||||
skin[@"force_download"] = @(YES);
|
skin[@"force_download"] = @(YES);
|
||||||
NSLog(@"⬇️[SkinDetail] download request id=%@ zip=%@ force=YES",
|
NSLog(@"⬇️[SkinDetail] download request id=%@ zip=%@ force=YES",
|
||||||
skin[@"id"], skin[@"zip_url"]);
|
skin[@"id"], skin[@"zip_url"]);
|
||||||
[KBHUD showWithStatus:@"正在下载..."];
|
[KBHUD showWithStatus:KBLocalized(@"Downloading...")];
|
||||||
[[KBSkinService shared] applySkinWithJSON:skin
|
[[KBSkinService shared] applySkinWithJSON:skin
|
||||||
fromViewController:self
|
fromViewController:self
|
||||||
mode:KBSkinSourceModeRemoteZip
|
mode:KBSkinSourceModeRemoteZip
|
||||||
|
|||||||
@@ -213,9 +213,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NSString *themeIdValue = [[self kb_themeIdParamFromString:themeId] description];
|
NSString *themeIdValue = [[self kb_themeIdParamFromString:themeId] description];
|
||||||
NSString *path = [NSString stringWithFormat:@"%@?themeId=%@", API_THEME_RESTORE, themeIdValue ?: @""];
|
// NSString *path = [NSString stringWithFormat:@"%@?themeId=%@", API_THEME_RESTORE, themeIdValue ?: @""];
|
||||||
[[KBNetworkManager shared] POST:path
|
NSDictionary *body = @{@"themeId": [self kb_themeIdParamFromString:themeId]};
|
||||||
jsonBody:nil
|
|
||||||
|
[[KBNetworkManager shared] POST:API_THEME_RESTORE
|
||||||
|
jsonBody:body
|
||||||
headers:nil
|
headers:nil
|
||||||
autoShowBusinessError:NO
|
autoShowBusinessError:NO
|
||||||
completion:^(NSDictionary * _Nullable json,
|
completion:^(NSDictionary * _Nullable json,
|
||||||
|
|||||||
@@ -15,18 +15,12 @@
|
|||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio</string>
|
|
||||||
</array>
|
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIDesignRequiresCompatibility</key>
|
||||||
<array>
|
<true/>
|
||||||
<string>audio</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
76
keyBoard/PrivacyInfo.xcprivacy
Normal file
76
keyBoard/PrivacyInfo.xcprivacy
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeEmailAddress</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeUserID</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeOtherUserContent</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>CA92.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryActiveKeyboards</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>3EC4.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>C617.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Reference in New Issue
Block a user