This commit is contained in:
2026-03-09 17:34:08 +08:00
parent c1ace5f53e
commit 0af7428353
55 changed files with 1630 additions and 665 deletions

View File

@@ -9,7 +9,8 @@
"Bash(ls:*)",
"Bash(wc:*)",
"Bash(chmod +x:*)",
"Bash(python3:*)"
"Bash(python3:*)",
"Bash(/usr/libexec/PlistBuddy:*)"
]
}
}

View File

@@ -16,7 +16,6 @@
#import "KBKeyBoardMainView.h"
#import "KBKeyboardSubscriptionView.h"
#import "KBLocalizationManager.h"
#import "KBSettingView.h"
#import "KBSkinManager.h"
#import "KBSkinInstallBridge.h"
#import "KBSuggestionEngine.h"
@@ -296,10 +295,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
[_functionView removeFromSuperview];
_functionView = nil;
}
if (_settingView) {
[_settingView removeFromSuperview];
_settingView = nil;
}
if (_subscriptionView) {
[_subscriptionView removeFromSuperview];
_subscriptionView = nil;

View File

@@ -17,7 +17,6 @@
#import "KBKey.h"
#import "KBKeyboardSubscriptionProduct.h"
#import "KBKeyboardSubscriptionView.h"
#import "KBSettingView.h"
#import "KBChatMessage.h"
#import "KBChatPanelView.h"
#import "KBChatLimitPopView.h"
@@ -79,7 +78,6 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
*keyBoardMainView; // 0
@property(nonatomic, strong)
KBFunctionView *functionView; // 0
@property(nonatomic, strong) KBSettingView *settingView; //
@property(nonatomic, strong) UIImageView *bgImageView; //
@property(nonatomic, strong) KBChatPanelView *chatPanelView;
@property(nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
@@ -539,68 +537,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
}
}
/// / keyBoardMainView /
- (void)showSettingView:(BOOL)show {
if (show) {
[self showChatPanel:NO];
[[KBMaiPointReporter sharedReporter]
reportPageExposureWithEventName:@"enter_keyboard_settings"
pageId:@"keyboard_settings"
extra:nil
completion:nil];
KBSettingView *settingView = self.settingView;
if (!settingView.superview) {
settingView.hidden = YES;
[self.contentView addSubview:settingView];
[settingView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
[settingView.backButton addTarget:self
action:@selector(onTapSettingsBack)
forControlEvents:UIControlEventTouchUpInside];
}
[self.contentView bringSubviewToFront:settingView];
// keyBoardMainView self.view
[self.contentView layoutIfNeeded];
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
if (w <= 0) {
w = CGRectGetWidth(self.contentView.bounds);
}
if (w <= 0) {
w = [self kb_portraitWidth];
}
settingView.transform = CGAffineTransformMakeTranslation(w, 0);
settingView.hidden = NO;
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
settingView.transform = CGAffineTransformIdentity;
}
completion:nil];
} else {
KBSettingView *settingView = self.settingView;
if (!settingView.superview || settingView.hidden)
return;
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
if (w <= 0) {
w = CGRectGetWidth(self.contentView.bounds);
}
if (w <= 0) {
w = [self kb_portraitWidth];
}
[UIView animateWithDuration:0.22
delay:0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
settingView.transform = CGAffineTransformMakeTranslation(w, 0);
}
completion:^(BOOL finished) {
settingView.hidden = YES;
}];
}
}
/// /
- (void)showChatPanel:(BOOL)show {
if (show == self.chatPanelVisible) {
@@ -619,7 +555,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
_functionView.hidden = YES;
}
[self hideSubscriptionPanel];
[self showSettingView:NO];
[UIView animateWithDuration:0.2
delay:0
options:UIViewAnimationOptionCurveEaseOut
@@ -748,10 +683,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
[_subscriptionView removeFromSuperview];
_subscriptionView = nil;
}
if (_settingView) {
[_settingView removeFromSuperview];
_settingView = nil;
}
}
- (void)showSubscriptionPanel {
@@ -915,16 +846,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
[self showChatPanel:NO];
}
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView {
[[KBMaiPointReporter sharedReporter]
reportClickWithEventName:@"click_keyboard_settings_btn"
pageId:@"keyboard_main_panel"
elementId:@"settings_btn"
extra:nil
completion:nil];
[self showSettingView:YES];
}
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView
didSelectEmoji:(NSString *)emoji {
if (emoji.length == 0) {
@@ -947,17 +868,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
[self kb_scheduleContextRefreshResetSuppression:YES];
}
- (void)keyBoardMainViewDidTapEmojiSearch:
(KBKeyBoardMainView *)keyBoardMainView {
// [[KBMaiPointReporter sharedReporter]
// reportClickWithEventName:@"click_keyboard_emoji_search_btn"
// pageId:@"keyboard_main_panel"
// elementId:@"emoji_search_btn"
// extra:nil
// completion:nil];
[KBHUD showInfo:KBLocalized(@"Search coming soon")];
}
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView
didSelectSuggestion:(NSString *)suggestion {
if (suggestion.length == 0) {
@@ -1032,7 +942,7 @@ static NSString *KBFormatMB(uint64_t bytes) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD showInfo:@"当前应用不允许键盘直接唤起App请回到桌面手动打开App进行充值"];
[KBHUD showInfo:KBLocalized(@"This app does not allow the keyboard to open the main app directly. Please return to the Home screen and open the app manually to recharge")];
});
}];
}
@@ -1745,13 +1655,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
return _functionView;
}
- (KBSettingView *)settingView {
if (!_settingView) {
_settingView = [[KBSettingView alloc] init];
}
return _settingView;
}
- (KBChatPanelView *)chatPanelView {
if (!_chatPanelView) {
NSLog(@"[Keyboard] ⚠️ chatPanelView 被创建!");
@@ -1831,16 +1734,6 @@ static NSString *KBFormatMB(uint64_t bytes) {
?: @"";
}
- (void)onTapSettingsBack {
[[KBMaiPointReporter sharedReporter]
reportClickWithEventName:@"click_keyboard_settings_back_btn"
pageId:@"keyboard_settings"
elementId:@"back_btn"
extra:nil
completion:nil];
[self showSettingView:NO];
}
- (void)dealloc {
if (self.kb_fullAccessObserverToken) {
[[NSNotificationCenter defaultCenter]
@@ -2187,15 +2080,14 @@ static NSString *KBFormatMB(uint64_t bytes) {
NSLog(@"[Keyboard] skin request failed: %@",
error);
[KBHUD
showInfo:
KBLocalized(
@"皮肤资源准备失败,请稍后再试")];
showInfo:KBLocalized(
@"Theme resource preparation failed, please try again later")];
}
return;
}
[weakSelf kb_applyTheme];
[KBHUD showInfo:KBLocalized(
@"皮肤已更新,立即体验吧")];
@"Theme updated, try it now")];
}];
}

View File

@@ -18,7 +18,6 @@
#import "KBKey.h"
#import "KBKeyBoardMainView.h"
#import "KBKeyboardSubscriptionView.h"
#import "KBSettingView.h"
#import "Masonry.h"
#import <SDWebImage/SDWebImage.h>
#import <AVFoundation/AVAudioPlayer.h>
@@ -81,7 +80,6 @@
// 1) /
[self kb_setSubscriptionPanelVisible:NO animated:animated];
[self kb_setSettingViewVisible:NO animated:animated];
[self kb_setChatPanelVisible:NO animated:animated];
[self kb_setFunctionPanelVisible:NO];
@@ -93,9 +91,6 @@
case KBKeyboardPanelModeChat:
[self kb_setChatPanelVisible:YES animated:animated];
break;
case KBKeyboardPanelModeSettings:
[self kb_setSettingViewVisible:YES animated:animated];
break;
case KBKeyboardPanelModeSubscription:
[self kb_setSubscriptionPanelVisible:YES animated:animated];
break;
@@ -118,12 +113,6 @@
pageId:@"keyboard_main_panel"
extra:nil
completion:nil];
} else if (mode == KBKeyboardPanelModeSettings) {
[[KBMaiPointReporter sharedReporter]
reportPageExposureWithEventName:@"enter_keyboard_settings"
pageId:@"keyboard_settings"
extra:nil
completion:nil];
} else if (mode == KBKeyboardPanelModeSubscription) {
[[KBMaiPointReporter sharedReporter]
reportPageExposureWithEventName:@"enter_keyboard_subscription_panel"
@@ -135,8 +124,6 @@
// 4)
if (mode == KBKeyboardPanelModeSubscription) {
[self.contentView bringSubviewToFront:self.subscriptionView];
} else if (mode == KBKeyboardPanelModeSettings) {
[self.contentView bringSubviewToFront:self.settingView];
} else if (mode == KBKeyboardPanelModeChat) {
[self.contentView bringSubviewToFront:self.chatPanelView];
} else if (mode == KBKeyboardPanelModeFunction) {
@@ -157,17 +144,6 @@
}
}
/// / keyBoardMainView /
- (void)showSettingView:(BOOL)show {
if (show) {
[self kb_setPanelMode:KBKeyboardPanelModeSettings animated:YES];
return;
}
if (self.kb_panelMode == KBKeyboardPanelModeSettings) {
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
}
}
/// /
- (void)showChatPanel:(BOOL)show {
if (show) {
@@ -241,74 +217,6 @@
[self kb_updateKeyboardLayoutIfNeeded];
}
- (void)kb_setSettingViewVisible:(BOOL)visible animated:(BOOL)animated {
if (visible) {
KBSettingView *settingView = self.settingView;
if (!settingView.superview) {
settingView.hidden = YES;
[self.contentView addSubview:settingView];
[settingView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
[settingView.backButton addTarget:self
action:@selector(onTapSettingsBack)
forControlEvents:UIControlEventTouchUpInside];
}
[self.contentView bringSubviewToFront:settingView];
// keyBoardMainView self.view
[self.contentView layoutIfNeeded];
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
if (w <= 0) {
w = CGRectGetWidth(self.contentView.bounds);
}
if (w <= 0) {
w = [self kb_portraitWidth];
}
settingView.transform = CGAffineTransformMakeTranslation(w, 0);
settingView.hidden = NO;
if (animated) {
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
settingView.transform = CGAffineTransformIdentity;
}
completion:nil];
} else {
settingView.transform = CGAffineTransformIdentity;
}
} else {
KBSettingView *settingView = _settingView;
if (!settingView) {
return;
}
if (!settingView.superview || settingView.hidden) {
return;
}
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
if (w <= 0) {
w = CGRectGetWidth(self.contentView.bounds);
}
if (w <= 0) {
w = [self kb_portraitWidth];
}
if (animated) {
[UIView animateWithDuration:0.22
delay:0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
settingView.transform = CGAffineTransformMakeTranslation(w, 0);
}
completion:^(BOOL finished) {
settingView.hidden = YES;
}];
} else {
settingView.transform = CGAffineTransformMakeTranslation(w, 0);
settingView.hidden = YES;
}
}
}
- (void)kb_setSubscriptionPanelVisible:(BOOL)visible animated:(BOOL)animated {
if (visible) {
KBKeyboardSubscriptionView *panel = self.subscriptionView;
@@ -478,10 +386,6 @@
[_subscriptionView removeFromSuperview];
_subscriptionView = nil;
}
if (_settingView) {
[_settingView removeFromSuperview];
_settingView = nil;
}
}
// MARK: - KBKeyBoardMainViewDelegate
@@ -564,16 +468,6 @@
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
}
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView {
[[KBMaiPointReporter sharedReporter]
reportClickWithEventName:@"click_keyboard_settings_btn"
pageId:@"keyboard_main_panel"
elementId:@"settings_btn"
extra:nil
completion:nil];
[self kb_setPanelMode:KBKeyboardPanelModeSettings animated:YES];
}
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView
didSelectEmoji:(NSString *)emoji {
if (emoji.length == 0) {
@@ -596,17 +490,6 @@
[self kb_scheduleContextRefreshResetSuppression:YES];
}
- (void)keyBoardMainViewDidTapEmojiSearch:
(KBKeyBoardMainView *)keyBoardMainView {
// [[KBMaiPointReporter sharedReporter]
// reportClickWithEventName:@"click_keyboard_emoji_search_btn"
// pageId:@"keyboard_main_panel"
// elementId:@"emoji_search_btn"
// extra:nil
// completion:nil];
[KBHUD showInfo:KBLocalized(@"Search coming soon")];
}
// MARK: - KBFunctionViewDelegate
- (void)functionView:(KBFunctionView *)functionView
@@ -650,7 +533,7 @@
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD showInfo:@"当前应用不允许键盘直接唤起App请回到桌面手动打开App进行充值"];
[KBHUD showInfo:KBLocalized(@"This app does not allow the keyboard to open the main app directly. Please return to the Home screen and open the app manually to recharge")];
});
}];
}
@@ -659,16 +542,4 @@
[self showSubscriptionPanel];
}
#pragma mark - Actions
- (void)onTapSettingsBack {
[[KBMaiPointReporter sharedReporter]
reportClickWithEventName:@"click_keyboard_settings_back_btn"
pageId:@"keyboard_settings"
elementId:@"back_btn"
extra:nil
completion:nil];
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
}
@end

View File

@@ -15,7 +15,6 @@
@class KBFunctionView;
@class KBKeyBoardMainView;
@class KBKeyboardSubscriptionView;
@class KBSettingView;
@class KBSuggestionEngine;
@protocol KBChatLimitPopViewDelegate;
@@ -28,7 +27,6 @@ typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
KBKeyboardPanelModeMain = 0,
KBKeyboardPanelModeFunction,
KBKeyboardPanelModeChat,
KBKeyboardPanelModeSettings,
KBKeyboardPanelModeSubscription,
};
@@ -42,7 +40,6 @@ typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
UIView *_contentView;
KBKeyBoardMainView *_keyBoardMainView;
KBFunctionView *_functionView;
KBSettingView *_settingView;
UIImageView *_bgImageView;
KBChatPanelView *_chatPanelView;
KBKeyboardSubscriptionView *_subscriptionView;
@@ -79,7 +76,6 @@ typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
*keyBoardMainView; // 功能面板视图点击工具栏第0个时显示
@property(nonatomic, strong)
KBFunctionView *functionView; // 功能面板视图点击工具栏第0个时显示
@property(nonatomic, strong) KBSettingView *settingView; // 设置页
@property(nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
@property(nonatomic, strong) KBChatPanelView *chatPanelView;
@property(nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
@@ -119,7 +115,6 @@ typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
// Panels
- (void)showFunctionPanel:(BOOL)show;
- (void)showSettingView:(BOOL)show;
- (void)showChatPanel:(BOOL)show;
- (void)showSubscriptionPanel;
- (void)hideSubscriptionPanel;

View File

@@ -322,15 +322,14 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
NSLog(@"[Keyboard] skin request failed: %@",
error);
[KBHUD
showInfo:
KBLocalized(
@"皮肤资源准备失败,请稍后再试")];
showInfo:KBLocalized(
@"Theme resource preparation failed, please try again later")];
}
return;
}
[weakSelf kb_applyTheme];
[KBHUD showInfo:KBLocalized(
@"皮肤已更新,立即体验吧")];
@"Theme updated, try it now")];
}];
}

View File

@@ -12,7 +12,6 @@
#import "KBFunctionView.h"
#import "KBKeyBoardMainView.h"
#import "KBKeyboardSubscriptionView.h"
#import "KBSettingView.h"
#import "Masonry.h"
@implementation KeyboardViewController (UI)
@@ -115,13 +114,6 @@
return _functionView;
}
- (KBSettingView *)settingView {
if (!_settingView) {
_settingView = [[KBSettingView alloc] init];
}
return _settingView;
}
- (KBChatPanelView *)chatPanelView {
if (!_chatPanelView) {
NSLog(@"[Keyboard] ⚠️ chatPanelView 被创建!");

View File

@@ -34,10 +34,8 @@
// 说明:
// - `extensionContext openURL:` 是 Apple 官方推荐方式,但部分宿主 App尤其是“B 类应用”)
// 可能会拦截该调用,导致无法直接唤起容器 App
// - 行业内不少键盘会加“响应链 openURL”兜底来提升唤起成功率但该方式存在上架审核风险。
// 如你要走更稳妥的上架策略:把该宏改为 0仅保留 extensionContext 方案)。
#ifndef KB_URL_BRIDGE_ENABLE
// 上架建议Release 关闭“响应链 openURL”兜底避免审核风险。
#if DEBUG
#define KB_URL_BRIDGE_ENABLE 1
#else

View File

@@ -8,6 +8,19 @@
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeUserID</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeOtherUserContent</string>
@@ -20,6 +33,18 @@
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeProductInteraction</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>

View File

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

View File

@@ -12,7 +12,6 @@ NS_ASSUME_NONNULL_BEGIN
@protocol KBEmojiPanelViewDelegate <NSObject>
- (void)emojiPanelView:(KBEmojiPanelView *)panel didSelectEmoji:(NSString *)emoji;
- (void)emojiPanelViewDidRequestClose:(KBEmojiPanelView *)panel;
- (void)emojiPanelViewDidTapSearch:(KBEmojiPanelView *)panel;
@optional
- (void)emojiPanelViewDidTapDelete:(KBEmojiPanelView *)panel;
@end

View File

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

View File

@@ -21,17 +21,12 @@ NS_ASSUME_NONNULL_BEGIN
/// 需求:当 index == 0 时由外部KeyboardViewController决定是否切换到功能面板
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index;
/// 点击了右侧设置按钮
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView;
/// 点击了撤销删除按钮
- (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView;
/// emoji 视图里选择了一个表情
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectEmoji:(NSString *)emoji;
/// emoji 面板点击搜索
- (void)keyBoardMainViewDidTapEmojiSearch:(KBKeyBoardMainView *)keyBoardMainView;
/// 选择了联想词
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion;
@end

View File

@@ -183,12 +183,6 @@
}
}
- (void)toolBarDidTapSettings:(KBToolBar *)toolBar {
if ([self.delegate respondsToSelector:@selector(keyBoardMainViewDidTapSettings:)]) {
[self.delegate keyBoardMainViewDidTapSettings:self];
}
}
- (void)toolBarDidTapUndo:(KBToolBar *)toolBar {
if ([self.delegate respondsToSelector:@selector(keyBoardMainViewDidTapUndo:)]) {
[self.delegate keyBoardMainViewDidTapUndo:self];
@@ -261,12 +255,6 @@
[self setEmojiPanelVisible:NO animated:YES];
}
- (void)emojiPanelViewDidTapSearch:(KBEmojiPanelView *)panel {
if ([self.delegate respondsToSelector:@selector(keyBoardMainViewDidTapEmojiSearch:)]) {
[self.delegate keyBoardMainViewDidTapEmojiSearch:self];
}
}
- (void)emojiPanelViewDidTapDelete:(KBEmojiPanelView *)panel {
if ([self.delegate respondsToSelector:@selector(keyBoardMainView:didTapKey:)]) {
KBKey *backspace = [KBKey keyWithTitle:@"" type:KBKeyTypeBackspace];

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,6 @@
@interface KBToolBar ()
@property (nonatomic, strong) UIView *leftContainer;
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
//@property (nonatomic, strong) UIButton *settingsButtonInternal;
@property (nonatomic, strong) UIButton *globeButtonInternal; //
@property (nonatomic, strong) UIImageView *avatarImageView; // AppGroup persona_cover.jpg
@property (nonatomic, strong) UIButton *undoButtonInternal; //
@@ -65,10 +64,6 @@
return self.undoButtonInternal;
}
//- (UIButton *)settingsButton {
// return self.settingsButtonInternal;
//}
- (void)setLeftButtonTitles:(NSArray<NSString *> *)leftButtonTitles {
_leftButtonTitles = [leftButtonTitles copy];
// Update titles if buttons already exist
@@ -84,18 +79,10 @@
- (void)setupUI {
[self addSubview:self.leftContainer];
// [self addSubview:self.settingsButtonInternal];
[self addSubview:self.globeButtonInternal];
[self addSubview:self.undoButtonInternal];
[self addSubview:self.avatarImageView];
//
// [self.settingsButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
// make.right.equalTo(self.mas_right).offset(-12);
// make.centerY.equalTo(self.mas_centerY);
// make.width.height.mas_equalTo(32);
// }];
//
[self.globeButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.mas_left).offset(12);
@@ -313,12 +300,6 @@
}
}
- (void)onSettings {
if ([self.delegate respondsToSelector:@selector(toolBarDidTapSettings:)]) {
[self.delegate toolBarDidTapSettings:self];
}
}
- (void)onUndo {
if ([self.delegate respondsToSelector:@selector(toolBarDidTapUndo:)]) {
[self.delegate toolBarDidTapUndo:self];
@@ -362,19 +343,6 @@
return _avatarImageView;
}
//- (UIButton *)settingsButtonInternal {
// if (!_settingsButtonInternal) {
// _settingsButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
// _settingsButtonInternal.layer.cornerRadius = 16;
// _settingsButtonInternal.layer.masksToBounds = YES;
// _settingsButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
// [_settingsButtonInternal setTitle:@"⚙︎" forState:UIControlStateNormal]; // 齿
// [_settingsButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
// [_settingsButtonInternal addTarget:self action:@selector(onSettings) forControlEvents:UIControlEventTouchUpInside];
// }
// return _settingsButtonInternal;
//}
- (UIButton *)globeButtonInternal {
if (!_globeButtonInternal) {
_globeButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];

View File

@@ -76,15 +76,19 @@
// 法律文档 URL。
// 若未配置线上地址,主 App 会自动回退到内置 HTML 页面,避免出现空入口。
#ifndef KB_TERMS_OF_SERVICE_URL
#define KB_TERMS_OF_SERVICE_URL @""
#define KB_TERMS_OF_SERVICE_URL @"https://loveamorkey.com/agreement/user"
#endif
#ifndef KB_PRIVACY_POLICY_URL
#define KB_PRIVACY_POLICY_URL @""
#define KB_PRIVACY_POLICY_URL @"https://loveamorkey.com/agreement/privacy"
#endif
#ifndef KB_MEMBERSHIP_AGREEMENT_URL
#define KB_MEMBERSHIP_AGREEMENT_URL @""
#define KB_MEMBERSHIP_AGREEMENT_URL @"https://loveamorkey.com/agreement/vip"
#endif
#ifndef KB_AUTO_RENEWAL_AGREEMENT_URL
#define KB_AUTO_RENEWAL_AGREEMENT_URL @"https://loveamorkey.com/agreement/aotu"
#endif
#endif /* KBConfig_h */

View File

@@ -9,9 +9,7 @@
#if DEBUG
#define KB_MAI_POINT_BASE_URL @"http://192.168.2.21:35310/api"
#else
/// Release 默认关闭埋点上报(避免内网地址/HTTP 出现在上架包里)。
/// 线上如需开启,请在 Build SettingsPreprocessor Macros中覆盖该宏为 HTTPS 地址。
#define KB_MAI_POINT_BASE_URL @""
#define KB_MAI_POINT_BASE_URL @"https://track.loveamorkey.com/api"
#endif
#endif

View File

@@ -1,3 +1,3 @@
"NSMicrophoneUsageDescription" = "Microphone access is required for voice input.";
"NSMicrophoneUsageDescription" = "Microphone access is required for voice input and speech transcription.";
"NSPhotoLibraryUsageDescription" = "Photo library access is required to change your avatar.";
"NSPhotoLibraryAddUsageDescription" = "Photo library write access is required to save images.";

View File

@@ -85,7 +85,7 @@
// Network
"Network unavailable" = "Network unavailable";
"Network disabled (Full Access may be off)" = "Network disabled (Full Access may be off)";
"Network disabled (Full Access may be off)" = "Network-based keyboard features are unavailable (Full Access may be off)";
"Invalid URL" = "Invalid URL";
"Invalid response" = "Invalid response";
"No data" = "No data";
@@ -117,7 +117,7 @@
"Rank" = "Rank";
"Recharge Now" = "Recharge Now";
"By clicking Pay, you indicate your agreement to the" = "By clicking Pay, you indicate your agreement to the";
"《Embership Agreement》" = "《Embership Agreement";
"《Embership Agreement》" = "Membership Agreement";
// Mine
"Settings" = "Settings";
@@ -262,9 +262,16 @@
"AI Assistant" = "AI Assistant";
"AI Chat" = "AI Chat";
"Membership Agreement" = "《Embership Agreement";
"Membership Agreement" = "Membership Agreement";
"Download information missing" = "Download information missing";
"Download failed" = "Download failed";
"Theme resource preparation failed, please try again later" = "Theme resource preparation failed, please try again later";
"Theme updated, try it now" = "Theme updated, try it now";
"This app does not allow the keyboard to open the main app directly. Please return to the Home screen and open the app manually to recharge" = "This app does not allow the keyboard to open the main app directly. Please return to the Home screen and open the app manually to recharge";
"Content is empty" = "Content is empty";
"audioId is empty" = "audioId is empty";
"Polling failed after %ld retries" = "Polling failed after %ld retries";
"URL is empty" = "URL is empty";
"Theme information missing" = "Theme information missing";
"Delete failed, please try again" = "Delete failed, please try again";
"Processing..." = "Processing...";
@@ -292,8 +299,14 @@
"Voice-to-text failed, please try again" = "Voice-to-text failed, please try again";
"Please sign in before using AI features" = "Please sign in before using AI features";
"Please return to the Home screen and open the app to sign in" = "Please return to the Home screen and open the app to sign in";
"Please enable Full Access to continue" = "Please enable Full Access to continue";
"Please enable Full Access to continue" = "Please enable Full Access to use network-based keyboard features";
"Request failed" = "Request failed";
"Invalid data format" = "Invalid data format";
"Comment content cannot be empty" = "Comment content cannot be empty";
"Chat response is empty" = "Chat response is empty";
"Delete This Record" = "Delete This Record";
"Deleted" = "Deleted";
"Are you sure to delete?" = "Are you sure to delete?";
"Please enter content" = "Please enter content";
"Purchase failed" = "Purchase failed";
"Remote skin" = "Remote skin";
@@ -363,3 +376,26 @@
"I highly recommend this app." = "I highly recommend this app.";
"Allow log in with Apple ID?" = "Allow log in with Apple ID?";
"Continue" = "Continue";
"Me" = "Me";
"No comments yet" = "No comments yet";
"Be the first to comment" = "Be the first to comment";
"Load failed" = "Load failed";
"Tap to retry" = "Tap to retry";
"%.1fw comments" = "%.1fw comments";
"%ld comments" = "%ld comments";
"Reply to @%@" = "Reply to @%@";
"Say something..." = "Say something...";
"Hold To Start Talking" = "Hold To Start Talking";
"Hold To Speak" = "Hold To Speak";
"Release To Finish" = "Release To Finish";
"Send A Message To Her" = "Send A Message To Her";
"Like" = "Like";
"Reply" = "Reply";
"View %ld replies" = "View %ld replies";
"View more replies (%ld)" = "View more replies (%ld)";
"Collapse" = "Collapse";
"Just now" = "Just now";
"%.0f minutes ago" = "%.0f minutes ago";
"%.0f hours ago" = "%.0f hours ago";
"%.0f days ago" = "%.0f days ago";
"Yesterday" = "Yesterday";

View File

@@ -1,3 +1,3 @@
"NSMicrophoneUsageDescription" = "Se requiere acceso al micrófono para la entrada por voz.";
"NSMicrophoneUsageDescription" = "Se requiere acceso al micrófono para la entrada por voz y la transcripción del habla.";
"NSPhotoLibraryUsageDescription" = "Se requiere acceso a la fototeca para cambiar tu avatar.";
"NSPhotoLibraryAddUsageDescription" = "Se requiere permiso de escritura en la fototeca para guardar imágenes.";

View File

@@ -85,7 +85,7 @@
// Network
"Network unavailable" = "Red no disponible";
"Network disabled (Full Access may be off)" = "Red desactivada (el acceso total puede estar desactivado)";
"Network disabled (Full Access may be off)" = "Las funciones de red del teclado no están disponibles (el acceso total puede estar desactivado)";
"Invalid URL" = "URL no válida";
"Invalid response" = "Respuesta no válida";
"No data" = "Sin datos";
@@ -273,9 +273,16 @@
"AI Chat" = "AI Chat";
"Membership Agreement" = "《Acuerdo de membresía》";
"Download information missing" = "Download information missing";
"Download failed" = "Download failed";
"Download failed" = "Error al descargar";
"Theme resource preparation failed, please try again later" = "No se pudieron preparar los recursos del tema. Inténtalo de nuevo más tarde";
"Theme updated, try it now" = "Tema actualizado. Pruébalo ahora";
"This app does not allow the keyboard to open the main app directly. Please return to the Home screen and open the app manually to recharge" = "Esta app no permite que el teclado abra la app principal directamente. Vuelve a la pantalla de inicio y abre la app manualmente para recargar";
"Content is empty" = "El contenido está vacío";
"audioId is empty" = "audioId está vacío";
"Polling failed after %ld retries" = "La consulta falló tras %ld reintentos";
"URL is empty" = "La URL está vacía";
"Theme information missing" = "Theme information missing";
"Delete failed, please try again" = "Delete failed, please try again";
"Delete failed, please try again" = "La eliminación falló, inténtalo de nuevo";
"Processing..." = "Processing...";
"Copied" = "Copied";
"Default keyboard skin restored" = "Default keyboard skin restored";
@@ -301,8 +308,14 @@
"Voice-to-text failed, please try again" = "Voice-to-text failed, please try again";
"Please sign in before using AI features" = "Please sign in before using AI features";
"Please return to the Home screen and open the app to sign in" = "Please return to the Home screen and open the app to sign in";
"Please enable Full Access to continue" = "Please enable Full Access to continue";
"Request failed" = "Request failed";
"Please enable Full Access to continue" = "Activa \"Permitir acceso total\" para usar las funciones de red del teclado";
"Request failed" = "La solicitud falló";
"Invalid data format" = "Formato de datos no válido";
"Comment content cannot be empty" = "El contenido del comentario no puede estar vacío";
"Chat response is empty" = "La respuesta del chat está vacía";
"Delete This Record" = "Eliminar este registro";
"Deleted" = "Eliminado";
"Are you sure to delete?" = "¿Seguro que quieres eliminarlo?";
"Please enter content" = "Please enter content";
"Purchase failed" = "Purchase failed";
"Remote skin" = "Remote skin";
@@ -372,3 +385,26 @@
"I highly recommend this app." = "Recomiendo mucho esta aplicación.";
"Allow log in with Apple ID?" = "¿Permitir iniciar sesión con Apple ID?";
"Continue" = "Continuar";
"Me" = "Yo";
"No comments yet" = "Aún no hay comentarios";
"Be the first to comment" = "Sé el primero en comentar";
"Load failed" = "Error al cargar";
"Tap to retry" = "Toca para reintentar";
"%.1fw comments" = "%.1fw comentarios";
"%ld comments" = "%ld comentarios";
"Reply to @%@" = "Responder a @%@";
"Say something..." = "Escribe algo...";
"Hold To Start Talking" = "Mantén pulsado para empezar a hablar";
"Hold To Speak" = "Mantén pulsado para hablar";
"Release To Finish" = "Suelta para terminar";
"Send A Message To Her" = "Enviarle un mensaje";
"Like" = "Me gusta";
"Reply" = "Responder";
"View %ld replies" = "Ver %ld respuestas";
"View more replies (%ld)" = "Ver más respuestas (%ld)";
"Collapse" = "Ocultar";
"Just now" = "Justo ahora";
"%.0f minutes ago" = "Hace %.0f minutos";
"%.0f hours ago" = "Hace %.0f horas";
"%.0f days ago" = "Hace %.0f días";
"Yesterday" = "Ayer";

View File

@@ -1,3 +1,3 @@
"NSMicrophoneUsageDescription" = "Akses mikrofon diperlukan untuk input suara.";
"NSMicrophoneUsageDescription" = "Akses mikrofon diperlukan untuk input suara dan transkripsi ucapan.";
"NSPhotoLibraryUsageDescription" = "Akses galeri foto diperlukan untuk mengganti avatar Anda.";
"NSPhotoLibraryAddUsageDescription" = "Izin menulis ke galeri foto diperlukan untuk menyimpan gambar.";

View File

@@ -85,7 +85,7 @@
// Network
"Network unavailable" = "Jaringan tidak tersedia";
"Network disabled (Full Access may be off)" = "Jaringan dinonaktifkan (Akses Penuh mungkin nonaktif)";
"Network disabled (Full Access may be off)" = "Fitur keyboard berbasis jaringan tidak tersedia (Akses Penuh mungkin nonaktif)";
"Invalid URL" = "URL tidak valid";
"Invalid response" = "Respons tidak valid";
"No data" = "Tidak ada data";
@@ -274,9 +274,16 @@
"AI Chat" = "AI Chat";
"Membership Agreement" = "《Perjanjian Keanggotaan》";
"Download information missing" = "Download information missing";
"Download failed" = "Download failed";
"Download failed" = "Gagal mengunduh";
"Theme resource preparation failed, please try again later" = "Persiapan sumber daya tema gagal, silakan coba lagi nanti";
"Theme updated, try it now" = "Tema diperbarui, coba sekarang";
"This app does not allow the keyboard to open the main app directly. Please return to the Home screen and open the app manually to recharge" = "Aplikasi ini tidak mengizinkan keyboard membuka aplikasi utama secara langsung. Silakan kembali ke layar utama dan buka aplikasi secara manual untuk mengisi ulang";
"Content is empty" = "Konten kosong";
"audioId is empty" = "audioId kosong";
"Polling failed after %ld retries" = "Polling gagal setelah %ld kali percobaan ulang";
"URL is empty" = "URL kosong";
"Theme information missing" = "Theme information missing";
"Delete failed, please try again" = "Delete failed, please try again";
"Delete failed, please try again" = "Gagal menghapus, silakan coba lagi";
"Processing..." = "Processing...";
"Copied" = "Copied";
"Default keyboard skin restored" = "Default keyboard skin restored";
@@ -302,8 +309,14 @@
"Voice-to-text failed, please try again" = "Voice-to-text failed, please try again";
"Please sign in before using AI features" = "Please sign in before using AI features";
"Please return to the Home screen and open the app to sign in" = "Please return to the Home screen and open the app to sign in";
"Please enable Full Access to continue" = "Please enable Full Access to continue";
"Request failed" = "Request failed";
"Please enable Full Access to continue" = "Aktifkan Akses Penuh untuk menggunakan fitur keyboard berbasis jaringan";
"Request failed" = "Permintaan gagal";
"Invalid data format" = "Format data tidak valid";
"Comment content cannot be empty" = "Konten komentar tidak boleh kosong";
"Chat response is empty" = "Respons chat kosong";
"Delete This Record" = "Hapus riwayat ini";
"Deleted" = "Berhasil dihapus";
"Are you sure to delete?" = "Yakin ingin menghapus?";
"Please enter content" = "Please enter content";
"Purchase failed" = "Purchase failed";
"Remote skin" = "Remote skin";
@@ -373,3 +386,26 @@
"I highly recommend this app." = "Saya sangat merekomendasikan aplikasi ini.";
"Allow log in with Apple ID?" = "Izinkan masuk dengan Apple ID?";
"Continue" = "Lanjutkan";
"Me" = "Saya";
"No comments yet" = "Belum ada komentar";
"Be the first to comment" = "Jadilah yang pertama berkomentar";
"Load failed" = "Gagal memuat";
"Tap to retry" = "Ketuk untuk mencoba lagi";
"%.1fw comments" = "%.1fw komentar";
"%ld comments" = "%ld komentar";
"Reply to @%@" = "Balas @%@";
"Say something..." = "Tulis sesuatu...";
"Hold To Start Talking" = "Tahan untuk mulai berbicara";
"Hold To Speak" = "Tahan untuk berbicara";
"Release To Finish" = "Lepas untuk selesai";
"Send A Message To Her" = "Kirim pesan kepadanya";
"Like" = "Suka";
"Reply" = "Balas";
"View %ld replies" = "Lihat %ld balasan";
"View more replies (%ld)" = "Lihat lebih banyak balasan (%ld)";
"Collapse" = "Ciutkan";
"Just now" = "Baru saja";
"%.0f minutes ago" = "%.0f menit yang lalu";
"%.0f hours ago" = "%.0f jam yang lalu";
"%.0f days ago" = "%.0f hari yang lalu";
"Yesterday" = "Kemarin";

View File

@@ -1,3 +1,3 @@
"NSMicrophoneUsageDescription" = "O acesso ao microfone é necessário para entrada por voz.";
"NSMicrophoneUsageDescription" = "O acesso ao microfone é necessário para entrada por voz e transcrição de voz.";
"NSPhotoLibraryUsageDescription" = "O acesso à fototeca é necessário para alterar o seu avatar.";
"NSPhotoLibraryAddUsageDescription" = "A permissão de escrita na fototeca é necessária para guardar imagens.";

View File

@@ -85,7 +85,7 @@
// Network
"Network unavailable" = "Rede indisponível";
"Network disabled (Full Access may be off)" = "Rede desativada (o Acesso Total pode estar desativado)";
"Network disabled (Full Access may be off)" = "As funcionalidades de rede do teclado não estão disponíveis (o Acesso Total pode estar desativado)";
"Invalid URL" = "URL inválido";
"Invalid response" = "Resposta inválida";
"No data" = "Sem dados";
@@ -274,9 +274,16 @@
"AI Chat" = "AI Chat";
"Membership Agreement" = "《Acordo de Associação》";
"Download information missing" = "Download information missing";
"Download failed" = "Download failed";
"Download failed" = "Falha ao transferir";
"Theme resource preparation failed, please try again later" = "Falha ao preparar os recursos do tema. Tente novamente mais tarde";
"Theme updated, try it now" = "Tema atualizado. Experimente agora";
"This app does not allow the keyboard to open the main app directly. Please return to the Home screen and open the app manually to recharge" = "Esta app não permite que o teclado abra diretamente a app principal. Volte ao ecrã principal e abra a app manualmente para recarregar";
"Content is empty" = "O conteúdo está vazio";
"audioId is empty" = "O audioId está vazio";
"Polling failed after %ld retries" = "A sondagem falhou após %ld tentativas";
"URL is empty" = "O URL está vazio";
"Theme information missing" = "Theme information missing";
"Delete failed, please try again" = "Delete failed, please try again";
"Delete failed, please try again" = "Falha ao eliminar, tente novamente";
"Processing..." = "Processing...";
"Copied" = "Copied";
"Default keyboard skin restored" = "Default keyboard skin restored";
@@ -302,8 +309,14 @@
"Voice-to-text failed, please try again" = "Voice-to-text failed, please try again";
"Please sign in before using AI features" = "Please sign in before using AI features";
"Please return to the Home screen and open the app to sign in" = "Please return to the Home screen and open the app to sign in";
"Please enable Full Access to continue" = "Please enable Full Access to continue";
"Request failed" = "Request failed";
"Please enable Full Access to continue" = "Ative o Acesso Total para usar as funcionalidades de rede do teclado";
"Request failed" = "O pedido falhou";
"Invalid data format" = "Formato de dados inválido";
"Comment content cannot be empty" = "O conteúdo do comentário não pode estar vazio";
"Chat response is empty" = "A resposta do chat está vazia";
"Delete This Record" = "Eliminar este registo";
"Deleted" = "Eliminado";
"Are you sure to delete?" = "Tem a certeza de que quer eliminar?";
"Please enter content" = "Please enter content";
"Purchase failed" = "Purchase failed";
"Remote skin" = "Remote skin";
@@ -373,3 +386,26 @@
"I highly recommend this app." = "Recomendo muito esta aplicação.";
"Allow log in with Apple ID?" = "Permitir iniciar sessão com o Apple ID?";
"Continue" = "Continuar";
"Me" = "Eu";
"No comments yet" = "Ainda não há comentários";
"Be the first to comment" = "Seja o primeiro a comentar";
"Load failed" = "Falha ao carregar";
"Tap to retry" = "Toque para tentar novamente";
"%.1fw comments" = "%.1fw comentários";
"%ld comments" = "%ld comentários";
"Reply to @%@" = "Responder a @%@";
"Say something..." = "Diga algo...";
"Hold To Start Talking" = "Mantenha premido para começar a falar";
"Hold To Speak" = "Mantenha premido para falar";
"Release To Finish" = "Solte para terminar";
"Send A Message To Her" = "Enviar-lhe uma mensagem";
"Like" = "Gosto";
"Reply" = "Responder";
"View %ld replies" = "Ver %ld respostas";
"View more replies (%ld)" = "Ver mais respostas (%ld)";
"Collapse" = "Recolher";
"Just now" = "Agora mesmo";
"%.0f minutes ago" = "Há %.0f minutos";
"%.0f hours ago" = "Há %.0f horas";
"%.0f days ago" = "Há %.0f dias";
"Yesterday" = "Ontem";

View File

@@ -1,3 +1,3 @@
"NSMicrophoneUsageDescription" = "需要使用麥克風進行語音輸入。";
"NSMicrophoneUsageDescription" = "需要使用麥克風進行語音輸入與語音轉寫。";
"NSPhotoLibraryUsageDescription" = "更換頭像需要存取你的相簿。";
"NSPhotoLibraryAddUsageDescription" = "儲存圖片需要寫入你的相簿。";

View File

@@ -85,7 +85,7 @@
// Network
"Network unavailable" = "網路不可用";
"Network disabled (Full Access may be off)" = "網路未啟用(可能未開啟完整存取)";
"Network disabled (Full Access may be off)" = "鍵盤的網路功能無法使用(可能未開啟完整存取)";
"Invalid URL" = "無效的 URL";
"Invalid response" = "無效的回應";
"No data" = "暫無資料";
@@ -273,9 +273,16 @@
"AI Chat" = "AI Chat";
"Membership Agreement" = "《會員協議》";
"Download information missing" = "Download information missing";
"Download failed" = "Download failed";
"Download failed" = "下載失敗";
"Theme resource preparation failed, please try again later" = "主題資源準備失敗,請稍後再試";
"Theme updated, try it now" = "主題已更新,立即體驗吧";
"This app does not allow the keyboard to open the main app directly. Please return to the Home screen and open the app manually to recharge" = "目前這個 App 不允許鍵盤直接喚起主 App請回到主畫面手動開啟 App 進行儲值";
"Content is empty" = "內容為空";
"audioId is empty" = "audioId 為空";
"Polling failed after %ld retries" = "輪詢失敗,已重試 %ld 次";
"URL is empty" = "URL 為空";
"Theme information missing" = "Theme information missing";
"Delete failed, please try again" = "Delete failed, please try again";
"Delete failed, please try again" = "刪除失敗,請稍後再試";
"Processing..." = "Processing...";
"Copied" = "Copied";
"Default keyboard skin restored" = "Default keyboard skin restored";
@@ -301,8 +308,14 @@
"Voice-to-text failed, please try again" = "Voice-to-text failed, please try again";
"Please sign in before using AI features" = "Please sign in before using AI features";
"Please return to the Home screen and open the app to sign in" = "Please return to the Home screen and open the app to sign in";
"Please enable Full Access to continue" = "Please enable Full Access to continue";
"Request failed" = "Request failed";
"Please enable Full Access to continue" = "請先開啟完整存取,以使用鍵盤的網路功能";
"Request failed" = "請求失敗";
"Invalid data format" = "資料格式錯誤";
"Comment content cannot be empty" = "評論內容不能為空";
"Chat response is empty" = "聊天回應為空";
"Delete This Record" = "刪除此記錄";
"Deleted" = "已刪除";
"Are you sure to delete?" = "確定要刪除嗎?";
"Please enter content" = "Please enter content";
"Purchase failed" = "Purchase failed";
"Remote skin" = "Remote skin";
@@ -372,3 +385,26 @@
"I highly recommend this app." = "我非常推薦這個 App。";
"Allow log in with Apple ID?" = "允許使用 Apple ID 登入嗎?";
"Continue" = "繼續";
"Me" = "我";
"No comments yet" = "暫無評論";
"Be the first to comment" = "快來搶沙發";
"Load failed" = "載入失敗";
"Tap to retry" = "點擊重試";
"%.1fw comments" = "%.1f萬則評論";
"%ld comments" = "%ld則評論";
"Reply to @%@" = "回覆 @%@";
"Say something..." = "說點什麼...";
"Hold To Start Talking" = "按住開始說話";
"Hold To Speak" = "按住說話";
"Release To Finish" = "鬆開結束";
"Send A Message To Her" = "發送訊息給她";
"Like" = "讚";
"Reply" = "回覆";
"View %ld replies" = "展開%ld則回覆";
"View more replies (%ld)" = "展開更多回覆(%ld則";
"Collapse" = "收起";
"Just now" = "剛剛";
"%.0f minutes ago" = "%.0f分鐘前";
"%.0f hours ago" = "%.0f小時前";
"%.0f days ago" = "%.0f天前";
"Yesterday" = "昨天";

View File

@@ -0,0 +1,395 @@
# KeyBoard 提审最终检查清单
更新时间2026-03-08
适用目标2026-03-09 准备提交 Apple App Store 审核
## 一、当前结论
基于当前代码、工程配置以及 Apple 截至 2026-03-08 的公开规则,这个项目现在不是“不能上”,而是仍然存在几项会明显拉低一次过审率的阻塞点。
如果今天不补,最可能的拒审方向仍然是:
1. `2.1 App Completeness`
2. `5.1.1 Data Collection and Storage`
3. `2.3.1 Accurate Metadata`
4. `1.2 User-Generated Content`
5. `3.1.2` / 订阅展示不完整
## 二、当前发现的问题
下面按严重程度排序。
### P0法律文档仍是“未发布正式版”的回退状态
证据:
1. [KBConfig.h](/Users/mac/Desktop/项目/公司/KeyBoard/Shared/KBConfig.h#L76) 中 3 个正式 URL 仍然是空字符串:
- `KB_TERMS_OF_SERVICE_URL`
- `KB_PRIVACY_POLICY_URL`
- `KB_MEMBERSHIP_AGREEMENT_URL`
2. [KBWebViewViewController.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/WebView/KBWebViewViewController.m#L273) 到 [KBWebViewViewController.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/WebView/KBWebViewViewController.m#L284) 的内置 HTML 明确写着:
- `Replace this fallback page ... before App Store submission`
- `Built-in fallback document. Configure the final public URL ...`
风险:
1. 审核员点进协议/隐私/会员协议页面后,看到的是明显占位性质的回退文案。
2. 这会直接伤到:
- `2.1 App Completeness`
- `2.3.1 Accurate Metadata`
- `5.1.1`
解决方案:
1. 今天必须把 3 个 URL 换成正式线上地址。
2. 正式页面必须可公开访问,不需要登录,不是占位页。
3. 页面正文必须和真实数据流、App Privacy、Review Notes 完全一致。
结论:
这是当前最明确、最直接的提审阻塞项。
### P0隐私披露与当前代码能力相比明显不完整
证据:
1. 主 App 的 [PrivacyInfo.xcprivacy](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/PrivacyInfo.xcprivacy#L9) 目前只声明:
- `EmailAddress`
- `UserID`
- `OtherUserContent`
2. 键盘扩展的 [PrivacyInfo.xcprivacy](/Users/mac/Desktop/项目/公司/KeyBoard/CustomKeyboard/PrivacyInfo.xcprivacy#L9) 目前只声明:
- `OtherUserContent`
3. 但代码中已经能确认至少还涉及:
- 语音录音上传转写
[KBAIHomeVC.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m#L1238)
[AiVM.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/AiTalk/VM/AiVM.m#L271)
- 头像上传
[KBMyVM.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/Me/VM/KBMyVM.m#L362)
- 购买记录 / 钱包流水
[KBMyVM.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/Me/VM/KBMyVM.m#L220)
- Release 下 Bugly 崩溃收集
[AppDelegate.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/AppDelegate.m#L93)
- 可配置的埋点上报器
[KBMaiPointReporter.h](/Users/mac/Desktop/项目/公司/KeyBoard/Shared/KBMaiPointReporter.h)
风险:
1. 代码实际行为、隐私政策、App Store Connect `App Privacy` 如果不一致,会非常容易被 `5.1.1` 打回。
2. 对你这种“自定义键盘 + Full Access + AI + 语音”的组合,这一项是审核重点。
解决方案:
1. 今天必须做一张内部数据流表,确认以下数据到底是否收集、是否上传、是否保存、是否关联账号:
- `Audio Data`
- `Other User Content`
- `Photos or Videos`
- `Purchase History`
- `Crash Data`
- `Product Interaction`(如果 Release 真开埋点)
2. 用这张表同时更新:
- 隐私政策
- App Store Connect `App Privacy`
- 必要时更新 `PrivacyInfo.xcprivacy`
3. 如果你拿不准 Bugly 和埋点,就先按“更保守的披露口径”处理。
结论:
这是当前第二个明确阻塞项,而且是被 Apple 追问概率非常高的一项。
### P0键盘 `Full Access` 的用户可见披露仍然太泛
证据:
1. 键盘扩展开启了 `RequestsOpenAccess = true`
[CustomKeyboard/Info.plist](/Users/mac/Desktop/项目/公司/KeyBoard/CustomKeyboard/Info.plist#L21)
2. 当前引导文案还是:
`Turn on Allow Full Access to experience all features`
[KBFullAccessGuideView.m](/Users/mac/Desktop/项目/公司/KeyBoard/CustomKeyboard/View/KBFullAccessGuideView.m#L50)
3. 但代码里已经存在:
- 联网 AI 功能
- 登录态依赖
- 语音相关功能
- 订阅能力
风险:
1. Apple 对第三方键盘的 `Full Access` 审核比普通 App 更敏感。
2. 如果你只是告诉用户“开权限体验全部功能”,但没说清楚会发生什么,审核员会继续追问:
- 开了 Full Access 后会不会上传输入内容
- 哪些功能必须依赖 Full Access
- 语音会不会上传服务器
解决方案:
1. App 内用户可见文案要补充说明:
- 只有主动使用 AI / 语音 / 账号 / 订阅等联网能力时,相关数据才可能发送到服务器处理
2. 隐私政策也要写同样的口径。
3. Review Notes 必须单独解释:
- 为什么键盘需要 Full Access
- 哪些功能离不开 Full Access
- 不开启时会看到什么
结论:
这项如果不补清楚,即使代码能跑,也很容易被当成隐私说明不足。
### P1语音链路已经是“上传转写”不能再按“仅本地语音输入”口径描述
证据:
1. 录音结束后会调用转写接口:
[KBAIHomeVC.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m#L1238)
2. `AiVM` 里明确存在上传音频文件并转写:
[AiVM.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/AiTalk/VM/AiVM.m#L226)
[AiVM.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/AiTalk/VM/AiVM.m#L271)
3. 主 App 和扩展都声明了麦克风权限:
[Info.plist](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Info.plist#L23)
[Info.plist](/Users/mac/Desktop/项目/公司/KeyBoard/CustomKeyboard/Info.plist#L9)
风险:
1. 如果你后台和隐私政策没有按 `Audio Data` 相关能力申报,这会构成明显不一致。
2. 现在权限默认文案已经改成更准确的英文,但这还不够;审核员更看重“你有没有明确说明上传转写”。
解决方案:
1. 隐私政策里明确写:
- 语音是否上传
- 是否只用于转写
- 是否保存原始音频
- 是否用于训练
2. App Store Connect `App Privacy` 重新核对 `Audio Data`
3. Review Notes 中说明“语音输入会调用服务端转写”
### P1AI 内容安全有举报,但缺少可见的前置审核/拦截证据
证据:
1. 举报入口是存在的:
[AIReportVC.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/AiTalk/VC/AIReportVC.m#L467)
2. 举报接口也存在:
[AiVM.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/AiTalk/VM/AiVM.m#L870)
3. 但从当前代码里,没有看到足够明确的 AI 输出前置审核或策略拦截证据。
风险:
1. Apple 对 AI / 聊天 / persona 这类内容,会从 `1.1``1.2` 的角度看你是否有治理能力。
2. 只有举报,没有审核/拦截说明,通常不够稳。
解决方案:
1. 你需要和后端确认:是否存在服务端内容审核。
2. 如果有Review Notes 里写清楚:
- 有内容过滤
- 有举报
- 有处理机制
3. 如果没有,建议先补服务端审核兜底再提。
结论:
这是“中高风险但代码里不一定能完全解决”的问题,取决于你后端现在是否已经有审核。
### P1Review Notes、测试账号、审核路径说明无法从工程内验证
风险:
1. 这是键盘类 App 的典型拒审来源,不在代码里,但对过审非常关键。
2. 审核员如果没走对路径,会直接判:
- 功能不可用
- 无法完成登录
- 无法完成购买
解决方案:
1. 你必须在 App Store Connect 手工准备:
- 测试账号
- 启用键盘步骤
- 开启 Full Access 步骤
- 登录步骤
- AI 使用路径
- 购买和恢复购买路径
- 哪些操作会拉起主 App
结论:
这项虽然不是代码 bug但如果你明天就提这是必须完成的外部阻塞项。
## 三、当前已具备的正向项
这些不是问题,属于可加分或至少不拖后腿的项:
1. 账号注销能力存在
[KBCancelAccountVC.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/Me/VC/KBCancelAccountVC.m#L82)
[KBMyVM.m](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/Me/VM/KBMyVM.m#L519)
2. 订阅管理能力存在
[StoreKitService.swift](/Users/mac/Desktop/项目/公司/KeyBoard/keyBoard/Class/Pay/StoreKit2Manager/Internal/StoreKitService.swift#L823)
3. 协议入口已经打通,不再是空实现
4. 麦克风权限 fallback 文案已经统一成准确英文
5. AI 举报链路存在
## 四、明天前必须完成的清单
下面这部分按“必须完成 / 最好完成”分。
### A. 必须完成
#### A1. 正式法律文档上线
- [ ] 填写 `KB_TERMS_OF_SERVICE_URL`
- [ ] 填写 `KB_PRIVACY_POLICY_URL`
- [ ] 填写 `KB_MEMBERSHIP_AGREEMENT_URL`
- [ ] 真机点开 3 个页面,确认不是 fallback HTML
- [ ] 浏览器外部访问 3 个 URL确认审核员在外部也能打开
#### A2. 隐私披露一致化
- [ ] 确认 `Audio Data` 是否收集、是否保存
- [ ] 确认键盘 AI 输入文本是否会上传、是否保存
- [ ] 确认头像上传是否需要申报 `Photos or Videos`
- [ ] 确认购买记录 / 订阅状态是否申报 `Purchase History`
- [ ] 确认 Release 下 Bugly 是否视为 `Crash Data`
- [ ] 如果 Release 开启埋点,确认是否申报 `Product Interaction`
- [ ] 完成 App Store Connect `App Privacy` 填写
- [ ] 确保隐私政策正文和后台填写一致
#### A3. Full Access 和语音说明补清楚
- [ ] 在 App 内明确说明 Full Access 与联网功能关系
- [ ] 明确说明语音会进行服务器转写
- [ ] Review Notes 解释 Full Access 的必要性和作用边界
#### A4. Review Notes 和测试账号准备完成
- [ ] 提供审核账号
- [ ] 写清启用键盘路径
- [ ] 写清打开 Full Access 路径
- [ ] 写清登录路径
- [ ] 写清购买路径
- [ ] 写清恢复购买路径
- [ ] 写清哪些操作会拉起主 App
### B. 最好今天也完成
- [ ] 后端确认 AI 内容审核是否已开启
- [ ] Review Notes 里写明有举报和内容治理机制
- [ ] 检查商店描述里是否有夸大键盘能力的文案
- [ ] 核对订阅名称、周期、价格、试用和 App Store Connect 一致
## 五、今晚的检查方案
建议按下面顺序做,不要乱跳。
### 第 1 轮:配置检查
目标:先排除“点进去就是错的”问题。
步骤:
1. 检查 3 个法律文档 URL 是否已经填正式地址
2. 检查主 App 和扩展的 `Info.plist` 权限文案
3. 检查 `PrivacyInfo.xcprivacy`
4. 检查 Release 配置下是否保留 Bugly / 埋点
通过标准:
1. 没有 fallback URL
2. 没有 placeholder 文案
3. 没有明显和真实行为冲突的隐私声明
### 第 2 轮:真机功能链路检查
目标:按审核员路径完整走一遍。
步骤:
1. 卸载旧包,重新安装
2. 首次启动
3. 注册 / 登录
4. 启用键盘
5. 打开 Full Access
6. 进入任意聊天/输入场景,切换到你的键盘
7. 触发 AI 功能
8. 触发语音功能
9. 从键盘拉起主 App 登录
10. 从键盘拉起主 App 购买
11. 恢复购买
12. 打开隐私政策 / 服务条款 / 会员协议
13. 注销账号
通过标准:
1. 整条路径没有卡死、空白页、无响应
2. 所有异常都有用户能理解的提示
### 第 3 轮:异常场景检查
目标:避免审核员踩到异常直接拒审。
步骤:
1. 未登录时进 AI
2. 未开 Full Access 时进联网能力
3. 断网时使用 AI / 购买 / 登录
4. 服务端返回错误时看提示是否明确
5. IAP 商品加载失败时看是否有兜底
通过标准:
1. 不出现无提示失败
2. 不出现 reviewer 无法理解的状态
### 第 4 轮:提审物料检查
目标:把代码外的审核材料准备齐。
步骤:
1. 完成 App Store Connect `App Privacy`
2. 准备 Review Notes
3. 准备测试账号
4. 检查商店截图和描述
5. 检查订阅商品配置
通过标准:
1. 审核员不需要猜你产品流程
2. 元数据和真实能力一致
## 六、明天提审当天的执行顺序
建议顺序:
1. 先确认线上法律文档已经发布
2. 再核对 App Store Connect 的 `App Privacy`
3. 再写 Review Notes
4. 再做一次真机完整走查
5. 最后 Archive / 上传 / 提交审核
不要反过来先提交再补文案。
## 七、我给你的最终判断
如果你明天提审之前把下面 4 件事补完:
1. 正式法律文档 URL 与正文
2. App Privacy 与隐私政策完全对齐
3. Review Notes + 测试账号
4. Full Access / 语音上传 / AI 内容安全说明
这次的送审质量会比你现在直接提交高很多。
如果这 4 件事中有 2 件以上没完成,我不建议明天直接提。
## 八、官方参考
1. App Review Guidelines
https://developer.apple.com/app-store/review/guidelines/
2. App Privacy Details
https://developer.apple.com/app-store/app-privacy-details/
3. App PrivacyApp Store Connect Help
https://developer.apple.com/help/app-store-connect/reference/app-privacy
4. Offering account deletion in your app
https://developer.apple.com/support/offering-account-deletion-in-your-app/
5. Custom Keyboard Guide
https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/CustomKeyboard.html

View File

@@ -0,0 +1,103 @@
Key of Love会员服务协议
版本生效日期2026年3月9日
欢迎您使用Key of Love会员服务。
本《Key of Love会员服务协议》以下简称“本协议”是您与Niu Ge以下简称“我们”之间就您购买、开通、使用Key of Love会员服务所订立的协议。
在您购买、开通或使用会员服务前请您务必认真阅读本协议、《Key of Love用户协议》、《Key of Love隐私政策》以及页面展示的相关规则。您点击“开通”“支付”“订阅”“继续”或完成购买并使用会员服务的即视为您已阅读、理解并同意接受本协议全部内容。
一、会员服务说明
1. Key of Love会员服务是我们向符合条件的用户提供的付费数字服务。会员服务可能包括但不限于AI相关高级能力、会员专属功能、主题素材、专属权益、订阅优惠或其他页面展示的付费权益。
2. 会员名称、权益范围、展示方式、可用期限、适用设备和功能边界可能因产品版本、地区、活动、系统限制、权限状态或业务调整而有所不同,具体以购买页面、订阅页及实际开通后的页面展示为准。
3. 由于Key of Love包含自定义键盘能力部分会员相关功能可能受系统限制、网络状态、账号状态、“允许完全访问Allow Full Access”状态及主App/键盘扩展交互路径影响。部分操作可能需要拉起主App完成。
二、开通方式、服务期限与价格
1. 您可通过我们提供的页面和Apple App Store内购渠道购买会员服务。会员服务的价格、计费周期、试用期、结算货币、展示名称和续费规则以购买页面和Apple结算页面展示为准。
2. 会员服务期限以您成功购买并经系统确认生效的期限为准。到期后,如您未再次购买或未触发自动续费,则相应会员权益终止。
3. 我们有权基于业务调整、市场变化、成本变化、活动安排或法律法规要求,对后续新订单的会员价格、权益、活动规则进行调整。已生效订单在已购买期限内通常不受影响,法律法规或页面另有说明的除外。
三、会员权益
1. 会员权益以实际购买页面和服务页面展示为准。我们可能根据产品运营需要,对会员权益进行增加、减少、升级、优化、替换或调整。
2. 您理解并同意,部分权益的使用可能受以下因素影响:
1系统版本、设备兼容性或地区差异
2网络连接状态
3是否登录账号
4是否已启用键盘及是否开启“允许完全访问”
5第三方平台能力限制、Apple系统限制或支付状态
6服务维护、故障修复、合规要求或安全策略。
3. 会员权益仅限您本人在符合本协议及相关规则的前提下使用,不得转让、出租、转借、共享、售卖或以其他方式授权他人使用。
四、自动续费与订阅管理
1. 如您购买的是自动续费订阅产品相关续费、扣款、取消、订阅管理和价格变更规则除本协议外还应适用《Key of Love会员自动续费服务协议》以及Apple适用规则。
2. 您可在App内使用恢复购买功能对于Apple订阅管理您也可通过Apple账户的订阅管理页面进行查看和管理。
3. 会员服务与账号删除并非完全等同。若您删除Key of Love账号但Apple订阅仍在有效期内Apple侧订阅不会因账号删除而自动取消您仍需通过Apple订阅管理路径自行处理。
五、使用规范
1. 您应遵守法律法规、本协议、《Key of Love用户协议》及其他相关规则合法、合规地使用会员服务。
2. 您不得通过以下方式非法获取或使用会员服务:
1利用系统漏洞、外挂、自动化程序、模拟器、脚本、抓包、逆向工程等方式绕过正常购买流程
2通过盗用、共享、出售、转租、代充、异常退款、欺诈支付等方式获取会员资格
3将会员权益用于商业牟利、批量账号、异常营销、账号转售或其他违背会员服务初衷的用途
4其他违反法律法规、诚信原则或平台规则的行为。
3. 若我们发现或有合理理由认为您存在上述行为,我们有权采取限制使用、暂停权益、取消会员资格、拒绝服务、终止协议等措施,并有权依法追究责任。
六、退款与责任说明
1. Key of Love会员服务属于数字化服务。除法律法规另有规定或Apple平台规则另有适用外已生效的会员服务通常不支持无理由退费。
2. 如您通过Apple渠道购买会员服务退款申请、退款审核及退款结果以Apple规则和Apple处理结果为准。
3. 如因您个人原因、设备原因、网络原因、系统权限状态、Apple系统限制或第三方服务异常导致部分功能体验受限但相关情况已在页面、协议、规则或系统限制中明确说明的我们不因此承担退款义务法律法规另有规定的除外。
七、服务中止、终止与变更
1. 在以下情形下,我们有权中止、限制或终止向您提供全部或部分会员服务:
1您违反本协议、《Key of Love用户协议》或其他规则
2您账号存在异常风险、欺诈支付、滥用权益、违规共享等情形
3因监管要求、系统升级、产品下线、不可抗力、第三方平台限制等原因
4其他为保障平台安全、用户权益或合法合规运营所必要的情形。
2. 如会员服务发生重大变更,我们将以应用内公告、站内消息、弹窗、页面提示或其他合理方式进行说明。
八、法律适用与争议解决
1. 本协议的订立、生效、履行、解释及争议解决,适用中华人民共和国法律(不含冲突规范)。
2. 因本协议产生的任何争议,双方应友好协商解决;协商不成的,任一方可向蚌埠市蚌山区人民法院提起诉讼。
九、联系我们
开发者姓名Niu Ge
联系邮箱18715192152@163.com
联系地址安徽蚌埠万达广场23楼

View File

@@ -0,0 +1,83 @@
Key of Love会员自动续费服务协议
版本生效日期2026年3月9日
本《Key of Love会员自动续费服务协议》以下简称“本续费协议”是您与Niu Ge以下简称“我们”之间就您通过Apple App Store购买并使用Key of Love自动续费订阅服务所订立的协议。
请您在开通自动续费服务前认真阅读本续费协议、《Key of Love用户协议》、《Key of Love隐私政策》和《Key of Love会员服务协议》。您点击订阅、购买、继续、确认支付或完成自动续费订阅购买并开始使用相关服务的即视为您已阅读、理解并同意接受本续费协议全部内容。
一、自动续费服务说明
1. 自动续费服务是指您通过Apple App Store购买Key of Love自动续费订阅后在当前订阅周期到期前如您未按Apple规则取消订阅系统将通过您的Apple账户在下一个计费周期开始前后自动扣取下一周期的订阅费用并在扣款成功后延长对应订阅权益期限。
2. 自动续费服务的前提是:
1您已通过Apple App Store成功购买自动续费订阅
2您的Apple账户状态正常且可完成续费扣款
3相关订阅产品仍处于可续费状态。
二、订阅价格、周期与续费
1. 自动续费订阅的名称、价格、计费周期、试用规则及权益内容以购买页面、Apple结算页面、订阅页及App Store Connect中生效的订阅配置为准。
2. 购买确认后费用将从您的Apple账户扣取。
3. 除非您在当前订阅周期结束前至少24小时关闭自动续费否则订阅将自动续期。
4. Apple通常会在当前订阅周期结束前24小时内尝试收取下一订阅周期的费用。扣款成功后订阅周期将自动顺延。
5. 如订阅价格发生调整Apple可能会根据适用规则向您发出通知并按Apple的规则处理价格变更、用户确认或续费管理。
三、取消自动续费与订阅管理
1. 您可随时通过Apple订阅管理页面关闭自动续费
https://apps.apple.com/account/subscriptions
2. 您也可在iPhone或iPad中通过Apple账户订阅设置管理、查看或取消订阅。具体路径可能因系统版本不同而略有差异请以Apple系统实际页面为准。
3. 取消自动续费后,不影响您已支付并已生效的当前订阅周期权益;当前周期结束后将不再自动续费。
4. 如果您在当前订阅周期结束前不足24小时才关闭自动续费下一周期是否已完成扣款以Apple系统实际处理结果为准。
四、账号删除与自动续费
1. 删除Key of Love账号不会自动取消Apple订阅。
2. 若您在申请删除账号时仍存在有效的Apple自动续费订阅您的订阅可能仍将按照Apple规则继续计费直至您在Apple订阅管理页面中成功取消。
3. 为避免账号删除后仍发生续费建议您在删除账号前先前往Apple订阅管理页面查看并处理有效订阅。
五、恢复购买
1. 若您已通过Apple购买过有效订阅或非消耗型付费内容您可使用App内提供的“恢复购买”功能恢复相应权益。
2. 恢复购买是否成功以Apple返回结果及当前Apple账户下的有效购买记录为准。
六、退款说明
1. 通过Apple渠道购买的自动续费订阅退款申请、退款审核和退款结果以Apple规则和Apple处理结果为准。
2. 我们无法绕过Apple系统直接处理Apple订阅的退款。
3. 如您需申请Apple订阅退款可根据Apple支持页面或Apple官方流程发起申请。相关路径和规则以Apple届时公布内容为准。
七、服务中止、终止与异常情况
1. 如因Apple账户余额不足、支付方式失效、Apple账户异常、订阅产品下架、系统故障、监管要求或其他非我们可控原因导致自动续费失败则相应会员权益可能在当前订阅周期结束后停止。
2. 如您存在欺诈支付、违规共享、滥用订阅权益、违反《Key of Love用户协议》或其他规则的行为我们有权根据规则限制相关服务并依法保留追究责任的权利。
八、其他
1. 本续费协议未尽事宜以《Key of Love用户协议》《Key of Love会员服务协议》《Key of Love隐私政策》以及Apple适用规则为准。
2. 本续费协议的订立、生效、履行、解释及争议解决,适用中华人民共和国法律(不含冲突规范)。
3. 因本续费协议产生的任何争议,双方应友好协商解决;协商不成的,任一方可向蚌埠市蚌山区人民法院提起诉讼。
开发者姓名Niu Ge
联系邮箱18715192152@163.com
联系地址安徽蚌埠万达广场23楼

View File

@@ -0,0 +1,157 @@
Key of Love用户协议
版本生效日期2026年3月9日
欢迎您使用Key of Love产品及服务。
本《Key of Love用户协议》以下简称“本协议”由您与Niu Ge以下简称“我们”就您下载、安装、注册、登录、使用Key of Love应用程序、Key of Love自定义键盘及相关功能和服务所订立。
在您使用本产品及服务前,请您务必仔细阅读并充分理解本协议全部内容,尤其是与责任限制、争议解决、未成年人使用、付费服务、账号管理、服务中止或终止等相关的条款。您点击同意、注册、登录、购买、启用键盘、使用自定义键盘或继续使用本产品及服务的,即视为您已阅读、理解并同意接受本协议全部内容。
如您不同意本协议的任何内容,请您立即停止访问或使用本产品及服务。
一、定义与适用范围
1. Key of Love是我们提供的一款输入法及相关智能辅助服务产品功能可能包括但不限于自定义键盘、AI文本辅助、语音输入、账号系统、订阅服务、素材或主题、同步与反馈等。
2. 本协议适用于Key of Love已发布或未来更新的移动应用、键盘扩展、相关网页、软件版本、功能模块、升级版本及配套服务。
3. 本协议的组成还包括我们已经发布或未来发布的下列文件及规则:
1《Key of Love隐私政策》
2《Key of Love会员服务协议》
3《Key of Love会员自动续费服务协议》
4与特定功能、活动、付费服务、促销规则、页面说明相关的单独规则、公告、提示或说明。
上述内容一经发布,即构成本协议不可分割的组成部分,与本协议具有同等法律效力。
二、服务内容与使用限制
1. 您理解并同意Key of Love属于第三方输入法产品受iOS系统限制自定义键盘在安全输入框、密码输入场景以及部分系统限制场景下可能无法使用具体以系统实际限制为准。
2. Key of Love的部分功能依赖网络连接、账号状态、系统权限或“允许完全访问Allow Full Access”设置。若您未授予相关权限、未登录账号、网络不可用或未开启系统要求的设置部分功能可能无法使用。
3. 当您主动使用AI回复、AI改写、云端同步、账号功能、订阅相关功能或语音转写等联网功能时完成该请求所必需的文本、语音、账号标识或相关上下文信息可能会被传输至服务器处理。相关数据处理规则以《Key of Love隐私政策》为准。
4. AI生成、改写、推荐或转写的内容可能存在错误、遗漏、延迟、不准确或不适宜情形您应自行审慎判断并对发送、传播、采纳或依赖相关内容的行为负责。
5. 我们有权根据产品运营、业务调整、法律法规要求、系统兼容性或用户体验需要,对产品功能、界面、权益、价格、展示方式、可用范围进行更新、调整、中止或优化。
三、账号注册、登录与安全
1. 您在使用部分服务前,可能需要注册或登录账号。您应保证注册信息真实、准确、完整、合法,并在信息发生变化时及时更新。
2. 您应妥善保管账号、验证码、密码、登录凭证及与账号相关的设备信息。因您保管不善、主动泄露、转借、出售、共享或其他非我们原因导致的损失,由您自行承担。
3. 未经我们书面同意,您的账号及账号下的权益、服务资格不得赠与、出租、出借、转让、售卖、共享或以其他方式许可他人使用。
4. 如我们有合理理由认为您的账号存在异常登录、盗用、批量注册、恶意营销、违规转售、欺诈支付、破坏系统或其他风险情形,我们有权采取限制登录、暂停使用、冻结权益、要求验证、终止服务等措施。
四、用户行为规范
1. 您在使用本产品及服务时,不得从事以下行为:
1违反法律法规、公序良俗或本协议约定
2发布、传播、存储、生成、诱导、协助制作违法违规内容或侵害国家安全、社会公共利益、他人合法权益的内容
3发布、传播、生成、存储侮辱、诽谤、骚扰、威胁、仇恨、歧视、色情、暴力、欺诈、侵权、恶意营销或其他不当内容
4利用本产品或服务实施刷量、作弊、批量操作、异常注册、自动化调用、接口滥用、机器人行为、爬虫抓取、逆向工程、破解、干扰、破坏系统安全等行为
5擅自复制、搬运、镜像、抓取、出售、传播本产品中的模板、素材、主题、文案、界面、音频、图像、代码或其他内容
6以任何方式干扰、破坏或试图绕过本产品及服务的安全措施、限制机制、付费规则、风控措施或权限管理机制
7利用本产品及服务从事其他可能损害我们、其他用户、合作方或第三方合法权益的行为。
2. 您应对通过本产品生成、输入、编辑、发送、上传、同步、反馈、举报或分享的内容负责。
3. 如您发现任何违法违规、侵权、滥用、冒用、骚扰或其他不当内容,您可使用应用内举报、反馈或联系我们处理。
五、知识产权
1. 本产品及服务中包含的应用程序、键盘界面、主题、素材、文字、图片、音频、视频、商标、标识、技术、代码、数据、文档及其他内容的知识产权,归我们或相关权利人所有。
2. 未经我们或相关权利人事先书面许可,您不得对前述内容进行复制、改编、传播、公开展示、反向工程、反编译、反汇编、抓取、售卖、出租、出借、授权、开发衍生产品或作其他超出授权范围的使用。
3. 您在使用本产品及服务过程中提交、上传或依法享有权利的内容,不因上传行为而转移权利归属;但为向您提供服务、展示内容、处理反馈、执行安全审核、处理投诉举报、履行法律义务之必要,您授予我们在全球范围内、非独占、可转授权的必要使用许可。
六、第三方服务
1. 本产品及服务可能接入第三方提供的支付、登录、崩溃诊断、内容分发、系统能力或其他服务。第三方服务由相应第三方独立提供,我们不对第三方服务的可用性、准确性、稳定性承担保证责任。
2. 您通过Apple App Store购买的订阅、自动续费及相关支付行为还应同时受Apple相关条款、支付规则和订阅管理规则约束。
七、付费服务与订阅
1. 本产品的部分功能或权益属于付费服务,具体服务内容、价格、期限、适用范围、试用规则、自动续费规则,以购买页面、订阅页及相关协议展示为准。
2. 付费服务可能包括订阅、会员权益、主题素材或其他数字化服务。不同付费服务的内容和期限可能不同。
3. 您通过Apple渠道购买的自动续费订阅购买确认后将通过您的Apple账户扣费并按Apple订阅规则进行续期、取消、管理及退款处理。自动续费规则详见《Key of Love会员自动续费服务协议》。
4. 我们有权根据法律法规、业务调整、市场变化、运营需要等因素调整付费服务的价格、权益、适用范围和展示方式,但已生效订单在其已购买期限内通常不受影响,法律法规另有规定或页面另有说明的除外。
八、隐私保护与账号删除
1. 我们重视您的个人信息和隐私保护并将根据《Key of Love隐私政策》处理您的个人信息。
2. 若本产品支持账号创建我们将按照适用法律法规及Apple审核要求在应用内提供账号删除入口。账号删除后我们将删除或匿名化不再需要保留的个人信息但法律法规另有要求、履行账务义务、防止欺诈或保障安全所必需保留的信息除外。
3. 若您的账号存在仍由Apple管理的有效订阅删除账号并不会自动取消Apple订阅。您应按照系统订阅管理路径自行取消Apple订阅。
九、服务中止、终止与变更
1. 在以下情形下,我们有权中止、限制、暂停或终止向您提供全部或部分服务:
1您违反法律法规、本协议、相关规则或页面说明
2您的账号存在安全风险、异常行为、滥用行为或欺诈嫌疑
3因监管要求、司法机关要求、政策变化、不可抗力、系统维护、网络故障、第三方服务异常等客观原因
4其他我们依据法律法规或平台治理需要采取措施的情形。
2. 服务的中止、终止或变更,不影响在该等行为发生前您与我们之间已经形成的权利义务。
十、责任限制
1. 本产品及服务将按照现有技术和条件提供。我们将尽合理努力保障服务稳定、安全、连续,但不对以下事项作绝对保证:
1服务绝对不中断、无错误、无延迟
2AI生成、推荐、改写、转写或主题内容绝对准确、完整、适用于您的具体需求
3第三方服务、网络环境、设备兼容性、系统权限状态始终满足您的使用需求。
2. 在适用法律允许的最大范围内,对于因网络故障、系统限制、设备差异、第三方服务异常、不可抗力、监管要求、用户自身操作或其他非我们可控因素导致的损失,我们不承担责任。
3. 因我们原因导致您遭受损失的,在法律允许范围内,我们承担的责任以您就相关服务已实际支付的直接费用为上限;法律法规另有强制性规定的除外。
十一、未成年人使用
1. 若您为未满18周岁的未成年人应在监护人监护、指导并取得监护人同意后阅读和使用本产品及服务。
2. 监护人应妥善保管支付设备、Apple账户、验证码及其他支付凭证避免未成年人在未经同意情况下完成付费。
十二、法律适用与争议解决
1. 本协议的订立、生效、履行、解释及争议解决,适用中华人民共和国法律(不含冲突规范)。
2. 因本协议引起或与本协议有关的任何争议,双方应优先友好协商解决;协商不成的,任一方可向蚌埠市蚌山区人民法院提起诉讼。
十三、协议更新与联系我们
1. 我们有权根据法律法规变化、业务发展、产品调整、风险控制需要更新本协议,并通过应用内页面、官网、公告、弹窗或其他合理方式发布。
2. 更新后的协议自公布之日起生效。若您在协议更新后继续使用本产品及服务,视为您已接受更新后的协议。
3. 如您对本协议或本产品及服务有任何疑问、意见或建议,可通过以下方式联系我们:
开发者姓名Niu Ge
联系邮箱18715192152@163.com
联系地址安徽蚌埠万达广场23楼

View File

@@ -0,0 +1,163 @@
Key of Love隐私政策
版本生效日期2026年3月9日
欢迎您使用Key of Love。
本《Key of Love隐私政策》以下简称“本政策”由Niu Ge以下简称“我们”制定适用于您在使用Key of Love应用程序、Key of Love自定义键盘及相关服务时我们对您的个人信息和相关数据的收集、使用、存储、共享、删除和保护。
请您在使用我们的产品和服务前,认真阅读并充分理解本政策全部内容。
一、我们是谁
开发者姓名Niu Ge
联系邮箱18715192152@163.com
联系地址安徽蚌埠万达广场23楼
二、我们收集的信息类型
根据您使用的具体功能,我们可能收集以下类型的信息:
1. 账号信息
当您注册、登录或管理账号时,我们可能收集:
1邮箱地址
2用户ID、账号标识、登录状态
3您主动填写或上传的昵称、头像、性别等个人资料
4为维持登录和保障账号安全所必需的认证信息。
2. 您主动提交的文本内容
当您主动使用AI回复、AI改写、AI聊天、云端同步、反馈、举报、客服沟通等联网功能时我们可能处理您主动提交的文本内容以及完成该请求所必需的上下文信息。
普通本地输入并不当然意味着会被上传;只有在您主动触发相关联网功能时,完成该请求所必需的文本才可能被发送至服务器处理。
相关文本内容会保存在服务器用于向您提供AI功能、同步、反馈处理、举报处理、客服支持、内容安全治理及争议排查。我们不会将前述文本内容用于模型训练。除法律法规另有要求外我们会在实现本政策所述目的所必需期限内保留相关文本内容如您发起账号删除我们将根据适用法律法规及业务规则删除或匿名化不再需要保留的相关信息。
3. 语音数据
当您主动使用语音输入、语音转文字或相关语音功能时,我们可能处理您的录音文件、转写结果及与语音请求有关的必要技术信息。
根据当前产品功能,语音输入可能需要将录音上传至服务器或服务提供商进行语音转写处理。
原始录音不会被长期保存。语音数据仅在完成语音转写及相关功能处理所必需的范围内进行传输和处理,我们不会将原始录音用于模型训练。
4. 购买与订阅信息
当您购买、恢复购买、管理订阅或使用会员服务时,我们可能处理与订阅状态、购买记录、交易结果、会员期限和权益状态相关的信息,以便:
1为您开通和维持付费权益
2支持恢复购买
3处理账务、风控、客服与争议排查。
Apple支付交易由Apple按照其规则处理。
5. 反馈、举报与客服信息
当您提交意见反馈、举报内容、举报描述、聊天上下文快照、证据链接或客服问题时,我们可能处理相应信息,用于安全审核、问题排查、客户支持和规则执行。
6. 设备、日志与诊断信息
为保障服务稳定、安全和质量,我们可能处理与设备、应用运行、网络请求、错误日志、崩溃诊断和性能有关的必要信息。
我们当前使用第三方崩溃诊断服务 Bugly用于收集应用崩溃相关信息以便定位问题、修复故障并提升稳定性。当前仅收集崩溃诊断所必需的信息不进行额外性能分析该等崩溃数据原则上不直接与您的账号身份关联也不会用于广告跟踪。
三、我们如何使用您的信息
我们可能将收集到的信息用于以下目的:
1. 提供、维护、改进和优化产品及服务;
2. 实现注册、登录、账号管理、身份识别和安全验证;
3. 提供AI回复、改写、聊天、语音转写、同步、主题、订阅等功能
4. 处理购买、恢复购买、会员权益发放、账务与客服;
5. 处理反馈、举报、风控和内容安全治理;
6. 诊断故障、分析异常、提升稳定性、保障系统安全;
7. 履行法律法规规定的义务,或应对监管要求、司法要求与争议处理。
四、自定义键盘、完全访问与系统限制
1. Key of Love包含自定义键盘扩展。
2. 部分键盘功能需要您在iOS系统中启用“允许完全访问Allow Full Access”。若您未启用相关联网功能可能无法使用。
3. 当您主动使用AI、账号、同步、订阅或语音等联网功能时完成该请求所必需的数据可能被发送至服务器处理。
4. 第三方键盘在安全输入框、密码输入框及部分受iOS限制的场景中无法工作这是系统限制不代表我们能够访问这些内容。
五、我们如何共享、委托处理或披露信息
1. 我们不会出售您的个人信息。
2. 在以下情形下,我们可能与第三方共享、委托处理或依法披露必要信息:
1为实现支付、订阅、崩溃诊断、内容处理、语音转写、存储或技术支持等服务而与服务提供商合作
2在获得您明确授权或您主动发起请求的情况下
3为履行法律法规义务、配合司法机关或监管机关要求
4为保护我们、您或其他用户的生命财产安全、合法权益所必要
5其他法律法规允许的情形。
3. 如我们接入第三方SDK或服务我们会要求其按照适用法律法规和必要的安全标准处理信息。
六、我们如何存储和保护信息
1. 我们会采取合理可行的安全措施保护您的信息,防止未经授权的访问、披露、篡改、丢失或滥用。
2. 我们仅在实现本政策所述目的所必需期限内保留您的信息,法律法规另有要求的除外。
账号信息会保存在服务器,用于维持账号体系和相关服务功能;如您发起账号删除,我们将根据适用法律法规及业务规则删除或匿名化不再需要保留的账号信息。文本内容、反馈、举报、客服记录及必要日志通常仅在实现本政策所述目的所必需期限内保留;原始录音不作长期保存。对于法律法规要求留存、账务处理、防止欺诈、解决争议或保障安全所必需的信息,我们可能在合理期限内继续保留。
七、您的权利
在适用法律法规允许范围内,您通常享有以下权利:
1. 访问、更正、补充您的个人信息;
2. 删除您的账号或要求删除相关个人信息;
3. 管理订阅、取消自动续费;
4. 对某些信息处理行为进行说明或提出异议;
5. 通过联系我们行使法律法规赋予您的其他权利。
八、账号删除与数据删除
1. 若您在应用内发起账号删除,我们会根据法律法规和业务规则删除或匿名化不再需要保留的个人信息。
2. 但以下信息可能因法律义务、账务处理、防止欺诈、解决争议或安全需要而在合理期限内继续保留:
1交易与账务记录
2必要的日志与安全审计信息
3依法必须留存的信息。
3. 若您存在Apple自动续费订阅删除Key of Love账号不会自动取消Apple订阅。您仍需通过Apple订阅管理页面自行取消
https://apps.apple.com/account/subscriptions
九、未成年人保护
1. 若您是未满18周岁的未成年人应在监护人同意和指导下使用我们的产品和服务。
2. 如我们发现自己在未取得法定授权情况下收集了未成年人的个人信息,我们将依法尽快处理。
十、本政策的更新
1. 我们可能根据法律法规变化、业务调整、产品升级或运营需要更新本政策。
2. 更新后的版本将通过应用内页面、官网、公告、弹窗或其他合理方式向您展示。更新后如您继续使用我们的产品和服务,即视为您已阅读并同意更新后的本政策。
十一、联系我们
如您对本政策有任何疑问、意见、建议,或需要行使相关权利,可通过以下方式联系我们:
开发者姓名Niu Ge
联系邮箱18715192152@163.com
联系地址安徽蚌埠万达广场23楼

View File

@@ -0,0 +1,57 @@
# 法律文档上线前待补字段清单
下面这些字段你必须补完,才能交给前端生成正式 URL。
## 1. 个人开发者信息
以下 4 份文档里都要统一:
1. 《Key of Love用户协议》
2. 《Key of Love会员服务协议》
3. 《Key of Love会员自动续费服务协议》
4. 《Key of Love隐私政策》
必须补:
1. `Niu Ge`
2. `18715192152@163.com`
3. `安徽蚌埠万达广场23楼`
4. `蚌埠市蚌山区人民法院`
## 2. 隐私政策里的真实数据处理口径
必须和真实后端行为一致:
1. 文本内容是否保存
2. 文本保存多久
3. 文本是否用于模型训练
4. 原始语音是否保存
5. 语音保存多久
6. 语音是否用于模型训练
7. 崩溃/诊断服务提供方是谁
8. 崩溃/诊断收集哪些字段
9. 数据是否与账号关联
10. 账号删除后哪些数据立即删除,哪些继续保留
## 3. 订阅展示口径
前端上线前再核对:
1. 会员名称是否与 App Store Connect 一致
2. 周期是否与 App Store Connect 一致
3. 价格说明是否与 App Store Connect 一致
4. 试用说明是否与 App Store Connect 一致
## 4. 最终上线前建议
1. 这 4 份文档统一放到正式 HTTPS 页面
2. 不要再保留旧版本混乱术语:
- `Lovekey`
- `猜猜看公司`
- `Key of Love公司`
3. 统一用一个正式开发者姓名
4. 前端上线后,你要亲自点开检查:
- 页面能否公开访问
- 页面是否适配手机
- 页面标题是否正确
- 页面中是否还残留占位符

View File

@@ -0,0 +1,88 @@
# 隐私政策补充正文草稿
下面这份是面向你线上隐私政策页面的补充正文草稿。
注意:
1. 这是可提交版本草稿,不是最终法律意见。
2. 其中带占位的部分,必须按你们真实后端行为确认后再发布。
## English Draft
### Custom Keyboard and Full Access
Our app includes a custom keyboard extension. Some keyboard features require `Allow Full Access` because they rely on network connectivity for account, AI, subscription, sync, and voice-related functionality.
If you do not enable `Allow Full Access`, the keyboard may still appear, but network-based features will remain unavailable.
The custom keyboard does not operate in secure text fields, password fields, or other contexts where iOS restricts third-party keyboards.
### Text You Actively Submit Through Network-Based Features
When you actively use network-based features such as AI reply, AI chat, text rewriting, cloud sync, account-related actions, or subscription-related actions, the text content and limited related context required to complete your request may be transmitted to our servers or service providers.
We do not treat ordinary local typing as server-bound data unless you actively trigger a network-based feature that requires processing.
Please replace the following sentence with your confirmed real behavior before publishing:
`[REPLACE: state whether submitted text is stored, for how long, and whether it is used for model training.]`
### Voice Input and Speech Transcription
When you choose to use voice input or speech-to-text features, your recorded audio may be uploaded to our servers or service providers for speech transcription and related feature processing.
Please replace the following sentence with your confirmed real behavior before publishing:
`[REPLACE: state whether raw audio is stored, how long it is retained, and whether it is used for model training or only for the requested transcription.]`
### Account Data
We may collect and process account-related information such as your email address, user identifier, authentication status, and basic profile information in order to provide login, account security, syncing, customer support, and account management features.
### Profile Content and Uploaded Images
If you upload profile content such as an avatar image, nickname, or other profile information, we may process and store that content to display and maintain your account profile and related in-app features.
### Purchases and Subscriptions
We may process subscription status, purchase records, restore-purchase state, and related transaction information to provide paid features, restore prior purchases, prevent fraud, and support customer service.
Billing transactions are handled by Apple under Apple's payment framework and App Store rules.
### Diagnostics and Stability
We may collect limited diagnostic or crash-related information to maintain app stability, fix errors, and improve service reliability.
Please replace the following sentence with your confirmed real behavior before publishing:
`[REPLACE: identify the crash/diagnostic provider, what is collected, and whether the data is associated with a user account.]`
### Reporting and Safety
We provide in-app reporting tools for AI companion/persona content and related interactions. Information submitted through a report, such as report reason, description, and any related evidence or context you choose to provide, may be processed for safety review, abuse prevention, and policy enforcement.
### Data Deletion
The app provides in-app account deletion.
Please replace the following sentence with your confirmed real behavior before publishing:
`[REPLACE: describe what is deleted immediately, what is retained for legal, billing, fraud-prevention, or security reasons, and any retention period.]`
## 发布前必须确认的占位项
上面英文草稿里有 4 类必须你确认的内容:
1. 文本内容是否保存、保存多久、是否用于训练
2. 原始语音是否保存、保存多久、是否用于训练
3. 崩溃/诊断数据由谁收集、收集什么、是否关联账号
4. 账号删除后哪些数据立即删除,哪些因合规/账务/安全暂时保留
## 这份草稿要和哪些地方保持一致
发布前请核对以下 4 处:
1. 线上隐私政策正文
2. App Store Connect `App Privacy`
3. App 内 Full Access / 语音说明
4. Review Notes

View File

@@ -0,0 +1,100 @@
# App Store Review Notes
下面这份为英文成稿,可直接按需整理后粘贴到 App Store Connect 的 `Review Notes`
## Suggested Review Notes
Thank you for reviewing `Key of Love`.
This app includes a custom keyboard extension. Some keyboard features require the user to enable the keyboard in iOS Settings and turn on `Allow Full Access`, because those features use network requests for account, AI, subscription, sync, and voice transcription.
### 1. How to enable the keyboard
1. Install and open the main app.
2. Go to iOS Settings.
3. Open `General` -> `Keyboard` -> `Keyboards` -> `Add New Keyboard`.
4. Select `Key of Love`.
5. Tap the added keyboard and turn on `Allow Full Access`.
### 2. How to access the keyboard experience
1. Open any app with a standard text input field.
2. Bring up the system keyboard.
3. Switch to the `Key of Love` keyboard.
Please note that iOS does not allow third-party keyboards in secure text fields, password fields, or certain restricted system contexts.
### 3. Full Access behavior
`Allow Full Access` is required only for network-based features.
Without `Allow Full Access`, the keyboard can still appear, but network-based features such as AI reply, account-dependent features, subscription-related actions, sync, and voice transcription will not work.
When the user actively uses those network-based features, related text, voice audio, and required account identifiers may be transmitted to our server to complete the request.
### 4. Login flow
Some account-related features are completed in the main app.
If the reviewer starts a login-related action from the keyboard extension, the extension may open the main app to finish login. This is expected behavior and is required by our product flow.
### 5. Subscription / purchase flow
Some purchase and subscription actions started from the keyboard extension may open the main app to complete the transaction.
This is also expected behavior. The keyboard extension provides access to the feature entry points, while the main app completes account, purchase, restore, and subscription-management flows.
### 6. Restore purchase
The app includes a restore purchase entry on the subscription page.
The app also supports Apple subscription management through the standard App Store subscription management flow.
### 7. Voice feature
The main app includes optional voice input.
When the user records voice for speech-to-text, the audio file is uploaded to our server for transcription. This behavior is disclosed in our privacy materials.
### 8. Account deletion
The app includes in-app account deletion.
Path:
`Mine` -> `Personal` -> `Cancel Account`
### 9. Reporting / safety
The app includes a reporting flow for AI companion/persona content.
Path examples:
- Persona detail page -> `Report`
- Chat message action menu -> `Report`
### 10. Test account
Please replace the placeholders below with your real review account before submission.
Account:
`[REPLACE_WITH_REVIEW_EMAIL]`
Password:
`[REPLACE_WITH_REVIEW_PASSWORD]`
Additional notes:
`[REPLACE_WITH_ANY_SERVER_AVAILABILITY_OR_TESTING_NOTE]`
## Before Submission
在提交前,把上面 3 个占位项替换掉:
1. `REPLACE_WITH_REVIEW_EMAIL`
2. `REPLACE_WITH_REVIEW_PASSWORD`
3. `REPLACE_WITH_ANY_SERVER_AVAILABILITY_OR_TESTING_NOTE`
另外确认以下内容已经和 Review Notes 一致:
1. 隐私政策
2. App Privacy
3. App 内 Full Access 提示
4. 语音转写说明

View File

@@ -260,7 +260,6 @@
04FC95732EB09570007BD342 /* KBFunctionBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95722EB09570007BD342 /* KBFunctionBarView.m */; };
04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95752EB095DE007BD342 /* KBFunctionPasteView.m */; };
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95782EB09BC8007BD342 /* KBKeyBoardMainView.m */; };
04FC95B22EB0B2CC007BD342 /* KBSettingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95B12EB0B2CC007BD342 /* KBSettingView.m */; };
04FC95C92EB1E4C9007BD342 /* BaseNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95C82EB1E4C9007BD342 /* BaseNavigationController.m */; };
04FC95CC2EB1E780007BD342 /* BaseTabBarController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95CB2EB1E780007BD342 /* BaseTabBarController.m */; };
04FC95D22EB1E7AE007BD342 /* MyVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95D12EB1E7AE007BD342 /* MyVC.m */; };
@@ -801,8 +800,6 @@
04FC95752EB095DE007BD342 /* KBFunctionPasteView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionPasteView.m; sourceTree = "<group>"; };
04FC95772EB09BC8007BD342 /* KBKeyBoardMainView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyBoardMainView.h; sourceTree = "<group>"; };
04FC95782EB09BC8007BD342 /* KBKeyBoardMainView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyBoardMainView.m; sourceTree = "<group>"; };
04FC95B02EB0B2CC007BD342 /* KBSettingView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSettingView.h; sourceTree = "<group>"; };
04FC95B12EB0B2CC007BD342 /* KBSettingView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSettingView.m; sourceTree = "<group>"; };
04FC95C72EB1E4C9007BD342 /* BaseNavigationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BaseNavigationController.h; sourceTree = "<group>"; };
04FC95C82EB1E4C9007BD342 /* BaseNavigationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BaseNavigationController.m; sourceTree = "<group>"; };
04FC95CA2EB1E780007BD342 /* BaseTabBarController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BaseTabBarController.h; sourceTree = "<group>"; };
@@ -1758,8 +1755,6 @@
A1B2C3D32EB0A0A100000001 /* KBFunctionTagCell.m */,
A1B2C3F12EB35A9900000001 /* KBFullAccessGuideView.h */,
A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */,
04FC95B02EB0B2CC007BD342 /* KBSettingView.h */,
04FC95B12EB0B2CC007BD342 /* KBSettingView.m */,
A1B2C9222FC9000100000001 /* KBChatMessageCell.h */,
A1B2C9232FC9000100000001 /* KBChatMessageCell.m */,
049FB22D2EC34EB900FAB05D /* KBStreamTextView.h */,
@@ -2548,7 +2543,6 @@
A1B2C3E22EB0C0A100000001 /* KBNetworkManager.m in Sources */,
04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */,
04FEDAA12EEDB00100123456 /* KBEmojiDataProvider.m in Sources */,
04FC95B22EB0B2CC007BD342 /* KBSettingView.m in Sources */,
04FEDC122F00010000999999 /* KBKeyboardSubscriptionView.m in Sources */,
04FEDC322F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.m in Sources */,
04FEDC422F00040000999999 /* KBKeyboardSubscriptionFeatureMarqueeView.m in Sources */,

View File

@@ -85,13 +85,16 @@
NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:date];
if (interval < 60) {
return @"刚刚";
return KBLocalized(@"Just now");
} else if (interval < 3600) {
return [NSString stringWithFormat:@"%.0f分钟前", interval / 60];
return [NSString stringWithFormat:KBLocalized(@"%.0f minutes ago"),
interval / 60];
} else if (interval < 86400) {
return [NSString stringWithFormat:@"%.0f小时前", interval / 3600];
return [NSString stringWithFormat:KBLocalized(@"%.0f hours ago"),
interval / 3600];
} else if (interval < 86400 * 30) {
return [NSString stringWithFormat:@"%.0f天前", interval / 86400];
return [NSString stringWithFormat:KBLocalized(@"%.0f days ago"),
interval / 86400];
} else {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @"MM-dd";

View File

@@ -68,13 +68,16 @@
NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:date];
if (interval < 60) {
return @"刚刚";
return KBLocalized(@"Just now");
} else if (interval < 3600) {
return [NSString stringWithFormat:@"%.0f分钟前", interval / 60];
return [NSString stringWithFormat:KBLocalized(@"%.0f minutes ago"),
interval / 60];
} else if (interval < 86400) {
return [NSString stringWithFormat:@"%.0f小时前", interval / 3600];
return [NSString stringWithFormat:KBLocalized(@"%.0f hours ago"),
interval / 3600];
} else if (interval < 86400 * 30) {
return [NSString stringWithFormat:@"%.0f天前", interval / 86400];
return [NSString stringWithFormat:KBLocalized(@"%.0f days ago"),
interval / 86400];
} else {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @"MM-dd";
@@ -95,7 +98,8 @@
// "回复 @xxx"
NSMutableString *userNameText = [NSMutableString stringWithString:self.userName ?: @""];
if (self.replyToUserName.length > 0) {
[userNameText appendFormat:@" 回复 @%@", self.replyToUserName];
[userNameText appendFormat:@" %@ @%@", KBLocalized(@"Reply"),
self.replyToUserName];
}
UIFont *userNameFont = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
CGRect userNameRect = [userNameText boundingRectWithSize:CGSizeMake(contentWidth, CGFLOAT_MAX)

View File

@@ -58,10 +58,12 @@
formatter.dateFormat = @"HH:mm";
} else if ([calendar isDateInYesterday:timestamp]) {
//
formatter.dateFormat = @"'昨天' HH:mm";
formatter.dateFormat = @"HH:mm";
NSString *timeText = [formatter stringFromDate:timestamp];
return [NSString stringWithFormat:@"%@ %@", KBLocalized(@"Yesterday"), timeText];
} else {
// +
formatter.dateFormat = @"MMdd HH:mm";
formatter.dateFormat = @"MM-dd HH:mm";
}
return [formatter stringFromDate:timestamp];

View File

@@ -61,8 +61,8 @@
}
case KBAIReplyFooterStateExpand: {
self.actionButton.hidden = NO;
title = [NSString
stringWithFormat:@"展开%ld条回复", (long)comment.totalReplyCount];
title = [NSString stringWithFormat:KBLocalized(@"View %ld replies"),
(long)comment.totalReplyCount];
[self.actionButton setImage:[UIImage systemImageNamed:@"chevron.down"]
forState:UIControlStateNormal];
break;
@@ -71,15 +71,15 @@
self.actionButton.hidden = NO;
NSInteger remaining =
comment.totalReplyCount - comment.displayedReplies.count;
title =
[NSString stringWithFormat:@"展开更多回复(%ld条", (long)remaining];
title = [NSString stringWithFormat:KBLocalized(@"View more replies (%ld)"),
(long)remaining];
[self.actionButton setImage:[UIImage systemImageNamed:@"chevron.down"]
forState:UIControlStateNormal];
break;
}
case KBAIReplyFooterStateCollapse: {
self.actionButton.hidden = NO;
title = @"收起";
title = KBLocalized(@"Collapse");
[self.actionButton setImage:[UIImage systemImageNamed:@"chevron.up"]
forState:UIControlStateNormal];
break;

View File

@@ -94,8 +94,9 @@
self.timeLabel.text = [comment formattedTime];
//
NSString *likeText =
comment.likeCount > 0 ? [self formatLikeCount:comment.likeCount] : @"赞";
NSString *likeText = comment.likeCount > 0
? [self formatLikeCount:comment.likeCount]
: KBLocalized(@"Like");
self.likeButton.textLabel.text = likeText;
UIImage *likeImage = comment.liked
@@ -174,7 +175,7 @@
if (!_replyButton) {
_replyButton = [UIButton buttonWithType:UIButtonTypeCustom];
_replyButton.titleLabel.font = [UIFont systemFontOfSize:12];
[_replyButton setTitle:@"回复" forState:UIControlStateNormal];
[_replyButton setTitle:KBLocalized(@"Reply") forState:UIControlStateNormal];
[_replyButton setTitleColor:[UIColor colorWithHex:0x9F9F9F] forState:UIControlStateNormal];
[_replyButton addTarget:self
action:@selector(replyButtonTapped)

View File

@@ -35,23 +35,23 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
@property(nonatomic, strong) NSMutableArray<KBAICommentModel *> *comments;
///
/// Pagination params
@property(nonatomic, assign) NSInteger currentPage;
@property(nonatomic, assign) NSInteger pageSize;
@property(nonatomic, assign) BOOL isLoading;
@property(nonatomic, assign) BOOL hasMoreData;
///
/// Keyboard height
@property(nonatomic, assign) CGFloat keyboardHeight;
///
/// Bottom constraint for input view
@property(nonatomic, strong) MASConstraint *inputBottomConstraint;
///
/// Current reply target (top-level comment)
@property(nonatomic, weak) KBAICommentModel *replyToComment;
///
/// Current reply target (reply)
@property(nonatomic, weak) KBAIReplyModel *replyToReply;
/// AiVM
/// AiVM instance
@property(nonatomic, strong) AiVM *aiVM;
@end
@@ -65,7 +65,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
if (user.nickName.length > 0) {
return user.nickName;
}
return @"我";
return KBLocalized(@"Me");
}
- (NSString *)currentUserId {
@@ -80,7 +80,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
- (NSString *)generateTempIdString {
long long ms = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
// 使 ID
// Use negative values to avoid colliding with server IDs
long long tmp = -ms;
return [NSString stringWithFormat:@"%lld", tmp];
}
@@ -155,13 +155,13 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
#pragma mark - UI Setup
- (void)setupUI {
//
// Make background transparent so blur effect is visible
self.backgroundColor = [UIColor clearColor];
self.layer.cornerRadius = 12;
self.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
self.clipsToBounds = YES;
//
// Add blur background (bottom-most layer)
[self addSubview:self.blurBackgroundView];
[self.blurBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
@@ -201,7 +201,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
make.bottom.equalTo(self.inputView.mas_top);
}];
//
// Load more on pull-up
__weak typeof(self) weakSelf = self;
MJRefreshAutoNormalFooter *footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
@@ -302,7 +302,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
- (void)fetchCommentsAtPage:(NSInteger)page append:(BOOL)append {
if (self.companionId <= 0) {
NSLog(@"[KBAICommentView] companionId 未设置,无法加载评论");
NSLog(@"[KBAICommentView] companionId is not set, cannot load comments");
[self showEmptyState];
[self.tableView.mj_footer endRefreshing];
return;
@@ -323,7 +323,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
strongSelf.isLoading = NO;
if (error) {
NSLog(@"[KBAICommentView] 加载评论失败:%@", error.localizedDescription);
NSLog(@"[KBAICommentView] Failed to load comments: %@", error.localizedDescription);
dispatch_async(dispatch_get_main_queue(), ^{
if (append) {
[strongSelf.tableView.mj_footer endRefreshing];
@@ -340,11 +340,11 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
}];
}
/// KBCommentPageModel UI KBAICommentModel
/// Update comments (convert backend KBCommentPageModel to UI KBAICommentModel)
- (void)updateCommentsWithPageModel:(KBCommentPageModel *)pageModel append:(BOOL)append {
if (!pageModel) {
NSLog(@"[KBAICommentView] pageModel 为空");
//
NSLog(@"[KBAICommentView] pageModel is nil");
// Data is empty, show empty state
[self showEmptyState];
[self.tableView.mj_footer endRefreshing];
return;
@@ -356,19 +356,20 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
[self.comments removeAllObjects];
}
// tableView
// Get tableView width for height calculation
CGFloat tableWidth = self.tableView.bounds.size.width;
if (tableWidth <= 0) {
tableWidth = [UIScreen mainScreen].bounds.size.width;
}
NSLog(@"[KBAICommentView] 加载到 %ld 条评论,共 %ld 条,页码:%ld/%ld", (long)pageModel.records.count, (long)pageModel.total, (long)pageModel.current, (long)pageModel.pages);
NSLog(@"[KBAICommentView] Loaded %ld comments, total %ld, page: %ld/%ld", (long)pageModel.records.count, (long)pageModel.total, (long)pageModel.current, (long)pageModel.pages);
for (KBCommentItem *item in pageModel.records) {
// KBAICommentModel使 MJExtension
// KBCommentItem MJExtension id commentId
// mj_keyValues commentIdKBAICommentModel/KBAIReplyModel
// commentId/replyId -> id commentId/replyId parentId/rootId
// Convert to KBAICommentModel (via MJExtension)
// Note: KBCommentItem maps backend field id to commentId via MJExtension.
// If we directly use mj_keyValues, the dictionary only has commentId and
// KBAICommentModel/KBAIReplyModel mapping (commentId/replyId -> id) misses it.
// That would make commentId/replyId empty and break parentId/rootId when replying.
NSMutableDictionary *itemKV = [[item mj_keyValues] mutableCopy];
id commentIdVal = itemKV[@"commentId"];
if (commentIdVal) {
@@ -397,10 +398,10 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
KBAICommentModel *comment = [KBAICommentModel mj_objectWithKeyValues:[itemKV copy]];
// Header
// Precompute and cache header height
comment.cachedHeaderHeight = [comment calculateHeaderHeightWithMaxWidth:tableWidth];
// Reply
// Precompute and cache all reply heights
for (KBAIReplyModel *reply in comment.replies) {
reply.cachedCellHeight = [reply calculateCellHeightWithMaxWidth:tableWidth];
}
@@ -411,7 +412,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
[self updateTitle];
[self.tableView reloadData];
//
// Update pagination state
self.currentPage = pageModel.current > 0 ? pageModel.current : self.currentPage;
if (pageModel.pages > 0) {
self.hasMoreData = pageModel.current < pageModel.pages;
@@ -425,7 +426,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
[self.tableView.mj_footer endRefreshingWithNoMoreData];
}
//
// Toggle empty state based on data
if (self.comments.count == 0) {
[self showEmptyState];
} else {
@@ -433,25 +434,25 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
}
}
///
/// Show empty state view
- (void)showEmptyState {
self.tableView.useEmptyDataSet = YES;
self.tableView.emptyTitleText = @"暂无评论";
self.tableView.emptyDescriptionText = @"快来抢沙发吧~";
self.tableView.emptyImage = nil; //
self.tableView.emptyVerticalOffset = -50; //
self.tableView.emptyTitleText = KBLocalized(@"No comments yet");
self.tableView.emptyDescriptionText = KBLocalized(@"Be the first to comment");
self.tableView.emptyImage = nil; // Optional: set empty-state image
self.tableView.emptyVerticalOffset = -50; // Slight upward offset
[self.tableView kb_reloadEmptyDataSet];
}
///
/// Show error empty state view
- (void)showEmptyStateWithError {
self.tableView.useEmptyDataSet = YES;
self.tableView.emptyTitleText = @"加载失败";
self.tableView.emptyDescriptionText = @"点击重新加载";
self.tableView.emptyTitleText = KBLocalized(@"Load failed");
self.tableView.emptyDescriptionText = KBLocalized(@"Tap to retry");
self.tableView.emptyImage = nil;
self.tableView.emptyVerticalOffset = -50;
//
// Tap to reload
__weak typeof(self) weakSelf = self;
self.tableView.emptyDidTapView = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
@@ -463,7 +464,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
[self.tableView kb_reloadEmptyDataSet];
}
///
/// Hide empty state view
- (void)hideEmptyState {
self.tableView.useEmptyDataSet = NO;
[self.tableView kb_reloadEmptyDataSet];
@@ -473,10 +474,10 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
NSString *countText;
if (self.totalCommentCount >= 10000) {
countText = [NSString
stringWithFormat:@"%.1fw条评论", self.totalCommentCount / 10000.0];
stringWithFormat:KBLocalized(@"%.1fw comments"), self.totalCommentCount / 10000.0];
} else {
countText =
[NSString stringWithFormat:@"%ld条评论", (long)self.totalCommentCount];
[NSString stringWithFormat:KBLocalized(@"%ld comments"), (long)self.totalCommentCount];
}
self.titleLabel.text = countText;
}
@@ -510,41 +511,41 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
return;
}
// ID NSInteger
// Get comment ID (convert to NSInteger)
NSInteger commentId = [reply.replyId integerValue];
//
// Call like API
[strongSelf.aiVM likeCommentWithCommentId:commentId completion:^(KBCommentLikeResponse * _Nullable response, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(@"[KBAICommentView] 二级评论点赞失败:%@", error.localizedDescription);
// TODO:
NSLog(@"[KBAICommentView] Failed to like reply: %@", error.localizedDescription);
// TODO: Show error message
return;
}
if (response && response.code == 0) {
// data = true: data = false:
// data = true: liked, data = false: unliked
BOOL isNowLiked = response.data;
//
// Update model state
if (isNowLiked) {
// +1
// Like succeeded: like count +1
reply.liked = YES;
reply.likeCount = MAX(0, reply.likeCount + 1);
NSLog(@"[KBAICommentView] 二级评论点赞成功,ID: %ld", (long)commentId);
NSLog(@"[KBAICommentView] Reply liked successfully, ID: %ld", (long)commentId);
} else {
// -1
// Unlike succeeded: like count -1
reply.liked = NO;
reply.likeCount = MAX(0, reply.likeCount - 1);
NSLog(@"[KBAICommentView] 二级评论取消点赞成功,ID: %ld", (long)commentId);
NSLog(@"[KBAICommentView] Reply unliked successfully, ID: %ld", (long)commentId);
}
//
// Refresh target row
[strongSelf.tableView reloadRowsAtIndexPaths:@[ indexPath ]
withRowAnimation:UITableViewRowAnimationNone];
} else {
NSLog(@"[KBAICommentView] 二级评论点赞失败:%@", response.message ?: @"未知错误");
// TODO:
NSLog(@"[KBAICommentView] Failed to like reply: %@", response.message ?: @"Unknown error");
// TODO: Show error message
}
});
}];
@@ -574,41 +575,41 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
return;
}
// ID NSInteger
// Get comment ID (convert to NSInteger)
NSInteger commentId = [comment.commentId integerValue];
//
// Call like API
[strongSelf.aiVM likeCommentWithCommentId:commentId completion:^(KBCommentLikeResponse * _Nullable response, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(@"[KBAICommentView] 一级评论点赞失败:%@", error.localizedDescription);
// TODO:
NSLog(@"[KBAICommentView] Failed to like top-level comment: %@", error.localizedDescription);
// TODO: Show error message
return;
}
if (response && response.code == 0) {
// data = true: data = false:
// data = true: liked, data = false: unliked
BOOL isNowLiked = response.data;
//
// Update model state
if (isNowLiked) {
// +1
// Like succeeded: like count +1
comment.liked = YES;
comment.likeCount = MAX(0, comment.likeCount + 1);
NSLog(@"[KBAICommentView] 一级评论点赞成功,ID: %ld", (long)commentId);
NSLog(@"[KBAICommentView] Top-level comment liked successfully, ID: %ld", (long)commentId);
} else {
// -1
// Unlike succeeded: like count -1
comment.liked = NO;
comment.likeCount = MAX(0, comment.likeCount - 1);
NSLog(@"[KBAICommentView] 一级评论取消点赞成功,ID: %ld", (long)commentId);
NSLog(@"[KBAICommentView] Top-level comment unliked successfully, ID: %ld", (long)commentId);
}
// section
// Refresh target section
[strongSelf.tableView reloadSections:[NSIndexSet indexSetWithIndex:section]
withRowAnimation:UITableViewRowAnimationNone];
} else {
NSLog(@"[KBAICommentView] 一级评论点赞失败:%@", response.message ?: @"未知错误");
// TODO:
NSLog(@"[KBAICommentView] Failed to like top-level comment: %@", response.message ?: @"Unknown error");
// TODO: Show error message
}
});
}];
@@ -626,7 +627,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
KBAICommentModel *comment = self.comments[section];
KBAIReplyFooterState state = [comment footerState];
//
// Return empty view when there are no replies
if (state == KBAIReplyFooterStateHidden) {
return nil;
}
@@ -669,7 +670,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
#pragma mark - Footer Actions
///
/// Number of replies loaded each time
static NSInteger const kRepliesLoadCount = 5;
- (void)handleFooterActionForSection:(NSInteger)section {
@@ -695,10 +696,10 @@ static NSInteger const kRepliesLoadCount = 5;
KBAICommentModel *comment = self.comments[section];
NSInteger currentCount = comment.displayedReplies.count;
//
// Load more replies
[comment loadMoreReplies:kRepliesLoadCount];
//
// Calculate newly inserted rows
NSInteger newCount = comment.displayedReplies.count;
NSMutableArray *insertIndexPaths = [NSMutableArray array];
for (NSInteger i = currentCount; i < newCount; i++) {
@@ -706,7 +707,7 @@ static NSInteger const kRepliesLoadCount = 5;
inSection:section]];
}
// Header
// Insert rows (do not refresh header to avoid avatar flicker)
[self.tableView beginUpdates];
if (insertIndexPaths.count > 0) {
[self.tableView insertRowsAtIndexPaths:insertIndexPaths
@@ -714,7 +715,7 @@ static NSInteger const kRepliesLoadCount = 5;
}
[self.tableView endUpdates];
// Footer
// Manually refresh footer
KBAICommentFooterView *footerView =
(KBAICommentFooterView *)[self.tableView footerViewForSection:section];
if (footerView) {
@@ -726,17 +727,17 @@ static NSInteger const kRepliesLoadCount = 5;
KBAICommentModel *comment = self.comments[section];
NSInteger rowCount = comment.displayedReplies.count;
//
// Calculate rows to delete
NSMutableArray *deleteIndexPaths = [NSMutableArray array];
for (NSInteger i = 0; i < rowCount; i++) {
[deleteIndexPaths addObject:[NSIndexPath indexPathForRow:i
inSection:section]];
}
//
// Collapse all replies
[comment collapseReplies];
// Header
// Delete rows (do not refresh header to avoid avatar flicker)
[self.tableView beginUpdates];
if (deleteIndexPaths.count > 0) {
[self.tableView deleteRowsAtIndexPaths:deleteIndexPaths
@@ -744,7 +745,7 @@ static NSInteger const kRepliesLoadCount = 5;
}
[self.tableView endUpdates];
// Footer
// Manually refresh footer
KBAICommentFooterView *footerView =
(KBAICommentFooterView *)[self.tableView footerViewForSection:section];
if (footerView) {
@@ -756,7 +757,7 @@ static NSInteger const kRepliesLoadCount = 5;
- (void)closeButtonTapped {
[self.popView dismiss];
//
// Close comment view (handled by outside)
// [[NSNotificationCenter defaultCenter]
// postNotificationName:@"KBAICommentViewCloseNotification"
// object:nil];
@@ -766,13 +767,13 @@ static NSInteger const kRepliesLoadCount = 5;
- (UIVisualEffectView *)blurBackgroundView {
if (!_blurBackgroundView) {
// 43pt
// iOS UIBlurEffect API使 dark
// Create blur effect (43pt blur radius in design)
// iOS UIBlurEffect has no direct blur-radius API; use system dark style
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
_blurBackgroundView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
//
// #0000000.31
// Overlay a semi-transparent black layer to tune tone and opacity
// Color: #000000, alpha: 0.31
UIView *darkOverlay = [[UIView alloc] init];
darkOverlay.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.31];
[_blurBackgroundView.contentView addSubview:darkOverlay];
@@ -796,7 +797,7 @@ static NSInteger const kRepliesLoadCount = 5;
_titleLabel = [[UILabel alloc] init];
_titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium];
_titleLabel.textColor = [UIColor whiteColor];
_titleLabel.text = @"0条评论";
_titleLabel.text = [NSString stringWithFormat:KBLocalized(@"%ld comments"), (long)0];
}
return _titleLabel;
}
@@ -824,10 +825,10 @@ static NSInteger const kRepliesLoadCount = 5;
_tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
_tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 0.01)];
// "暂无数据"
// Disable empty placeholder by default to avoid showing "No data" while loading
_tableView.useEmptyDataSet = NO;
// Header/Cell/Footer
// Register Header/Cell/Footer
[_tableView registerClass:[KBAICommentHeaderView class]
forHeaderFooterViewReuseIdentifier:kCommentHeaderIdentifier];
[_tableView registerClass:[KBAIReplyCell class]
@@ -835,7 +836,7 @@ static NSInteger const kRepliesLoadCount = 5;
[_tableView registerClass:[KBAICommentFooterView class]
forHeaderFooterViewReuseIdentifier:kCommentFooterIdentifier];
//
// Remove top padding
if (@available(iOS 15.0, *)) {
_tableView.sectionHeaderTopPadding = 0;
}
@@ -846,7 +847,7 @@ static NSInteger const kRepliesLoadCount = 5;
- (KBAICommentInputView *)inputView {
if (!_inputView) {
_inputView = [[KBAICommentInputView alloc] init];
_inputView.placeholder = @"Send A Message";
_inputView.placeholder = KBLocalized(@"Send A Message");
_inputView.layer.cornerRadius = 26;
_inputView.clipsToBounds = true;
__weak typeof(self) weakSelf = self;
@@ -865,26 +866,26 @@ static NSInteger const kRepliesLoadCount = 5;
self.replyToReply = reply;
if (reply) {
//
// Reply to a second-level comment
self.inputView.placeholder =
[NSString stringWithFormat:@"回复 @%@", reply.userName];
[NSString stringWithFormat:KBLocalized(@"Reply to @%@"), reply.userName];
} else if (comment) {
//
// Reply to a top-level comment
self.inputView.placeholder =
[NSString stringWithFormat:@"回复 @%@", comment.userName];
[NSString stringWithFormat:KBLocalized(@"Reply to @%@"), comment.userName];
} else {
//
self.inputView.placeholder = @"说点什么...";
// New comment
self.inputView.placeholder = KBLocalized(@"Say something...");
}
//
// Show keyboard
[self.inputView showKeyboard];
}
- (void)clearReplyTarget {
self.replyToComment = nil;
self.replyToReply = nil;
self.inputView.placeholder = @"说点什么...";
self.inputView.placeholder = KBLocalized(@"Say something...");
}
#pragma mark - Send Comment
@@ -899,20 +900,20 @@ static NSInteger const kRepliesLoadCount = 5;
}
if (self.replyToComment) {
//
// Send reply (add second-level comment)
[self sendReplyWithText:text tableWidth:tableWidth];
} else {
//
// Send top-level comment
[self sendNewCommentWithText:text tableWidth:tableWidth];
}
//
// Clear input and reply target
[self.inputView clearText];
[self clearReplyTarget];
}
- (void)sendNewCommentWithText:(NSString *)text tableWidth:(CGFloat)tableWidth {
NSLog(@"[KBAICommentView] 发送一级评论:%@", text);
NSLog(@"[KBAICommentView] Send top-level comment: %@", text);
__weak typeof(self) weakSelf = self;
[self.aiVM addCommentWithCompanionId:self.companionId
@@ -927,11 +928,11 @@ static NSInteger const kRepliesLoadCount = 5;
dispatch_async(dispatch_get_main_queue(), ^{
if (error || code != 0) {
NSLog(@"[KBAICommentView] 发送一级评论失败:%@", error.localizedDescription ?: @"");
NSLog(@"[KBAICommentView] Failed to send top-level comment: %@", error.localizedDescription ?: @"");
return;
}
//
// Insert new comment locally at first position; avoid full reload
KBAICommentModel *localComment =
[strongSelf buildLocalNewCommentWithText:text
serverItem:newItem
@@ -954,29 +955,29 @@ static NSInteger const kRepliesLoadCount = 5;
});
}];
//
// Example code:
// [self.aiVM sendCommentWithCompanionId:self.companionId
// content:text
// completion:^(KBCommentItem *newItem, NSError *error) {
// if (error) {
// NSLog(@"[KBAICommentView] 发送评论失败:%@", error.localizedDescription);
// NSLog(@"[KBAICommentView] Failed to send comment: %@", error.localizedDescription);
// return;
// }
//
// // KBAICommentModel
// // Convert to KBAICommentModel
// KBAICommentModel *comment = [KBAICommentModel mj_objectWithKeyValues:[newItem mj_keyValues]];
// comment.cachedHeaderHeight = [comment calculateHeaderHeightWithMaxWidth:tableWidth];
//
// //
// // Insert into array at index 0
// [self.comments insertObject:comment atIndex:0];
// self.totalCommentCount++;
// [self updateTitle];
//
// // section
// // Insert new section
// [self.tableView insertSections:[NSIndexSet indexSetWithIndex:0]
// withRowAnimation:UITableViewRowAnimationAutomatic];
//
// //
// // Scroll to top
// [self.tableView setContentOffset:CGPointZero animated:YES];
// }];
}
@@ -986,7 +987,7 @@ static NSInteger const kRepliesLoadCount = 5;
if (!comment)
return;
NSLog(@"[KBAICommentView] 回复评论 %@%@", comment.commentId, text);
NSLog(@"[KBAICommentView] Reply to comment %@: %@", comment.commentId, text);
NSInteger root = [comment.commentId integerValue];
NSNumber *rootId = @(root);
@@ -1011,7 +1012,7 @@ static NSInteger const kRepliesLoadCount = 5;
dispatch_async(dispatch_get_main_queue(), ^{
if (error || code != 0) {
NSLog(@"[KBAICommentView] 回复评论失败:%@", error.localizedDescription ?: @"");
NSLog(@"[KBAICommentView] Failed to send reply: %@", error.localizedDescription ?: @"");
return;
}
@@ -1051,7 +1052,7 @@ static NSInteger const kRepliesLoadCount = 5;
[strongSelf.delegate commentView:strongSelf didUpdateTotalCommentCount:strongSelf.totalCommentCount];
}
// displayedReplies loadMoreReplies
// If fully expanded, insert new row directly; otherwise keep displayedReplies as prefix to preserve loadMoreReplies behavior
if (wasFullyExpanded) {
[comment.displayedReplies addObject:localReply];
NSInteger newRowIndex = comment.displayedReplies.count - 1;
@@ -1085,31 +1086,31 @@ static NSInteger const kRepliesLoadCount = 5;
});
}];
//
// Example code:
// NSInteger parentId = [comment.commentId integerValue];
// [self.aiVM replyCommentWithParentId:parentId
// content:text
// completion:^(KBCommentItem *newItem, NSError *error) {
// if (error) {
// NSLog(@"[KBAICommentView] 回复评论失败:%@", error.localizedDescription);
// NSLog(@"[KBAICommentView] Failed to reply comment: %@", error.localizedDescription);
// return;
// }
//
// // KBAIReplyModel
// // Convert to KBAIReplyModel
// KBAIReplyModel *newReply = [KBAIReplyModel mj_objectWithKeyValues:[newItem mj_keyValues]];
// newReply.cachedCellHeight = [newReply calculateCellHeightWithMaxWidth:tableWidth];
//
// // replies
// // Append to replies array
// NSMutableArray *newReplies = [NSMutableArray arrayWithArray:comment.replies];
// [newReplies addObject:newReply];
// comment.replies = newReplies;
// comment.totalReplyCount = newReplies.count;
//
// // section
// // Find section for this comment
// NSInteger section = [self.comments indexOfObject:comment];
// if (section == NSNotFound) return;
//
// // displayedReplies
// // If expanded, append to displayedReplies and insert row
// if (comment.isRepliesExpanded) {
// NSInteger newRowIndex = comment.displayedReplies.count;
// [comment.displayedReplies addObject:newReply];
@@ -1118,18 +1119,18 @@ static NSInteger const kRepliesLoadCount = 5;
// [self.tableView insertRowsAtIndexPaths:@[indexPath]
// withRowAnimation:UITableViewRowAnimationAutomatic];
//
// // Footer
// // Refresh footer
// KBAICommentFooterView *footerView = (KBAICommentFooterView *)[self.tableView footerViewForSection:section];
// if (footerView) {
// [footerView configureWithComment:comment];
// }
//
// //
// // Scroll to new reply
// [self.tableView scrollToRowAtIndexPath:indexPath
// atScrollPosition:UITableViewScrollPositionBottom
// animated:YES];
// } else {
// // Footer
// // If not expanded, refresh footer to show updated reply count
// KBAICommentFooterView *footerView = (KBAICommentFooterView *)[self.tableView footerViewForSection:section];
// if (footerView) {
// [footerView configureWithComment:comment];

View File

@@ -108,8 +108,10 @@
NSFontAttributeName : [UIFont systemFontOfSize:13],
NSForegroundColorAttributeName : [UIColor whiteColor]
};
NSString *replyText =
[NSString stringWithFormat:@" %@ ", KBLocalized(@"Reply")];
[attrName appendAttributedString:[[NSAttributedString alloc]
initWithString:@" 回复 "
initWithString:replyText
attributes:replyAttrs]];
NSDictionary *toUserAttrs = @{
@@ -133,8 +135,9 @@
self.timeLabel.text = [reply formattedTime];
//
NSString *likeText =
reply.likeCount > 0 ? [self formatLikeCount:reply.likeCount] : @"赞";
NSString *likeText = reply.likeCount > 0
? [self formatLikeCount:reply.likeCount]
: KBLocalized(@"Like");
self.likeButton.textLabel.text = likeText;
UIImage *likeImage = reply.liked
@@ -212,7 +215,7 @@
if (!_replyButton) {
_replyButton = [UIButton buttonWithType:UIButtonTypeCustom];
_replyButton.titleLabel.font = [UIFont systemFontOfSize:11];
[_replyButton setTitle:@"回复" forState:UIControlStateNormal];
[_replyButton setTitle:KBLocalized(@"Reply") forState:UIControlStateNormal];
[_replyButton setTitleColor:[UIColor colorWithHex:0x9F9F9F] forState:UIControlStateNormal];
[_replyButton addTarget:self
action:@selector(replyButtonTapped)

View File

@@ -39,8 +39,8 @@
- (void)setup {
_state = KBAiRecordButtonStateNormal;
_normalTitle = @"按住说话";
_recordingTitle = @"松开结束";
_normalTitle = KBLocalized(@"Hold To Speak");
_recordingTitle = KBLocalized(@"Release To Finish");
_tintColor = [UIColor systemBlueColor];
//

View File

@@ -298,7 +298,7 @@
- (UILabel *)statusLabel {
if (!_statusLabel) {
_statusLabel = [[UILabel alloc] init];
_statusLabel.text = @"按住按钮开始对话";
_statusLabel.text = KBLocalized(@"Hold To Start Talking");
_statusLabel.font = [UIFont systemFontOfSize:14];
_statusLabel.textColor = [UIColor secondaryLabelColor];
_statusLabel.textAlignment = NSTextAlignmentCenter;
@@ -310,8 +310,8 @@
if (!_recordButton) {
_recordButton = [[KBAiRecordButton alloc] init];
_recordButton.delegate = self;
_recordButton.normalTitle = @"按住说话";
_recordButton.recordingTitle = @"松开结束";
_recordButton.normalTitle = KBLocalized(@"Hold To Speak");
_recordButton.recordingTitle = KBLocalized(@"Release To Finish");
_recordButton.normalIconImage = [UIImage imageNamed:@"ai_jianpan_icon"];
_recordButton.recordingIconImage = [UIImage imageNamed:@"ai_luyining_icon"];
_recordButton.hidden = YES;
@@ -340,7 +340,7 @@
- (UIButton *)textCenterButton {
if (!_textCenterButton) {
_textCenterButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_textCenterButton setTitle:@"发送一个消息给她" forState:UIControlStateNormal];
[_textCenterButton setTitle:KBLocalized(@"Send A Message To Her") forState:UIControlStateNormal];
_textCenterButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
[_textCenterButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_textCenterButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
@@ -362,7 +362,7 @@
- (UILabel *)voiceCenterLabel {
if (!_voiceCenterLabel) {
_voiceCenterLabel = [[UILabel alloc] init];
_voiceCenterLabel.text = @"按住说话";
_voiceCenterLabel.text = KBLocalized(@"Hold To Speak");
_voiceCenterLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
_voiceCenterLabel.textColor = [UIColor whiteColor];
_voiceCenterLabel.textAlignment = NSTextAlignmentCenter;
@@ -526,9 +526,9 @@
- (void)updateCenterTextIfNeeded {
if (self.inputState == KBVoiceInputBarStateText) {
[self.textCenterButton setTitle:@"发送一个消息给她" forState:UIControlStateNormal];
[self.textCenterButton setTitle:KBLocalized(@"Send A Message To Her") forState:UIControlStateNormal];
} else if (self.inputState == KBVoiceInputBarStateVoice) {
self.voiceCenterLabel.text = @"按住说话";
self.voiceCenterLabel.text = KBLocalized(@"Hold To Speak");
}
}

View File

@@ -936,7 +936,7 @@ static void KBChatUpdatedDarwinCallback(CFNotificationCenterRef center,
- (KBVoiceInputBar *)voiceInputBar {
if (!_voiceInputBar) {
_voiceInputBar = [[KBVoiceInputBar alloc] init];
_voiceInputBar.statusText = @"按住按钮开始对话";
_voiceInputBar.statusText = KBLocalized(@"Hold To Start Talking");
}
return _voiceInputBar;
}
@@ -1372,7 +1372,7 @@ static void KBChatUpdatedDarwinCallback(CFNotificationCenterRef center,
if (cell) {
[cell removeLoadingAssistantMessageWithRequestId:requestId];
}
NSString *message = response.message ?: @"聊天响应为空";
NSString *message = response.message ?: KBLocalized(@"Chat response is empty");
NSLog(@"[KBAIHomeVC] 聊天响应为空:%@", message);
if (message.length > 0) {
[KBHUD showError:message];

View File

@@ -144,7 +144,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
//
if (!self.deleteButton) {
self.deleteButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.deleteButton setTitle:@"删除此记录" forState:UIControlStateNormal];
[self.deleteButton setTitle:KBLocalized(@"Delete This Record") forState:UIControlStateNormal];
[self.deleteButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
self.deleteButton.titleLabel.font = [UIFont systemFontOfSize:14];
self.deleteButton.backgroundColor = [UIColor colorWithRed:244/255.0 green:67/255.0 blue:54/255.0 alpha:1.0]; // #F44336
@@ -317,7 +317,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
if (error) {
NSLog(@"[KBAIMessageChatingVC] 删除失败:%@", error.localizedDescription);
[KBHUD showError:@"删除失败,请重试"];
[KBHUD showError:KBLocalized(@"Delete failed, please try again")];
return;
}
@@ -346,7 +346,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
NSLog(@"[KBAIMessageChatingVC] ✅ 已发送重置通知companionId=%ld", (long)companionId);
// 5.
[KBHUD showSuccess:@"已删除"];
[KBHUD showSuccess:KBLocalized(@"Deleted")];
});
}];
}

View File

@@ -336,7 +336,7 @@ autoShowBusinessError:NO
//
NSInteger code = [json[@"code"] integerValue];
if (code != 0) {
NSString *message = json[@"message"] ?: @"请求失败";
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
NSError *bizError = [NSError errorWithDomain:@"AiVM"
code:code
userInfo:@{NSLocalizedDescriptionKey: message}];
@@ -356,7 +356,7 @@ autoShowBusinessError:NO
} else {
NSError *parseError = [NSError errorWithDomain:@"AiVM"
code:-1
userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}];
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid data format")}];
if (completion) {
completion(nil, parseError);
}
@@ -398,7 +398,7 @@ autoShowBusinessError:NO
//
NSInteger code = [json[@"code"] integerValue];
if (code != 0) {
NSString *message = json[@"message"] ?: @"请求失败";
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
NSError *bizError = [NSError errorWithDomain:@"AiVM"
code:code
userInfo:@{NSLocalizedDescriptionKey: message}];
@@ -418,7 +418,7 @@ autoShowBusinessError:NO
} else {
NSError *parseError = [NSError errorWithDomain:@"AiVM"
code:-1
userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}];
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid data format")}];
if (completion) {
completion(nil, parseError);
}
@@ -463,7 +463,7 @@ autoShowBusinessError:NO
if (![json isKindOfClass:[NSDictionary class]]) {
NSError *parseError = [NSError errorWithDomain:@"AiVM"
code:-1
userInfo:@{NSLocalizedDescriptionKey : @"数据格式错误"}];
userInfo:@{NSLocalizedDescriptionKey : KBLocalized(@"Invalid data format")}];
if (completion) {
completion(NO, parseError);
}
@@ -472,7 +472,7 @@ autoShowBusinessError:NO
NSInteger code = [json[@"code"] integerValue];
if (code != 0) {
NSString *message = json[@"message"] ?: @"请求失败";
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
NSError *bizError = [NSError errorWithDomain:@"AiVM"
code:code
userInfo:@{NSLocalizedDescriptionKey : message}];
@@ -498,7 +498,7 @@ autoShowBusinessError:NO
if (content.length == 0) {
NSError *error = [NSError errorWithDomain:@"AiVM"
code:-1
userInfo:@{NSLocalizedDescriptionKey: @"评论内容不能为空"}];
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Comment content cannot be empty")}];
if (completion) {
completion(nil, -1, error);
}
@@ -535,7 +535,7 @@ autoShowBusinessError:NO
NSLog(@"[AiVM] /ai-companion/comment/add response: %@", json);
NSInteger code = [json[@"code"] integerValue];
if (code != 0) {
NSString *message = json[@"message"] ?: @"请求失败";
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
NSError *bizError = [NSError errorWithDomain:@"AiVM"
code:code
userInfo:@{NSLocalizedDescriptionKey: message}];
@@ -596,7 +596,7 @@ autoShowBusinessError:NO
NSInteger code = [json[@"code"] integerValue];
if (code != 0) {
NSString *message = json[@"message"] ?: @"请求失败";
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
NSError *bizError = [NSError errorWithDomain:@"AiVM"
code:code
userInfo:@{NSLocalizedDescriptionKey: message}];
@@ -615,7 +615,7 @@ autoShowBusinessError:NO
} else {
NSError *parseError = [NSError errorWithDomain:@"AiVM"
code:-1
userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}];
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid data format")}];
if (completion) {
completion(nil, parseError);
}
@@ -711,7 +711,7 @@ autoShowBusinessError:NO
NSInteger code = [json[@"code"] integerValue];
if (code != 0) {
NSString *message = json[@"message"] ?: @"请求失败";
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
NSError *bizError = [NSError errorWithDomain:@"AiVM"
code:code
userInfo:@{NSLocalizedDescriptionKey: message}];
@@ -755,7 +755,7 @@ autoShowBusinessError:NO
NSInteger code = [json[@"code"] integerValue];
if (code != 0) {
NSString *message = json[@"message"] ?: @"请求失败";
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
NSError *bizError = [NSError errorWithDomain:@"AiVM"
code:code
userInfo:@{NSLocalizedDescriptionKey: message}];
@@ -838,7 +838,7 @@ autoShowBusinessError:NO
NSInteger code = [json[@"code"] integerValue];
if (code != 0) {
NSString *message = json[@"message"] ?: @"请求失败";
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
NSError *bizError = [NSError errorWithDomain:@"AiVM"
code:code
userInfo:@{NSLocalizedDescriptionKey: message}];
@@ -857,7 +857,7 @@ autoShowBusinessError:NO
} else {
NSError *parseError = [NSError errorWithDomain:@"AiVM"
code:-1
userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}];
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid data format")}];
if (completion) {
completion(nil, parseError);
}
@@ -936,7 +936,7 @@ autoShowBusinessError:NO
if (![json isKindOfClass:[NSDictionary class]]) {
NSError *parseError = [NSError errorWithDomain:@"AiVM"
code:-1
userInfo:@{NSLocalizedDescriptionKey : @"数据格式错误"}];
userInfo:@{NSLocalizedDescriptionKey : KBLocalized(@"Invalid data format")}];
if (completion) {
completion(NO, parseError);
}
@@ -945,7 +945,7 @@ autoShowBusinessError:NO
NSInteger code = [json[@"code"] integerValue];
if (code != 0) {
NSString *message = json[@"message"] ?: @"请求失败";
NSString *message = json[@"message"] ?: KBLocalized(@"Request failed");
NSError *bizError = [NSError errorWithDomain:@"AiVM"
code:code
userInfo:@{NSLocalizedDescriptionKey : message}];

View File

@@ -513,7 +513,11 @@ typedef void(^KBInputProfileSelectHandler)(NSString *languageCode, NSString *lay
@"profileId": layout.profileId
}];
}
// STTODO
if ([profile.code isEqualToString:@"zh-Hant-Pinyin"]) {
// NSLog(@"===");
continue;
}
[configs addObject:@{
@"code": profile.code,
@"name": profile.name,

View File

@@ -54,7 +54,7 @@
@{ @"title": KBLocalized(@"Agreement"), @"icon": @"my_agreement_icon", @"color": @(0x4CD964),@"id":@"5" },
@{ @"title": KBLocalized(@"Privacy Policy"), @"icon": @"my_privacy_icon", @"color": @(0x5AC8FA),@"id":@"6" },
#if DEBUG
@{ @"title": KBLocalized(@"Test"), @"icon": @"", @"color": @(0x5AC8FA),@"id":@"7" },
// @{ @"title": KBLocalized(@"Test"), @"icon": @"", @"color": @(0x5AC8FA),@"id":@"7" },
#endif
]

View File

@@ -28,7 +28,7 @@
if (remoteURL.length > 0) {
vc.url = remoteURL;
} else {
vc.htmlString = [self kb_htmlForLegalDocumentType:type];
// vc.htmlString = [self kb_htmlForLegalDocumentType:type];
}
return vc;
}
@@ -265,25 +265,6 @@ didFailProvisionalNavigation:(WKNavigation *)navigation
}
}
+ (NSString *)kb_htmlForLegalDocumentType:(KBLegalDocumentType)type {
NSString *title = [self kb_titleForLegalDocumentType:type];
NSString *body = @"";
switch (type) {
case KBLegalDocumentTypePrivacyPolicy:
body = @"<section><h2>Overview</h2><p>This in-app privacy disclosure explains how the app and the custom keyboard handle data when you use account, AI, subscription, sync, and voice features.</p></section><section><h2>Full Access</h2><p>Network-based features inside the keyboard require Full Access. If you do not enable Full Access, those features stay unavailable.</p></section><section><h2>Data Used For Features You Trigger</h2><p>When you actively use AI reply, cloud sync, account, purchase verification, or voice input, the content required for that feature may be transmitted to the service provider to complete your request.</p><p>This may include typed text you choose to send, voice audio you record, account identifiers, email address, subscription status, and limited diagnostics needed for app functionality and fraud prevention.</p></section><section><h2>Keyboard Boundaries</h2><p>The custom keyboard does not operate in secure text fields and cannot access content in contexts where iOS blocks third-party keyboards.</p></section><section><h2>Retention And Deletion</h2><p>Account-related data is retained only as needed for app functionality, purchases, support, and legal compliance. Use the in-app account deletion flow to request account removal and associated cleanup.</p></section><section><h2>Important</h2><p>Replace this fallback page with your final published privacy policy URL before App Store submission so that the wording exactly matches App Store Connect privacy labels and your backend behavior.</p></section>";
break;
case KBLegalDocumentTypeMembershipAgreement:
body = @"<section><h2>Subscription Terms</h2><p>Paid membership unlocks subscription benefits for eligible premium features. Pricing, billing period, and any trial details are shown on the purchase sheet before you confirm payment.</p></section><section><h2>Auto-Renewal</h2><p>Subscriptions renew automatically unless cancelled at least 24 hours before the end of the current billing period. Renewal charges are handled by Apple through your App Store account.</p></section><section><h2>Managing Your Subscription</h2><p>You can restore purchases inside the app and manage or cancel subscriptions in Apple ID subscription settings after purchase.</p></section><section><h2>Feature Availability</h2><p>Some premium actions started from the custom keyboard may open the main app to complete login, purchase, or subscription management.</p></section><section><h2>Important</h2><p>Replace this fallback page with your final published membership agreement URL before App Store submission.</p></section>";
break;
case KBLegalDocumentTypeTermsOfService:
default:
body = @"<section><h2>Service Scope</h2><p>This app provides a custom keyboard, account features, premium subscriptions, and optional AI-assisted and voice features. Some capabilities require network access and may open the main app to complete the flow.</p></section><section><h2>Acceptable Use</h2><p>You must not use the service to violate law, harass others, infringe rights, or generate abusive, sexual, hateful, or otherwise prohibited content.</p></section><section><h2>AI And Voice Features</h2><p>AI-generated or transcribed content may be inaccurate, incomplete, or inappropriate. You remain responsible for reviewing content before sending or relying on it.</p></section><section><h2>Accounts And Purchases</h2><p>You are responsible for activity performed through your account. Paid features are subject to Apple billing rules and any product limitations shown in the app.</p></section><section><h2>Important</h2><p>Replace this fallback page with your final published terms URL before App Store submission.</p></section>";
break;
}
return [NSString stringWithFormat:@"<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1,maximum-scale=1'><style>body{font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue',sans-serif;margin:0;padding:24px 18px 48px;color:#1f2937;background:#ffffff;line-height:1.6;}h1{font-size:28px;line-height:1.2;margin:0 0 20px;color:#111827;}h2{font-size:18px;line-height:1.3;margin:24px 0 10px;color:#111827;}p{font-size:15px;margin:0 0 10px;color:#4b5563;}section{padding-bottom:4px;border-bottom:1px solid #eef2f7;}section:last-child{border-bottom:none;} .note{margin-top:18px;font-size:13px;color:#6b7280;}</style></head><body><h1>%@</h1>%@<p class='note'>Built-in fallback document. Configure the final public URL in KBConfig.h when release content is ready.</p></body></html>", title, body];
}
+ (NSString *)kb_fallbackErrorHTML {
return @"<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><style>body{font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue',sans-serif;padding:32px;color:#1f2937;}h1{font-size:22px;margin:0 0 12px;}p{font-size:15px;line-height:1.6;color:#4b5563;}</style></head><body><h1>Page unavailable</h1><p>The requested document could not be loaded.</p></body></html>";
}

View File

@@ -30,6 +30,7 @@
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
<dict>
@@ -44,6 +45,42 @@
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeAudioData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeProductInteraction</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeCrashData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>