Compare commits
13 Commits
2d5919016f
...
dev_上报埋点
| Author | SHA1 | Date | |
|---|---|---|---|
| bdf2a9af80 | |||
| e858d35722 | |||
| f2d5210313 | |||
| 1b0af3e2d6 | |||
| 0965cd3c7e | |||
| c3909d63da | |||
| 1096f24c57 | |||
| 7ed84fd445 | |||
| 4e2d7d2908 | |||
| 34089ddeea | |||
| 6ec98468de | |||
| ae37730da6 | |||
| 203f104ece |
@@ -20,6 +20,7 @@
|
|||||||
#import "KBKeyboardSubscriptionView.h"
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
#import "KBKeyboardSubscriptionProduct.h"
|
#import "KBKeyboardSubscriptionProduct.h"
|
||||||
#import "KBBackspaceUndoManager.h"
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
#import "KBSuggestionEngine.h"
|
#import "KBSuggestionEngine.h"
|
||||||
|
|
||||||
// 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。
|
// 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。
|
||||||
@@ -46,6 +47,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
|
|
||||||
@interface KeyboardViewController () <KBKeyBoardMainViewDelegate, KBFunctionViewDelegate, KBKeyboardSubscriptionViewDelegate>
|
@interface KeyboardViewController () <KBKeyBoardMainViewDelegate, KBFunctionViewDelegate, KBKeyboardSubscriptionViewDelegate>
|
||||||
@property (nonatomic, strong) UIButton *nextKeyboardButton; // 系统“下一个键盘”按钮(可选)
|
@property (nonatomic, strong) UIButton *nextKeyboardButton; // 系统“下一个键盘”按钮(可选)
|
||||||
|
@property (nonatomic, strong) UIView *contentView;
|
||||||
@property (nonatomic, strong) KBKeyBoardMainView *keyBoardMainView; // 功能面板视图(点击工具栏第0个时显示)
|
@property (nonatomic, strong) KBKeyBoardMainView *keyBoardMainView; // 功能面板视图(点击工具栏第0个时显示)
|
||||||
@property (nonatomic, strong) KBFunctionView *functionView; // 功能面板视图(点击工具栏第0个时显示)
|
@property (nonatomic, strong) KBFunctionView *functionView; // 功能面板视图(点击工具栏第0个时显示)
|
||||||
@property (nonatomic, strong) KBSettingView *settingView; // 设置页
|
@property (nonatomic, strong) KBSettingView *settingView; // 设置页
|
||||||
@@ -54,6 +56,12 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
@property (nonatomic, strong) KBSuggestionEngine *suggestionEngine;
|
@property (nonatomic, strong) KBSuggestionEngine *suggestionEngine;
|
||||||
@property (nonatomic, copy) NSString *currentWord;
|
@property (nonatomic, copy) NSString *currentWord;
|
||||||
@property (nonatomic, assign) BOOL suppressSuggestions;
|
@property (nonatomic, assign) BOOL suppressSuggestions;
|
||||||
|
@property (nonatomic, strong) MASConstraint *contentWidthConstraint;
|
||||||
|
@property (nonatomic, strong) MASConstraint *contentHeightConstraint;
|
||||||
|
@property (nonatomic, strong) NSLayoutConstraint *kb_heightConstraint;
|
||||||
|
@property (nonatomic, strong) NSLayoutConstraint *kb_widthConstraint;
|
||||||
|
@property (nonatomic, assign) CGFloat kb_lastPortraitWidth;
|
||||||
|
@property (nonatomic, assign) CGFloat kb_lastKeyboardHeight;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KeyboardViewController
|
@implementation KeyboardViewController
|
||||||
@@ -64,6 +72,8 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
|
|
||||||
- (void)viewDidLoad {
|
- (void)viewDidLoad {
|
||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
|
// 撤销删除是“上一段删除操作”的临时状态;键盘被系统回收/重建或跨页面回来时应当清空,避免误显示。
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
[self setupUI];
|
[self setupUI];
|
||||||
self.suggestionEngine = [KBSuggestionEngine shared];
|
self.suggestionEngine = [KBSuggestionEngine shared];
|
||||||
self.currentWord = @"";
|
self.currentWord = @"";
|
||||||
@@ -92,20 +102,40 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
|
|
||||||
- (void)viewWillAppear:(BOOL)animated{
|
- (void)viewWillAppear:(BOOL)animated{
|
||||||
[super viewWillAppear:animated];
|
[super viewWillAppear:animated];
|
||||||
|
// 进入/重新进入输入界面时,清理上一次会话残留的撤销状态与缓存,避免显示“撤销删除”但实际上已不可撤销。
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
[[KBInputBufferManager shared] resetWithText:@""];
|
||||||
[[KBLocalizationManager shared] reloadFromSharedStorageIfNeeded];
|
[[KBLocalizationManager shared] reloadFromSharedStorageIfNeeded];
|
||||||
|
// 注意:微信/QQ 等宿主的 documentContext 可能是“截断窗口”,这里只更新 liveText,不要把它当作全文 manualSnapshot。
|
||||||
|
[[KBInputBufferManager shared] updateFromExternalContextBefore:self.textDocumentProxy.documentContextBeforeInput
|
||||||
|
after:self.textDocumentProxy.documentContextAfterInput];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewWillDisappear:(BOOL)animated {
|
||||||
|
[super viewWillDisappear:animated];
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)textDidChange:(id<UITextInput>)textInput {
|
||||||
|
[super textDidChange:textInput];
|
||||||
|
[[KBInputBufferManager shared] updateFromExternalContextBefore:self.textDocumentProxy.documentContextBeforeInput
|
||||||
|
after:self.textDocumentProxy.documentContextAfterInput];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void)setupUI {
|
- (void)setupUI {
|
||||||
self.view.translatesAutoresizingMaskIntoConstraints = NO;
|
self.view.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
|
||||||
// 按屏幕宽度对设计值做等比缩放,避免在不同机型上键盘整体高度失真导致皮肤被压缩/拉伸
|
// 按“短边”宽度等比缩放,横屏保持竖屏布局比例
|
||||||
CGFloat keyboardHeight = KBFit(kKBKeyboardBaseHeight);
|
CGFloat portraitWidth = [self kb_portraitWidth];
|
||||||
CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
|
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
|
||||||
|
CGFloat screenWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
|
||||||
CGFloat outerVerticalInset = KBFit(4.0f);
|
CGFloat outerVerticalInset = KBFit(4.0f);
|
||||||
|
|
||||||
NSLayoutConstraint *h = [self.view.heightAnchor constraintEqualToConstant:keyboardHeight];
|
NSLayoutConstraint *h = [self.view.heightAnchor constraintEqualToConstant:keyboardHeight];
|
||||||
NSLayoutConstraint *w = [self.view.widthAnchor constraintEqualToConstant:screenWidth];
|
NSLayoutConstraint *w = [self.view.widthAnchor constraintEqualToConstant:screenWidth];
|
||||||
|
self.kb_heightConstraint = h;
|
||||||
|
self.kb_widthConstraint = w;
|
||||||
|
|
||||||
h.priority = UILayoutPriorityRequired;
|
h.priority = UILayoutPriorityRequired;
|
||||||
w.priority = UILayoutPriorityRequired;
|
w.priority = UILayoutPriorityRequired;
|
||||||
@@ -117,25 +147,30 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
iv.allowsSelfSizing = NO;
|
iv.allowsSelfSizing = NO;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 背景图铺底
|
// 内容容器:横屏时保持竖屏宽度,居中显示
|
||||||
[self.view addSubview:self.bgImageView];
|
[self.view addSubview:self.contentView];
|
||||||
|
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.centerX.equalTo(self.view);
|
||||||
|
make.bottom.equalTo(self.view);
|
||||||
|
self.contentWidthConstraint = make.width.mas_equalTo(portraitWidth);
|
||||||
|
self.contentHeightConstraint = make.height.mas_equalTo(keyboardHeight);
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 背景图铺底(仅在内容容器内)
|
||||||
|
[self.contentView addSubview:self.bgImageView];
|
||||||
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.edges.equalTo(self.view);
|
make.edges.equalTo(self.contentView);
|
||||||
}];
|
}];
|
||||||
// 预置功能面板(默认隐藏),与键盘区域共享相同布局
|
// 预置功能面板(默认隐藏),与键盘区域共享相同布局
|
||||||
self.functionView.hidden = YES;
|
self.functionView.hidden = YES;
|
||||||
[self.view addSubview:self.functionView];
|
[self.contentView addSubview:self.functionView];
|
||||||
[self.functionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.functionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.right.equalTo(self.view);
|
make.edges.equalTo(self.contentView);
|
||||||
make.top.equalTo(self.view).offset(0);
|
|
||||||
make.bottom.equalTo(self.view).offset(0);
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
[self.view addSubview:self.keyBoardMainView];
|
[self.contentView addSubview:self.keyBoardMainView];
|
||||||
[self.keyBoardMainView mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.keyBoardMainView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.right.equalTo(self.view);
|
make.edges.equalTo(self.contentView);
|
||||||
make.top.equalTo(self.view).offset(0);
|
|
||||||
make.bottom.equalTo(self.view.mas_bottom).offset(-0);
|
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,36 +292,49 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
self.keyBoardMainView.hidden = show;
|
self.keyBoardMainView.hidden = show;
|
||||||
|
|
||||||
if (show) {
|
if (show) {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_function_panel"
|
||||||
|
pageId:@"keyboard_function_panel"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[self hideSubscriptionPanel];
|
[self hideSubscriptionPanel];
|
||||||
|
} else {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_main_panel"
|
||||||
|
pageId:@"keyboard_main_panel"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可选:把当前显示的视图置顶,避免层级遮挡
|
// 可选:把当前显示的视图置顶,避免层级遮挡
|
||||||
if (show) {
|
if (show) {
|
||||||
[self.view bringSubviewToFront:self.functionView];
|
[self.contentView bringSubviewToFront:self.functionView];
|
||||||
} else {
|
} else {
|
||||||
[self.view bringSubviewToFront:self.keyBoardMainView];
|
[self.contentView bringSubviewToFront:self.keyBoardMainView];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示/隐藏设置页(高度与 keyBoardMainView 一致),右侧滑入/滑出
|
/// 显示/隐藏设置页(高度与 keyBoardMainView 一致),右侧滑入/滑出
|
||||||
- (void)showSettingView:(BOOL)show {
|
- (void)showSettingView:(BOOL)show {
|
||||||
if (show) {
|
if (show) {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_settings"
|
||||||
|
pageId:@"keyboard_settings"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
// if (!self.settingView) {
|
// if (!self.settingView) {
|
||||||
self.settingView = [[KBSettingView alloc] init];
|
self.settingView = [[KBSettingView alloc] init];
|
||||||
self.settingView.hidden = YES;
|
self.settingView.hidden = YES;
|
||||||
[self.view addSubview:self.settingView];
|
[self.contentView addSubview:self.settingView];
|
||||||
[self.settingView mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.settingView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
// 与键盘主视图完全等同的区域,保证高度、宽度一致
|
// 与键盘主视图完全等同的区域,保证高度、宽度一致
|
||||||
make.edges.equalTo(self.keyBoardMainView);
|
make.edges.equalTo(self.contentView);
|
||||||
}];
|
}];
|
||||||
[self.settingView.backButton addTarget:self action:@selector(onTapSettingsBack) forControlEvents:UIControlEventTouchUpInside];
|
[self.settingView.backButton addTarget:self action:@selector(onTapSettingsBack) forControlEvents:UIControlEventTouchUpInside];
|
||||||
// }
|
// }
|
||||||
[self.view bringSubviewToFront:self.settingView];
|
[self.contentView bringSubviewToFront:self.settingView];
|
||||||
// 以 keyBoardMainView 的实际宽度为准,避免首次添加时 self.view 宽度尚未计算
|
// 以 keyBoardMainView 的实际宽度为准,避免首次添加时 self.view 宽度尚未计算
|
||||||
[self.view layoutIfNeeded];
|
[self.contentView layoutIfNeeded];
|
||||||
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
|
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
|
||||||
if (w <= 0) { w = CGRectGetWidth(self.view.bounds); }
|
if (w <= 0) { w = CGRectGetWidth(self.contentView.bounds); }
|
||||||
if (w <= 0) { w = [UIScreen mainScreen].bounds.size.width; }
|
if (w <= 0) { w = [self kb_portraitWidth]; }
|
||||||
self.settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
self.settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
||||||
self.settingView.hidden = NO;
|
self.settingView.hidden = NO;
|
||||||
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||||||
@@ -295,8 +343,8 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
} else {
|
} else {
|
||||||
if (!self.settingView || self.settingView.hidden) return;
|
if (!self.settingView || self.settingView.hidden) return;
|
||||||
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
|
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
|
||||||
if (w <= 0) { w = CGRectGetWidth(self.view.bounds); }
|
if (w <= 0) { w = CGRectGetWidth(self.contentView.bounds); }
|
||||||
if (w <= 0) { w = [UIScreen mainScreen].bounds.size.width; }
|
if (w <= 0) { w = [self kb_portraitWidth]; }
|
||||||
[UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
[UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||||
self.settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
self.settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
||||||
} completion:^(BOOL finished) {
|
} completion:^(BOOL finished) {
|
||||||
@@ -322,19 +370,23 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_subscription_panel"
|
||||||
|
pageId:@"keyboard_subscription_panel"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[self showFunctionPanel:NO];
|
[self showFunctionPanel:NO];
|
||||||
KBKeyboardSubscriptionView *panel = self.subscriptionView;
|
KBKeyboardSubscriptionView *panel = self.subscriptionView;
|
||||||
if (!panel.superview) {
|
if (!panel.superview) {
|
||||||
panel.hidden = YES;
|
panel.hidden = YES;
|
||||||
[self.view addSubview:panel];
|
[self.contentView addSubview:panel];
|
||||||
[panel mas_makeConstraints:^(MASConstraintMaker *make) {
|
[panel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.edges.equalTo(self.keyBoardMainView);
|
make.edges.equalTo(self.contentView);
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
[self.view bringSubviewToFront:panel];
|
[self.contentView bringSubviewToFront:panel];
|
||||||
panel.hidden = NO;
|
panel.hidden = NO;
|
||||||
panel.alpha = 0.0;
|
panel.alpha = 0.0;
|
||||||
CGFloat height = CGRectGetHeight(self.view.bounds);
|
CGFloat height = CGRectGetHeight(self.contentView.bounds);
|
||||||
if (height <= 0) { height = 260; }
|
if (height <= 0) { height = 260; }
|
||||||
panel.transform = CGAffineTransformMakeTranslation(0, height);
|
panel.transform = CGAffineTransformMakeTranslation(0, height);
|
||||||
[panel refreshProductsIfNeeded];
|
[panel refreshProductsIfNeeded];
|
||||||
@@ -347,7 +399,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
- (void)hideSubscriptionPanel {
|
- (void)hideSubscriptionPanel {
|
||||||
if (!self.subscriptionView || self.subscriptionView.hidden) { return; }
|
if (!self.subscriptionView || self.subscriptionView.hidden) { return; }
|
||||||
CGFloat height = CGRectGetHeight(self.subscriptionView.bounds);
|
CGFloat height = CGRectGetHeight(self.subscriptionView.bounds);
|
||||||
if (height <= 0) { height = CGRectGetHeight(self.view.bounds); }
|
if (height <= 0) { height = CGRectGetHeight(self.contentView.bounds); }
|
||||||
KBKeyboardSubscriptionView *panel = self.subscriptionView;
|
KBKeyboardSubscriptionView *panel = self.subscriptionView;
|
||||||
[UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
[UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||||
panel.alpha = 0.0;
|
panel.alpha = 0.0;
|
||||||
@@ -369,22 +421,27 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
NSString *text = key.output ?: key.title ?: @"";
|
NSString *text = key.output ?: key.title ?: @"";
|
||||||
[self.textDocumentProxy insertText:text];
|
[self.textDocumentProxy insertText:text];
|
||||||
[self kb_updateCurrentWordWithInsertedText:text];
|
[self kb_updateCurrentWordWithInsertedText:text];
|
||||||
|
[[KBInputBufferManager shared] appendText:text];
|
||||||
} break;
|
} break;
|
||||||
case KBKeyTypeBackspace:
|
case KBKeyTypeBackspace:
|
||||||
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:self.textDocumentProxy.documentContextBeforeInput
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:self.textDocumentProxy];
|
||||||
after:self.textDocumentProxy.documentContextAfterInput];
|
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:self.textDocumentProxy.documentContextBeforeInput
|
||||||
[self.textDocumentProxy deleteBackward];
|
after:self.textDocumentProxy.documentContextAfterInput];
|
||||||
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:self.textDocumentProxy count:1];
|
||||||
[self kb_scheduleContextRefreshResetSuppression:NO];
|
[self kb_scheduleContextRefreshResetSuppression:NO];
|
||||||
|
[[KBInputBufferManager shared] applyHoldDeleteCount:1];
|
||||||
break;
|
break;
|
||||||
case KBKeyTypeSpace:
|
case KBKeyTypeSpace:
|
||||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
[self.textDocumentProxy insertText:@" "];
|
[self.textDocumentProxy insertText:@" "];
|
||||||
[self kb_clearCurrentWord];
|
[self kb_clearCurrentWord];
|
||||||
|
[[KBInputBufferManager shared] appendText:@" "];
|
||||||
break;
|
break;
|
||||||
case KBKeyTypeReturn:
|
case KBKeyTypeReturn:
|
||||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
[self.textDocumentProxy insertText:@"\n"];
|
[self.textDocumentProxy insertText:@"\n"];
|
||||||
[self kb_clearCurrentWord];
|
[self kb_clearCurrentWord];
|
||||||
|
[[KBInputBufferManager shared] appendText:@"\n"];
|
||||||
break;
|
break;
|
||||||
case KBKeyTypeGlobe:
|
case KBKeyTypeGlobe:
|
||||||
[self advanceToNextInputMode]; break;
|
[self advanceToNextInputMode]; break;
|
||||||
@@ -402,6 +459,12 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index {
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index {
|
||||||
|
NSDictionary *extra = @{@"index": @(index)};
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_toolbar_action"
|
||||||
|
pageId:@"keyboard_main_panel"
|
||||||
|
elementId:@"toolbar_action"
|
||||||
|
extra:extra
|
||||||
|
completion:nil];
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
[self showFunctionPanel:YES];
|
[self showFunctionPanel:YES];
|
||||||
[self kb_clearCurrentWord];
|
[self kb_clearCurrentWord];
|
||||||
@@ -411,6 +474,11 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView {
|
- (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];
|
[self showSettingView:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,19 +487,37 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
[self.textDocumentProxy insertText:emoji];
|
[self.textDocumentProxy insertText:emoji];
|
||||||
[self kb_clearCurrentWord];
|
[self kb_clearCurrentWord];
|
||||||
|
[[KBInputBufferManager shared] appendText:emoji];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView {
|
- (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_undo_btn"
|
||||||
|
pageId:@"keyboard_main_panel"
|
||||||
|
elementId:@"undo_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[[KBBackspaceUndoManager shared] performUndoFromResponder:self.view];
|
[[KBBackspaceUndoManager shared] performUndoFromResponder:self.view];
|
||||||
[self kb_scheduleContextRefreshResetSuppression:YES];
|
[self kb_scheduleContextRefreshResetSuppression:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)keyBoardMainViewDidTapEmojiSearch:(KBKeyBoardMainView *)keyBoardMainView {
|
- (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")];
|
[KBHUD showInfo:KBLocalized(@"Search coming soon")];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion {
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion {
|
||||||
if (suggestion.length == 0) { return; }
|
if (suggestion.length == 0) { return; }
|
||||||
|
NSDictionary *extra = @{@"suggestion_len": @(suggestion.length)};
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_suggestion_item"
|
||||||
|
pageId:@"keyboard_main_panel"
|
||||||
|
elementId:@"suggestion_item"
|
||||||
|
extra:extra
|
||||||
|
completion:nil];
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
NSString *current = self.currentWord ?: @"";
|
NSString *current = self.currentWord ?: @"";
|
||||||
if (current.length > 0) {
|
if (current.length > 0) {
|
||||||
for (NSUInteger i = 0; i < current.length; i++) {
|
for (NSUInteger i = 0; i < current.length; i++) {
|
||||||
@@ -443,6 +529,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
[self.suggestionEngine recordSelection:suggestion];
|
[self.suggestionEngine recordSelection:suggestion];
|
||||||
self.suppressSuggestions = YES;
|
self.suppressSuggestions = YES;
|
||||||
[self.keyBoardMainView kb_setSuggestions:@[]];
|
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||||
|
[[KBInputBufferManager shared] replaceTailWithText:suggestion deleteCount:current.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - KBFunctionViewDelegate
|
// MARK: - KBFunctionViewDelegate
|
||||||
@@ -453,6 +540,18 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
- (void)functionView:(KBFunctionView *_Nullable)functionView didRightTapToolActionAtIndex:(NSInteger)index{
|
- (void)functionView:(KBFunctionView *_Nullable)functionView didRightTapToolActionAtIndex:(NSInteger)index{
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_right_action"
|
||||||
|
pageId:@"keyboard_function_panel"
|
||||||
|
elementId:@"right_action"
|
||||||
|
extra:@{@"action": @"login_or_recharge"}
|
||||||
|
completion:nil];
|
||||||
|
if (!KBAuthManager.shared.isLoggedIn) {
|
||||||
|
NSString *schemeStr = [NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
|
||||||
|
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
||||||
|
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
||||||
|
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
||||||
|
return;
|
||||||
|
}
|
||||||
NSString *schemeStr = [NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME];
|
NSString *schemeStr = [NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME];
|
||||||
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
||||||
//
|
//
|
||||||
@@ -475,10 +574,24 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
#pragma mark - KBKeyboardSubscriptionViewDelegate
|
#pragma mark - KBKeyboardSubscriptionViewDelegate
|
||||||
|
|
||||||
- (void)subscriptionViewDidTapClose:(KBKeyboardSubscriptionView *)view {
|
- (void)subscriptionViewDidTapClose:(KBKeyboardSubscriptionView *)view {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_subscription_close_btn"
|
||||||
|
pageId:@"keyboard_subscription_panel"
|
||||||
|
elementId:@"close_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[self hideSubscriptionPanel];
|
[self hideSubscriptionPanel];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)subscriptionView:(KBKeyboardSubscriptionView *)view didTapPurchaseForProduct:(KBKeyboardSubscriptionProduct *)product {
|
- (void)subscriptionView:(KBKeyboardSubscriptionView *)view didTapPurchaseForProduct:(KBKeyboardSubscriptionProduct *)product {
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
if ([product.productId isKindOfClass:NSString.class] && product.productId.length > 0) {
|
||||||
|
extra[@"product_id"] = product.productId;
|
||||||
|
}
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_subscription_product_btn"
|
||||||
|
pageId:@"keyboard_subscription_panel"
|
||||||
|
elementId:@"product_btn"
|
||||||
|
extra:extra.copy
|
||||||
|
completion:nil];
|
||||||
[self hideSubscriptionPanel];
|
[self hideSubscriptionPanel];
|
||||||
[self kb_openRechargeForProduct:product];
|
[self kb_openRechargeForProduct:product];
|
||||||
}
|
}
|
||||||
@@ -553,6 +666,11 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)onTapSettingsBack {
|
- (void)onTapSettingsBack {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_settings_back_btn"
|
||||||
|
pageId:@"keyboard_settings"
|
||||||
|
elementId:@"back_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[self showSettingView:NO];
|
[self showSettingView:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,6 +694,21 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)viewDidLayoutSubviews {
|
||||||
|
[super viewDidLayoutSubviews];
|
||||||
|
[self kb_updateKeyboardLayoutIfNeeded];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
|
||||||
|
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
||||||
|
[weakSelf kb_updateKeyboardLayoutIfNeeded];
|
||||||
|
} completion:^(__unused id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
||||||
|
[weakSelf kb_updateKeyboardLayoutIfNeeded];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
//- (void)kb_tryOpenContainerForLoginIfNeeded {
|
//- (void)kb_tryOpenContainerForLoginIfNeeded {
|
||||||
// // 使用与主 App 一致的自定义 Scheme
|
// // 使用与主 App 一致的自定义 Scheme
|
||||||
// NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=keyboard", KB_APP_SCHEME]];
|
// NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=keyboard", KB_APP_SCHEME]];
|
||||||
@@ -596,6 +729,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
self.bgImageView.image = img;
|
self.bgImageView.image = img;
|
||||||
BOOL hasImg = (img != nil);
|
BOOL hasImg = (img != nil);
|
||||||
self.view.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
self.view.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
||||||
|
self.contentView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
||||||
self.keyBoardMainView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
self.keyBoardMainView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
||||||
// 触发键区按主题重绘
|
// 触发键区按主题重绘
|
||||||
if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
|
if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
|
||||||
@@ -629,8 +763,62 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - Layout Helpers
|
||||||
|
|
||||||
|
- (CGFloat)kb_portraitWidth {
|
||||||
|
CGSize s = [UIScreen mainScreen].bounds.size;
|
||||||
|
return MIN(s.width, s.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CGFloat)kb_keyboardHeightForWidth:(CGFloat)width {
|
||||||
|
if (width <= 0) { width = KB_DESIGN_WIDTH; }
|
||||||
|
return kKBKeyboardBaseHeight * (width / KB_DESIGN_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_updateKeyboardLayoutIfNeeded {
|
||||||
|
CGFloat portraitWidth = [self kb_portraitWidth];
|
||||||
|
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
|
||||||
|
CGFloat containerWidth = CGRectGetWidth(self.view.superview.bounds);
|
||||||
|
if (containerWidth <= 0) {
|
||||||
|
containerWidth = CGRectGetWidth(self.view.window.bounds);
|
||||||
|
}
|
||||||
|
if (containerWidth <= 0) {
|
||||||
|
containerWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL widthChanged = (fabs(self.kb_lastPortraitWidth - portraitWidth) >= 0.5);
|
||||||
|
BOOL heightChanged = (fabs(self.kb_lastKeyboardHeight - keyboardHeight) >= 0.5);
|
||||||
|
if (!widthChanged && !heightChanged && containerWidth > 0 && self.kb_widthConstraint.constant == containerWidth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.kb_lastPortraitWidth = portraitWidth;
|
||||||
|
self.kb_lastKeyboardHeight = keyboardHeight;
|
||||||
|
|
||||||
|
if (self.kb_heightConstraint) {
|
||||||
|
self.kb_heightConstraint.constant = keyboardHeight;
|
||||||
|
}
|
||||||
|
if (containerWidth > 0 && self.kb_widthConstraint) {
|
||||||
|
self.kb_widthConstraint.constant = containerWidth;
|
||||||
|
}
|
||||||
|
if (self.contentWidthConstraint) {
|
||||||
|
[self.contentWidthConstraint setOffset:portraitWidth];
|
||||||
|
}
|
||||||
|
if (self.contentHeightConstraint) {
|
||||||
|
[self.contentHeightConstraint setOffset:keyboardHeight];
|
||||||
|
}
|
||||||
|
[self.view layoutIfNeeded];
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Lazy
|
#pragma mark - Lazy
|
||||||
|
|
||||||
|
- (UIView *)contentView {
|
||||||
|
if (!_contentView) {
|
||||||
|
_contentView = [[UIView alloc] init];
|
||||||
|
_contentView.backgroundColor = [UIColor clearColor];
|
||||||
|
}
|
||||||
|
return _contentView;
|
||||||
|
}
|
||||||
|
|
||||||
- (UIImageView *)bgImageView {
|
- (UIImageView *)bgImageView {
|
||||||
if (!_bgImageView) {
|
if (!_bgImageView) {
|
||||||
_bgImageView = [[UIImageView alloc] init];
|
_bgImageView = [[UIImageView alloc] init];
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#import "KBHUD.h" // 复用 App 内的 HUD 封装
|
#import "KBHUD.h" // 复用 App 内的 HUD 封装
|
||||||
#import "KBLocalizationManager.h" // 复用多语言封装(可在扩展内使用)
|
#import "KBLocalizationManager.h" // 复用多语言封装(可在扩展内使用)
|
||||||
#import "KBMaiPointReporter.h"
|
#import "KBMaiPointReporter.h"
|
||||||
|
//#import "KBLog.h"
|
||||||
|
|
||||||
|
|
||||||
// 通用链接(Universal Links)统一配置
|
// 通用链接(Universal Links)统一配置
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
|
|
||||||
- (instancetype)initWithContainerView:(UIView *)containerView;
|
- (instancetype)initWithContainerView:(UIView *)containerView;
|
||||||
|
|
||||||
/// 配置删除按钮(包含长按删除;可选是否显示“立刻清空”提示)
|
/// 配置删除按钮(包含长按删除;可选是否显示“上滑清空”提示)
|
||||||
- (void)bindDeleteButton:(nullable UIView *)button showClearLabel:(BOOL)showClearLabel;
|
- (void)bindDeleteButton:(nullable UIView *)button showClearLabel:(BOOL)showClearLabel;
|
||||||
|
|
||||||
/// 触发“立刻清空”逻辑(可用于功能面板的清空按钮)
|
/// 触发“上滑清空”逻辑(可用于功能面板的清空按钮)
|
||||||
- (void)performClearAction;
|
- (void)performClearAction;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -7,24 +7,25 @@
|
|||||||
#import "KBResponderUtils.h"
|
#import "KBResponderUtils.h"
|
||||||
#import "KBSkinManager.h"
|
#import "KBSkinManager.h"
|
||||||
#import "KBBackspaceUndoManager.h"
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
|
||||||
static const NSTimeInterval kKBBackspaceLongPressMinDuration = 0.35;
|
static const NSTimeInterval kKBBackspaceLongPressMinDuration = 0.35;
|
||||||
static const NSTimeInterval kKBBackspaceRepeatInterval = 0.06;
|
static const NSTimeInterval kKBBackspaceRepeatInterval = 0.06;
|
||||||
static const NSTimeInterval kKBBackspaceChunkStartDelay = 1.0;
|
static const NSTimeInterval kKBBackspaceChunkStartDelay = 0.6;
|
||||||
static const NSTimeInterval kKBBackspaceChunkRepeatInterval = 0.1;
|
static const NSTimeInterval kKBBackspaceChunkRepeatInterval = 0.1;
|
||||||
static const NSTimeInterval kKBBackspaceChunkFastDelay = 1.4;
|
static const NSTimeInterval kKBBackspaceChunkFastDelay = 1.2;
|
||||||
static const NSInteger kKBBackspaceChunkSize = 6;
|
static const NSInteger kKBBackspaceChunkSize = 8;
|
||||||
static const NSInteger kKBBackspaceChunkSizeFast = 12;
|
static const NSInteger kKBBackspaceChunkSizeFast = 16;
|
||||||
static const CGFloat kKBBackspaceClearLabelCornerRadius = 8.0;
|
static const CGFloat kKBBackspaceClearLabelCornerRadius = 8.0;
|
||||||
static const CGFloat kKBBackspaceClearLabelHeight = 34;
|
static const CGFloat kKBBackspaceClearLabelHeight = 34;
|
||||||
static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0;
|
static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0;
|
||||||
static const CGFloat kKBBackspaceClearLabelTopGap = 6.0;
|
static const CGFloat kKBBackspaceClearLabelTopGap = 6.0;
|
||||||
static const CGFloat kKBBackspaceClearLabelHorizontalInset = 6.0;
|
static const CGFloat kKBBackspaceClearLabelHorizontalInset = 6.0;
|
||||||
static const NSInteger kKBBackspaceClearBatchSize = 24;
|
static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.02;
|
||||||
static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.005;
|
|
||||||
static const NSInteger kKBBackspaceClearMaxDeletes = 10000;
|
static const NSInteger kKBBackspaceClearMaxDeletes = 10000;
|
||||||
static const NSInteger kKBBackspaceClearEmptyContextMaxRounds = 40;
|
static const NSInteger kKBBackspaceClearEmptyContextMaxRounds = 40;
|
||||||
static const NSInteger kKBBackspaceClearMaxStep = 80;
|
static const NSInteger kKBBackspaceClearMaxStep = 80;
|
||||||
|
static const NSInteger kKBBackspaceClearDeletesPerTick = 10;
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||||
KBBackspaceChunkClassUnknown = 0,
|
KBBackspaceChunkClassUnknown = 0,
|
||||||
@@ -34,6 +35,12 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
KBBackspaceChunkClassOther
|
KBBackspaceChunkClassOther
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, KBClearPhase) {
|
||||||
|
KBClearPhaseSkipWhitespace = 0,
|
||||||
|
KBClearPhaseSkipTrailingBoundary,
|
||||||
|
KBClearPhaseDeleteUntilBoundary
|
||||||
|
};
|
||||||
|
|
||||||
@interface KBBackspaceLongPressHandler ()
|
@interface KBBackspaceLongPressHandler ()
|
||||||
@property (nonatomic, weak) UIView *containerView;
|
@property (nonatomic, weak) UIView *containerView;
|
||||||
@property (nonatomic, weak) UIView *backspaceButton;
|
@property (nonatomic, weak) UIView *backspaceButton;
|
||||||
@@ -50,6 +57,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
||||||
@property (nonatomic, copy) NSString *pendingClearBefore;
|
@property (nonatomic, copy) NSString *pendingClearBefore;
|
||||||
@property (nonatomic, copy) NSString *pendingClearAfter;
|
@property (nonatomic, copy) NSString *pendingClearAfter;
|
||||||
|
@property (nonatomic, assign) KBClearPhase backspaceClearPhase;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KBBackspaceLongPressHandler
|
@implementation KBBackspaceLongPressHandler
|
||||||
@@ -57,6 +65,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
- (instancetype)initWithContainerView:(UIView *)containerView {
|
- (instancetype)initWithContainerView:(UIView *)containerView {
|
||||||
if (self = [super init]) {
|
if (self = [super init]) {
|
||||||
_containerView = containerView;
|
_containerView = containerView;
|
||||||
|
_backspaceClearPhase = KBClearPhaseSkipWhitespace;
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -103,9 +112,17 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
}
|
}
|
||||||
switch (gr.state) {
|
switch (gr.state) {
|
||||||
case UIGestureRecognizerStateBegan: {
|
case UIGestureRecognizerStateBegan: {
|
||||||
[self kb_captureDeletionSnapshotIfNeeded];
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
|
if (ivc) {
|
||||||
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
||||||
|
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:proxy.documentContextBeforeInput
|
||||||
|
after:proxy.documentContextAfterInput];
|
||||||
|
}
|
||||||
if (self.showClearLabelEnabled) {
|
if (self.showClearLabelEnabled) {
|
||||||
[self kb_capturePendingClearSnapshotIfNeeded];
|
[self kb_capturePendingClearSnapshotIfNeeded];
|
||||||
|
[[KBInputBufferManager shared] beginPendingClearSnapshot];
|
||||||
}
|
}
|
||||||
self.backspaceHoldToken += 1;
|
self.backspaceHoldToken += 1;
|
||||||
NSUInteger token = self.backspaceHoldToken;
|
NSUInteger token = self.backspaceHoldToken;
|
||||||
@@ -141,6 +158,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
if (before.length == 0) { before = [KBInputBufferManager shared].liveText ?: @""; }
|
||||||
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
|
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
|
||||||
NSInteger deleteCount = 1;
|
NSInteger deleteCount = 1;
|
||||||
if (before.length > 0) {
|
if (before.length > 0) {
|
||||||
@@ -152,9 +170,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
[self kb_showBackspaceClearLabelIfNeeded];
|
[self kb_showBackspaceClearLabelIfNeeded];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (NSInteger i = 0; i < deleteCount; i++) {
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:(NSUInteger)deleteCount];
|
||||||
[proxy deleteBackward];
|
[[KBInputBufferManager shared] applyHoldDeleteCount:(NSUInteger)deleteCount];
|
||||||
}
|
|
||||||
|
|
||||||
NSTimeInterval interval = [self kb_backspaceRepeatIntervalForElapsed:elapsed];
|
NSTimeInterval interval = [self kb_backspaceRepeatIntervalForElapsed:elapsed];
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
@@ -193,34 +210,77 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||||
asciiWordSet = [NSCharacterSet characterSetWithCharactersInString:
|
asciiWordSet = [NSCharacterSet characterSetWithCharactersInString:
|
||||||
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"];
|
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"];
|
||||||
punctuationSet = [NSCharacterSet punctuationCharacterSet];
|
NSMutableCharacterSet *punct = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
|
||||||
|
// 补齐常见中文/全角标点(避免 chunk 总是只删 1 个符号)
|
||||||
|
[punct addCharactersInString:@",。!?;:、()【】《》“”‘’·…—"];
|
||||||
|
punctuationSet = [punct copy];
|
||||||
});
|
});
|
||||||
|
|
||||||
__block NSInteger deleteCount = 0;
|
__block NSInteger deleteCount = 0;
|
||||||
__block KBBackspaceChunkClass chunkClass = KBBackspaceChunkClassUnknown;
|
typedef NS_ENUM(NSInteger, KBBackspaceChunkPhase) {
|
||||||
|
KBBackspaceChunkPhaseWhitespace = 0,
|
||||||
|
KBBackspaceChunkPhasePunctuation,
|
||||||
|
KBBackspaceChunkPhaseCore
|
||||||
|
};
|
||||||
|
__block KBBackspaceChunkPhase phase = KBBackspaceChunkPhaseWhitespace;
|
||||||
|
__block KBBackspaceChunkClass coreClass = KBBackspaceChunkClassUnknown;
|
||||||
|
|
||||||
[context enumerateSubstringsInRange:NSMakeRange(0, context.length)
|
[context enumerateSubstringsInRange:NSMakeRange(0, context.length)
|
||||||
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
||||||
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
||||||
if (substring.length == 0) { return; }
|
if (substring.length == 0) { return; }
|
||||||
KBBackspaceChunkClass currentClass = KBBackspaceChunkClassOther;
|
if (deleteCount >= maxCount) {
|
||||||
if ([substring rangeOfCharacterFromSet:whitespaceSet].location != NSNotFound) {
|
|
||||||
currentClass = KBBackspaceChunkClassWhitespace;
|
|
||||||
} else if ([substring rangeOfCharacterFromSet:asciiWordSet].location != NSNotFound) {
|
|
||||||
currentClass = KBBackspaceChunkClassASCIIWord;
|
|
||||||
} else if ([substring rangeOfCharacterFromSet:punctuationSet].location != NSNotFound) {
|
|
||||||
currentClass = KBBackspaceChunkClassPunctuation;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunkClass == KBBackspaceChunkClassUnknown) {
|
|
||||||
chunkClass = currentClass;
|
|
||||||
} else if (chunkClass != currentClass) {
|
|
||||||
*stop = YES;
|
*stop = YES;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCount += 1;
|
KBBackspaceChunkClass currentClass = KBBackspaceChunkClassOther;
|
||||||
|
if ([substring rangeOfCharacterFromSet:whitespaceSet].location != NSNotFound) {
|
||||||
|
currentClass = KBBackspaceChunkClassWhitespace;
|
||||||
|
} else if ([substring rangeOfCharacterFromSet:punctuationSet].location != NSNotFound) {
|
||||||
|
currentClass = KBBackspaceChunkClassPunctuation;
|
||||||
|
} else if ([substring rangeOfCharacterFromSet:asciiWordSet].location != NSNotFound) {
|
||||||
|
currentClass = KBBackspaceChunkClassASCIIWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL consumed = NO;
|
||||||
|
while (!consumed) {
|
||||||
|
if (phase == KBBackspaceChunkPhaseWhitespace) {
|
||||||
|
if (currentClass == KBBackspaceChunkClassWhitespace) {
|
||||||
|
deleteCount += 1;
|
||||||
|
consumed = YES;
|
||||||
|
} else {
|
||||||
|
phase = KBBackspaceChunkPhasePunctuation;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase == KBBackspaceChunkPhasePunctuation) {
|
||||||
|
if (currentClass == KBBackspaceChunkClassPunctuation) {
|
||||||
|
deleteCount += 1;
|
||||||
|
consumed = YES;
|
||||||
|
} else {
|
||||||
|
phase = KBBackspaceChunkPhaseCore;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phase == Core:连续删同一类(ASCII 单词 / 其它),让效果更像微信“几个字一组”
|
||||||
|
if (coreClass == KBBackspaceChunkClassUnknown) {
|
||||||
|
coreClass = currentClass;
|
||||||
|
}
|
||||||
|
if (currentClass != coreClass) {
|
||||||
|
*stop = YES;
|
||||||
|
consumed = YES;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
deleteCount += 1;
|
||||||
|
consumed = YES;
|
||||||
|
}
|
||||||
|
|
||||||
if (deleteCount >= maxCount) {
|
if (deleteCount >= maxCount) {
|
||||||
*stop = YES;
|
*stop = YES;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
|
||||||
@@ -229,13 +289,16 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
|
|
||||||
- (NSInteger)kb_clearDeleteCountForContext:(NSString *)context
|
- (NSInteger)kb_clearDeleteCountForContext:(NSString *)context
|
||||||
hitBoundary:(BOOL *)hitBoundary {
|
hitBoundary:(BOOL *)hitBoundary {
|
||||||
if (context.length == 0) { return kKBBackspaceClearBatchSize; }
|
if (context.length == 0) {
|
||||||
|
if (hitBoundary) { *hitBoundary = NO; }
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
static NSCharacterSet *sentenceBoundarySet = nil;
|
static NSCharacterSet *sentenceBoundarySet = nil;
|
||||||
static NSCharacterSet *whitespaceSet = nil;
|
static NSCharacterSet *whitespaceSet = nil;
|
||||||
static dispatch_once_t onceToken;
|
static dispatch_once_t onceToken;
|
||||||
dispatch_once(&onceToken, ^{
|
dispatch_once(&onceToken, ^{
|
||||||
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
|
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"];
|
||||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -310,6 +373,12 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
shouldClear = [self kb_isPointInsideBackspaceClearLabel:point];
|
shouldClear = [self kb_isPointInsideBackspaceClearLabel:point];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if DEBUG
|
||||||
|
NSLog(@"[kb_handleBackspaceLongPressEnded] shouldClear=%@ highlighted=%@ labelHidden=%@",
|
||||||
|
shouldClear ? @"YES" : @"NO",
|
||||||
|
self.backspaceClearHighlighted ? @"YES" : @"NO",
|
||||||
|
self.backspaceClearLabel.hidden ? @"YES" : @"NO");
|
||||||
|
#endif
|
||||||
self.backspaceHoldActive = NO;
|
self.backspaceHoldActive = NO;
|
||||||
self.backspaceChunkModeActive = NO;
|
self.backspaceChunkModeActive = NO;
|
||||||
self.backspaceHoldToken += 1;
|
self.backspaceHoldToken += 1;
|
||||||
@@ -320,6 +389,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
} else {
|
} else {
|
||||||
self.pendingClearBefore = nil;
|
self.pendingClearBefore = nil;
|
||||||
self.pendingClearAfter = nil;
|
self.pendingClearAfter = nil;
|
||||||
|
[[KBInputBufferManager shared] clearPendingClearSnapshot];
|
||||||
|
[[KBInputBufferManager shared] commitLiveToManual];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,13 +502,14 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
if (ivc) {
|
if (ivc) {
|
||||||
NSString *before = self.pendingClearBefore ?: (ivc.textDocumentProxy.documentContextBeforeInput ?: @"");
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
NSString *after = self.pendingClearAfter ?: (ivc.textDocumentProxy.documentContextAfterInput ?: @"");
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
||||||
[[KBBackspaceUndoManager shared] recordClearWithContextBefore:before after:after];
|
|
||||||
}
|
}
|
||||||
self.pendingClearBefore = nil;
|
self.pendingClearBefore = nil;
|
||||||
self.pendingClearAfter = nil;
|
self.pendingClearAfter = nil;
|
||||||
|
[[KBInputBufferManager shared] clearPendingClearSnapshot];
|
||||||
self.backspaceClearToken += 1;
|
self.backspaceClearToken += 1;
|
||||||
|
self.backspaceClearPhase = KBClearPhaseSkipWhitespace;
|
||||||
NSUInteger token = self.backspaceClearToken;
|
NSUInteger token = self.backspaceClearToken;
|
||||||
[self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0];
|
[self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0];
|
||||||
}
|
}
|
||||||
@@ -450,40 +522,101 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
if (!ivc) { return; }
|
if (!ivc) { return; }
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
|
||||||
NSInteger count = before.length;
|
|
||||||
NSInteger batch = 0;
|
|
||||||
NSInteger nextEmptyRounds = emptyRounds;
|
NSInteger nextEmptyRounds = emptyRounds;
|
||||||
BOOL hitBoundary = NO;
|
static NSCharacterSet *stopBoundarySet = nil;
|
||||||
if (count > 0) {
|
static NSCharacterSet *trailingBoundarySet = nil;
|
||||||
batch = [self kb_clearDeleteCountForContext:before hitBoundary:&hitBoundary];
|
static NSCharacterSet *trailingWhitespaceSet = nil;
|
||||||
nextEmptyRounds = 0;
|
static dispatch_once_t onceToken;
|
||||||
} else {
|
dispatch_once(&onceToken, ^{
|
||||||
batch = kKBBackspaceClearBatchSize;
|
// stopBoundary: 遇到这些符号就停(不删除它)
|
||||||
nextEmptyRounds = emptyRounds + 1;
|
// - 句末符号:. ! ? 。!?
|
||||||
}
|
// - 省略号:…(中文里“……”常用作句/段落的停顿)
|
||||||
if (batch <= 0) { batch = 1; }
|
// - 换行:\n \r(段落边界,避免一次“清空”跨段把全文删完)
|
||||||
|
stopBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?…\n\r\u2028\u2029"];
|
||||||
|
|
||||||
if (guard >= kKBBackspaceClearMaxDeletes ||
|
// trailingBoundary: 允许作为“尾部句末符号”先删掉,再继续删上一句(更接近微信体验)
|
||||||
nextEmptyRounds > kKBBackspaceClearEmptyContextMaxRounds) {
|
// 注意:不要把换行/省略号放进来,否则可能跨段/跨停顿继续删。
|
||||||
|
trailingBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"];
|
||||||
|
|
||||||
|
// trailingWhitespace: 只跳过空格/Tab(不包含换行,换行由 stopBoundarySet 处理)
|
||||||
|
trailingWhitespaceSet = [NSCharacterSet whitespaceCharacterSet];
|
||||||
|
});
|
||||||
|
KBClearPhase phase = self.backspaceClearPhase;
|
||||||
|
|
||||||
|
NSInteger deletedThisTick = 0;
|
||||||
|
BOOL shouldStop = NO;
|
||||||
|
NSString *lastBefore = nil;
|
||||||
|
for (NSInteger i = 0; i < kKBBackspaceClearDeletesPerTick; i++) {
|
||||||
|
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
if (before.length == 0) {
|
||||||
|
nextEmptyRounds += 1;
|
||||||
|
// 宿主(微信/QQ 等)可能在长文本场景下返回空 context,即使还有很多内容。
|
||||||
|
// 为了避免一次“清空”误删全文:一旦拿不到 before,就立刻停止本次清空。
|
||||||
|
shouldStop = YES;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
nextEmptyRounds = 0;
|
||||||
|
|
||||||
|
if (lastBefore && [before isEqualToString:lastBefore] && deletedThisTick > 0) {
|
||||||
|
// 宿主未及时刷新 context,留到下一 tick 再继续,避免越界/重复记录
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lastBefore = before;
|
||||||
|
|
||||||
|
// 取最后一个组合字符
|
||||||
|
__block NSString *lastChar = @"";
|
||||||
|
[before enumerateSubstringsInRange:NSMakeRange(0, before.length)
|
||||||
|
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
||||||
|
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
||||||
|
lastChar = substring ?: @"";
|
||||||
|
*stop = YES;
|
||||||
|
}];
|
||||||
|
if (lastChar.length == 0) { break; }
|
||||||
|
|
||||||
|
BOOL isWhitespace = ([lastChar rangeOfCharacterFromSet:trailingWhitespaceSet].location != NSNotFound);
|
||||||
|
BOOL isStopBoundary = ([lastChar rangeOfCharacterFromSet:stopBoundarySet].location != NSNotFound);
|
||||||
|
BOOL isTrailingBoundary = ([lastChar rangeOfCharacterFromSet:trailingBoundarySet].location != NSNotFound);
|
||||||
|
|
||||||
|
if (phase == KBClearPhaseSkipWhitespace) {
|
||||||
|
if (isWhitespace) {
|
||||||
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
||||||
|
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
||||||
|
deletedThisTick += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
phase = KBClearPhaseSkipTrailingBoundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase == KBClearPhaseSkipTrailingBoundary) {
|
||||||
|
if (isTrailingBoundary) {
|
||||||
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
||||||
|
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
||||||
|
deletedThisTick += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
phase = KBClearPhaseDeleteUntilBoundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phase == DeleteUntilBoundary
|
||||||
|
if (isStopBoundary) {
|
||||||
|
shouldStop = YES; // 保留该句末符号
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
||||||
|
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
||||||
|
deletedThisTick += 1;
|
||||||
|
if (guard + deletedThisTick >= kKBBackspaceClearMaxDeletes) { break; }
|
||||||
|
if (deletedThisTick >= kKBBackspaceClearMaxStep) { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backspaceClearPhase = phase;
|
||||||
|
NSInteger nextGuard = guard + deletedThisTick;
|
||||||
|
if (nextGuard >= kKBBackspaceClearMaxDeletes ||
|
||||||
|
nextEmptyRounds > kKBBackspaceClearEmptyContextMaxRounds ||
|
||||||
|
shouldStop) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (NSInteger i = 0; i < batch; i++) {
|
|
||||||
[proxy deleteBackward];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSInteger nextGuard = guard + batch;
|
|
||||||
BOOL shouldContinue = NO;
|
|
||||||
if (count > 0 && !hitBoundary) {
|
|
||||||
if (count > batch) {
|
|
||||||
shouldContinue = YES;
|
|
||||||
} else if ([proxy hasText]) {
|
|
||||||
shouldContinue = YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldContinue) { return; }
|
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
||||||
(int64_t)(kKBBackspaceClearBatchInterval * NSEC_PER_SEC)),
|
(int64_t)(kKBBackspaceClearBatchInterval * NSEC_PER_SEC)),
|
||||||
@@ -513,7 +646,6 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)kb_capturePendingClearSnapshotIfNeeded {
|
- (void)kb_capturePendingClearSnapshotIfNeeded {
|
||||||
if ([KBBackspaceUndoManager shared].hasUndo) { return; }
|
|
||||||
if (self.pendingClearBefore.length > 0 || self.pendingClearAfter.length > 0) { return; }
|
if (self.pendingClearBefore.length > 0 || self.pendingClearAfter.length > 0) { return; }
|
||||||
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
@@ -521,6 +653,10 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
self.pendingClearBefore = proxy.documentContextBeforeInput ?: @"";
|
self.pendingClearBefore = proxy.documentContextBeforeInput ?: @"";
|
||||||
self.pendingClearAfter = proxy.documentContextAfterInput ?: @"";
|
self.pendingClearAfter = proxy.documentContextAfterInput ?: @"";
|
||||||
|
#if DEBUG
|
||||||
|
NSLog(@"[kb_capturePendingClearSnapshotIfNeeded/before] len=%lu text=%@", (unsigned long)self.pendingClearBefore.length, self.pendingClearBefore);
|
||||||
|
NSLog(@"[kb_capturePendingClearSnapshotIfNeeded/after] len=%lu text=%@", (unsigned long)self.pendingClearAfter.length, self.pendingClearAfter);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ extern NSNotificationName const KBBackspaceUndoStateDidChangeNotification;
|
|||||||
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput/AfterInput)。
|
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput/AfterInput)。
|
||||||
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after;
|
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after;
|
||||||
|
|
||||||
|
/// 记录本次将被 deleteBackward 的内容,并执行 deleteBackward(支持多次累计,撤销时一次性插回)。
|
||||||
|
- (void)captureAndDeleteBackwardFromProxy:(id<UITextDocumentProxy>)proxy count:(NSUInteger)count;
|
||||||
|
|
||||||
/// 在指定 responder 处执行撤销(向光标处插回删除的内容)
|
/// 在指定 responder 处执行撤销(向光标处插回删除的内容)
|
||||||
- (void)performUndoFromResponder:(UIResponder *)responder;
|
- (void)performUndoFromResponder:(UIResponder *)responder;
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,38 @@
|
|||||||
|
|
||||||
#import "KBBackspaceUndoManager.h"
|
#import "KBBackspaceUndoManager.h"
|
||||||
#import "KBResponderUtils.h"
|
#import "KBResponderUtils.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
|
||||||
NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification";
|
NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification";
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
static NSString *KBLogString(NSString *tag, NSString *text) {
|
||||||
|
NSString *safeTag = tag ?: @"";
|
||||||
|
NSString *safeText = text ?: @"";
|
||||||
|
if (safeText.length <= 2000) {
|
||||||
|
return [NSString stringWithFormat:@"[%@] len=%lu text=%@", safeTag, (unsigned long)safeText.length, safeText];
|
||||||
|
}
|
||||||
|
NSString *head = [safeText substringToIndex:800];
|
||||||
|
NSString *tail = [safeText substringFromIndex:safeText.length - 800];
|
||||||
|
return [NSString stringWithFormat:@"[%@] len=%lu head=%@ ... tail=%@", safeTag, (unsigned long)safeText.length, head, tail];
|
||||||
|
}
|
||||||
|
#define KB_UNDO_LOG(tag, text) NSLog(@"%@", KBLogString((tag), (text)))
|
||||||
|
#else
|
||||||
|
#define KB_UNDO_LOG(tag, text) do {} while(0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, KBUndoSnapshotSource) {
|
||||||
|
KBUndoSnapshotSourceNone = 0,
|
||||||
|
KBUndoSnapshotSourceDeletionSnapshot,
|
||||||
|
KBUndoSnapshotSourceClear
|
||||||
|
};
|
||||||
|
|
||||||
@interface KBBackspaceUndoManager ()
|
@interface KBBackspaceUndoManager ()
|
||||||
@property (nonatomic, copy) NSString *undoText;
|
@property (nonatomic, copy) NSString *undoText;
|
||||||
@property (nonatomic, assign) NSInteger undoAfterLength;
|
@property (nonatomic, assign) NSInteger undoAfterLength;
|
||||||
@property (nonatomic, assign) BOOL hasUndo;
|
@property (nonatomic, assign) BOOL hasUndo;
|
||||||
|
@property (nonatomic, assign) KBUndoSnapshotSource snapshotSource;
|
||||||
|
@property (nonatomic, strong) NSMutableArray<NSString *> *undoDeletedPieces;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KBBackspaceUndoManager
|
@implementation KBBackspaceUndoManager
|
||||||
@@ -29,55 +54,189 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
|
|||||||
if (self = [super init]) {
|
if (self = [super init]) {
|
||||||
_undoText = @"";
|
_undoText = @"";
|
||||||
_undoAfterLength = 0;
|
_undoAfterLength = 0;
|
||||||
|
_snapshotSource = KBUndoSnapshotSourceNone;
|
||||||
|
_undoDeletedPieces = [NSMutableArray array];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)captureAndDeleteBackwardFromProxy:(id<UITextDocumentProxy>)proxy count:(NSUInteger)count {
|
||||||
|
if (!proxy || count == 0) { return; }
|
||||||
|
|
||||||
|
NSString *selected = proxy.selectedText ?: @"";
|
||||||
|
NSString *ctxBefore = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
NSString *ctxAfter = proxy.documentContextAfterInput ?: @"";
|
||||||
|
NSUInteger ctxLen = ctxBefore.length + ctxAfter.length;
|
||||||
|
BOOL isSelectAllLike = (selected.length > 0 &&
|
||||||
|
(ctxLen == 0 || selected.length >= MAX((NSUInteger)40, ctxLen * 2)));
|
||||||
|
if (isSelectAllLike) {
|
||||||
|
// “全选删除”在微信/QQ中通常拿不到可靠的全文,因此禁用撤销,避免插回错误/不完整内容。
|
||||||
|
if (self.hasUndo) {
|
||||||
|
[self registerNonClearAction];
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
KB_UNDO_LOG(@"captureAndDelete/selectAllDisableUndo", selected);
|
||||||
|
#endif
|
||||||
|
[proxy deleteBackward];
|
||||||
|
[[KBInputBufferManager shared] resetWithText:@""];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self.hasUndo) {
|
||||||
|
[self.undoDeletedPieces removeAllObjects];
|
||||||
|
self.undoText = @"";
|
||||||
|
self.undoAfterLength = 0;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot;
|
||||||
|
[self kb_updateHasUndo:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL didAppend = NO;
|
||||||
|
NSString *lastObservedBefore = nil;
|
||||||
|
for (NSUInteger i = 0; i < count; i++) {
|
||||||
|
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
if (before.length > 0) {
|
||||||
|
// 若宿主在同一 runloop 内不更新 context,则跳过记录,避免把同一个字符重复记录成“多句”。
|
||||||
|
if (lastObservedBefore && [before isEqualToString:lastObservedBefore]) {
|
||||||
|
// still delete, but don't record
|
||||||
|
} else {
|
||||||
|
NSString *piece = [self kb_lastComposedCharacterFromString:before];
|
||||||
|
if (piece.length > 0) {
|
||||||
|
[self.undoDeletedPieces addObject:piece];
|
||||||
|
didAppend = YES;
|
||||||
|
}
|
||||||
|
lastObservedBefore = before;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[proxy deleteBackward];
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
if (didAppend) {
|
||||||
|
NSUInteger piecesCount = self.undoDeletedPieces.count;
|
||||||
|
if (piecesCount <= 20) {
|
||||||
|
KB_UNDO_LOG(@"captureAndDelete/undoInsertTextNow", [self kb_buildUndoInsertTextFromPieces]);
|
||||||
|
} else if (piecesCount % 50 == 0) {
|
||||||
|
NSString *lastPiece = self.undoDeletedPieces.lastObject ?: @"";
|
||||||
|
NSLog(@"[captureAndDelete/undoPieces] pieces=%lu lastPiece=%@",
|
||||||
|
(unsigned long)piecesCount,
|
||||||
|
lastPiece);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
- (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after {
|
- (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after {
|
||||||
if (self.undoText.length > 0) { return; }
|
if (self.hasUndo) { return; }
|
||||||
|
NSString *pending = [KBInputBufferManager shared].pendingClearSnapshot;
|
||||||
|
NSString *manual = [KBInputBufferManager shared].manualSnapshot;
|
||||||
|
NSString *fallbackText = (pending.length > 0) ? pending : ((manual.length > 0) ? manual : [KBInputBufferManager shared].liveText);
|
||||||
|
if (fallbackText.length > 0) {
|
||||||
|
self.undoText = fallbackText;
|
||||||
|
self.undoAfterLength = 0;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot;
|
||||||
|
KB_UNDO_LOG(@"recordDeletionSnapshot/fallback", self.undoText);
|
||||||
|
[self kb_updateHasUndo:YES];
|
||||||
|
return;
|
||||||
|
}
|
||||||
NSString *safeBefore = before ?: @"";
|
NSString *safeBefore = before ?: @"";
|
||||||
NSString *safeAfter = after ?: @"";
|
NSString *safeAfter = after ?: @"";
|
||||||
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
||||||
if (full.length == 0) { return; }
|
if (full.length == 0) { return; }
|
||||||
self.undoText = full;
|
self.undoText = full;
|
||||||
self.undoAfterLength = (NSInteger)safeAfter.length;
|
self.undoAfterLength = (NSInteger)safeAfter.length;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot;
|
||||||
|
KB_UNDO_LOG(@"recordDeletionSnapshot/context", self.undoText);
|
||||||
|
[self kb_updateHasUndo:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after {
|
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after {
|
||||||
if (self.undoText.length == 0) {
|
NSString *pending = [KBInputBufferManager shared].pendingClearSnapshot;
|
||||||
NSString *safeBefore = before ?: @"";
|
NSString *manual = [KBInputBufferManager shared].manualSnapshot;
|
||||||
NSString *safeAfter = after ?: @"";
|
NSString *fallbackText = (pending.length > 0) ? pending : ((manual.length > 0) ? manual : [KBInputBufferManager shared].liveText);
|
||||||
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
|
||||||
if (full.length > 0) {
|
NSString *safeBefore = before ?: @"";
|
||||||
self.undoText = full;
|
NSString *safeAfter = after ?: @"";
|
||||||
self.undoAfterLength = (NSInteger)safeAfter.length;
|
NSString *contextText = [[safeBefore stringByAppendingString:safeAfter] copy];
|
||||||
|
|
||||||
|
NSString *candidate = (fallbackText.length > 0) ? fallbackText : contextText;
|
||||||
|
NSInteger candidateAfterLen = (fallbackText.length > 0) ? 0 : (NSInteger)safeAfter.length;
|
||||||
|
|
||||||
|
if (candidate.length == 0) { return; }
|
||||||
|
|
||||||
|
KB_UNDO_LOG(@"recordClear/candidate", candidate);
|
||||||
|
|
||||||
|
if (self.undoText.length > 0) {
|
||||||
|
if (self.snapshotSource == KBUndoSnapshotSourceClear) {
|
||||||
|
KB_UNDO_LOG(@"recordClear/ignored(alreadyClear)", self.undoText);
|
||||||
|
[self kb_updateHasUndo:YES];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (self.snapshotSource == KBUndoSnapshotSourceDeletionSnapshot) {
|
||||||
|
if (candidate.length > self.undoText.length) {
|
||||||
|
self.undoText = candidate;
|
||||||
|
self.undoAfterLength = candidateAfterLen;
|
||||||
|
KB_UNDO_LOG(@"recordClear/upgradedFromDeletion", self.undoText);
|
||||||
|
} else {
|
||||||
|
KB_UNDO_LOG(@"recordClear/keepDeletionSnapshot", self.undoText);
|
||||||
|
}
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceClear;
|
||||||
|
[self kb_updateHasUndo:YES];
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (self.undoText.length == 0) { return; }
|
|
||||||
|
self.undoText = candidate;
|
||||||
|
self.undoAfterLength = candidateAfterLen;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceClear;
|
||||||
|
KB_UNDO_LOG(@"recordClear/set", self.undoText);
|
||||||
[self kb_updateHasUndo:YES];
|
[self kb_updateHasUndo:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)performUndoFromResponder:(UIResponder *)responder {
|
- (void)performUndoFromResponder:(UIResponder *)responder {
|
||||||
if (self.undoText.length == 0) { return; }
|
if (!self.hasUndo) { return; }
|
||||||
UIInputViewController *ivc = KBFindInputViewController(responder);
|
UIInputViewController *ivc = KBFindInputViewController(responder);
|
||||||
if (!ivc) { return; }
|
if (!ivc) { return; }
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
[self kb_clearAllTextForProxy:proxy];
|
NSString *curBefore = proxy.documentContextBeforeInput ?: @"";
|
||||||
[proxy insertText:self.undoText];
|
NSString *curAfter = proxy.documentContextAfterInput ?: @"";
|
||||||
if (self.undoAfterLength > 0 &&
|
KB_UNDO_LOG(@"performUndo/currentBefore", curBefore);
|
||||||
[proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
|
KB_UNDO_LOG(@"performUndo/currentAfter", curAfter);
|
||||||
[proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength];
|
NSString *insertText = [self kb_buildUndoInsertTextFromPieces];
|
||||||
|
if (insertText.length > 0) {
|
||||||
|
KB_UNDO_LOG(@"performUndo/insertDeletedText", insertText);
|
||||||
|
[proxy insertText:insertText];
|
||||||
|
[[KBInputBufferManager shared] appendText:insertText];
|
||||||
|
} else if (self.undoText.length > 0) {
|
||||||
|
KB_UNDO_LOG(@"performUndo/fallbackUndoText", self.undoText);
|
||||||
|
[self kb_clearAllTextForProxy:proxy];
|
||||||
|
[proxy insertText:self.undoText];
|
||||||
|
if (self.undoAfterLength > 0 &&
|
||||||
|
[proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
|
||||||
|
[proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength];
|
||||||
|
}
|
||||||
|
[[KBInputBufferManager shared] resetWithText:self.undoText];
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.undoText = @"";
|
self.undoText = @"";
|
||||||
self.undoAfterLength = 0;
|
self.undoAfterLength = 0;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceNone;
|
||||||
|
[self.undoDeletedPieces removeAllObjects];
|
||||||
[self kb_updateHasUndo:NO];
|
[self kb_updateHasUndo:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)registerNonClearAction {
|
- (void)registerNonClearAction {
|
||||||
if (self.undoText.length == 0) { return; }
|
if (!self.hasUndo) { return; }
|
||||||
|
if (self.undoText.length > 0) {
|
||||||
|
KB_UNDO_LOG(@"registerNonClearAction/clearUndoText", self.undoText);
|
||||||
|
}
|
||||||
|
if (self.undoDeletedPieces.count > 0) {
|
||||||
|
KB_UNDO_LOG(@"registerNonClearAction/clearDeletedPieces", [self kb_buildUndoInsertTextFromPieces]);
|
||||||
|
}
|
||||||
self.undoText = @"";
|
self.undoText = @"";
|
||||||
self.undoAfterLength = 0;
|
self.undoAfterLength = 0;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceNone;
|
||||||
|
[self.undoDeletedPieces removeAllObjects];
|
||||||
[self kb_updateHasUndo:NO];
|
[self kb_updateHasUndo:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +248,29 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
|
|||||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self];
|
[[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_lastComposedCharacterFromString:(NSString *)text {
|
||||||
|
if (text.length == 0) { return @""; }
|
||||||
|
__block NSString *last = @"";
|
||||||
|
[text enumerateSubstringsInRange:NSMakeRange(0, text.length)
|
||||||
|
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
||||||
|
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
||||||
|
last = substring ?: @"";
|
||||||
|
*stop = YES;
|
||||||
|
}];
|
||||||
|
return last ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_buildUndoInsertTextFromPieces {
|
||||||
|
if (self.undoDeletedPieces.count == 0) { return @""; }
|
||||||
|
NSMutableString *result = [NSMutableString string];
|
||||||
|
for (NSInteger i = (NSInteger)self.undoDeletedPieces.count - 1; i >= 0; i--) {
|
||||||
|
NSString *piece = self.undoDeletedPieces[(NSUInteger)i] ?: @"";
|
||||||
|
if (piece.length == 0) { continue; }
|
||||||
|
[result appendString:piece];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
static const NSInteger kKBUndoClearMaxRounds = 200;
|
static const NSInteger kKBUndoClearMaxRounds = 200;
|
||||||
|
|
||||||
- (void)kb_clearAllTextForProxy:(id<UITextDocumentProxy>)proxy {
|
- (void)kb_clearAllTextForProxy:(id<UITextDocumentProxy>)proxy {
|
||||||
|
|||||||
34
CustomKeyboard/Utils/KBInputBufferManager.h
Normal file
34
CustomKeyboard/Utils/KBInputBufferManager.h
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@protocol UITextDocumentProxy;
|
||||||
|
|
||||||
|
@interface KBInputBufferManager : NSObject
|
||||||
|
|
||||||
|
+ (instancetype)shared;
|
||||||
|
|
||||||
|
@property (nonatomic, copy, readonly) NSString *liveText;
|
||||||
|
@property (nonatomic, copy, readonly) NSString *manualSnapshot;
|
||||||
|
@property (nonatomic, copy, readonly) NSString *pendingClearSnapshot;
|
||||||
|
|
||||||
|
- (void)seedIfEmptyWithContextBefore:(nullable NSString *)before after:(nullable NSString *)after;
|
||||||
|
- (void)updateFromExternalContextBefore:(nullable NSString *)before after:(nullable NSString *)after;
|
||||||
|
- (void)refreshFromProxyIfPossible:(nullable id<UITextDocumentProxy>)proxy;
|
||||||
|
- (void)prepareSnapshotForDeleteWithContextBefore:(nullable NSString *)before
|
||||||
|
after:(nullable NSString *)after;
|
||||||
|
- (void)beginPendingClearSnapshot;
|
||||||
|
- (void)clearPendingClearSnapshot;
|
||||||
|
- (void)resetWithText:(NSString *)text;
|
||||||
|
- (void)appendText:(NSString *)text;
|
||||||
|
- (void)deleteBackwardByCount:(NSUInteger)count;
|
||||||
|
- (void)replaceTailWithText:(NSString *)text deleteCount:(NSUInteger)count;
|
||||||
|
- (void)applyHoldDeleteCount:(NSUInteger)count;
|
||||||
|
- (void)applyClearDeleteCount:(NSUInteger)count;
|
||||||
|
- (void)clearAllLiveText;
|
||||||
|
- (void)commitLiveToManual;
|
||||||
|
- (void)restoreManualSnapshot;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
279
CustomKeyboard/Utils/KBInputBufferManager.m
Normal file
279
CustomKeyboard/Utils/KBInputBufferManager.m
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
static NSString *KBLogString2(NSString *tag, NSString *text) {
|
||||||
|
NSString *safeTag = tag ?: @"";
|
||||||
|
NSString *safeText = text ?: @"";
|
||||||
|
if (safeText.length <= 2000) {
|
||||||
|
return [NSString stringWithFormat:@"[%@] len=%lu text=%@", safeTag, (unsigned long)safeText.length, safeText];
|
||||||
|
}
|
||||||
|
NSString *head = [safeText substringToIndex:800];
|
||||||
|
NSString *tail = [safeText substringFromIndex:safeText.length - 800];
|
||||||
|
return [NSString stringWithFormat:@"[%@] len=%lu head=%@ ... tail=%@", safeTag, (unsigned long)safeText.length, head, tail];
|
||||||
|
}
|
||||||
|
#define KB_BUF_LOG(tag, text) NSLog(@"❤️=%@", KBLogString2((tag), (text)))
|
||||||
|
#else
|
||||||
|
#define KB_BUF_LOG(tag, text) do {} while(0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@interface KBInputBufferManager ()
|
||||||
|
@property (nonatomic, copy, readwrite) NSString *liveText;
|
||||||
|
@property (nonatomic, copy, readwrite) NSString *manualSnapshot;
|
||||||
|
@property (nonatomic, copy, readwrite) NSString *pendingClearSnapshot;
|
||||||
|
@property (nonatomic, assign) BOOL manualSnapshotDirty;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBInputBufferManager
|
||||||
|
|
||||||
|
+ (instancetype)shared {
|
||||||
|
static KBInputBufferManager *mgr = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
mgr = [[KBInputBufferManager alloc] init];
|
||||||
|
});
|
||||||
|
return mgr;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)init {
|
||||||
|
if (self = [super init]) {
|
||||||
|
_liveText = @"";
|
||||||
|
_manualSnapshot = @"";
|
||||||
|
_pendingClearSnapshot = @"";
|
||||||
|
_manualSnapshotDirty = NO;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)seedIfEmptyWithContextBefore:(NSString *)before after:(NSString *)after {
|
||||||
|
if (self.liveText.length > 0 || self.manualSnapshot.length > 0) { return; }
|
||||||
|
NSString *safeBefore = before ?: @"";
|
||||||
|
NSString *safeAfter = after ?: @"";
|
||||||
|
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
||||||
|
if (full.length == 0) { return; }
|
||||||
|
self.liveText = full;
|
||||||
|
self.manualSnapshot = full;
|
||||||
|
self.manualSnapshotDirty = NO;
|
||||||
|
KB_BUF_LOG(@"seedIfEmpty", full);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)updateFromExternalContextBefore:(NSString *)before after:(NSString *)after {
|
||||||
|
NSString *safeBefore = before ?: @"";
|
||||||
|
NSString *safeAfter = after ?: @"";
|
||||||
|
NSString *context = [safeBefore stringByAppendingString:safeAfter];
|
||||||
|
if (context.length == 0) { return; }
|
||||||
|
|
||||||
|
// 微信/QQ 等宿主通常只提供光标附近“截断窗口”,不应当作为全文快照。
|
||||||
|
// 这里只更新 liveText,给删除/清空逻辑做参考;manualSnapshot 仅由键盘自身输入/撤销来维护。
|
||||||
|
self.liveText = context;
|
||||||
|
self.manualSnapshotDirty = YES;
|
||||||
|
#if DEBUG
|
||||||
|
static NSUInteger sExternalLogCounter = 0;
|
||||||
|
sExternalLogCounter += 1;
|
||||||
|
if (sExternalLogCounter % 12 == 0) {
|
||||||
|
KB_BUF_LOG(@"updateFromExternalContext/liveOnly", context);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)refreshFromProxyIfPossible:(id<UITextDocumentProxy>)proxy {
|
||||||
|
NSString *harvested = [self kb_harvestFullTextFromProxy:proxy];
|
||||||
|
if (harvested.length == 0) {
|
||||||
|
KB_BUF_LOG(@"refreshFromProxy/failedOrUnsupported", @"");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL manualEmpty = (self.manualSnapshot.length == 0);
|
||||||
|
BOOL longerThanManual = (harvested.length > self.manualSnapshot.length);
|
||||||
|
if (!(manualEmpty || longerThanManual)) {
|
||||||
|
KB_BUF_LOG(@"refreshFromProxy/ignoredShorter", harvested);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.liveText = harvested;
|
||||||
|
self.manualSnapshot = harvested;
|
||||||
|
self.manualSnapshotDirty = NO;
|
||||||
|
KB_BUF_LOG(@"refreshFromProxy/accepted", harvested);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)prepareSnapshotForDeleteWithContextBefore:(NSString *)before
|
||||||
|
after:(NSString *)after {
|
||||||
|
NSString *safeBefore = before ?: @"";
|
||||||
|
NSString *safeAfter = after ?: @"";
|
||||||
|
NSString *context = [safeBefore stringByAppendingString:safeAfter];
|
||||||
|
|
||||||
|
BOOL manualValid = (self.manualSnapshot.length > 0 &&
|
||||||
|
(context.length == 0 ||
|
||||||
|
(self.manualSnapshot.length >= context.length &&
|
||||||
|
[self.manualSnapshot rangeOfString:context].location != NSNotFound)));
|
||||||
|
if (manualValid) { return; }
|
||||||
|
|
||||||
|
if (self.liveText.length > 0) {
|
||||||
|
self.manualSnapshot = self.liveText;
|
||||||
|
self.manualSnapshotDirty = NO;
|
||||||
|
KB_BUF_LOG(@"prepareSnapshotForDelete/fromLiveText", self.manualSnapshot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (context.length > 0) {
|
||||||
|
self.manualSnapshot = context;
|
||||||
|
self.manualSnapshotDirty = NO;
|
||||||
|
KB_BUF_LOG(@"prepareSnapshotForDelete/fromContext", self.manualSnapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)beginPendingClearSnapshot {
|
||||||
|
if (self.pendingClearSnapshot.length > 0) { return; }
|
||||||
|
if (self.manualSnapshot.length > 0) {
|
||||||
|
self.pendingClearSnapshot = self.manualSnapshot;
|
||||||
|
KB_BUF_LOG(@"beginPendingClearSnapshot/fromManual", self.pendingClearSnapshot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (self.liveText.length > 0) {
|
||||||
|
self.pendingClearSnapshot = self.liveText;
|
||||||
|
KB_BUF_LOG(@"beginPendingClearSnapshot/fromLive", self.pendingClearSnapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)clearPendingClearSnapshot {
|
||||||
|
self.pendingClearSnapshot = @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)resetWithText:(NSString *)text {
|
||||||
|
NSString *safe = text ?: @"";
|
||||||
|
self.liveText = safe;
|
||||||
|
self.manualSnapshot = safe;
|
||||||
|
self.pendingClearSnapshot = @"";
|
||||||
|
self.manualSnapshotDirty = NO;
|
||||||
|
KB_BUF_LOG(@"resetWithText", safe);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)appendText:(NSString *)text {
|
||||||
|
if (text.length == 0) { return; }
|
||||||
|
[self kb_syncManualSnapshotIfNeeded];
|
||||||
|
self.liveText = [self.liveText stringByAppendingString:text];
|
||||||
|
self.manualSnapshot = [self.manualSnapshot stringByAppendingString:text];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)deleteBackwardByCount:(NSUInteger)count {
|
||||||
|
if (count == 0) { return; }
|
||||||
|
self.liveText = [self kb_stringByDeletingComposedCharacters:count from:self.liveText];
|
||||||
|
self.manualSnapshot = [self kb_stringByDeletingComposedCharacters:count from:self.manualSnapshot];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)replaceTailWithText:(NSString *)text deleteCount:(NSUInteger)count {
|
||||||
|
[self kb_syncManualSnapshotIfNeeded];
|
||||||
|
[self deleteBackwardByCount:count];
|
||||||
|
[self appendText:text];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)applyHoldDeleteCount:(NSUInteger)count {
|
||||||
|
if (count == 0) { return; }
|
||||||
|
self.liveText = [self kb_stringByDeletingComposedCharacters:count from:self.liveText];
|
||||||
|
self.manualSnapshotDirty = YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)applyClearDeleteCount:(NSUInteger)count {
|
||||||
|
if (count == 0) { return; }
|
||||||
|
self.liveText = [self kb_stringByDeletingComposedCharacters:count from:self.liveText];
|
||||||
|
self.manualSnapshotDirty = YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)clearAllLiveText {
|
||||||
|
self.liveText = @"";
|
||||||
|
self.pendingClearSnapshot = @"";
|
||||||
|
self.manualSnapshotDirty = YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)commitLiveToManual {
|
||||||
|
self.manualSnapshot = self.liveText ?: @"";
|
||||||
|
self.manualSnapshotDirty = NO;
|
||||||
|
KB_BUF_LOG(@"commitLiveToManual", self.manualSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)restoreManualSnapshot {
|
||||||
|
self.liveText = self.manualSnapshot ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Helpers
|
||||||
|
|
||||||
|
- (void)kb_syncManualSnapshotIfNeeded {
|
||||||
|
if (!self.manualSnapshotDirty) { return; }
|
||||||
|
self.manualSnapshot = self.liveText ?: @"";
|
||||||
|
self.manualSnapshotDirty = NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_stringByDeletingComposedCharacters:(NSUInteger)count
|
||||||
|
from:(NSString *)text {
|
||||||
|
if (count == 0) { return text ?: @""; }
|
||||||
|
NSString *source = text ?: @"";
|
||||||
|
if (source.length == 0) { return @""; }
|
||||||
|
|
||||||
|
__block NSUInteger removed = 0;
|
||||||
|
__block NSUInteger endIndex = source.length;
|
||||||
|
[source enumerateSubstringsInRange:NSMakeRange(0, source.length)
|
||||||
|
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
||||||
|
usingBlock:^(__unused NSString *substring, NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
||||||
|
removed += 1;
|
||||||
|
endIndex = substringRange.location;
|
||||||
|
if (removed >= count) {
|
||||||
|
*stop = YES;
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
if (removed < count) { return @""; }
|
||||||
|
return [source substringToIndex:endIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_harvestFullTextFromProxy:(id<UITextDocumentProxy>)proxy {
|
||||||
|
if (!proxy) { return @""; }
|
||||||
|
if (![proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) { return @""; }
|
||||||
|
|
||||||
|
static const NSInteger kKBHarvestMaxRounds = 160;
|
||||||
|
static const NSInteger kKBHarvestMaxChars = 50000;
|
||||||
|
|
||||||
|
NSInteger movedToEnd = 0;
|
||||||
|
NSInteger movedLeft = 0;
|
||||||
|
NSMutableArray<NSString *> *chunks = [NSMutableArray array];
|
||||||
|
NSInteger totalChars = 0;
|
||||||
|
|
||||||
|
@try {
|
||||||
|
NSInteger guard = 0;
|
||||||
|
NSString *after = proxy.documentContextAfterInput ?: @"";
|
||||||
|
while (after.length > 0 && guard < kKBHarvestMaxRounds) {
|
||||||
|
NSInteger step = (NSInteger)after.length;
|
||||||
|
[(id)proxy adjustTextPositionByCharacterOffset:step];
|
||||||
|
movedToEnd += step;
|
||||||
|
guard += 1;
|
||||||
|
after = proxy.documentContextAfterInput ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
guard = 0;
|
||||||
|
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
while (before.length > 0 && guard < kKBHarvestMaxRounds && totalChars < kKBHarvestMaxChars) {
|
||||||
|
[chunks addObject:before];
|
||||||
|
totalChars += (NSInteger)before.length;
|
||||||
|
NSInteger step = (NSInteger)before.length;
|
||||||
|
[(id)proxy adjustTextPositionByCharacterOffset:-step];
|
||||||
|
movedLeft += step;
|
||||||
|
guard += 1;
|
||||||
|
before = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
}
|
||||||
|
} @finally {
|
||||||
|
if (movedLeft != 0) {
|
||||||
|
[(id)proxy adjustTextPositionByCharacterOffset:movedLeft];
|
||||||
|
}
|
||||||
|
if (movedToEnd != 0) {
|
||||||
|
[(id)proxy adjustTextPositionByCharacterOffset:-movedToEnd];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunks.count == 0) { return @""; }
|
||||||
|
NSMutableString *result = [NSMutableString stringWithCapacity:(NSUInteger)totalChars];
|
||||||
|
for (NSInteger i = (NSInteger)chunks.count - 1; i >= 0; i--) {
|
||||||
|
NSString *part = chunks[(NSUInteger)i] ?: @"";
|
||||||
|
if (part.length == 0) { continue; }
|
||||||
|
[result appendString:part];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#import "KBFunctionTagListView.h"
|
#import "KBFunctionTagListView.h"
|
||||||
#import "KBFunctionTagCell.h"
|
#import "KBFunctionTagCell.h"
|
||||||
|
#import "KBMaiPointReporter.h"
|
||||||
|
|
||||||
static NSString * const kKBFunctionTagCellId2 = @"KBFunctionTagCellId2";
|
static NSString * const kKBFunctionTagCellId2 = @"KBFunctionTagCellId2";
|
||||||
static CGFloat const kKBItemSpace = 4;
|
static CGFloat const kKBItemSpace = 4;
|
||||||
@@ -66,8 +67,23 @@ static CGFloat const kKBItemSpace = 4;
|
|||||||
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return kKBItemSpace; }
|
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return kKBItemSpace; }
|
||||||
|
|
||||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
KBTagItemModel *model = (indexPath.item < self.items.count) ? self.items[indexPath.item] : [KBTagItemModel new];
|
||||||
|
NSInteger personaId = 0;
|
||||||
|
if ([model isKindOfClass:KBTagItemModel.class]) {
|
||||||
|
personaId = model.characterId > 0 ? model.characterId : model.tagId;
|
||||||
|
}
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
extra[@"index"] = @(indexPath.item);
|
||||||
|
extra[@"id"] = @(personaId);
|
||||||
|
if ([model.characterName isKindOfClass:NSString.class] && model.characterName.length > 0) {
|
||||||
|
extra[@"name"] = model.characterName;
|
||||||
|
}
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_tag_item"
|
||||||
|
pageId:@"keyboard_function_panel"
|
||||||
|
elementId:@"renshe_item"
|
||||||
|
extra:extra.copy
|
||||||
|
completion:nil];
|
||||||
if ([self.delegate respondsToSelector:@selector(tagListView:didSelectIndex:title:)]) {
|
if ([self.delegate respondsToSelector:@selector(tagListView:didSelectIndex:title:)]) {
|
||||||
KBTagItemModel *model = (indexPath.item < self.items.count) ? self.items[indexPath.item] : [KBTagItemModel new];
|
|
||||||
[self.delegate tagListView:self didSelectIndex:indexPath.item title:model.characterName];
|
[self.delegate tagListView:self didSelectIndex:indexPath.item title:model.characterName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#import "KBBizCode.h"
|
#import "KBBizCode.h"
|
||||||
#import "KBBackspaceLongPressHandler.h"
|
#import "KBBackspaceLongPressHandler.h"
|
||||||
#import "KBBackspaceUndoManager.h"
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
|
||||||
@interface KBFunctionView () <KBFunctionBarViewDelegate, KBStreamOverlayViewDelegate, KBFunctionTagListViewDelegate>
|
@interface KBFunctionView () <KBFunctionBarViewDelegate, KBStreamOverlayViewDelegate, KBFunctionTagListViewDelegate>
|
||||||
// UI
|
// UI
|
||||||
@@ -639,6 +640,7 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
|
|||||||
// 用户点击功能标签:优先 UL 拉起主App,失败再 Scheme;两次都失败则提示开启完全访问。
|
// 用户点击功能标签:优先 UL 拉起主App,失败再 Scheme;两次都失败则提示开启完全访问。
|
||||||
// 若已开启“完全访问”,则直接在键盘侧创建 KBStreamTextView,并在其右上角提供删除按钮关闭。
|
// 若已开启“完全访问”,则直接在键盘侧创建 KBStreamTextView,并在其右上角提供删除按钮关闭。
|
||||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
// 点击上报已下沉到 KBFunctionTagListView(保证能拿到人设 id/name)
|
||||||
// 权限全部打开(键盘已启用 + 完全访问)。在扩展进程中仅需判断“完全访问”。
|
// 权限全部打开(键盘已启用 + 完全访问)。在扩展进程中仅需判断“完全访问”。
|
||||||
if ([[KBFullAccessManager shared] hasFullAccess]) {
|
if ([[KBFullAccessManager shared] hasFullAccess]) {
|
||||||
KBTagItemModel *selModel = self.modelArray[indexPath.item];
|
KBTagItemModel *selModel = self.modelArray[indexPath.item];
|
||||||
@@ -681,6 +683,11 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
|
|||||||
#pragma mark - Button Actions
|
#pragma mark - Button Actions
|
||||||
|
|
||||||
- (void)onTapPaste {
|
- (void)onTapPaste {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_paste_btn"
|
||||||
|
pageId:@"keyboard_function_panel"
|
||||||
|
elementId:@"paste_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
// 用户点击“粘贴”时才读取剪贴板:
|
// 用户点击“粘贴”时才读取剪贴板:
|
||||||
// - iOS16+ 会在跨 App 首次读取时自动弹出系统权限弹窗;
|
// - iOS16+ 会在跨 App 首次读取时自动弹出系统权限弹窗;
|
||||||
// - iOS15 及以下不会弹窗,直接返回内容;
|
// - iOS15 及以下不会弹窗,直接返回内容;
|
||||||
@@ -721,6 +728,8 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
|
|||||||
// - 无论允许/拒绝,都把本次 changeCount 记为已处理,避免一直重复询问。
|
// - 无论允许/拒绝,都把本次 changeCount 记为已处理,避免一直重复询问。
|
||||||
|
|
||||||
- (void)startPasteboardMonitor {
|
- (void)startPasteboardMonitor {
|
||||||
|
// 禁用自动读取剪贴板,避免触发系统“允许粘贴”弹窗
|
||||||
|
return;
|
||||||
// 未开启“完全访问”时不做自动读取,避免宿主/系统拒绝并刷错误日志
|
// 未开启“完全访问”时不做自动读取,避免宿主/系统拒绝并刷错误日志
|
||||||
if (![[KBFullAccessManager shared] hasFullAccess]) return;
|
if (![[KBFullAccessManager shared] hasFullAccess]) return;
|
||||||
if (self.pasteboardTimer) return;
|
if (self.pasteboardTimer) return;
|
||||||
@@ -773,24 +782,42 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
|
|||||||
|
|
||||||
- (void)onTapDelete {
|
- (void)onTapDelete {
|
||||||
NSLog(@"点击:删除");
|
NSLog(@"点击:删除");
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_delete_btn"
|
||||||
|
pageId:@"keyboard_function_panel"
|
||||||
|
elementId:@"delete_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:proxy.documentContextBeforeInput
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
||||||
after:proxy.documentContextAfterInput];
|
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:proxy.documentContextBeforeInput
|
||||||
[proxy deleteBackward];
|
after:proxy.documentContextAfterInput];
|
||||||
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
||||||
|
[[KBInputBufferManager shared] applyHoldDeleteCount:1];
|
||||||
}
|
}
|
||||||
- (void)onTapClear {
|
- (void)onTapClear {
|
||||||
NSLog(@"点击:清空");
|
NSLog(@"点击:清空");
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_clear_btn"
|
||||||
|
pageId:@"keyboard_function_panel"
|
||||||
|
elementId:@"clear_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[self.backspaceHandler performClearAction];
|
[self.backspaceHandler performClearAction];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onTapSend {
|
- (void)onTapSend {
|
||||||
NSLog(@"点击:发送");
|
NSLog(@"点击:发送");
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_send_btn"
|
||||||
|
pageId:@"keyboard_function_panel"
|
||||||
|
elementId:@"send_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
// 发送:插入换行。大多数聊天类 App 会把回车视为“发送”
|
// 发送:插入换行。大多数聊天类 App 会把回车视为“发送”
|
||||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
[proxy insertText:@"\n"];
|
[proxy insertText:@"\n"];
|
||||||
|
[[KBInputBufferManager shared] appendText:@"\n"];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Lazy
|
#pragma mark - Lazy
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#import "KBStreamTextView.h"
|
#import "KBStreamTextView.h"
|
||||||
#import "KBResponderUtils.h" // 通过响应链找到 UIInputViewController,并将文本输出到宿主
|
#import "KBResponderUtils.h" // 通过响应链找到 UIInputViewController,并将文本输出到宿主
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
|
||||||
@interface KBStreamTextView ()
|
@interface KBStreamTextView ()
|
||||||
|
|
||||||
@@ -371,6 +372,7 @@ static inline NSString *KBTrimRight(NSString *s) {
|
|||||||
if (rawText.length > 0) {
|
if (rawText.length > 0) {
|
||||||
[proxy insertText:rawText];
|
[proxy insertText:rawText];
|
||||||
}
|
}
|
||||||
|
[[KBInputBufferManager shared] resetWithText:rawText ?: @""];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
148
KBMaiPointEventTable.md
Normal file
148
KBMaiPointEventTable.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# KBMaiPoint 埋点事件表(统一口径:iOS / Android / 后端)
|
||||||
|
|
||||||
|
## 统一约定(全端一致)
|
||||||
|
|
||||||
|
### 1)事件类型(event_type)
|
||||||
|
- 页面曝光:`page_exposure`
|
||||||
|
- 点击事件:`click`
|
||||||
|
|
||||||
|
> iOS 侧可映射为:`KBMaiPointGenericReportTypePage / KBMaiPointGenericReportTypeClick`
|
||||||
|
|
||||||
|
### 2)事件名称(event_name)
|
||||||
|
- 统一使用 `lower_snake_case`,不绑定任何端的类名/资源名
|
||||||
|
- 页面曝光统一前缀:`enter_`
|
||||||
|
- 点击事件统一前缀:`click_`
|
||||||
|
|
||||||
|
### 3)事件参数(value / params)
|
||||||
|
- **所有事件都固定带**:`token`(`NSString`,有就传真实值;没有就传空字符串 `""`)
|
||||||
|
- 建议额外固定带:`page_id`(页面/区域统一ID)
|
||||||
|
- 点击类事件建议固定带:`element_id`(控件/入口统一ID)
|
||||||
|
- 列表/集合类点击建议带:`index`(`NSInteger`)与业务 `id`(如 `theme_id` / `product_id`)
|
||||||
|
|
||||||
|
参数示例(最小):
|
||||||
|
```json
|
||||||
|
{ "token": "", "page_id": "shop", "element_id": "search_btn" }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A. 主工程(keyBoard)
|
||||||
|
|
||||||
|
### A1)页面曝光(触发:VC 的 `viewDidAppear`)
|
||||||
|
|
||||||
|
| 注释 | 事件类型 | 事件名称 | page_id | iOS 对应页面 | Android 对应页面 | 触发时机 | 事件参数(示例) |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 进入首页 | page_exposure | enter_home_main | home_main | HomeMainVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"home_main" }` |
|
||||||
|
| 进入首页Tab容器 | page_exposure | enter_home | home | HomeVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"home" }` |
|
||||||
|
| 进入热门页 | page_exposure | enter_home_hot | home_hot | HomeHotVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"home_hot" }` |
|
||||||
|
| 进入排行榜页 | page_exposure | enter_home_rank | home_rank | HomeRankVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"home_rank" }` |
|
||||||
|
| 进入排行榜内容页 | page_exposure | enter_home_rank_content | home_rank_content | HomeRankContentVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"home_rank_content" }` |
|
||||||
|
| 进入首页底部弹层 | page_exposure | enter_home_sheet | home_sheet | HomeSheetVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"home_sheet" }` |
|
||||||
|
| 进入社区页 | page_exposure | enter_community | community | KBCommunityVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"community" }` |
|
||||||
|
| 进入搜索页 | page_exposure | enter_search | search | KBSearchVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"search" }` |
|
||||||
|
| 进入搜索结果页 | page_exposure | enter_search_result | search_result | KBSearchResultVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"search_result" }` |
|
||||||
|
| 进入商店页 | page_exposure | enter_shop | shop | KBShopVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"shop" }` |
|
||||||
|
| 进入商店分类列表页 | page_exposure | enter_shop_item_list | shop_item_list | KBShopItemVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"shop_item_list" }` |
|
||||||
|
| 进入皮肤详情页 | page_exposure | enter_skin_detail | skin_detail | KBSkinDetailVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"skin_detail", "theme_id":"" }` |
|
||||||
|
| 进入我的页 | page_exposure | enter_my | my | MyVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"my" }` |
|
||||||
|
| 进入我的皮肤页 | page_exposure | enter_my_skin | my_skin | MySkinVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"my_skin" }` |
|
||||||
|
| 进入我的键盘配置页 | page_exposure | enter_my_keyboard | my_keyboard | KBMyKeyBoardVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"my_keyboard" }` |
|
||||||
|
| 进入个人信息页 | page_exposure | enter_person_info | person_info | KBPersonInfoVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"person_info" }` |
|
||||||
|
| 进入反馈页 | page_exposure | enter_feedback | feedback | KBFeedBackVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"feedback" }` |
|
||||||
|
| 进入公告页 | page_exposure | enter_notice | notice | KBNoticeVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"notice" }` |
|
||||||
|
| 进入消费记录页 | page_exposure | enter_consumption_record | consumption_record | KBConsumptionRecordVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"consumption_record" }` |
|
||||||
|
| 进入VIP购买页 | page_exposure | enter_vip_pay | vip_pay | KBVipPay | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"vip_pay" }` |
|
||||||
|
| 进入积分充值页 | page_exposure | enter_points_recharge | points_recharge | KBJfPay | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"points_recharge" }` |
|
||||||
|
| 进入登录页 | page_exposure | enter_login | login | KBLoginVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"login" }` |
|
||||||
|
| 进入邮箱登录页 | page_exposure | enter_login_email | login_email | KBEmailLoginVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"login_email" }` |
|
||||||
|
| 进入邮箱注册页 | page_exposure | enter_register_email | register_email | KBEmailRegistVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"register_email" }` |
|
||||||
|
| 进入注册验证码页 | page_exposure | enter_register_verify_email | register_verify_email | KBRegistVerEmailVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"register_verify_email" }` |
|
||||||
|
| 进入忘记密码页 | page_exposure | enter_forgot_password_email | forgot_password_email | KBForgetPwdVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"forgot_password_email" }` |
|
||||||
|
| 进入忘记密码验证码页 | page_exposure | enter_forgot_password_verify | forgot_password_verify | KBForgetVerPwdVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"forgot_password_verify" }` |
|
||||||
|
| 进入忘记密码新密码页 | page_exposure | enter_forgot_password_newpwd | forgot_password_newpwd | KBForgetPwdNewPwdVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"forgot_password_newpwd" }` |
|
||||||
|
| 进入键盘权限引导页(App内) | page_exposure | enter_keyboard_permission_guide | keyboard_permission_guide | KBPermissionViewController | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"keyboard_permission_guide" }` |
|
||||||
|
| 进入首次引导页 | page_exposure | enter_guide | guide | KBGuideVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"guide" }` |
|
||||||
|
| 进入性别选择页 | page_exposure | enter_sex_select | sex_select | KBSexSelVC | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"sex_select" }` |
|
||||||
|
| 进入WebView页 | page_exposure | enter_webview | webview | KBWebViewViewController | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"webview", "url":"" }` |
|
||||||
|
|
||||||
|
> 测试/工具页(建议仅 DEBUG 或按需接入):`KBTestVC / KBLangTestVC / KBSkinCenterVC / ViewController / LoginViewController / KBLoginSheetViewController`。
|
||||||
|
|
||||||
|
### A2)点击事件(按钮/列表/入口)
|
||||||
|
|
||||||
|
| 注释 | 事件类型 | 事件名称 | page_id | element_id | iOS 对应控件/方法 | Android 对应控件 | 触发时机 | 事件参数(示例) |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 首页点击“购买会员” | click | click_home_buy_vip_btn | home_main | buy_vip_btn | HomeHeadView `onTapBuyAction` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"home_main", "element_id":"buy_vip_btn" }` |
|
||||||
|
| 首页点击“权限悬浮按钮” | click | click_home_permission_float_btn | home_main | permission_float_btn | HomeMainVC `keyPermissButton.clickDragViewBlock` | Android 自定义 | 点击悬浮按钮 | `{ "token":"", "page_id":"home_main", "element_id":"permission_float_btn" }` |
|
||||||
|
| 权限引导页点击“去设置” | click | click_permission_open_settings_btn | keyboard_permission_guide | open_settings_btn | KBPermissionViewController `openSettings` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"keyboard_permission_guide", "element_id":"open_settings_btn" }` |
|
||||||
|
| 权限引导页点击“关闭” | click | click_permission_close_btn | keyboard_permission_guide | close_btn | KBPermissionViewController `closeButtonAction` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"keyboard_permission_guide", "element_id":"close_btn" }` |
|
||||||
|
| 商店页点击“搜索” | click | click_shop_search_btn | shop | search_btn | KBShopVC `searchBtnAction` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"shop", "element_id":"search_btn" }` |
|
||||||
|
| 商店页点击“我的皮肤” | click | click_shop_my_skin_btn | shop | my_skin_btn | KBShopVC `skinBtnAction` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"shop", "element_id":"my_skin_btn" }` |
|
||||||
|
| 商店列表点击皮肤卡片 | click | click_shop_theme_card | shop_item_list | theme_card | KBShopItemVC `didSelectItemAtIndexPath` | Android 自定义 | didSelect | `{ "token":"", "page_id":"shop_item_list", "element_id":"theme_card", "theme_id":"", "index":0 }` |
|
||||||
|
| 皮肤详情点击“下载/购买” | click | click_skin_download_btn | skin_detail | download_btn | KBSkinDetailVC `handleDownloadAction` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"skin_detail", "element_id":"download_btn", "theme_id":"", "purchased":0 }` |
|
||||||
|
| 皮肤详情点击“推荐皮肤” | click | click_skin_recommend_card | skin_detail | recommend_card | KBSkinDetailVC `didSelectItemAtIndexPath` | Android 自定义 | didSelect | `{ "token":"", "page_id":"skin_detail", "element_id":"recommend_card", "from_theme_id":"", "to_theme_id":"", "index":0 }` |
|
||||||
|
| 搜索栏点击搜索 | click | click_search_submit | search | search_submit | KBSearchBarView `onSearch` | Android 自定义 | 点击搜索 | `{ "token":"", "page_id":"search", "element_id":"search_submit", "keyword_len":0 }` |
|
||||||
|
| 搜索页点击历史词条 | click | click_search_history_item | search | history_item | KBSearchVC `didSelectItemAtIndexPath` | Android 自定义 | didSelect | `{ "token":"", "page_id":"search", "element_id":"history_item", "index":0 }` |
|
||||||
|
| 搜索页点击“展开更多历史” | click | click_search_history_more | search | history_more | KBSearchVC `didSelectItemAtIndexPath` | Android 自定义 | didSelect | `{ "token":"", "page_id":"search", "element_id":"history_more" }` |
|
||||||
|
| 搜索页点击“清空历史” | click | click_search_clear_history | search | clear_history | KBSearchVC `clearHistory`(header trash) | Android 自定义 | 点击垃圾桶 | `{ "token":"", "page_id":"search", "element_id":"clear_history" }` |
|
||||||
|
| 搜索页点击推荐皮肤 | click | click_search_recommend_theme | search | recommend_theme_card | KBSearchVC `didSelectItemAtIndexPath` | Android 自定义 | didSelect | `{ "token":"", "page_id":"search", "element_id":"recommend_theme_card", "theme_id":"", "index":0 }` |
|
||||||
|
| 搜索结果页点击皮肤 | click | click_search_result_theme | search_result | result_theme_card | KBSearchResultVC `didSelectItemAtIndexPath` | Android 自定义 | didSelect | `{ "token":"", "page_id":"search_result", "element_id":"result_theme_card", "theme_id":"", "index":0 }` |
|
||||||
|
| 我的页点击菜单项 | click | click_my_menu_item | my | menu_item | MyVC `didSelectRowAtIndexPath` | Android 自定义 | didSelect | `{ "token":"", "page_id":"my", "element_id":"menu_item", "item_id":"", "item_title":"" }` |
|
||||||
|
| 我的页点击“邀请”成功复制 | click | click_my_invite_copy | my | invite_copy | MyVC(邀请分支) | Android 自定义 | 复制时机 | `{ "token":"", "page_id":"my", "element_id":"invite_copy" }` |
|
||||||
|
| 反馈页点击提交 | click | click_feedback_commit_btn | feedback | commit_btn | KBFeedBackVC `onTapCommit` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"feedback", "element_id":"commit_btn", "content_len":0 }` |
|
||||||
|
| 个人信息点击更换头像 | click | click_person_avatar_edit | person_info | avatar_edit | KBPersonInfoVC `onTapAvatarEdit` | Android 自定义 | tapGesture | `{ "token":"", "page_id":"person_info", "element_id":"avatar_edit" }` |
|
||||||
|
| 个人信息点击退出登录 | click | click_person_logout_btn | person_info | logout_btn | KBPersonInfoVC `onTapLogout` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"person_info", "element_id":"logout_btn" }` |
|
||||||
|
| 我的键盘页点击保存 | click | click_my_keyboard_save_btn | my_keyboard | save_btn | KBMyKeyBoardVC `onSave` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"my_keyboard", "element_id":"save_btn" }` |
|
||||||
|
| 我的皮肤页点击编辑/取消 | click | click_my_skin_toggle_edit | my_skin | toggle_edit | MySkinVC `onToggleEdit` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"my_skin", "element_id":"toggle_edit", "editing":0 }` |
|
||||||
|
| 我的皮肤页点击删除 | click | click_my_skin_delete_btn | my_skin | delete_btn | MySkinVC `onDelete` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"my_skin", "element_id":"delete_btn", "selected_count":0 }` |
|
||||||
|
| 我的皮肤页点击皮肤(进入详情) | click | click_my_skin_theme_card | my_skin | theme_card | MySkinVC `didSelectItemAtIndexPath` | Android 自定义 | didSelect | `{ "token":"", "page_id":"my_skin", "element_id":"theme_card", "theme_id":"", "index":0 }` |
|
||||||
|
| 登录页点击 Apple 登录 | click | click_login_apple_btn | login | apple_btn | KBLoginVC `onTapAppleLogin` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"login", "element_id":"apple_btn" }` |
|
||||||
|
| 登录页点击邮箱登录 | click | click_login_email_btn | login | email_btn | KBLoginVC `onTapEmailLogin` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"login", "element_id":"email_btn" }` |
|
||||||
|
| 登录页点击注册 | click | click_login_signup_btn | login | signup_btn | KBLoginVC `onTapSignUp` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"login", "element_id":"signup_btn" }` |
|
||||||
|
| 登录页点击忘记密码 | click | click_login_forgot_btn | login | forgot_btn | KBLoginVC `onTapForgotPassword` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"login", "element_id":"forgot_btn" }` |
|
||||||
|
| 邮箱登录页点击提交 | click | click_login_email_submit_btn | login_email | submit_btn | KBEmailLoginVC `onTapSubmit` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"login_email", "element_id":"submit_btn" }` |
|
||||||
|
| 邮箱注册页点击提交 | click | click_register_email_submit_btn | register_email | submit_btn | KBEmailRegistVC `onTapSubmit` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"register_email", "element_id":"submit_btn" }` |
|
||||||
|
| 注册验证码页点击确认 | click | click_register_verify_confirm_btn | register_verify_email | confirm_btn | KBRegistVerEmailVC `onTapConfirm` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"register_verify_email", "element_id":"confirm_btn" }` |
|
||||||
|
| 忘记密码(邮箱)点击下一步 | click | click_forgot_email_next_btn | forgot_password_email | next_btn | KBForgetPwdVC `onTapNext` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"forgot_password_email", "element_id":"next_btn" }` |
|
||||||
|
| 忘记密码(验证码)点击下一步 | click | click_forgot_verify_next_btn | forgot_password_verify | next_btn | KBForgetVerPwdVC `onTapNext` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"forgot_password_verify", "element_id":"next_btn" }` |
|
||||||
|
| 忘记密码(新密码)点击下一步 | click | click_forgot_newpwd_next_btn | forgot_password_newpwd | next_btn | KBForgetPwdNewPwdVC `onTapNext` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"forgot_password_newpwd", "element_id":"next_btn" }` |
|
||||||
|
| VIP页选择套餐 | click | click_vip_select_plan | vip_pay | plan_item | KBVipPay `didSelectItemAtIndexPath` | Android 自定义 | didSelect | `{ "token":"", "page_id":"vip_pay", "element_id":"plan_item", "product_id":"", "index":0 }` |
|
||||||
|
| VIP页点击支付 | click | click_vip_pay_btn | vip_pay | pay_btn | KBVipPay `onTapPayButton` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"vip_pay", "element_id":"pay_btn", "product_id":"" }` |
|
||||||
|
| VIP页点击恢复购买 | click | click_vip_restore_btn | vip_pay | restore_btn | KBVipPay `onTapRestoreButton` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"vip_pay", "element_id":"restore_btn" }` |
|
||||||
|
| VIP页点击关闭 | click | click_vip_close_btn | vip_pay | close_btn | KBVipPay `onTapClose` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"vip_pay", "element_id":"close_btn" }` |
|
||||||
|
| 积分充值页选择商品 | click | click_points_select_product | points_recharge | product_item | KBJfPay `didSelectItemAtIndexPath` | Android 自定义 | didSelect | `{ "token":"", "page_id":"points_recharge", "element_id":"product_item", "product_id":"", "index":0 }` |
|
||||||
|
| 积分充值页点击充值 | click | click_points_pay_btn | points_recharge | pay_btn | KBJfPay `onTapPayButton` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"points_recharge", "element_id":"pay_btn", "product_id":"" }` |
|
||||||
|
| 引导页点击复制示例1 | click | click_guide_copy_example_1 | guide | copy_example_1 | KBGuideTopCell `kb_onTapQ1` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"guide", "element_id":"copy_example_1" }` |
|
||||||
|
| 引导页点击复制示例2 | click | click_guide_copy_example_2 | guide | copy_example_2 | KBGuideTopCell `kb_onTapQ2` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"guide", "element_id":"copy_example_2" }` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B. 键盘扩展(CustomKeyboard)
|
||||||
|
|
||||||
|
### B1)页面曝光(触发:显示/切换时机)
|
||||||
|
|
||||||
|
| 注释 | 事件类型 | 事件名称 | page_id | iOS 对应页面/视图 | Android 对应页面 | 触发时机 | 事件参数(示例) |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 键盘首次显示 | page_exposure | enter_keyboard | keyboard | KeyboardViewController | Android 自定义 | viewDidAppear | `{ "token":"", "page_id":"keyboard" }` |
|
||||||
|
| 打开功能面板 | page_exposure | enter_keyboard_function_panel | keyboard_function_panel | KBFunctionView | Android 自定义 | showFunctionPanel:YES | `{ "token":"", "page_id":"keyboard_function_panel" }` |
|
||||||
|
| 关闭功能面板(回到主键盘) | page_exposure | enter_keyboard_main_panel | keyboard_main_panel | KBKeyBoardMainView | Android 自定义 | showFunctionPanel:NO | `{ "token":"", "page_id":"keyboard_main_panel" }` |
|
||||||
|
| 打开设置页 | page_exposure | enter_keyboard_settings | keyboard_settings | KBSettingView | Android 自定义 | showSettingView:YES | `{ "token":"", "page_id":"keyboard_settings" }` |
|
||||||
|
| 打开订阅/充值面板 | page_exposure | enter_keyboard_subscription_panel | keyboard_subscription_panel | KBKeyboardSubscriptionView | Android 自定义 | showSubscriptionPanel | `{ "token":"", "page_id":"keyboard_subscription_panel" }` |
|
||||||
|
|
||||||
|
### B2)点击事件(键盘工具栏 / 功能面板 / 订阅面板)
|
||||||
|
|
||||||
|
| 注释 | 事件类型 | 事件名称 | page_id | element_id | iOS 对应控件/方法 | Android 对应控件 | 触发时机 | 事件参数(示例) |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 点击键盘顶部工具栏(index=0 打开功能面板) | click | click_keyboard_toolbar_action | keyboard_main_panel | toolbar_action | KBKeyBoardMainViewDelegate `didTapToolActionAtIndex:` | Android 自定义 | 点击工具栏 | `{ "token":"", "page_id":"keyboard_main_panel", "element_id":"toolbar_action", "index":0 }` |
|
||||||
|
| 点击键盘设置按钮 | click | click_keyboard_settings_btn | keyboard_main_panel | settings_btn | `keyBoardMainViewDidTapSettings:` | Android 自定义 | 点击设置 | `{ "token":"", "page_id":"keyboard_main_panel", "element_id":"settings_btn" }` |
|
||||||
|
| 点击设置页返回 | click | click_keyboard_settings_back_btn | keyboard_settings | back_btn | KeyboardViewController `onTapSettingsBack` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"keyboard_settings", "element_id":"back_btn" }` |
|
||||||
|
| 点击撤销删除 | click | click_keyboard_undo_btn | keyboard_main_panel | undo_btn | `keyBoardMainViewDidTapUndo:` | Android 自定义 | 点击撤销 | `{ "token":"", "page_id":"keyboard_main_panel", "element_id":"undo_btn" }` |
|
||||||
|
| 点击表情面板搜索 | click | click_keyboard_emoji_search_btn | keyboard_main_panel | emoji_search_btn | `keyBoardMainViewDidTapEmojiSearch:` | Android 自定义 | 点击搜索 | `{ "token":"", "page_id":"keyboard_main_panel", "element_id":"emoji_search_btn" }` |
|
||||||
|
| 点击联想词条 | click | click_keyboard_suggestion_item | keyboard_main_panel | suggestion_item | `didSelectSuggestion:` | Android 自定义 | 点击候选 | `{ "token":"", "page_id":"keyboard_main_panel", "element_id":"suggestion_item", "index":0 }` |
|
||||||
|
| 功能面板点击“粘贴” | click | click_keyboard_function_paste_btn | keyboard_function_panel | paste_btn | KBFunctionView `onTapPaste` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"keyboard_function_panel", "element_id":"paste_btn" }` |
|
||||||
|
| 功能面板点击“删除” | click | click_keyboard_function_delete_btn | keyboard_function_panel | delete_btn | KBFunctionView `onTapDelete` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"keyboard_function_panel", "element_id":"delete_btn" }` |
|
||||||
|
| 功能面板点击“清空” | click | click_keyboard_function_clear_btn | keyboard_function_panel | clear_btn | KBFunctionView `onTapClear` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"keyboard_function_panel", "element_id":"clear_btn" }` |
|
||||||
|
| 功能面板点击“发送” | click | click_keyboard_function_send_btn | keyboard_function_panel | send_btn | KBFunctionView `onTapSend` | Android 自定义 | touchUpInside | `{ "token":"", "page_id":"keyboard_function_panel", "element_id":"send_btn" }` |
|
||||||
|
| 功能面板点击“人设/标签”条目 | click | click_keyboard_function_tag_item | keyboard_function_panel | renshe_item | KBFunctionTagListView `didSelectItemAtIndexPath` | Android 自定义 | didSelect | `{ "token":"", "page_id":"keyboard_function_panel", "element_id":"renshe_item", "index":0, "id":456, "name":"" }` |
|
||||||
|
| 功能面板右侧点击“登录/充值”入口(未登录走登录) | click | click_keyboard_function_right_action | keyboard_function_panel | right_action | KeyboardViewController `didRightTapToolActionAtIndex:` | Android 自定义 | 点击右侧入口 | `{ "token":"", "page_id":"keyboard_function_panel", "element_id":"right_action", "action":"login_or_recharge" }` |
|
||||||
|
| 订阅面板点击关闭 | click | click_keyboard_subscription_close_btn | keyboard_subscription_panel | close_btn | `subscriptionViewDidTapClose:` | Android 自定义 | 点击关闭 | `{ "token":"", "page_id":"keyboard_subscription_panel", "element_id":"close_btn" }` |
|
||||||
|
| 订阅面板点击购买某商品 | click | click_keyboard_subscription_product_btn | keyboard_subscription_panel | product_btn | `didTapPurchaseForProduct:` | Android 自定义 | 点击购买 | `{ "token":"", "page_id":"keyboard_subscription_panel", "element_id":"product_btn", "product_id":"", "index":0 }` |
|
||||||
BIN
KBMaiPointEventTable.xlsx
Normal file
BIN
KBMaiPointEventTable.xlsx
Normal file
Binary file not shown.
@@ -88,7 +88,8 @@
|
|||||||
|
|
||||||
#if __OBJC__
|
#if __OBJC__
|
||||||
static inline CGFloat KBScreenWidth(void) {
|
static inline CGFloat KBScreenWidth(void) {
|
||||||
return [UIScreen mainScreen].bounds.size.width;
|
CGSize size = [UIScreen mainScreen].bounds.size;
|
||||||
|
return MIN(size.width, size.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline CGFloat KBScaleFactor(void) {
|
static inline CGFloat KBScaleFactor(void) {
|
||||||
|
|||||||
20
Shared/KBLog.h
Normal file
20
Shared/KBLog.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// KBLog.h
|
||||||
|
// Shared debug logging macro (App + Extension)
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
#ifndef KBLOG
|
||||||
|
// 调试专用日志(DEBUG 打印,RELEASE 不打印)。尽量显眼,包含函数与行号。
|
||||||
|
#if DEBUG
|
||||||
|
#define KBLOG(fmt, ...) do { \
|
||||||
|
NSString *kb_msg__ = [NSString stringWithFormat:(fmt), ##__VA_ARGS__]; \
|
||||||
|
NSString *kb_full_msg__ = [NSString stringWithFormat:@"\n==============================[KB DEBUG]==============================\n[Function] %s\n[Line] %d\n%@\n=====================================================================\n", __PRETTY_FUNCTION__, __LINE__, kb_msg__]; \
|
||||||
|
fprintf(stderr, "%s", kb_full_msg__.UTF8String); \
|
||||||
|
} while(0)
|
||||||
|
#else
|
||||||
|
#define KBLOG(...)
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
@@ -6,29 +6,77 @@
|
|||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
#ifndef KB_MAI_POINT_BASE_URL
|
#ifndef KB_MAI_POINT_BASE_URL
|
||||||
#define KB_MAI_POINT_BASE_URL @"http://192.168.1.188:35310/api"
|
#define KB_MAI_POINT_BASE_URL @"http://192.168.2.21:35310/api"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef KB_MAI_POINT_PATH_NEW_ACCOUNT
|
#ifndef KB_MAI_POINT_PATH_NEW_ACCOUNT
|
||||||
#define KB_MAI_POINT_PATH_NEW_ACCOUNT @"/newAccount"
|
#define KB_MAI_POINT_PATH_NEW_ACCOUNT @"/newAccount"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef KB_MAI_POINT_PATH_GENERIC_DATA
|
||||||
|
#define KB_MAI_POINT_PATH_GENERIC_DATA @"/genericData"
|
||||||
|
#endif
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
extern NSString * const KBMaiPointErrorDomain;
|
extern NSString * const KBMaiPointErrorDomain;
|
||||||
|
extern NSString * const KBMaiPointEventTypePageExposure;
|
||||||
|
extern NSString * const KBMaiPointEventTypeClick;
|
||||||
|
|
||||||
typedef void (^KBMaiPointReportCompletion)(BOOL success, NSError * _Nullable error);
|
typedef void (^KBMaiPointReportCompletion)(BOOL success, NSError * _Nullable error);
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, KBMaiPointGenericReportType) {
|
||||||
|
/// 未知/默认类型(按需扩展,具体含义以服务端约定为准)
|
||||||
|
KBMaiPointGenericReportTypeUnknown = 0,
|
||||||
|
/// 点击
|
||||||
|
KBMaiPointGenericReportTypeClick = 1,
|
||||||
|
/// 曝光
|
||||||
|
KBMaiPointGenericReportTypeExposure = 2,
|
||||||
|
/// 页面/进入
|
||||||
|
KBMaiPointGenericReportTypePage = 3,
|
||||||
|
};
|
||||||
|
|
||||||
/// Lightweight reporter for Mai point tracking. Safe for app + extension.
|
/// Lightweight reporter for Mai point tracking. Safe for app + extension.
|
||||||
@interface KBMaiPointReporter : NSObject
|
@interface KBMaiPointReporter : NSObject
|
||||||
|
|
||||||
+ (instancetype)sharedReporter;
|
+ (instancetype)sharedReporter;
|
||||||
|
|
||||||
|
/// 统一埋点:POST /genericData
|
||||||
|
/// - eventType: 建议取值 `page_exposure` / `click`
|
||||||
|
/// - eventName: 统一事件名(如 enter_xxx / click_xxx)
|
||||||
|
/// - value: 事件参数字典(内部会自动注入 token;无 token 时为 @"")
|
||||||
|
- (void)reportEventType:(NSString *)eventType
|
||||||
|
eventName:(NSString *)eventName
|
||||||
|
value:(nullable NSDictionary *)value
|
||||||
|
completion:(KBMaiPointReportCompletion _Nullable)completion;
|
||||||
|
|
||||||
|
/// 页面曝光快捷方法:内部会补齐 page_id
|
||||||
|
- (void)reportPageExposureWithEventName:(NSString *)eventName
|
||||||
|
pageId:(NSString *)pageId
|
||||||
|
extra:(nullable NSDictionary *)extra
|
||||||
|
completion:(KBMaiPointReportCompletion _Nullable)completion;
|
||||||
|
|
||||||
|
/// 点击快捷方法:内部会补齐 page_id / element_id
|
||||||
|
- (void)reportClickWithEventName:(NSString *)eventName
|
||||||
|
pageId:(NSString *)pageId
|
||||||
|
elementId:(NSString *)elementId
|
||||||
|
extra:(nullable NSDictionary *)extra
|
||||||
|
completion:(KBMaiPointReportCompletion _Nullable)completion;
|
||||||
|
|
||||||
/// POST /newAccount with type + account.
|
/// POST /newAccount with type + account.
|
||||||
- (void)reportNewAccountWithType:(NSString *)type
|
- (void)reportNewAccountWithType:(NSString *)type
|
||||||
account:(NSString *)account
|
account:(nullable NSString *)account
|
||||||
completion:(KBMaiPointReportCompletion _Nullable)completion;
|
completion:(KBMaiPointReportCompletion _Nullable)completion;
|
||||||
|
|
||||||
|
//- (void)reportGenericDataWithEvent:(NSString *)event
|
||||||
|
// account:(nullable NSString *)account
|
||||||
|
// completion:(KBMaiPointReportCompletion _Nullable)completion;
|
||||||
|
|
||||||
|
/// POST /genericData with type + event + account.
|
||||||
|
- (void)reportGenericDataWithEventType:(KBMaiPointGenericReportType)type
|
||||||
|
account:(nullable NSString *)account
|
||||||
|
completion:(KBMaiPointReportCompletion _Nullable)completion;
|
||||||
|
|
||||||
/// Generic POST for future endpoints.
|
/// Generic POST for future endpoints.
|
||||||
- (void)postPath:(NSString *)path
|
- (void)postPath:(NSString *)path
|
||||||
parameters:(NSDictionary *)parameters
|
parameters:(NSDictionary *)parameters
|
||||||
|
|||||||
@@ -4,8 +4,39 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "KBMaiPointReporter.h"
|
#import "KBMaiPointReporter.h"
|
||||||
|
#import "KBLog.h"
|
||||||
|
#import "KBAuthManager.h"
|
||||||
|
#if __has_include(<UIKit/UIKit.h>)
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
#import <objc/runtime.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
NSString * const KBMaiPointErrorDomain = @"KBMaiPointErrorDomain";
|
NSString * const KBMaiPointErrorDomain = @"KBMaiPointErrorDomain";
|
||||||
|
NSString * const KBMaiPointEventTypePageExposure = @"page_exposure";
|
||||||
|
NSString * const KBMaiPointEventTypeClick = @"click";
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
static void KBMaiPoint_DebugLogURL(NSURLRequest *request) {
|
||||||
|
NSString *url = request.URL.absoluteString ?: @"";
|
||||||
|
KBLOG(@"🍃[KBMaiPointReporter] url=%@", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void KBMaiPoint_DebugLogError(NSURLResponse *response, NSError *error) {
|
||||||
|
if (error) {
|
||||||
|
NSString *msg = error.localizedDescription ?: @"(no description)";
|
||||||
|
KBLOG(@"🍃[KBMaiPointReporter] error=%@ domain=%@ code=%ld", msg, error.domain ?: @"", (long)error.code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
|
||||||
|
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
|
||||||
|
if (statusCode >= 200 && statusCode < 300) {
|
||||||
|
KBLOG(@"🍃[KBMaiPointReporter] status=HTTP_%ld", (long)statusCode);
|
||||||
|
} else {
|
||||||
|
KBLOG(@"🍃[KBMaiPointReporter] error=HTTP_%ld", (long)statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
@implementation KBMaiPointReporter
|
@implementation KBMaiPointReporter
|
||||||
|
|
||||||
@@ -18,12 +49,96 @@ NSString * const KBMaiPointErrorDomain = @"KBMaiPointErrorDomain";
|
|||||||
return reporter;
|
return reporter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_trimmedStringOrEmpty:(NSString * _Nullable)string {
|
||||||
|
NSString *value = [string isKindOfClass:[NSString class]] ? string : @"";
|
||||||
|
return [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_currentTokenOrEmpty {
|
||||||
|
NSString *t = [KBAuthManager shared].current.accessToken;
|
||||||
|
return [self kb_trimmedStringOrEmpty:t];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)reportEventType:(NSString *)eventType
|
||||||
|
eventName:(NSString *)eventName
|
||||||
|
value:(NSDictionary * _Nullable)value
|
||||||
|
completion:(KBMaiPointReportCompletion _Nullable)completion {
|
||||||
|
NSString *trimmedType = [self kb_trimmedStringOrEmpty:eventType];
|
||||||
|
NSString *trimmedName = [self kb_trimmedStringOrEmpty:eventName];
|
||||||
|
if (trimmedType.length == 0 || trimmedName.length == 0) {
|
||||||
|
NSError *error = [NSError errorWithDomain:KBMaiPointErrorDomain
|
||||||
|
code:-1
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"Invalid parameter"}];
|
||||||
|
if (completion) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
completion(NO, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableDictionary *val = [NSMutableDictionary dictionary];
|
||||||
|
if ([value isKindOfClass:[NSDictionary class]] && value.count > 0) {
|
||||||
|
[val addEntriesFromDictionary:value];
|
||||||
|
}
|
||||||
|
if (![val[@"token"] isKindOfClass:NSString.class]) {
|
||||||
|
val[@"token"] = [self kb_currentTokenOrEmpty];
|
||||||
|
} else {
|
||||||
|
// 若外部传了 token,也做一次兜底(nil -> @"" / trim)
|
||||||
|
val[@"token"] = [self kb_trimmedStringOrEmpty:val[@"token"]];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSDictionary *params = @{
|
||||||
|
// 字段兼容:后端若用 eventId 统计,也能直接用 eventName
|
||||||
|
@"eventType": trimmedType,
|
||||||
|
@"eventName": trimmedName,
|
||||||
|
@"eventId": trimmedName,
|
||||||
|
@"value": val.copy
|
||||||
|
};
|
||||||
|
[self postPath:KB_MAI_POINT_PATH_GENERIC_DATA parameters:params completion:completion];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)reportPageExposureWithEventName:(NSString *)eventName
|
||||||
|
pageId:(NSString *)pageId
|
||||||
|
extra:(NSDictionary * _Nullable)extra
|
||||||
|
completion:(KBMaiPointReportCompletion _Nullable)completion {
|
||||||
|
NSString *pid = [self kb_trimmedStringOrEmpty:pageId];
|
||||||
|
NSMutableDictionary *val = [NSMutableDictionary dictionary];
|
||||||
|
if (pid.length > 0) {
|
||||||
|
val[@"page_id"] = pid;
|
||||||
|
}
|
||||||
|
if ([extra isKindOfClass:[NSDictionary class]] && extra.count > 0) {
|
||||||
|
[val addEntriesFromDictionary:extra];
|
||||||
|
}
|
||||||
|
[self reportEventType:KBMaiPointEventTypePageExposure eventName:eventName value:val completion:completion];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)reportClickWithEventName:(NSString *)eventName
|
||||||
|
pageId:(NSString *)pageId
|
||||||
|
elementId:(NSString *)elementId
|
||||||
|
extra:(NSDictionary * _Nullable)extra
|
||||||
|
completion:(KBMaiPointReportCompletion _Nullable)completion {
|
||||||
|
NSString *pid = [self kb_trimmedStringOrEmpty:pageId];
|
||||||
|
NSString *eid = [self kb_trimmedStringOrEmpty:elementId];
|
||||||
|
NSMutableDictionary *val = [NSMutableDictionary dictionary];
|
||||||
|
if (pid.length > 0) {
|
||||||
|
val[@"page_id"] = pid;
|
||||||
|
}
|
||||||
|
if (eid.length > 0) {
|
||||||
|
val[@"element_id"] = eid;
|
||||||
|
}
|
||||||
|
if ([extra isKindOfClass:[NSDictionary class]] && extra.count > 0) {
|
||||||
|
[val addEntriesFromDictionary:extra];
|
||||||
|
}
|
||||||
|
[self reportEventType:KBMaiPointEventTypeClick eventName:eventName value:val completion:completion];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)reportNewAccountWithType:(NSString *)type
|
- (void)reportNewAccountWithType:(NSString *)type
|
||||||
account:(NSString *)account
|
account:(NSString * _Nullable)account
|
||||||
completion:(KBMaiPointReportCompletion)completion {
|
completion:(KBMaiPointReportCompletion _Nullable)completion {
|
||||||
NSString *trimmedType = [type stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
NSString *trimmedType = [self kb_trimmedStringOrEmpty:type];
|
||||||
NSString *trimmedAccount = [account stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
NSString *trimmedAccount = [self kb_trimmedStringOrEmpty:account];
|
||||||
if (trimmedType.length == 0 || trimmedAccount.length == 0) {
|
if (trimmedType.length == 0) {
|
||||||
NSError *error = [NSError errorWithDomain:KBMaiPointErrorDomain
|
NSError *error = [NSError errorWithDomain:KBMaiPointErrorDomain
|
||||||
code:-1
|
code:-1
|
||||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid parameter"}];
|
userInfo:@{NSLocalizedDescriptionKey: @"Invalid parameter"}];
|
||||||
@@ -37,14 +152,43 @@ NSString * const KBMaiPointErrorDomain = @"KBMaiPointErrorDomain";
|
|||||||
|
|
||||||
NSDictionary *params = @{
|
NSDictionary *params = @{
|
||||||
@"type": trimmedType,
|
@"type": trimmedType,
|
||||||
@"account": trimmedAccount
|
@"account": trimmedAccount ?: @"",
|
||||||
|
@"token": [self kb_currentTokenOrEmpty]
|
||||||
};
|
};
|
||||||
[self postPath:KB_MAI_POINT_PATH_NEW_ACCOUNT parameters:params completion:completion];
|
[self postPath:KB_MAI_POINT_PATH_NEW_ACCOUNT parameters:params completion:completion];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//- (void)reportGenericDataWithEvent:(NSString *)event
|
||||||
|
// account:(NSString * _Nullable)account
|
||||||
|
// completion:(KBMaiPointReportCompletion _Nullable)completion {
|
||||||
|
// [self reportGenericDataWithType:KBMaiPointGenericReportTypeUnknown
|
||||||
|
// event:event
|
||||||
|
// account:account
|
||||||
|
// completion:completion];
|
||||||
|
//}
|
||||||
|
|
||||||
|
- (void)reportGenericDataWithEventType:(KBMaiPointGenericReportType)eventType
|
||||||
|
account:(nullable NSString *)account
|
||||||
|
completion:(KBMaiPointReportCompletion _Nullable)completion{
|
||||||
|
// 兼容旧接口:没有 eventName 时给一个默认值,避免调用方崩溃
|
||||||
|
NSString *typeStr = @"unknown";
|
||||||
|
switch (eventType) {
|
||||||
|
case KBMaiPointGenericReportTypeClick: typeStr = KBMaiPointEventTypeClick; break;
|
||||||
|
case KBMaiPointGenericReportTypeExposure: typeStr = @"exposure"; break;
|
||||||
|
case KBMaiPointGenericReportTypePage: typeStr = KBMaiPointEventTypePageExposure; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
NSMutableDictionary *val = [NSMutableDictionary dictionary];
|
||||||
|
NSString *trimmedAccount = [self kb_trimmedStringOrEmpty:account];
|
||||||
|
if (trimmedAccount.length > 0) {
|
||||||
|
val[@"account"] = trimmedAccount;
|
||||||
|
}
|
||||||
|
[self reportEventType:typeStr eventName:@"generic_event" value:val completion:completion];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)postPath:(NSString *)path
|
- (void)postPath:(NSString *)path
|
||||||
parameters:(NSDictionary *)parameters
|
parameters:(NSDictionary *)parameters
|
||||||
completion:(KBMaiPointReportCompletion)completion {
|
completion:(KBMaiPointReportCompletion _Nullable)completion {
|
||||||
if (path.length == 0 || ![parameters isKindOfClass:[NSDictionary class]]) {
|
if (path.length == 0 || ![parameters isKindOfClass:[NSDictionary class]]) {
|
||||||
NSError *error = [NSError errorWithDomain:KBMaiPointErrorDomain
|
NSError *error = [NSError errorWithDomain:KBMaiPointErrorDomain
|
||||||
code:-1
|
code:-1
|
||||||
@@ -89,6 +233,10 @@ NSString * const KBMaiPointErrorDomain = @"KBMaiPointErrorDomain";
|
|||||||
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
||||||
request.HTTPBody = body;
|
request.HTTPBody = body;
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
KBMaiPoint_DebugLogURL(request);
|
||||||
|
#endif
|
||||||
|
|
||||||
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||||
config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
||||||
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
|
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
|
||||||
@@ -115,6 +263,10 @@ NSString * const KBMaiPointErrorDomain = @"KBMaiPointErrorDomain";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
KBMaiPoint_DebugLogError(response, finalError);
|
||||||
|
#endif
|
||||||
|
|
||||||
if (completion) {
|
if (completion) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
completion(success, finalError);
|
completion(success, finalError);
|
||||||
@@ -125,3 +277,123 @@ NSString * const KBMaiPointErrorDomain = @"KBMaiPointErrorDomain";
|
|||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
#if __has_include(<UIKit/UIKit.h>)
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// 自动页面曝光(viewDidAppear)
|
||||||
|
// 说明:仅对“在表里登记过的 VC”生效;其它 VC 不处理。
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
static NSDictionary<NSString *, NSDictionary *> *KBMaiPoint_PageExposureMap(void) {
|
||||||
|
static NSDictionary<NSString *, NSDictionary *> *m;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
m = @{
|
||||||
|
// 主工程
|
||||||
|
@"HomeMainVC": @{@"event_name": @"enter_home_main", @"page_id": @"home_main"},
|
||||||
|
@"HomeVC": @{@"event_name": @"enter_home", @"page_id": @"home"},
|
||||||
|
@"HomeHotVC": @{@"event_name": @"enter_home_hot", @"page_id": @"home_hot"},
|
||||||
|
@"HomeRankVC": @{@"event_name": @"enter_home_rank", @"page_id": @"home_rank"},
|
||||||
|
@"HomeRankContentVC": @{@"event_name": @"enter_home_rank_content", @"page_id": @"home_rank_content"},
|
||||||
|
@"HomeSheetVC": @{@"event_name": @"enter_home_sheet", @"page_id": @"home_sheet"},
|
||||||
|
@"KBCommunityVC": @{@"event_name": @"enter_community", @"page_id": @"community"},
|
||||||
|
@"KBSearchVC": @{@"event_name": @"enter_search", @"page_id": @"search"},
|
||||||
|
@"KBSearchResultVC": @{@"event_name": @"enter_search_result", @"page_id": @"search_result"},
|
||||||
|
@"KBShopVC": @{@"event_name": @"enter_shop", @"page_id": @"shop"},
|
||||||
|
@"KBShopItemVC": @{@"event_name": @"enter_shop_item_list", @"page_id": @"shop_item_list"},
|
||||||
|
@"KBSkinDetailVC": @{@"event_name": @"enter_skin_detail", @"page_id": @"skin_detail"},
|
||||||
|
@"MyVC": @{@"event_name": @"enter_my", @"page_id": @"my"},
|
||||||
|
@"MySkinVC": @{@"event_name": @"enter_my_skin", @"page_id": @"my_skin"},
|
||||||
|
@"KBMyKeyBoardVC": @{@"event_name": @"enter_my_keyboard", @"page_id": @"my_keyboard"},
|
||||||
|
@"KBPersonInfoVC": @{@"event_name": @"enter_person_info", @"page_id": @"person_info"},
|
||||||
|
@"KBFeedBackVC": @{@"event_name": @"enter_feedback", @"page_id": @"feedback"},
|
||||||
|
@"KBNoticeVC": @{@"event_name": @"enter_notice", @"page_id": @"notice"},
|
||||||
|
@"KBConsumptionRecordVC": @{@"event_name": @"enter_consumption_record", @"page_id": @"consumption_record"},
|
||||||
|
@"KBVipPay": @{@"event_name": @"enter_vip_pay", @"page_id": @"vip_pay"},
|
||||||
|
@"KBJfPay": @{@"event_name": @"enter_points_recharge", @"page_id": @"points_recharge"},
|
||||||
|
@"KBLoginVC": @{@"event_name": @"enter_login", @"page_id": @"login"},
|
||||||
|
@"KBEmailLoginVC": @{@"event_name": @"enter_login_email", @"page_id": @"login_email"},
|
||||||
|
@"KBEmailRegistVC": @{@"event_name": @"enter_register_email", @"page_id": @"register_email"},
|
||||||
|
@"KBRegistVerEmailVC": @{@"event_name": @"enter_register_verify_email", @"page_id": @"register_verify_email"},
|
||||||
|
@"KBForgetPwdVC": @{@"event_name": @"enter_forgot_password_email", @"page_id": @"forgot_password_email"},
|
||||||
|
@"KBForgetVerPwdVC": @{@"event_name": @"enter_forgot_password_verify", @"page_id": @"forgot_password_verify"},
|
||||||
|
@"KBForgetPwdNewPwdVC": @{@"event_name": @"enter_forgot_password_newpwd", @"page_id": @"forgot_password_newpwd"},
|
||||||
|
@"KBPermissionViewController": @{@"event_name": @"enter_keyboard_permission_guide", @"page_id": @"keyboard_permission_guide"},
|
||||||
|
@"KBGuideVC": @{@"event_name": @"enter_guide", @"page_id": @"guide"},
|
||||||
|
@"KBSexSelVC": @{@"event_name": @"enter_sex_select", @"page_id": @"sex_select"},
|
||||||
|
@"KBWebViewViewController": @{@"event_name": @"enter_webview", @"page_id": @"webview"},
|
||||||
|
|
||||||
|
// 键盘扩展
|
||||||
|
@"KeyboardViewController": @{@"event_name": @"enter_keyboard", @"page_id": @"keyboard"},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void KBMaiPoint_SwizzleInstanceMethod(Class cls, SEL originalSel, SEL swizzledSel) {
|
||||||
|
Method original = class_getInstanceMethod(cls, originalSel);
|
||||||
|
Method swizzled = class_getInstanceMethod(cls, swizzledSel);
|
||||||
|
if (!original || !swizzled) return;
|
||||||
|
BOOL added = class_addMethod(cls,
|
||||||
|
originalSel,
|
||||||
|
method_getImplementation(swizzled),
|
||||||
|
method_getTypeEncoding(swizzled));
|
||||||
|
if (added) {
|
||||||
|
class_replaceMethod(cls,
|
||||||
|
swizzledSel,
|
||||||
|
method_getImplementation(original),
|
||||||
|
method_getTypeEncoding(original));
|
||||||
|
} else {
|
||||||
|
method_exchangeImplementations(original, swizzled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@interface UIViewController (KBMaiPointAutoReport)
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation UIViewController (KBMaiPointAutoReport)
|
||||||
|
|
||||||
|
+ (void)load {
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
KBMaiPoint_SwizzleInstanceMethod(self, @selector(viewDidAppear:), @selector(kb_maipoint_viewDidAppear:));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_maipoint_viewDidAppear:(BOOL)animated {
|
||||||
|
[self kb_maipoint_viewDidAppear:animated];
|
||||||
|
|
||||||
|
NSString *clsName = NSStringFromClass(self.class);
|
||||||
|
NSDictionary *cfg = KBMaiPoint_PageExposureMap()[clsName];
|
||||||
|
if (![cfg isKindOfClass:NSDictionary.class]) { return; }
|
||||||
|
|
||||||
|
NSString *eventName = cfg[@"event_name"];
|
||||||
|
NSString *pageId = cfg[@"page_id"];
|
||||||
|
if (![eventName isKindOfClass:NSString.class] || ![pageId isKindOfClass:NSString.class]) { return; }
|
||||||
|
|
||||||
|
// 少数页面带额外参数(尽量不取敏感信息)
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
if ([clsName isEqualToString:@"KBSkinDetailVC"]) {
|
||||||
|
id themeId = nil;
|
||||||
|
@try { themeId = [self valueForKey:@"themeId"]; } @catch (__unused NSException *e) { themeId = nil; }
|
||||||
|
if ([themeId isKindOfClass:NSString.class] && ((NSString *)themeId).length > 0) {
|
||||||
|
extra[@"theme_id"] = themeId;
|
||||||
|
}
|
||||||
|
} else if ([clsName isEqualToString:@"KBWebViewViewController"]) {
|
||||||
|
id url = nil;
|
||||||
|
@try { url = [self valueForKey:@"url"]; } @catch (__unused NSException *e) { url = nil; }
|
||||||
|
if ([url isKindOfClass:NSString.class] && ((NSString *)url).length > 0) {
|
||||||
|
extra[@"url"] = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:eventName
|
||||||
|
pageId:pageId
|
||||||
|
extra:(extra.count > 0 ? extra.copy : nil)
|
||||||
|
completion:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|||||||
10
_DerivedData/Logs/Build/LogStoreManifest.plist
Normal file
10
_DerivedData/Logs/Build/LogStoreManifest.plist
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>logFormatVersion</key>
|
||||||
|
<integer>11</integer>
|
||||||
|
<key>logs</key>
|
||||||
|
<dict/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
10
_DerivedData/Logs/Launch/LogStoreManifest.plist
Normal file
10
_DerivedData/Logs/Launch/LogStoreManifest.plist
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>logFormatVersion</key>
|
||||||
|
<integer>11</integer>
|
||||||
|
<key>logs</key>
|
||||||
|
<dict/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
10
_DerivedData/Logs/Localization/LogStoreManifest.plist
Normal file
10
_DerivedData/Logs/Localization/LogStoreManifest.plist
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>logFormatVersion</key>
|
||||||
|
<integer>11</integer>
|
||||||
|
<key>logs</key>
|
||||||
|
<dict/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Binary file not shown.
47
_DerivedData/Logs/Package/LogStoreManifest.plist
Normal file
47
_DerivedData/Logs/Package/LogStoreManifest.plist
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>logFormatVersion</key>
|
||||||
|
<integer>11</integer>
|
||||||
|
<key>logs</key>
|
||||||
|
<dict>
|
||||||
|
<key>800731DD-5595-43EC-B207-003BAB7870CE</key>
|
||||||
|
<dict>
|
||||||
|
<key>className</key>
|
||||||
|
<string>IDECommandLineBuildLog</string>
|
||||||
|
<key>documentTypeString</key>
|
||||||
|
<string><nil></string>
|
||||||
|
<key>domainType</key>
|
||||||
|
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
|
||||||
|
<key>fileName</key>
|
||||||
|
<string>800731DD-5595-43EC-B207-003BAB7870CE.xcactivitylog</string>
|
||||||
|
<key>hasPrimaryLog</key>
|
||||||
|
<true/>
|
||||||
|
<key>primaryObservable</key>
|
||||||
|
<dict>
|
||||||
|
<key>highLevelStatus</key>
|
||||||
|
<string>E</string>
|
||||||
|
<key>totalNumberOfAnalyzerIssues</key>
|
||||||
|
<integer>0</integer>
|
||||||
|
<key>totalNumberOfErrors</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>totalNumberOfTestFailures</key>
|
||||||
|
<integer>0</integer>
|
||||||
|
<key>totalNumberOfWarnings</key>
|
||||||
|
<integer>3</integer>
|
||||||
|
</dict>
|
||||||
|
<key>signature</key>
|
||||||
|
<string>Resolve Packages</string>
|
||||||
|
<key>timeStartedRecording</key>
|
||||||
|
<real>788359220.39837599</real>
|
||||||
|
<key>timeStoppedRecording</key>
|
||||||
|
<real>788359220.51885402</real>
|
||||||
|
<key>title</key>
|
||||||
|
<string>Resolve Packages</string>
|
||||||
|
<key>uniqueIdentifier</key>
|
||||||
|
<string>800731DD-5595-43EC-B207-003BAB7870CE</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
10
_DerivedData/Logs/Test/LogStoreManifest.plist
Normal file
10
_DerivedData/Logs/Test/LogStoreManifest.plist
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>logFormatVersion</key>
|
||||||
|
<integer>11</integer>
|
||||||
|
<key>logs</key>
|
||||||
|
<dict/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
29
_xcodebuild.xcresult/Info.plist
Normal file
29
_xcodebuild.xcresult/Info.plist
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>dateCreated</key>
|
||||||
|
<date>2025-12-25T12:40:20Z</date>
|
||||||
|
<key>externalLocations</key>
|
||||||
|
<array/>
|
||||||
|
<key>rootId</key>
|
||||||
|
<dict>
|
||||||
|
<key>hash</key>
|
||||||
|
<string>0~z4eUyi7LNyiJgMc9YvhirRPEbAqQY1U8Utz3Zonm5K5gXqlevHrHNamc2oelL32RyN2c9x-M59B2wBAeP3TOAg==</string>
|
||||||
|
</dict>
|
||||||
|
<key>storage</key>
|
||||||
|
<dict>
|
||||||
|
<key>backend</key>
|
||||||
|
<string>fileBacked2</string>
|
||||||
|
<key>compression</key>
|
||||||
|
<string>standard</string>
|
||||||
|
</dict>
|
||||||
|
<key>version</key>
|
||||||
|
<dict>
|
||||||
|
<key>major</key>
|
||||||
|
<integer>3</integer>
|
||||||
|
<key>minor</key>
|
||||||
|
<integer>53</integer>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -225,6 +225,7 @@
|
|||||||
A1B2C4212EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */; };
|
A1B2C4212EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */; };
|
||||||
A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */; };
|
A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */; };
|
||||||
A1B2C9052FBD000200000001 /* KBBackspaceUndoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */; };
|
A1B2C9052FBD000200000001 /* KBBackspaceUndoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */; };
|
||||||
|
A1B2C9092FBD000200000005 /* KBInputBufferManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9082FBD000200000004 /* KBInputBufferManager.m */; };
|
||||||
A1B2D7022EB8C00100000001 /* KBLangTestVC.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2D7012EB8C00100000001 /* KBLangTestVC.m */; };
|
A1B2D7022EB8C00100000001 /* KBLangTestVC.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2D7012EB8C00100000001 /* KBLangTestVC.m */; };
|
||||||
A1B2E1012EBC7AAA00000001 /* KBTopThreeView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E0022EBC7AAA00000001 /* KBTopThreeView.m */; };
|
A1B2E1012EBC7AAA00000001 /* KBTopThreeView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E0022EBC7AAA00000001 /* KBTopThreeView.m */; };
|
||||||
A1B2E1022EBC7AAA00000001 /* HomeHotCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E0042EBC7AAA00000001 /* HomeHotCell.m */; };
|
A1B2E1022EBC7AAA00000001 /* HomeHotCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E0042EBC7AAA00000001 /* HomeHotCell.m */; };
|
||||||
@@ -639,6 +640,8 @@
|
|||||||
A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBBackspaceLongPressHandler.m; sourceTree = "<group>"; };
|
A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBBackspaceLongPressHandler.m; sourceTree = "<group>"; };
|
||||||
A1B2C9032FBD000200000001 /* KBBackspaceUndoManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBBackspaceUndoManager.h; sourceTree = "<group>"; };
|
A1B2C9032FBD000200000001 /* KBBackspaceUndoManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBBackspaceUndoManager.h; sourceTree = "<group>"; };
|
||||||
A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBBackspaceUndoManager.m; sourceTree = "<group>"; };
|
A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBBackspaceUndoManager.m; sourceTree = "<group>"; };
|
||||||
|
A1B2C9072FBD000200000003 /* KBInputBufferManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBInputBufferManager.h; sourceTree = "<group>"; };
|
||||||
|
A1B2C9082FBD000200000004 /* KBInputBufferManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBInputBufferManager.m; sourceTree = "<group>"; };
|
||||||
A1B2D7002EB8C00100000001 /* KBLangTestVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBLangTestVC.h; sourceTree = "<group>"; };
|
A1B2D7002EB8C00100000001 /* KBLangTestVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBLangTestVC.h; sourceTree = "<group>"; };
|
||||||
A1B2D7012EB8C00100000001 /* KBLangTestVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBLangTestVC.m; sourceTree = "<group>"; };
|
A1B2D7012EB8C00100000001 /* KBLangTestVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBLangTestVC.m; sourceTree = "<group>"; };
|
||||||
A1B2E0012EBC7AAA00000001 /* KBTopThreeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBTopThreeView.h; sourceTree = "<group>"; };
|
A1B2E0012EBC7AAA00000001 /* KBTopThreeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBTopThreeView.h; sourceTree = "<group>"; };
|
||||||
@@ -841,6 +844,8 @@
|
|||||||
A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */,
|
A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */,
|
||||||
A1B2C9032FBD000200000001 /* KBBackspaceUndoManager.h */,
|
A1B2C9032FBD000200000001 /* KBBackspaceUndoManager.h */,
|
||||||
A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */,
|
A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */,
|
||||||
|
A1B2C9072FBD000200000003 /* KBInputBufferManager.h */,
|
||||||
|
A1B2C9082FBD000200000004 /* KBInputBufferManager.m */,
|
||||||
);
|
);
|
||||||
path = Utils;
|
path = Utils;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1947,6 +1952,7 @@
|
|||||||
04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */,
|
04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */,
|
||||||
A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */,
|
A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */,
|
||||||
A1B2C9052FBD000200000001 /* KBBackspaceUndoManager.m in Sources */,
|
A1B2C9052FBD000200000001 /* KBBackspaceUndoManager.m in Sources */,
|
||||||
|
A1B2C9092FBD000200000005 /* KBInputBufferManager.m in Sources */,
|
||||||
049FB23F2EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */,
|
049FB23F2EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */,
|
||||||
04791F992ED49CE7004E8522 /* KBFont.m in Sources */,
|
04791F992ED49CE7004E8522 /* KBFont.m in Sources */,
|
||||||
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */,
|
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */,
|
||||||
|
|||||||
@@ -37,6 +37,10 @@
|
|||||||
[self kb_updateBackButtonVisibility];
|
[self kb_updateBackButtonVisibility];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)dealloc{
|
||||||
|
KBLOG(@"页面销毁 -- 💥💥 -- %@",[self class]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Custom NavBar
|
#pragma mark - Custom NavBar
|
||||||
|
|
||||||
|
|||||||
@@ -181,11 +181,21 @@
|
|||||||
|
|
||||||
/// 点击第一条示例文案
|
/// 点击第一条示例文案
|
||||||
- (void)kb_onTapQ1 {
|
- (void)kb_onTapQ1 {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_guide_copy_example_1"
|
||||||
|
pageId:@"guide"
|
||||||
|
elementId:@"copy_example_1"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[self kb_copyTextToPasteboard:[self.q1Button titleForState:UIControlStateNormal]];
|
[self kb_copyTextToPasteboard:[self.q1Button titleForState:UIControlStateNormal]];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 点击第二条示例文案
|
/// 点击第二条示例文案
|
||||||
- (void)kb_onTapQ2 {
|
- (void)kb_onTapQ2 {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_guide_copy_example_2"
|
||||||
|
pageId:@"guide"
|
||||||
|
elementId:@"copy_example_2"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[self kb_copyTextToPasteboard:[self.q2Button titleForState:UIControlStateNormal]];
|
[self kb_copyTextToPasteboard:[self.q2Button titleForState:UIControlStateNormal]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,11 @@
|
|||||||
#pragma mark - Actions
|
#pragma mark - Actions
|
||||||
- (void)onTapBuyAction {
|
- (void)onTapBuyAction {
|
||||||
// if (self.onTapBuy) { self.onTapBuy(); }
|
// if (self.onTapBuy) { self.onTapBuy(); }
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_home_buy_vip_btn"
|
||||||
|
pageId:@"home_main"
|
||||||
|
elementId:@"buy_vip_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
// 未登录时先跳到登录页;登录后才允许进入会员购买页
|
// 未登录时先跳到登录页;登录后才允许进入会员购买页
|
||||||
if (![KBUserSessionManager shared].isLoggedIn) {
|
if (![KBUserSessionManager shared].isLoggedIn) {
|
||||||
[[KBUserSessionManager shared] goLoginVC];
|
[[KBUserSessionManager shared] goLoginVC];
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
if (joined) {
|
if (joined) {
|
||||||
// 已加入状态:灰背景、打勾
|
// 已加入状态:灰背景、打勾
|
||||||
[self.actionButton setTitle:@"✓" forState:UIControlStateNormal];
|
[self.actionButton setTitle:@"✓" forState:UIControlStateNormal];
|
||||||
[self.actionButton setTitleColor:[UIColor colorWithWhite:0.45 alpha:1] forState:UIControlStateNormal];
|
[self.actionButton setTitleColor:[UIColor colorWithHex:0xBCBCBC] forState:UIControlStateNormal];
|
||||||
self.actionButton.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1];
|
self.actionButton.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1];
|
||||||
} else {
|
} else {
|
||||||
// 可加入状态:绿色加号
|
// 可加入状态:绿色加号
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
self.downloadLabel.text = character.download ?: @"";
|
self.downloadLabel.text = character.download ?: @"";
|
||||||
self.descLabel.text = character.characterBackground ?: @"";
|
self.descLabel.text = character.characterBackground ?: @"";
|
||||||
[self.avatarView kb_setImageURL:character.avatarUrl placeholder:KBAvatarPlaceholderImage];
|
[self.avatarView kb_setImageURL:character.avatarUrl placeholder:KBAvatarPlaceholderImage];
|
||||||
|
[self kb_updateSaveButtonWithAdded:character.added];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Build UI
|
#pragma mark - Build UI
|
||||||
@@ -138,6 +139,24 @@
|
|||||||
if (self.closeHandler) self.closeHandler();
|
if (self.closeHandler) self.closeHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - UI State
|
||||||
|
|
||||||
|
- (void)kb_updateSaveButtonWithAdded:(BOOL)added {
|
||||||
|
if (added) {
|
||||||
|
self.saveButton.enabled = NO;
|
||||||
|
self.saveButton.backgroundColor = [UIColor colorWithWhite:0.93 alpha:1.0];
|
||||||
|
[self.saveButton setTitle:@"✓" forState:UIControlStateNormal];
|
||||||
|
[self.saveButton setTitle:@"✓" forState:UIControlStateDisabled];
|
||||||
|
[self.saveButton setTitleColor:[UIColor colorWithWhite:0.55 alpha:1.0] forState:UIControlStateNormal];
|
||||||
|
[self.saveButton setTitleColor:[UIColor colorWithWhite:0.55 alpha:1.0] forState:UIControlStateDisabled];
|
||||||
|
} else {
|
||||||
|
self.saveButton.enabled = YES;
|
||||||
|
self.saveButton.backgroundColor = [UIColor colorWithRed:0.02 green:0.75 blue:0.67 alpha:1.0];
|
||||||
|
[self.saveButton setTitle:@"Save" forState:UIControlStateNormal];
|
||||||
|
[self.saveButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Helpers
|
#pragma mark - Helpers
|
||||||
|
|
||||||
- (UIImage *)placeholderAvatar {
|
- (UIImage *)placeholderAvatar {
|
||||||
@@ -246,4 +265,3 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -281,7 +281,7 @@
|
|||||||
if (added) {
|
if (added) {
|
||||||
// 已添加:灰色背景 + 勾选,且禁用点击
|
// 已添加:灰色背景 + 勾选,且禁用点击
|
||||||
[button setTitle:@"✓" forState:UIControlStateNormal];
|
[button setTitle:@"✓" forState:UIControlStateNormal];
|
||||||
[button setTitleColor:[UIColor colorWithWhite:0.45 alpha:1.0] forState:UIControlStateNormal];
|
[button setTitleColor:[UIColor colorWithHex:0xBCBCBC] forState:UIControlStateNormal];
|
||||||
button.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0];
|
button.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0];
|
||||||
button.enabled = NO;
|
button.enabled = NO;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -284,9 +284,44 @@
|
|||||||
pop.isClickBgDismiss = YES; // 点击背景关闭
|
pop.isClickBgDismiss = YES; // 点击背景关闭
|
||||||
pop.cornerRadius = 0; // 自定义 view 自处理圆角
|
pop.cornerRadius = 0; // 自定义 view 自处理圆角
|
||||||
|
|
||||||
|
KBWeakSelf
|
||||||
__weak typeof(pop) weakPop = pop;
|
__weak typeof(pop) weakPop = pop;
|
||||||
content.saveHandler = ^{ [weakPop dismiss]; };
|
content.saveHandler = ^{
|
||||||
content.closeHandler = ^{ [weakPop dismiss]; };
|
[weakPop dismiss];
|
||||||
|
|
||||||
|
if (![KBUserSessionManager shared].isLoggedIn) {
|
||||||
|
[[KBUserSessionManager shared] goLoginVC];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self) { return; }
|
||||||
|
if (indexPath.row >= self.listCharacters.count) { return; }
|
||||||
|
|
||||||
|
KBCharacter *mc = self.listCharacters[indexPath.row];
|
||||||
|
if (mc.added) { return; }
|
||||||
|
|
||||||
|
NSString *cidStr = mc.ID ?: @"";
|
||||||
|
if (cidStr.length == 0) { return; }
|
||||||
|
NSNumber *cid = @([cidStr integerValue]);
|
||||||
|
NSString *emoji = mc.emoji ? mc.emoji : @"";
|
||||||
|
|
||||||
|
[self.homeVM addUserCharacterWithId:cid emoji : emoji
|
||||||
|
completion:^(BOOL success, NSError * _Nullable error) {
|
||||||
|
if (success) {
|
||||||
|
mc.added = YES;
|
||||||
|
NSMutableArray *m = [self.listCharacters mutableCopy];
|
||||||
|
[m replaceObjectAtIndex:indexPath.row withObject:mc];
|
||||||
|
self.listCharacters = [m copy];
|
||||||
|
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
||||||
|
|
||||||
|
[self kb_refreshTopThreeView];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
content.closeHandler = ^{
|
||||||
|
[weakPop dismiss];
|
||||||
|
};
|
||||||
|
|
||||||
[pop pop];
|
[pop pop];
|
||||||
|
|
||||||
|
|||||||
@@ -48,10 +48,15 @@
|
|||||||
|
|
||||||
KBWeakSelf
|
KBWeakSelf
|
||||||
self.keyPermissButton.clickDragViewBlock = ^(WMDragView *dragView){
|
self.keyPermissButton.clickDragViewBlock = ^(WMDragView *dragView){
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_home_permission_float_btn"
|
||||||
|
pageId:@"home_main"
|
||||||
|
elementId:@"permission_float_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
KBGuideVC *vc = [KBGuideVC new];
|
KBGuideVC *vc = [KBGuideVC new];
|
||||||
[weakSelf.navigationController pushViewController:vc animated:true];
|
[weakSelf.navigationController pushViewController:vc animated:true];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 测试groups
|
// 测试groups
|
||||||
// NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
// NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
// // 写入一个简单字符串
|
// // 写入一个简单字符串
|
||||||
|
|||||||
@@ -210,9 +210,42 @@
|
|||||||
pop.isClickBgDismiss = YES; // 点击背景关闭
|
pop.isClickBgDismiss = YES; // 点击背景关闭
|
||||||
pop.cornerRadius = 0; // 自定义 view 自处理圆角
|
pop.cornerRadius = 0; // 自定义 view 自处理圆角
|
||||||
|
|
||||||
|
KBWeakSelf
|
||||||
__weak typeof(pop) weakPop = pop;
|
__weak typeof(pop) weakPop = pop;
|
||||||
content.saveHandler = ^{ [weakPop dismiss]; };
|
content.saveHandler = ^{
|
||||||
content.closeHandler = ^{ [weakPop dismiss]; };
|
[weakPop dismiss];
|
||||||
|
|
||||||
|
if (![KBUserSessionManager shared].isLoggedIn) {
|
||||||
|
[[KBUserSessionManager shared] goLoginVC];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self) { return; }
|
||||||
|
if (indexPath.item >= self.characters.count) { return; }
|
||||||
|
|
||||||
|
KBCharacter *mc = self.characters[indexPath.item];
|
||||||
|
if (mc.added) { return; }
|
||||||
|
|
||||||
|
NSString *cidStr = mc.ID ?: @"";
|
||||||
|
if (cidStr.length == 0) { return; }
|
||||||
|
NSNumber *cid = @([cidStr integerValue]);
|
||||||
|
NSString *emoji = mc.emoji ? mc.emoji : @"";
|
||||||
|
|
||||||
|
[self.homeVM addUserCharacterWithId:cid emoji : emoji
|
||||||
|
completion:^(BOOL success, NSError * _Nullable error) {
|
||||||
|
if (success) {
|
||||||
|
mc.added = YES;
|
||||||
|
NSMutableArray *m = [self.characters mutableCopy];
|
||||||
|
[m replaceObjectAtIndex:indexPath.item withObject:mc];
|
||||||
|
self.characters = [m copy];
|
||||||
|
[self.collectionView reloadItemsAtIndexPaths:@[indexPath]];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
content.closeHandler = ^{
|
||||||
|
[weakPop dismiss];
|
||||||
|
};
|
||||||
|
|
||||||
[pop pop];
|
[pop pop];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,6 +226,11 @@
|
|||||||
KBLOG(@"KBEmailLoginVC onTapSubmit, email=%@, pwdLen=%zd",
|
KBLOG(@"KBEmailLoginVC onTapSubmit, email=%@, pwdLen=%zd",
|
||||||
self.emailTextField.text,
|
self.emailTextField.text,
|
||||||
self.passwordTextField.text.length);
|
self.passwordTextField.text.length);
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_login_email_submit_btn"
|
||||||
|
pageId:@"login_email"
|
||||||
|
elementId:@"submit_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
NSString *email = self.emailTextField.text ? self.emailTextField.text : @"";
|
NSString *email = self.emailTextField.text ? self.emailTextField.text : @"";
|
||||||
NSString *password = self.passwordTextField.text ? self.passwordTextField.text : @"";
|
NSString *password = self.passwordTextField.text ? self.passwordTextField.text : @"";
|
||||||
KBWeakSelf;
|
KBWeakSelf;
|
||||||
|
|||||||
@@ -265,6 +265,12 @@
|
|||||||
NSString *pwd = self.passwordTextField.text ?: @"";
|
NSString *pwd = self.passwordTextField.text ?: @"";
|
||||||
NSString *repeat = self.repeatPasswordTextField.text ?: @"";
|
NSString *repeat = self.repeatPasswordTextField.text ?: @"";
|
||||||
|
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_register_email_submit_btn"
|
||||||
|
pageId:@"register_email"
|
||||||
|
elementId:@"submit_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
|
||||||
if (email.length == 0 || pwd.length == 0 || repeat.length == 0) {
|
if (email.length == 0 || pwd.length == 0 || repeat.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"Please complete all fields")];
|
[KBHUD showInfo:KBLocalized(@"Please complete all fields")];
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#import "KBForgetPwdNewPwdVC.h"
|
#import "KBForgetPwdNewPwdVC.h"
|
||||||
#import "KBLoginVM.h"
|
#import "KBLoginVM.h"
|
||||||
#import "KBEmailRegistVC.h"
|
#import "KBLoginVC.h"
|
||||||
@interface KBForgetPwdNewPwdVC () <UITextFieldDelegate>
|
@interface KBForgetPwdNewPwdVC () <UITextFieldDelegate>
|
||||||
|
|
||||||
@property (nonatomic, strong) UILabel *titleLabel; // Reset Password
|
@property (nonatomic, strong) UILabel *titleLabel; // Reset Password
|
||||||
@@ -113,6 +113,12 @@
|
|||||||
- (void)onTapNext {
|
- (void)onTapNext {
|
||||||
NSString *pwd = self.passwordTextField.text ?: @"";
|
NSString *pwd = self.passwordTextField.text ?: @"";
|
||||||
NSString *repeat = self.repeatPasswordTextField.text ?: @"";
|
NSString *repeat = self.repeatPasswordTextField.text ?: @"";
|
||||||
|
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_forgot_newpwd_next_btn"
|
||||||
|
pageId:@"forgot_password_newpwd"
|
||||||
|
elementId:@"next_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
|
||||||
if (pwd.length == 0 || repeat.length == 0) {
|
if (pwd.length == 0 || repeat.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"Please complete all fields")];
|
[KBHUD showInfo:KBLocalized(@"Please complete all fields")];
|
||||||
@@ -131,7 +137,7 @@
|
|||||||
if (success) {
|
if (success) {
|
||||||
UIViewController *targetVC = nil;
|
UIViewController *targetVC = nil;
|
||||||
for (UIViewController *vc in KB_CURRENT_NAV.viewControllers) {
|
for (UIViewController *vc in KB_CURRENT_NAV.viewControllers) {
|
||||||
if ([vc isKindOfClass:[KBEmailRegistVC class]]) {
|
if ([vc isKindOfClass:[KBLoginVC class]]) {
|
||||||
targetVC = vc;
|
targetVC = vc;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,11 @@
|
|||||||
- (void)onTapNext {
|
- (void)onTapNext {
|
||||||
NSString *email = [self.emailTextField.text ?: @"" stringByTrimmingCharactersInSet:
|
NSString *email = [self.emailTextField.text ?: @"" stringByTrimmingCharactersInSet:
|
||||||
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_forgot_email_next_btn"
|
||||||
|
pageId:@"forgot_password_email"
|
||||||
|
elementId:@"next_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
if (email.length == 0) {
|
if (email.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"Enter Email Address")];
|
[KBHUD showInfo:KBLocalized(@"Enter Email Address")];
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -76,6 +76,11 @@
|
|||||||
|
|
||||||
- (void)onTapNext {
|
- (void)onTapNext {
|
||||||
NSString *code = self.codeInputView.textValue ?: @"";
|
NSString *code = self.codeInputView.textValue ?: @"";
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_forgot_verify_next_btn"
|
||||||
|
pageId:@"forgot_password_verify"
|
||||||
|
elementId:@"next_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
if (code.length == 0) {
|
if (code.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"Enter Email Verification Code")];
|
[KBHUD showInfo:KBLocalized(@"Enter Email Verification Code")];
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -169,6 +169,11 @@
|
|||||||
|
|
||||||
- (void)onTapAppleLogin {
|
- (void)onTapAppleLogin {
|
||||||
KBLOG(@"onTapAppleLogin");
|
KBLOG(@"onTapAppleLogin");
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_login_apple_btn"
|
||||||
|
pageId:@"login"
|
||||||
|
elementId:@"apple_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[[KBLoginVM shared] signInWithAppleFromViewController:KB_CURRENT_NAV completion:^(BOOL success, NSError * _Nullable error) {
|
[[KBLoginVM shared] signInWithAppleFromViewController:KB_CURRENT_NAV completion:^(BOOL success, NSError * _Nullable error) {
|
||||||
if (success) {
|
if (success) {
|
||||||
[KBHUD showInfo:KBLocalized(@"Signed in successfully")];
|
[KBHUD showInfo:KBLocalized(@"Signed in successfully")];
|
||||||
@@ -190,6 +195,11 @@
|
|||||||
- (void)onTapEmailLogin {
|
- (void)onTapEmailLogin {
|
||||||
// 后续接入邮箱登录逻辑
|
// 后续接入邮箱登录逻辑
|
||||||
KBLOG(@"onTapEmailLogin");
|
KBLOG(@"onTapEmailLogin");
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_login_email_btn"
|
||||||
|
pageId:@"login"
|
||||||
|
elementId:@"email_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
KBEmailLoginVC *vc = [[KBEmailLoginVC alloc] init];
|
KBEmailLoginVC *vc = [[KBEmailLoginVC alloc] init];
|
||||||
UINavigationController *nav = KB_CURRENT_NAV;
|
UINavigationController *nav = KB_CURRENT_NAV;
|
||||||
if ([nav isKindOfClass:[BaseNavigationController class]]) {
|
if ([nav isKindOfClass:[BaseNavigationController class]]) {
|
||||||
@@ -207,6 +217,11 @@
|
|||||||
- (void)onTapSignUp {
|
- (void)onTapSignUp {
|
||||||
// 打开注册页
|
// 打开注册页
|
||||||
KBLOG(@"onTapSignUp");
|
KBLOG(@"onTapSignUp");
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_login_signup_btn"
|
||||||
|
pageId:@"login"
|
||||||
|
elementId:@"signup_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
KBEmailRegistVC *vc = [[KBEmailRegistVC alloc] init];
|
KBEmailRegistVC *vc = [[KBEmailRegistVC alloc] init];
|
||||||
UINavigationController *nav = KB_CURRENT_NAV;
|
UINavigationController *nav = KB_CURRENT_NAV;
|
||||||
if ([nav isKindOfClass:[BaseNavigationController class]]) {
|
if ([nav isKindOfClass:[BaseNavigationController class]]) {
|
||||||
@@ -219,6 +234,11 @@
|
|||||||
- (void)onTapForgotPassword {
|
- (void)onTapForgotPassword {
|
||||||
// 打开忘记密码页
|
// 打开忘记密码页
|
||||||
KBLOG(@"onTapForgotPassword");
|
KBLOG(@"onTapForgotPassword");
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_login_forgot_btn"
|
||||||
|
pageId:@"login"
|
||||||
|
elementId:@"forgot_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
KBForgetPwdVC *vc = [[KBForgetPwdVC alloc] init];
|
KBForgetPwdVC *vc = [[KBForgetPwdVC alloc] init];
|
||||||
UINavigationController *nav = KB_CURRENT_NAV;
|
UINavigationController *nav = KB_CURRENT_NAV;
|
||||||
if ([nav isKindOfClass:[BaseNavigationController class]]) {
|
if ([nav isKindOfClass:[BaseNavigationController class]]) {
|
||||||
|
|||||||
@@ -89,6 +89,11 @@
|
|||||||
|
|
||||||
- (void)onTapConfirm {
|
- (void)onTapConfirm {
|
||||||
NSString *code = self.codeInputView.textValue ?: @"";
|
NSString *code = self.codeInputView.textValue ?: @"";
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_register_verify_confirm_btn"
|
||||||
|
pageId:@"register_verify_email"
|
||||||
|
elementId:@"confirm_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
if (code.length == 0) {
|
if (code.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"Enter Email Verification Code")];
|
[KBHUD showInfo:KBLocalized(@"Enter Email Verification Code")];
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -90,6 +90,12 @@
|
|||||||
[KBHUD showInfo:KBLocalized(@"Please Enter The Content")];
|
[KBHUD showInfo:KBLocalized(@"Please Enter The Content")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
NSDictionary *extra = @{@"content_len": @(content.length)};
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_feedback_commit_btn"
|
||||||
|
pageId:@"feedback"
|
||||||
|
elementId:@"commit_btn"
|
||||||
|
extra:extra
|
||||||
|
completion:nil];
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
[self.viewModel submitFeedbackWithContent:content completion:^(BOOL success, NSError * _Nullable error) {
|
[self.viewModel submitFeedbackWithContent:content completion:^(BOOL success, NSError * _Nullable error) {
|
||||||
if (!success) { return; }
|
if (!success) { return; }
|
||||||
|
|||||||
@@ -257,6 +257,11 @@ static NSString * const kKBMyKeyboardCellId = @"kKBMyKeyboardCellId";
|
|||||||
|
|
||||||
- (void)onSave {
|
- (void)onSave {
|
||||||
// 点击底部保存按钮时,调用更新用户人设排序接口
|
// 点击底部保存按钮时,调用更新用户人设排序接口
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_my_keyboard_save_btn"
|
||||||
|
pageId:@"my_keyboard"
|
||||||
|
elementId:@"save_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[self kb_updateUserCharacterSortWithShowHUD:YES];
|
[self kb_updateUserCharacterSortWithShowHUD:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +307,11 @@ static NSString * const kKBMyKeyboardCellId = @"kKBMyKeyboardCellId";
|
|||||||
[KBHUD showInfo:msg];
|
[KBHUD showInfo:msg];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
[self.viewModel fetchCharacterListByUserWithCompletion:^(NSArray<KBCharacter *> * _Nonnull characterArray, NSError * _Nullable error) {
|
||||||
|
if (error) {
|
||||||
|
NSLog(@"[KBHomeVM] refresh user characters failed: %@", error);
|
||||||
|
}
|
||||||
|
}];
|
||||||
if (showHUD) {
|
if (showHUD) {
|
||||||
[KBHUD showSuccess:KBLocalized(@"Saved")];
|
[KBHUD showSuccess:KBLocalized(@"Saved")];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,9 +257,21 @@
|
|||||||
|
|
||||||
#pragma mark - Actions
|
#pragma mark - Actions
|
||||||
|
|
||||||
- (void)onTapAvatarEdit { [self presentImagePicker]; }
|
- (void)onTapAvatarEdit {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_person_avatar_edit"
|
||||||
|
pageId:@"person_info"
|
||||||
|
elementId:@"avatar_edit"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
[self presentImagePicker];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)onTapLogout {
|
- (void)onTapLogout {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_person_logout_btn"
|
||||||
|
pageId:@"person_info"
|
||||||
|
elementId:@"logout_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[self.myVM logout];
|
[self.myVM logout];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,13 @@ static NSString * const kMySkinCellId = @"kMySkinCellId";
|
|||||||
- (void)onToggleEdit {
|
- (void)onToggleEdit {
|
||||||
self.editingMode = !self.editingMode;
|
self.editingMode = !self.editingMode;
|
||||||
|
|
||||||
|
NSDictionary *extra = @{@"editing": @(self.isEditingMode)};
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_my_skin_toggle_edit"
|
||||||
|
pageId:@"my_skin"
|
||||||
|
elementId:@"toggle_edit"
|
||||||
|
extra:extra
|
||||||
|
completion:nil];
|
||||||
|
|
||||||
// 更新顶部按钮
|
// 更新顶部按钮
|
||||||
[self.kb_rightButton setTitle:(self.isEditingMode ? KBLocalized(@"Cancel") : KBLocalized(@"Editor"))
|
[self.kb_rightButton setTitle:(self.isEditingMode ? KBLocalized(@"Cancel") : KBLocalized(@"Editor"))
|
||||||
forState:UIControlStateNormal];
|
forState:UIControlStateNormal];
|
||||||
@@ -154,6 +161,13 @@ static NSString * const kMySkinCellId = @"kMySkinCellId";
|
|||||||
NSArray<NSIndexPath *> *selectedIndexPaths = [[self.collectionView indexPathsForSelectedItems] sortedArrayUsingSelector:@selector(compare:)];
|
NSArray<NSIndexPath *> *selectedIndexPaths = [[self.collectionView indexPathsForSelectedItems] sortedArrayUsingSelector:@selector(compare:)];
|
||||||
if (selectedIndexPaths.count == 0) return;
|
if (selectedIndexPaths.count == 0) return;
|
||||||
|
|
||||||
|
NSDictionary *preExtra = @{@"selected_count": @(selectedIndexPaths.count)};
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_my_skin_delete_btn"
|
||||||
|
pageId:@"my_skin"
|
||||||
|
elementId:@"delete_btn"
|
||||||
|
extra:preExtra
|
||||||
|
completion:nil];
|
||||||
|
|
||||||
NSMutableArray<NSString *> *themeIds = [NSMutableArray arrayWithCapacity:selectedIndexPaths.count];
|
NSMutableArray<NSString *> *themeIds = [NSMutableArray arrayWithCapacity:selectedIndexPaths.count];
|
||||||
for (NSIndexPath *ip in selectedIndexPaths) {
|
for (NSIndexPath *ip in selectedIndexPaths) {
|
||||||
if (ip.item >= self.data.count) { continue; }
|
if (ip.item >= self.data.count) { continue; }
|
||||||
@@ -245,6 +259,16 @@ static NSString * const kMySkinCellId = @"kMySkinCellId";
|
|||||||
// 非编辑态:可在此进入详情,当前示例不处理
|
// 非编辑态:可在此进入详情,当前示例不处理
|
||||||
KBMyTheme *theme = self.data[indexPath.item];
|
KBMyTheme *theme = self.data[indexPath.item];
|
||||||
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
|
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
if ([theme.themeId isKindOfClass:NSString.class] && theme.themeId.length > 0) {
|
||||||
|
extra[@"theme_id"] = theme.themeId;
|
||||||
|
}
|
||||||
|
extra[@"index"] = @(indexPath.item);
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_my_skin_theme_card"
|
||||||
|
pageId:@"my_skin"
|
||||||
|
elementId:@"theme_card"
|
||||||
|
extra:extra.copy
|
||||||
|
completion:nil];
|
||||||
KBSkinDetailVC *vc = [[KBSkinDetailVC alloc] init];
|
KBSkinDetailVC *vc = [[KBSkinDetailVC alloc] init];
|
||||||
vc.themeId = theme.themeId;
|
vc.themeId = theme.themeId;
|
||||||
[self.navigationController pushViewController:vc animated:true];
|
[self.navigationController pushViewController:vc animated:true];
|
||||||
|
|||||||
@@ -120,6 +120,19 @@
|
|||||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||||
NSDictionary *info = self.data[indexPath.section][indexPath.row];
|
NSDictionary *info = self.data[indexPath.section][indexPath.row];
|
||||||
NSString *itemID = info[@"id"];
|
NSString *itemID = info[@"id"];
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
if ([itemID isKindOfClass:NSString.class] && itemID.length > 0) {
|
||||||
|
extra[@"item_id"] = itemID;
|
||||||
|
}
|
||||||
|
NSString *title = info[@"title"];
|
||||||
|
if ([title isKindOfClass:NSString.class] && title.length > 0) {
|
||||||
|
extra[@"item_title"] = title;
|
||||||
|
}
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_my_menu_item"
|
||||||
|
pageId:@"my"
|
||||||
|
elementId:@"menu_item"
|
||||||
|
extra:extra.copy
|
||||||
|
completion:nil];
|
||||||
if ([itemID isEqualToString:@"1"]) {
|
if ([itemID isEqualToString:@"1"]) {
|
||||||
[self.navigationController pushViewController:[KBNoticeVC new] animated:true];
|
[self.navigationController pushViewController:[KBNoticeVC new] animated:true];
|
||||||
|
|
||||||
@@ -128,9 +141,12 @@
|
|||||||
if (!self.viewModel) {
|
if (!self.viewModel) {
|
||||||
self.viewModel = [[KBMyVM alloc] init];
|
self.viewModel = [[KBMyVM alloc] init];
|
||||||
}
|
}
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
[KBHUD show];
|
[KBHUD show];
|
||||||
[self.viewModel fetchInviteCodeWithCompletion:^(KBInviteCodeModel * _Nullable inviteCode, NSError * _Nullable error) {
|
[self.viewModel fetchInviteCodeWithCompletion:^(KBInviteCodeModel * _Nullable inviteCode, NSError * _Nullable error) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self) { return; }
|
||||||
[KBHUD dismiss];
|
[KBHUD dismiss];
|
||||||
if (error) {
|
if (error) {
|
||||||
[KBHUD showInfo:error.localizedDescription ?: KBLocalized(@"Network error")];
|
[KBHUD showInfo:error.localizedDescription ?: KBLocalized(@"Network error")];
|
||||||
@@ -142,9 +158,26 @@
|
|||||||
[KBHUD showInfo:KBLocalized(@"Failed")];
|
[KBHUD showInfo:KBLocalized(@"Failed")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
id shareItem = textToCopy;
|
||||||
|
NSURL *url = [NSURL URLWithString:textToCopy];
|
||||||
|
if (url) {
|
||||||
|
shareItem = url;
|
||||||
|
}
|
||||||
|
UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:@[shareItem] applicationActivities:nil];
|
||||||
|
UIPopoverPresentationController *popover = activityVC.popoverPresentationController;
|
||||||
|
if (popover) {
|
||||||
|
popover.sourceView = self.view;
|
||||||
|
popover.sourceRect = CGRectMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds), 1, 1);
|
||||||
|
popover.permittedArrowDirections = 0;
|
||||||
|
}
|
||||||
UIPasteboard.generalPasteboard.string = textToCopy;
|
UIPasteboard.generalPasteboard.string = textToCopy;
|
||||||
[KBHUD showInfo:KBLocalized(@"Copy Success")];
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_my_invite_copy"
|
||||||
|
pageId:@"my"
|
||||||
|
elementId:@"invite_copy"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
[self presentViewController:activityVC animated:YES completion:nil];
|
||||||
});
|
});
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
|
|
||||||
- (NSString *)coinsDisplayText {
|
- (NSString *)coinsDisplayText {
|
||||||
NSString *name = self.name ?: @"";
|
NSString *name = self.name ?: @"";
|
||||||
NSString *unit = self.unit ?: @"";
|
// NSString *unit = self.unit ?: @"";
|
||||||
if (name.length && unit.length) {
|
if (name.length) {
|
||||||
return [NSString stringWithFormat:@"%@ %@", name, unit];
|
return [NSString stringWithFormat:@"%@", name];
|
||||||
}
|
}
|
||||||
if (name.length) { return name; }
|
if (name.length) { return name; }
|
||||||
if (unit.length) { return unit; }
|
// if (unit.length) { return unit; }
|
||||||
return @"";
|
return @"";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,18 @@ static NSString * const kKBJfPayCellId = @"kKBJfPayCellId";
|
|||||||
NSInteger old = self.selectedIndex;
|
NSInteger old = self.selectedIndex;
|
||||||
self.selectedIndex = indexPath.item;
|
self.selectedIndex = indexPath.item;
|
||||||
|
|
||||||
|
KBPayProductModel *item = (indexPath.item < self.data.count) ? self.data[indexPath.item] : nil;
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
extra[@"index"] = @(indexPath.item);
|
||||||
|
if ([item.productId isKindOfClass:NSString.class] && item.productId.length > 0) {
|
||||||
|
extra[@"product_id"] = item.productId;
|
||||||
|
}
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_points_select_product"
|
||||||
|
pageId:@"points_recharge"
|
||||||
|
elementId:@"product_item"
|
||||||
|
extra:extra.copy
|
||||||
|
completion:nil];
|
||||||
|
|
||||||
KBJfPayCell *newCell = (KBJfPayCell *)[collectionView cellForItemAtIndexPath:indexPath];
|
KBJfPayCell *newCell = (KBJfPayCell *)[collectionView cellForItemAtIndexPath:indexPath];
|
||||||
[newCell applySelected:YES animated:YES];
|
[newCell applySelected:YES animated:YES];
|
||||||
if (old >= 0 && old < self.data.count) {
|
if (old >= 0 && old < self.data.count) {
|
||||||
@@ -319,6 +331,15 @@ static NSString * const kKBJfPayCellId = @"kKBJfPayCellId";
|
|||||||
[KBHUD showInfo:KBLocalized(@"Please select a product")];
|
[KBHUD showInfo:KBLocalized(@"Please select a product")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
if ([selectedItem.productId isKindOfClass:NSString.class] && selectedItem.productId.length > 0) {
|
||||||
|
extra[@"product_id"] = selectedItem.productId;
|
||||||
|
}
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_points_pay_btn"
|
||||||
|
pageId:@"points_recharge"
|
||||||
|
elementId:@"pay_btn"
|
||||||
|
extra:extra.copy
|
||||||
|
completion:nil];
|
||||||
NSString *productId = selectedItem.productId;
|
NSString *productId = selectedItem.productId;
|
||||||
if (productId.length == 0) {
|
if (productId.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"Product unavailable")];
|
[KBHUD showInfo:KBLocalized(@"Product unavailable")];
|
||||||
|
|||||||
@@ -293,6 +293,11 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId";
|
|||||||
|
|
||||||
#pragma mark - Action
|
#pragma mark - Action
|
||||||
- (void)onTapClose{
|
- (void)onTapClose{
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_vip_close_btn"
|
||||||
|
pageId:@"vip_pay"
|
||||||
|
elementId:@"close_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[self.navigationController popViewControllerAnimated:true];
|
[self.navigationController popViewControllerAnimated:true];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +308,15 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId";
|
|||||||
[KBHUD showInfo:KBLocalized(@"Please select a product")];
|
[KBHUD showInfo:KBLocalized(@"Please select a product")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
if ([plan.productId isKindOfClass:NSString.class] && plan.productId.length > 0) {
|
||||||
|
extra[@"product_id"] = plan.productId;
|
||||||
|
}
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_vip_pay_btn"
|
||||||
|
pageId:@"vip_pay"
|
||||||
|
elementId:@"pay_btn"
|
||||||
|
extra:extra.copy
|
||||||
|
completion:nil];
|
||||||
NSString *productId = plan.productId;
|
NSString *productId = plan.productId;
|
||||||
if (productId.length == 0) {
|
if (productId.length == 0) {
|
||||||
[KBHUD showInfo:KBLocalized(@"Product unavailable")];
|
[KBHUD showInfo:KBLocalized(@"Product unavailable")];
|
||||||
@@ -326,6 +340,11 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId";
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)onTapRestoreButton {
|
- (void)onTapRestoreButton {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_vip_restore_btn"
|
||||||
|
pageId:@"vip_pay"
|
||||||
|
elementId:@"restore_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[KBHUD show];
|
[KBHUD show];
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
[[KBStoreKitBridge shared] restorePurchasesWithCompletion:^(BOOL success, NSString * _Nullable message) {
|
[[KBStoreKitBridge shared] restorePurchasesWithCompletion:^(BOOL success, NSString * _Nullable message) {
|
||||||
@@ -386,6 +405,18 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId";
|
|||||||
if (self.selectedIndex == indexPath.item) { return; }
|
if (self.selectedIndex == indexPath.item) { return; }
|
||||||
NSInteger old = self.selectedIndex;
|
NSInteger old = self.selectedIndex;
|
||||||
self.selectedIndex = indexPath.item;
|
self.selectedIndex = indexPath.item;
|
||||||
|
|
||||||
|
KBPayProductModel *plan = (indexPath.item < self.plans.count) ? self.plans[indexPath.item] : nil;
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
extra[@"index"] = @(indexPath.item);
|
||||||
|
if ([plan.productId isKindOfClass:NSString.class] && plan.productId.length > 0) {
|
||||||
|
extra[@"product_id"] = plan.productId;
|
||||||
|
}
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_vip_select_plan"
|
||||||
|
pageId:@"vip_pay"
|
||||||
|
elementId:@"plan_item"
|
||||||
|
extra:extra.copy
|
||||||
|
completion:nil];
|
||||||
|
|
||||||
KBVipSubscribeCell *newCell = (KBVipSubscribeCell *)[collectionView cellForItemAtIndexPath:indexPath];
|
KBVipSubscribeCell *newCell = (KBVipSubscribeCell *)[collectionView cellForItemAtIndexPath:indexPath];
|
||||||
[newCell applySelected:YES animated:YES];
|
[newCell applySelected:YES animated:YES];
|
||||||
|
|||||||
@@ -14,8 +14,9 @@
|
|||||||
- (void)applePayReqWithParams:(NSDictionary *)params
|
- (void)applePayReqWithParams:(NSDictionary *)params
|
||||||
needShow:(BOOL)needShow
|
needShow:(BOOL)needShow
|
||||||
completion:(KBPayCompletion)completion {
|
completion:(KBPayCompletion)completion {
|
||||||
// if (needShow) { [KBHUD show]; }
|
if (needShow) {
|
||||||
[KBHUD showWithStatus:@"Please wait"];
|
[KBHUD showWithStatus:@"Please wait"];
|
||||||
|
}
|
||||||
[[KBNetworkManager shared] POST:API_VALIDATE_RECEIPT
|
[[KBNetworkManager shared] POST:API_VALIDATE_RECEIPT
|
||||||
jsonBody:params
|
jsonBody:params
|
||||||
headers:nil
|
headers:nil
|
||||||
@@ -31,11 +32,15 @@
|
|||||||
NSNumber *codeNum = error.userInfo[@"code"];
|
NSNumber *codeNum = error.userInfo[@"code"];
|
||||||
NSInteger bizCode = codeNum.integerValue; // 这里就是底层附带的业务 code
|
NSInteger bizCode = codeNum.integerValue; // 这里就是底层附带的业务 code
|
||||||
// 根据 bizCode 做处理,比如透传给上层 completion
|
// 根据 bizCode 做处理,比如透传给上层 completion
|
||||||
if (completion) completion(bizCode, error.localizedDescription ?: KBLocalized(@"Network error"));
|
NSString *msg = error.localizedDescription ?: KBLocalized(@"Network error");
|
||||||
|
if (needShow) { [KBHUD showError:msg]; }
|
||||||
|
if (completion) completion(bizCode, msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (completion) completion(ERROR_CODE, error.localizedDescription ?: KBLocalized(@"Network error"));
|
NSString *msg = error.localizedDescription ?: KBLocalized(@"Network error");
|
||||||
|
if (needShow) { [KBHUD showError:msg]; }
|
||||||
|
if (completion) completion(KBBizCodeSystemError, msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,13 @@ static NSString * const kResultCellId = @"KBSkinCardCell";
|
|||||||
KBSearchThemeModel *model = self.resultItems[indexPath.item];
|
KBSearchThemeModel *model = self.resultItems[indexPath.item];
|
||||||
NSString *themeId = [model.themeId stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
NSString *themeId = [model.themeId stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
if (themeId.length == 0) { return; }
|
if (themeId.length == 0) { return; }
|
||||||
|
NSDictionary *extra = @{@"theme_id": themeId,
|
||||||
|
@"index": @(indexPath.item)};
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_search_result_theme"
|
||||||
|
pageId:@"search_result"
|
||||||
|
elementId:@"result_theme_card"
|
||||||
|
extra:extra
|
||||||
|
completion:nil];
|
||||||
KBSkinDetailVC *vc = [[KBSkinDetailVC alloc] init];
|
KBSkinDetailVC *vc = [[KBSkinDetailVC alloc] init];
|
||||||
vc.themeId = themeId;
|
vc.themeId = themeId;
|
||||||
[self.navigationController pushViewController:vc animated:YES];
|
[self.navigationController pushViewController:vc animated:YES];
|
||||||
|
|||||||
@@ -244,6 +244,11 @@ typedef NS_ENUM(NSInteger, KBSearchSection) {
|
|||||||
|
|
||||||
/// 清空历史
|
/// 清空历史
|
||||||
- (void)clearHistory {
|
- (void)clearHistory {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_search_clear_history"
|
||||||
|
pageId:@"search"
|
||||||
|
elementId:@"clear_history"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[self.historyWords removeAllObjects];
|
[self.historyWords removeAllObjects];
|
||||||
self.historyExpanded = NO;
|
self.historyExpanded = NO;
|
||||||
[self saveHistoryWordsToLocal:self.historyWords];
|
[self saveHistoryWordsToLocal:self.historyWords];
|
||||||
@@ -369,10 +374,22 @@ typedef NS_ENUM(NSInteger, KBSearchSection) {
|
|||||||
NSArray *list = [self currentDisplayHistory];
|
NSArray *list = [self currentDisplayHistory];
|
||||||
NSString *kw = list[indexPath.item];
|
NSString *kw = list[indexPath.item];
|
||||||
if ([kw isEqualToString:kMoreToken]) {
|
if ([kw isEqualToString:kMoreToken]) {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_search_history_more"
|
||||||
|
pageId:@"search"
|
||||||
|
elementId:@"history_more"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
// 展开所有历史
|
// 展开所有历史
|
||||||
self.historyExpanded = YES;
|
self.historyExpanded = YES;
|
||||||
[self.collectionView reloadData];
|
[self.collectionView reloadData];
|
||||||
} else {
|
} else {
|
||||||
|
NSDictionary *extra = @{@"index": @(indexPath.item),
|
||||||
|
@"keyword_len": @(kw.length)};
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_search_history_item"
|
||||||
|
pageId:@"search"
|
||||||
|
elementId:@"history_item"
|
||||||
|
extra:extra
|
||||||
|
completion:nil];
|
||||||
[self.searchBarView updateKeyword:kw];
|
[self.searchBarView updateKeyword:kw];
|
||||||
[self performSearch:kw];
|
[self performSearch:kw];
|
||||||
[self openResultForKeyword:kw];
|
[self openResultForKeyword:kw];
|
||||||
@@ -381,6 +398,16 @@ typedef NS_ENUM(NSInteger, KBSearchSection) {
|
|||||||
}
|
}
|
||||||
if (indexPath.section == KBSearchSectionRecommend) {
|
if (indexPath.section == KBSearchSectionRecommend) {
|
||||||
KBShopThemeModel *model = self.recommendedThemes[indexPath.item];
|
KBShopThemeModel *model = self.recommendedThemes[indexPath.item];
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
extra[@"index"] = @(indexPath.item);
|
||||||
|
if ([model.themeId isKindOfClass:NSString.class] && model.themeId.length > 0) {
|
||||||
|
extra[@"theme_id"] = model.themeId;
|
||||||
|
}
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_search_recommend_theme"
|
||||||
|
pageId:@"search"
|
||||||
|
elementId:@"recommend_theme_card"
|
||||||
|
extra:extra.copy
|
||||||
|
completion:nil];
|
||||||
[self openDetailForThemeId:model.themeId ?: @""];
|
[self openDetailForThemeId:model.themeId ?: @""];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,6 +421,12 @@ typedef NS_ENUM(NSInteger, KBSearchSection) {
|
|||||||
// _searchBarView.placeholder = @"Themes";
|
// _searchBarView.placeholder = @"Themes";
|
||||||
KBWeakSelf
|
KBWeakSelf
|
||||||
_searchBarView.onSearch = ^(NSString * _Nonnull keyword) {
|
_searchBarView.onSearch = ^(NSString * _Nonnull keyword) {
|
||||||
|
NSDictionary *extra = @{@"keyword_len": @(keyword.length)};
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_search_submit"
|
||||||
|
pageId:@"search"
|
||||||
|
elementId:@"search_submit"
|
||||||
|
extra:extra
|
||||||
|
completion:nil];
|
||||||
// 置顶到历史 + 打开结果页
|
// 置顶到历史 + 打开结果页
|
||||||
[weakSelf performSearch:keyword];
|
[weakSelf performSearch:keyword];
|
||||||
[weakSelf openResultForKeyword:keyword];
|
[weakSelf openResultForKeyword:keyword];
|
||||||
|
|||||||
@@ -173,6 +173,16 @@
|
|||||||
|
|
||||||
- (void)kb_handleShopTapAtIndexPath:(NSIndexPath *)indexPath {
|
- (void)kb_handleShopTapAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
KBShopThemeModel *selTheme = (indexPath.item < self.dataSource.count) ? self.dataSource[indexPath.item] : nil;
|
KBShopThemeModel *selTheme = (indexPath.item < self.dataSource.count) ? self.dataSource[indexPath.item] : nil;
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
if ([selTheme.themeId isKindOfClass:NSString.class] && selTheme.themeId.length > 0) {
|
||||||
|
extra[@"theme_id"] = selTheme.themeId;
|
||||||
|
}
|
||||||
|
extra[@"index"] = @(indexPath.item);
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_shop_theme_card"
|
||||||
|
pageId:@"shop_item_list"
|
||||||
|
elementId:@"theme_card"
|
||||||
|
extra:extra.copy
|
||||||
|
completion:nil];
|
||||||
KBSkinDetailVC *vc = [[KBSkinDetailVC alloc] init];
|
KBSkinDetailVC *vc = [[KBSkinDetailVC alloc] init];
|
||||||
vc.themeId = selTheme.themeId;
|
vc.themeId = selTheme.themeId;
|
||||||
[self.navigationController pushViewController:vc animated:true];
|
[self.navigationController pushViewController:vc animated:true];
|
||||||
|
|||||||
@@ -244,11 +244,21 @@ static const CGFloat JXheightForHeaderInSection = 50;
|
|||||||
|
|
||||||
#pragma mark - action
|
#pragma mark - action
|
||||||
- (void)searchBtnAction{
|
- (void)searchBtnAction{
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_shop_search_btn"
|
||||||
|
pageId:@"shop"
|
||||||
|
elementId:@"search_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
KBSearchVC *vc = [[KBSearchVC alloc] init];
|
KBSearchVC *vc = [[KBSearchVC alloc] init];
|
||||||
[self.navigationController pushViewController:vc animated:true];
|
[self.navigationController pushViewController:vc animated:true];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)skinBtnAction{
|
- (void)skinBtnAction{
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_shop_my_skin_btn"
|
||||||
|
pageId:@"shop"
|
||||||
|
elementId:@"my_skin_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
MySkinVC *vc = [[MySkinVC alloc] init];
|
MySkinVC *vc = [[MySkinVC alloc] init];
|
||||||
[self.navigationController pushViewController:vc animated:true];
|
[self.navigationController pushViewController:vc animated:true];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,18 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
|
|||||||
if (nextThemeId.length == 0) { return; }
|
if (nextThemeId.length == 0) { return; }
|
||||||
if ([nextThemeId isEqualToString:self.themeId ?: @""]) { return; }
|
if ([nextThemeId isEqualToString:self.themeId ?: @""]) { return; }
|
||||||
|
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
if ([self.themeId isKindOfClass:NSString.class] && self.themeId.length > 0) {
|
||||||
|
extra[@"from_theme_id"] = self.themeId;
|
||||||
|
}
|
||||||
|
extra[@"to_theme_id"] = nextThemeId;
|
||||||
|
extra[@"index"] = @(indexPath.item);
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_skin_recommend_card"
|
||||||
|
pageId:@"skin_detail"
|
||||||
|
elementId:@"recommend_card"
|
||||||
|
extra:extra.copy
|
||||||
|
completion:nil];
|
||||||
|
|
||||||
// 不跳转新页面:直接切换 themeId,重新请求详情并刷新当前页面
|
// 不跳转新页面:直接切换 themeId,重新请求详情并刷新当前页面
|
||||||
self.themeId = nextThemeId;
|
self.themeId = nextThemeId;
|
||||||
self.detailModel = nil;
|
self.detailModel = nil;
|
||||||
@@ -242,6 +254,16 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
|
|||||||
[KBHUD showInfo:KBLocalized(@"正在加载主题详情")];
|
[KBHUD showInfo:KBLocalized(@"正在加载主题详情")];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
extra[@"theme_id"] = self.themeId ?: @"";
|
||||||
|
extra[@"purchased"] = @(self.detailModel.isPurchased);
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_skin_download_btn"
|
||||||
|
pageId:@"skin_detail"
|
||||||
|
elementId:@"download_btn"
|
||||||
|
extra:extra.copy
|
||||||
|
completion:nil];
|
||||||
|
|
||||||
NSLog(@"🧩[SkinDetail] action themeId=%@ purchased=%d", self.themeId, self.detailModel.isPurchased);
|
NSLog(@"🧩[SkinDetail] action themeId=%@ purchased=%d", self.themeId, self.detailModel.isPurchased);
|
||||||
if (self.detailModel.isPurchased) {
|
if (self.detailModel.isPurchased) {
|
||||||
[self requestDownload];
|
[self requestDownload];
|
||||||
@@ -296,6 +318,7 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
|
|||||||
fromViewController:self
|
fromViewController:self
|
||||||
mode:KBSkinSourceModeRemoteZip
|
mode:KBSkinSourceModeRemoteZip
|
||||||
completion:^(BOOL success) {
|
completion:^(BOOL success) {
|
||||||
|
[KBHUD dismiss];
|
||||||
NSLog(@"%@[SkinDetail] download result id=%@",
|
NSLog(@"%@[SkinDetail] download result id=%@",
|
||||||
(success ? @"✅" : @"❌"),
|
(success ? @"✅" : @"❌"),
|
||||||
self.detailModel.themeId);
|
self.detailModel.themeId);
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
|
|
||||||
//-----------------------------------------------宏定义全局----------------------------------------------------------/
|
//-----------------------------------------------宏定义全局----------------------------------------------------------/
|
||||||
// 调试专用日志(DEBUG 打印,RELEASE 不打印)。尽量显眼,包含函数与行号。
|
// 调试专用日志(DEBUG 打印,RELEASE 不打印)。尽量显眼,包含函数与行号。
|
||||||
|
#ifndef KBLOG
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
#define KBLOG(fmt, ...) do { \
|
#define KBLOG(fmt, ...) do { \
|
||||||
NSString *kb_msg__ = [NSString stringWithFormat:(fmt), ##__VA_ARGS__]; \
|
NSString *kb_msg__ = [NSString stringWithFormat:(fmt), ##__VA_ARGS__]; \
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
#else
|
#else
|
||||||
#define KBLOG(...)
|
#define KBLOG(...)
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
// 通用链接(Universal Links)统一配置
|
// 通用链接(Universal Links)统一配置
|
||||||
// 仅需修改这里的域名/前缀,工程内所有使用 UL 的地方都会同步。
|
// 仅需修改这里的域名/前缀,工程内所有使用 UL 的地方都会同步。
|
||||||
|
|||||||
@@ -193,7 +193,11 @@ static void *KBPermPlayerPresentationSizeContext = &KBPermPlayerPresentationSize
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)openSettings {
|
- (void)openSettings {
|
||||||
[self report];
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_permission_open_settings_btn"
|
||||||
|
pageId:@"keyboard_permission_guide"
|
||||||
|
elementId:@"open_settings_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
|
NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
|
||||||
UIApplication *app = [UIApplication sharedApplication];
|
UIApplication *app = [UIApplication sharedApplication];
|
||||||
if ([app canOpenURL:url]) {
|
if ([app canOpenURL:url]) {
|
||||||
@@ -206,6 +210,11 @@ static void *KBPermPlayerPresentationSizeContext = &KBPermPlayerPresentationSize
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)closeButtonAction{
|
- (void)closeButtonAction{
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_permission_close_btn"
|
||||||
|
pageId:@"keyboard_permission_guide"
|
||||||
|
elementId:@"close_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
[self.navigationController popViewControllerAnimated:true];
|
[self.navigationController popViewControllerAnimated:true];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,13 +295,6 @@ static void *KBPermPlayerPresentationSizeContext = &KBPermPlayerPresentationSize
|
|||||||
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - network
|
|
||||||
- (void)report{
|
|
||||||
[[KBMaiPointReporter sharedReporter] reportNewAccountWithType:@"键盘申请授权" account:nil completion:^(BOOL success, NSError * _Nullable error) {
|
|
||||||
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Lazy Subviews
|
#pragma mark - Lazy Subviews
|
||||||
|
|
||||||
- (UIButton *)backButton {
|
- (UIButton *)backButton {
|
||||||
|
|||||||
Reference in New Issue
Block a user