添加隐私,注销功能
This commit is contained in:
@@ -6,7 +6,8 @@
|
|||||||
"Bash(xcodebuild:*)",
|
"Bash(xcodebuild:*)",
|
||||||
"Bash(plutil:*)",
|
"Bash(plutil:*)",
|
||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(ls:*)"
|
"Bash(ls:*)",
|
||||||
|
"Bash(wc:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
|
|
||||||
#define API_LOGOUT @"/user/logout" // 退出登录
|
#define API_LOGOUT @"/user/logout" // 退出登录
|
||||||
|
#define API_USER_CANCEL_ACCOUNT @"/user/cancelAccount" // 注销账户
|
||||||
|
|
||||||
#define API_UPDATA_INFO @"/user/updateInfo" // 更新用户
|
#define API_UPDATA_INFO @"/user/updateInfo" // 更新用户
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,8 @@
|
|||||||
"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?";
|
||||||
"Ranking List" = "Ranking List";
|
"Ranking List" = "Ranking List";
|
||||||
"Persona circle" = "Persona circle";
|
"Persona circle" = "Persona circle";
|
||||||
"Clear" = "Clear";
|
"Clear" = "Clear";
|
||||||
|
|||||||
@@ -172,6 +172,8 @@
|
|||||||
"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?" = "注销后账号将被停用,并清除本地登录数据,是否继续?";
|
||||||
"Ranking List" = "排行榜";
|
"Ranking List" = "排行榜";
|
||||||
"Persona circle" = "圈子";
|
"Persona circle" = "圈子";
|
||||||
"Clear" = "立刻清空";
|
"Clear" = "立刻清空";
|
||||||
|
|||||||
@@ -233,6 +233,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 */; };
|
||||||
@@ -719,6 +721,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>"; };
|
||||||
@@ -1568,6 +1572,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 +1593,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 */,
|
||||||
@@ -2257,6 +2263,7 @@
|
|||||||
0460866B2F18D75500757C95 /* ai_test.m4a in Resources */,
|
0460866B2F18D75500757C95 /* ai_test.m4a in Resources */,
|
||||||
041007D42ECE012500D203BB /* 002.zip 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 */,
|
||||||
@@ -2272,6 +2279,7 @@
|
|||||||
04286A0F2ECDA71B00CE730C /* 001.zip in Resources */,
|
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 */,
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
#import "KBChangeNicknamePopView.h"
|
#import "KBChangeNicknamePopView.h"
|
||||||
#import "KBGenderPickerPopView.h"
|
#import "KBGenderPickerPopView.h"
|
||||||
#import "KBMyVM.h"
|
#import "KBMyVM.h"
|
||||||
|
#import "KBAlert.h"
|
||||||
@interface KBPersonInfoVC () <UITableViewDelegate, UITableViewDataSource, PHPickerViewControllerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate>
|
@interface KBPersonInfoVC () <UITableViewDelegate, UITableViewDataSource, PHPickerViewControllerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate>
|
||||||
|
|
||||||
// 列表
|
// 列表
|
||||||
@@ -25,8 +26,10 @@
|
|||||||
@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;
|
||||||
|
// 底部注销账户按钮
|
||||||
|
@property (nonatomic, strong) UIButton *cancelBtn;
|
||||||
|
|
||||||
// 数据
|
// 数据
|
||||||
@property (nonatomic, copy) NSArray<NSDictionary *> *items; // {title,value,arrow,copy}
|
@property (nonatomic, copy) NSArray<NSDictionary *> *items; // {title,value,arrow,copy}
|
||||||
@@ -64,9 +67,18 @@
|
|||||||
// 表头
|
// 表头
|
||||||
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.right.equalTo(self.view).offset(-16);
|
||||||
|
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-(12 + 56 + 10));
|
||||||
|
make.height.mas_equalTo(56);
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 底部注销账户按钮
|
||||||
|
[self.view addSubview:self.cancelBtn];
|
||||||
|
[self.cancelBtn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.equalTo(self.view).offset(16);
|
make.left.equalTo(self.view).offset(16);
|
||||||
make.right.equalTo(self.view).offset(-16);
|
make.right.equalTo(self.view).offset(-16);
|
||||||
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-12);
|
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-12);
|
||||||
@@ -75,7 +87,7 @@
|
|||||||
|
|
||||||
// 列表底部腾出空间,避免被按钮挡住
|
// 列表底部腾出空间,避免被按钮挡住
|
||||||
UIEdgeInsets inset = self.tableView.contentInset;
|
UIEdgeInsets inset = self.tableView.contentInset;
|
||||||
inset.bottom = 56 + 24; // 按钮高度 + 额外间距
|
inset.bottom = 56 + 10 + 56 + 24; // 两个按钮高度 + 间距 + 额外间距
|
||||||
self.tableView.contentInset = inset;
|
self.tableView.contentInset = inset;
|
||||||
self.viewModel = [[KBMyVM alloc] init];
|
self.viewModel = [[KBMyVM alloc] init];
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
@@ -275,6 +287,25 @@
|
|||||||
[self.myVM logout];
|
[self.myVM logout];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)onTapCancelAccount {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_person_cancel_account_btn"
|
||||||
|
pageId:@"person_info"
|
||||||
|
elementId:@"cancel_account_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")
|
||||||
|
okColor:[UIColor colorWithHex:0xFF0000]
|
||||||
|
cancelColor:nil
|
||||||
|
completion:^(BOOL ok) {
|
||||||
|
if (!ok) { return; }
|
||||||
|
[weakSelf.myVM cancelAccountWithCompletion:nil];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Lazy UI(懒加载)
|
#pragma mark - Lazy UI(懒加载)
|
||||||
|
|
||||||
- (UITableView *)tableView {
|
- (UITableView *)tableView {
|
||||||
@@ -378,6 +409,19 @@
|
|||||||
return _logoutBtn;
|
return _logoutBtn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (UIButton *)cancelBtn {
|
||||||
|
if (!_cancelBtn) {
|
||||||
|
_cancelBtn = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||||
|
[_cancelBtn setTitle:KBLocalized(@"Cancel Account") forState:UIControlStateNormal];
|
||||||
|
[_cancelBtn setTitleColor:[UIColor colorWithHex:0xFF0000] forState:UIControlStateNormal];
|
||||||
|
_cancelBtn.titleLabel.font = [KBFont medium:16];
|
||||||
|
_cancelBtn.backgroundColor = UIColor.whiteColor;
|
||||||
|
_cancelBtn.layer.cornerRadius = 12; _cancelBtn.layer.masksToBounds = YES;
|
||||||
|
[_cancelBtn addTarget:self action:@selector(onTapCancelAccount) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
}
|
||||||
|
return _cancelBtn;
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Image Picker
|
#pragma mark - Image Picker
|
||||||
|
|
||||||
- (void)presentImagePicker {
|
- (void)presentImagePicker {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ 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);
|
||||||
|
|
||||||
@interface KBMyVM : NSObject
|
@interface KBMyVM : NSObject
|
||||||
|
|
||||||
@@ -77,6 +78,9 @@ typedef void(^KBMyCustomerMailCompletion)(NSString *_Nullable customerMail, NSEr
|
|||||||
|
|
||||||
/// 退出登录
|
/// 退出登录
|
||||||
- (void)logout;
|
- (void)logout;
|
||||||
|
|
||||||
|
/// 注销账号(/user/cancelAccount)
|
||||||
|
- (void)cancelAccountWithCompletion:(KBCancelAccountCompletion)completion;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -457,11 +457,61 @@ 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];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
// 本地会话退出
|
- (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);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Private
|
||||||
|
|
||||||
|
/// 清理本地登录态及关联缓存,并回到首页。
|
||||||
|
- (void)kb_clearLoginInfoAndRouteHome {
|
||||||
[[KBUserSessionManager shared] logout];
|
[[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(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
id<UIApplicationDelegate> appDelegate = UIApplication.sharedApplication.delegate;
|
id<UIApplicationDelegate> appDelegate = UIApplication.sharedApplication.delegate;
|
||||||
if ([appDelegate respondsToSelector:@selector(toMainTabbarVC)]) {
|
if ([appDelegate respondsToSelector:@selector(toMainTabbarVC)]) {
|
||||||
@@ -469,6 +519,5 @@ NSString * const KBUserCharacterDeletedNotification = @"KBUserCharacterDeletedNo
|
|||||||
[delegate toMainTabbarVC];
|
[delegate toMainTabbarVC];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -24,10 +24,6 @@
|
|||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio</string>
|
|
||||||
</array>
|
|
||||||
<key>UIDesignRequiresCompatibility</key>
|
<key>UIDesignRequiresCompatibility</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
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