Compare commits
46 Commits
main
...
bdf2a9af80
| Author | SHA1 | Date | |
|---|---|---|---|
| bdf2a9af80 | |||
| e858d35722 | |||
| f2d5210313 | |||
| 1b0af3e2d6 | |||
| 0965cd3c7e | |||
| c3909d63da | |||
| 1096f24c57 | |||
| 7ed84fd445 | |||
| 4e2d7d2908 | |||
| 34089ddeea | |||
| 6ec98468de | |||
| 2d5919016f | |||
| c0fa51bb2e | |||
| 6713f36387 | |||
| f24750458a | |||
| 510a2f4d66 | |||
| ae37730da6 | |||
| 203f104ece | |||
| 8e934dd83a | |||
| 1676916a5c | |||
| 1af5a0e849 | |||
| 5b6e0a8fbf | |||
| 9968883bab | |||
| af5f637d31 | |||
| 0a725e845e | |||
| 6a539dc3c5 | |||
| 73d6ec933a | |||
| 000d603241 | |||
| fbf9fe9f2a | |||
| 8e4d7e1ee8 | |||
| 262eb57b36 | |||
| 2e1c261775 | |||
| 6ad2079351 | |||
| a477592f5d | |||
| 6f336e8368 | |||
| 17e038beb1 | |||
| 4e6fd90668 | |||
| 5cfc76e6c5 | |||
| 9e33c93763 | |||
| 1c9ae7bc06 | |||
| 472e9ad341 | |||
| 19c69f4f6f | |||
| 8788cbb105 | |||
| ea77e9a5f8 | |||
| eaaf0e1ed6 | |||
| 8a344b293d |
22
CustomKeyboard/KeyboardAssets.xcassets/key_revoke.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "key_revoke@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "key_revoke@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/key_revoke.imageset/key_revoke@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/key_revoke.imageset/key_revoke@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
@@ -20,14 +20,16 @@
|
|||||||
#import "KBKeyboardSubscriptionView.h"
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
#import "KBKeyboardSubscriptionProduct.h"
|
#import "KBKeyboardSubscriptionProduct.h"
|
||||||
#import "KBBackspaceUndoManager.h"
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
#import "KBSuggestionEngine.h"
|
||||||
|
|
||||||
// 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。
|
// 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。
|
||||||
@interface KeyboardViewController (KBSkinShopBridge)
|
@interface KeyboardViewController (KBSkinShopBridge)
|
||||||
- (void)kb_consumePendingShopSkin;
|
- (void)kb_consumePendingShopSkin;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
// 以 375 宽设计稿为基准的键盘总高度(包括顶部工具栏)
|
// 以 375 宽设计稿为基准的键盘总高度
|
||||||
static const CGFloat kKBKeyboardDesignHeight = 250.0f;
|
static const CGFloat kKBKeyboardBaseHeight = 250.0f;
|
||||||
|
|
||||||
static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||||
void *observer,
|
void *observer,
|
||||||
@@ -45,11 +47,21 @@ 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; // 设置页
|
||||||
@property (nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
|
@property (nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
|
||||||
@property (nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
|
@property (nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
|
||||||
|
@property (nonatomic, strong) KBSuggestionEngine *suggestionEngine;
|
||||||
|
@property (nonatomic, copy) NSString *currentWord;
|
||||||
|
@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
|
||||||
@@ -60,7 +72,11 @@ 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.currentWord = @"";
|
||||||
// 指定 HUD 的承载视图(扩展里无法取到 App 的 KeyWindow)
|
// 指定 HUD 的承载视图(扩展里无法取到 App 的 KeyWindow)
|
||||||
[KBHUD setContainerView:self.view];
|
[KBHUD setContainerView:self.view];
|
||||||
// 绑定完全访问管理器,便于统一感知和联动网络开关
|
// 绑定完全访问管理器,便于统一感知和联动网络开关
|
||||||
@@ -86,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(kKBKeyboardDesignHeight);
|
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;
|
||||||
@@ -111,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);
|
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +178,113 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
|
|
||||||
#pragma mark - Private
|
#pragma mark - Private
|
||||||
|
|
||||||
|
// MARK: - Suggestions
|
||||||
|
|
||||||
|
- (void)kb_updateCurrentWordWithInsertedText:(NSString *)text {
|
||||||
|
if (text.length == 0) { return; }
|
||||||
|
if ([self kb_isAlphabeticString:text]) {
|
||||||
|
NSString *current = self.currentWord ?: @"";
|
||||||
|
self.currentWord = [current stringByAppendingString:text];
|
||||||
|
self.suppressSuggestions = NO;
|
||||||
|
[self kb_updateSuggestionsForCurrentWord];
|
||||||
|
} else {
|
||||||
|
[self kb_clearCurrentWord];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_clearCurrentWord {
|
||||||
|
self.currentWord = @"";
|
||||||
|
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||||
|
self.suppressSuggestions = NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_scheduleContextRefreshResetSuppression:(BOOL)resetSuppression {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[self kb_refreshCurrentWordFromDocumentContextResetSuppression:resetSuppression];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_refreshCurrentWordFromDocumentContextResetSuppression:(BOOL)resetSuppression {
|
||||||
|
NSString *context = self.textDocumentProxy.documentContextBeforeInput ?: @"";
|
||||||
|
NSString *word = [self kb_extractTrailingWordFromContext:context];
|
||||||
|
self.currentWord = word ?: @"";
|
||||||
|
if (resetSuppression) {
|
||||||
|
self.suppressSuggestions = NO;
|
||||||
|
}
|
||||||
|
[self kb_updateSuggestionsForCurrentWord];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_extractTrailingWordFromContext:(NSString *)context {
|
||||||
|
if (context.length == 0) { return @""; }
|
||||||
|
static NSCharacterSet *letters = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
|
||||||
|
});
|
||||||
|
|
||||||
|
NSInteger idx = (NSInteger)context.length - 1;
|
||||||
|
while (idx >= 0) {
|
||||||
|
unichar ch = [context characterAtIndex:(NSUInteger)idx];
|
||||||
|
if (![letters characterIsMember:ch]) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
idx -= 1;
|
||||||
|
}
|
||||||
|
NSUInteger start = (NSUInteger)(idx + 1);
|
||||||
|
if (start >= context.length) { return @""; }
|
||||||
|
return [context substringFromIndex:start];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)kb_isAlphabeticString:(NSString *)text {
|
||||||
|
if (text.length == 0) { return NO; }
|
||||||
|
static NSCharacterSet *letters = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
|
||||||
|
});
|
||||||
|
for (NSUInteger i = 0; i < text.length; i++) {
|
||||||
|
if (![letters characterIsMember:[text characterAtIndex:i]]) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_updateSuggestionsForCurrentWord {
|
||||||
|
NSString *prefix = self.currentWord ?: @"";
|
||||||
|
if (prefix.length == 0) {
|
||||||
|
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (self.suppressSuggestions) {
|
||||||
|
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSArray<NSString *> *items = [self.suggestionEngine suggestionsForPrefix:prefix limit:5];
|
||||||
|
NSArray<NSString *> *cased = [self kb_applyCaseToSuggestions:items prefix:prefix];
|
||||||
|
[self.keyBoardMainView kb_setSuggestions:cased];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<NSString *> *)kb_applyCaseToSuggestions:(NSArray<NSString *> *)items prefix:(NSString *)prefix {
|
||||||
|
if (items.count == 0 || prefix.length == 0) { return items; }
|
||||||
|
BOOL allUpper = [prefix isEqualToString:prefix.uppercaseString];
|
||||||
|
BOOL firstUpper = [[prefix substringToIndex:1] isEqualToString:[[prefix substringToIndex:1] uppercaseString]];
|
||||||
|
|
||||||
|
if (!allUpper && !firstUpper) { return items; }
|
||||||
|
|
||||||
|
NSMutableArray<NSString *> *result = [NSMutableArray arrayWithCapacity:items.count];
|
||||||
|
for (NSString *word in items) {
|
||||||
|
if (allUpper) {
|
||||||
|
[result addObject:word.uppercaseString];
|
||||||
|
} else {
|
||||||
|
NSString *first = [[word substringToIndex:1] uppercaseString];
|
||||||
|
NSString *rest = (word.length > 1) ? [word substringFromIndex:1] : @"";
|
||||||
|
[result addObject:[first stringByAppendingString:rest]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.copy;
|
||||||
|
}
|
||||||
|
|
||||||
/// 切换显示功能面板/键盘主视图
|
/// 切换显示功能面板/键盘主视图
|
||||||
- (void)showFunctionPanel:(BOOL)show {
|
- (void)showFunctionPanel:(BOOL)show {
|
||||||
// 简单显隐切换,复用相同的布局区域
|
// 简单显隐切换,复用相同的布局区域
|
||||||
@@ -144,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:^{
|
||||||
@@ -182,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) {
|
||||||
@@ -209,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];
|
||||||
@@ -234,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;
|
||||||
@@ -250,23 +415,41 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
|
|
||||||
// MARK: - KBKeyBoardMainViewDelegate
|
// MARK: - KBKeyBoardMainViewDelegate
|
||||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapKey:(KBKey *)key {
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapKey:(KBKey *)key {
|
||||||
if (key.type != KBKeyTypeShift && key.type != KBKeyTypeModeChange) {
|
|
||||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
|
||||||
}
|
|
||||||
switch (key.type) {
|
switch (key.type) {
|
||||||
case KBKeyTypeCharacter:
|
case KBKeyTypeCharacter: {
|
||||||
[self.textDocumentProxy insertText:key.output ?: key.title ?: @""]; break;
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
NSString *text = key.output ?: key.title ?: @"";
|
||||||
|
[self.textDocumentProxy insertText:text];
|
||||||
|
[self kb_updateCurrentWordWithInsertedText:text];
|
||||||
|
[[KBInputBufferManager shared] appendText:text];
|
||||||
|
} break;
|
||||||
case KBKeyTypeBackspace:
|
case KBKeyTypeBackspace:
|
||||||
[self.textDocumentProxy deleteBackward]; break;
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:self.textDocumentProxy];
|
||||||
|
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:self.textDocumentProxy.documentContextBeforeInput
|
||||||
|
after:self.textDocumentProxy.documentContextAfterInput];
|
||||||
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:self.textDocumentProxy count:1];
|
||||||
|
[self kb_scheduleContextRefreshResetSuppression:NO];
|
||||||
|
[[KBInputBufferManager shared] applyHoldDeleteCount:1];
|
||||||
|
break;
|
||||||
case KBKeyTypeSpace:
|
case KBKeyTypeSpace:
|
||||||
[self.textDocumentProxy insertText:@" "]; break;
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
[self.textDocumentProxy insertText:@" "];
|
||||||
|
[self kb_clearCurrentWord];
|
||||||
|
[[KBInputBufferManager shared] appendText:@" "];
|
||||||
|
break;
|
||||||
case KBKeyTypeReturn:
|
case KBKeyTypeReturn:
|
||||||
[self.textDocumentProxy insertText:@"\n"]; break;
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
[self.textDocumentProxy insertText:@"\n"];
|
||||||
|
[self kb_clearCurrentWord];
|
||||||
|
[[KBInputBufferManager shared] appendText:@"\n"];
|
||||||
|
break;
|
||||||
case KBKeyTypeGlobe:
|
case KBKeyTypeGlobe:
|
||||||
[self advanceToNextInputMode]; break;
|
[self advanceToNextInputMode]; break;
|
||||||
case KBKeyTypeCustom:
|
case KBKeyTypeCustom:
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
// 点击自定义键切换到功能面板
|
// 点击自定义键切换到功能面板
|
||||||
[self showFunctionPanel:YES];
|
[self showFunctionPanel:YES];
|
||||||
|
[self kb_clearCurrentWord];
|
||||||
break;
|
break;
|
||||||
case KBKeyTypeModeChange:
|
case KBKeyTypeModeChange:
|
||||||
case KBKeyTypeShift:
|
case KBKeyTypeShift:
|
||||||
@@ -276,14 +459,26 @@ 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];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[self showFunctionPanel:NO];
|
[self showFunctionPanel:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (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];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,16 +486,52 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
if (emoji.length == 0) { return; }
|
if (emoji.length == 0) { return; }
|
||||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
[self.textDocumentProxy insertText:emoji];
|
[self.textDocumentProxy insertText:emoji];
|
||||||
|
[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];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (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 {
|
||||||
|
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 ?: @"";
|
||||||
|
if (current.length > 0) {
|
||||||
|
for (NSUInteger i = 0; i < current.length; i++) {
|
||||||
|
[self.textDocumentProxy deleteBackward];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[self.textDocumentProxy insertText:suggestion];
|
||||||
|
self.currentWord = suggestion;
|
||||||
|
[self.suggestionEngine recordSelection:suggestion];
|
||||||
|
self.suppressSuggestions = YES;
|
||||||
|
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||||
|
[[KBInputBufferManager shared] replaceTailWithText:suggestion deleteCount:current.length];
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - KBFunctionViewDelegate
|
// MARK: - KBFunctionViewDelegate
|
||||||
- (void)functionView:(KBFunctionView *)functionView didTapToolActionAtIndex:(NSInteger)index {
|
- (void)functionView:(KBFunctionView *)functionView didTapToolActionAtIndex:(NSInteger)index {
|
||||||
// 需求:当 index == 0 时,切回键盘主视图
|
// 需求:当 index == 0 时,切回键盘主视图
|
||||||
@@ -309,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];
|
||||||
//
|
//
|
||||||
@@ -331,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];
|
||||||
}
|
}
|
||||||
@@ -409,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];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,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]];
|
||||||
@@ -448,9 +725,11 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
- (void)kb_applyTheme {
|
- (void)kb_applyTheme {
|
||||||
KBSkinTheme *t = [KBSkinManager shared].current;
|
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||||
UIImage *img = [[KBSkinManager shared] currentBackgroundImage];
|
UIImage *img = [[KBSkinManager shared] currentBackgroundImage];
|
||||||
|
NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil));
|
||||||
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)]) {
|
||||||
@@ -484,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];
|
||||||
|
|||||||
23
CustomKeyboard/Manager/KBSuggestionEngine.h
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// KBSuggestionEngine.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// Simple local suggestion engine (prefix match + lightweight ranking).
|
||||||
|
@interface KBSuggestionEngine : NSObject
|
||||||
|
|
||||||
|
+ (instancetype)shared;
|
||||||
|
|
||||||
|
/// Returns suggestions for prefix (lowercase expected), limited by count.
|
||||||
|
- (NSArray<NSString *> *)suggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit;
|
||||||
|
|
||||||
|
/// Record a selection to slightly boost ranking next time.
|
||||||
|
- (void)recordSelection:(NSString *)word;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
167
CustomKeyboard/Manager/KBSuggestionEngine.m
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
//
|
||||||
|
// KBSuggestionEngine.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBSuggestionEngine.h"
|
||||||
|
#import "KBConfig.h"
|
||||||
|
|
||||||
|
@interface KBSuggestionEngine ()
|
||||||
|
@property (nonatomic, copy) NSArray<NSString *> *words;
|
||||||
|
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *selectionCounts;
|
||||||
|
@property (nonatomic, strong) NSSet<NSString *> *priorityWords;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBSuggestionEngine
|
||||||
|
|
||||||
|
+ (instancetype)shared {
|
||||||
|
static KBSuggestionEngine *engine;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
engine = [[KBSuggestionEngine alloc] init];
|
||||||
|
});
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)init {
|
||||||
|
if (self = [super init]) {
|
||||||
|
_selectionCounts = [NSMutableDictionary dictionary];
|
||||||
|
NSArray<NSString *> *defaults = [self.class kb_defaultWords];
|
||||||
|
_priorityWords = [NSSet setWithArray:defaults];
|
||||||
|
_words = [self kb_loadWords];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<NSString *> *)suggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
|
||||||
|
if (prefix.length == 0 || limit == 0) { return @[]; }
|
||||||
|
NSString *lower = prefix.lowercaseString;
|
||||||
|
NSMutableArray<NSString *> *matches = [NSMutableArray array];
|
||||||
|
|
||||||
|
for (NSString *word in self.words) {
|
||||||
|
if ([word hasPrefix:lower]) {
|
||||||
|
[matches addObject:word];
|
||||||
|
if (matches.count >= limit * 3) {
|
||||||
|
// Avoid scanning too many matches for long lists.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.count == 0) { return @[]; }
|
||||||
|
|
||||||
|
[matches sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
|
||||||
|
NSInteger ca = self.selectionCounts[a].integerValue;
|
||||||
|
NSInteger cb = self.selectionCounts[b].integerValue;
|
||||||
|
if (ca != cb) {
|
||||||
|
return (cb > ca) ? NSOrderedAscending : NSOrderedDescending;
|
||||||
|
}
|
||||||
|
BOOL pa = [self.priorityWords containsObject:a];
|
||||||
|
BOOL pb = [self.priorityWords containsObject:b];
|
||||||
|
if (pa != pb) {
|
||||||
|
return pa ? NSOrderedAscending : NSOrderedDescending;
|
||||||
|
}
|
||||||
|
return [a compare:b];
|
||||||
|
}];
|
||||||
|
|
||||||
|
if (matches.count > limit) {
|
||||||
|
return [matches subarrayWithRange:NSMakeRange(0, limit)];
|
||||||
|
}
|
||||||
|
return matches.copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)recordSelection:(NSString *)word {
|
||||||
|
if (word.length == 0) { return; }
|
||||||
|
NSString *key = word.lowercaseString;
|
||||||
|
NSInteger count = self.selectionCounts[key].integerValue + 1;
|
||||||
|
self.selectionCounts[key] = @(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Defaults
|
||||||
|
|
||||||
|
- (NSArray<NSString *> *)kb_loadWords {
|
||||||
|
NSMutableOrderedSet<NSString *> *set = [[NSMutableOrderedSet alloc] init];
|
||||||
|
[set addObjectsFromArray:[self.class kb_defaultWords]];
|
||||||
|
|
||||||
|
NSArray<NSString *> *paths = [self kb_wordListPaths];
|
||||||
|
for (NSString *path in paths) {
|
||||||
|
if (path.length == 0) { continue; }
|
||||||
|
NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
|
||||||
|
if (content.length == 0) { continue; }
|
||||||
|
NSArray<NSString *> *lines = [content componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
|
||||||
|
for (NSString *line in lines) {
|
||||||
|
NSString *word = [self kb_sanitizedWordFromLine:line];
|
||||||
|
if (word.length == 0) { continue; }
|
||||||
|
[set addObject:word];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray<NSString *> *result = set.array ?: @[];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<NSString *> *)kb_wordListPaths {
|
||||||
|
NSMutableArray<NSString *> *paths = [NSMutableArray array];
|
||||||
|
// 1) App Group override (allows server-downloaded large list).
|
||||||
|
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||||||
|
if (containerURL.path.length > 0) {
|
||||||
|
NSString *groupPath = [[containerURL path] stringByAppendingPathComponent:@"kb_words.txt"];
|
||||||
|
[paths addObject:groupPath];
|
||||||
|
}
|
||||||
|
// 2) Bundle fallback.
|
||||||
|
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"kb_words" ofType:@"txt"];
|
||||||
|
if (bundlePath.length > 0) {
|
||||||
|
[paths addObject:bundlePath];
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_sanitizedWordFromLine:(NSString *)line {
|
||||||
|
NSString *trimmed = [[line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString];
|
||||||
|
if (trimmed.length == 0) { return @""; }
|
||||||
|
static NSCharacterSet *letters = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyz"];
|
||||||
|
});
|
||||||
|
for (NSUInteger i = 0; i < trimmed.length; i++) {
|
||||||
|
if (![letters characterIsMember:[trimmed characterAtIndex:i]]) {
|
||||||
|
return @"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (NSArray<NSString *> *)kb_defaultWords {
|
||||||
|
return @[
|
||||||
|
@"a", @"an", @"and", @"are", @"as", @"at",
|
||||||
|
@"app", @"ap", @"apple", @"apply", @"april", @"application",
|
||||||
|
@"about", @"above", @"after", @"again", @"against", @"all",
|
||||||
|
@"am", @"among", @"amount", @"any", @"around",
|
||||||
|
@"be", @"because", @"been", @"before", @"being", @"below",
|
||||||
|
@"best", @"between", @"both", @"but", @"by",
|
||||||
|
@"can", @"could", @"come", @"common", @"case",
|
||||||
|
@"do", @"does", @"down", @"day",
|
||||||
|
@"each", @"early", @"end", @"even", @"every",
|
||||||
|
@"for", @"from", @"first", @"found", @"free",
|
||||||
|
@"get", @"good", @"great", @"go",
|
||||||
|
@"have", @"has", @"had", @"help", @"how",
|
||||||
|
@"in", @"is", @"it", @"if", @"into",
|
||||||
|
@"just", @"keep", @"kind", @"know",
|
||||||
|
@"like", @"look", @"long", @"last",
|
||||||
|
@"make", @"more", @"most", @"my",
|
||||||
|
@"new", @"no", @"not", @"now",
|
||||||
|
@"of", @"on", @"one", @"or", @"other", @"our", @"out",
|
||||||
|
@"people", @"place", @"please",
|
||||||
|
@"quick", @"quite",
|
||||||
|
@"right", @"read", @"real",
|
||||||
|
@"see", @"say", @"some", @"such", @"so",
|
||||||
|
@"the", @"to", @"this", @"that", @"them", @"then", @"there", @"they", @"these", @"time",
|
||||||
|
@"use", @"up", @"under",
|
||||||
|
@"very",
|
||||||
|
@"we", @"with", @"what", @"when", @"where", @"who", @"why", @"will", @"would",
|
||||||
|
@"you", @"your"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -21,6 +21,9 @@
|
|||||||
#import "Masonry.h"
|
#import "Masonry.h"
|
||||||
#import "KBHUD.h" // 复用 App 内的 HUD 封装
|
#import "KBHUD.h" // 复用 App 内的 HUD 封装
|
||||||
#import "KBLocalizationManager.h" // 复用多语言封装(可在扩展内使用)
|
#import "KBLocalizationManager.h" // 复用多语言封装(可在扩展内使用)
|
||||||
|
#import "KBMaiPointReporter.h"
|
||||||
|
//#import "KBLog.h"
|
||||||
|
|
||||||
|
|
||||||
// 通用链接(Universal Links)统一配置
|
// 通用链接(Universal Links)统一配置
|
||||||
// 配置好 AASA 与 Associated Domains 后,只需修改这里即可切换域名/path。
|
// 配置好 AASA 与 Associated Domains 后,只需修改这里即可切换域名/path。
|
||||||
|
|||||||
@@ -242,7 +242,8 @@
|
|||||||
/* 自定义 AI 功能键 */
|
/* 自定义 AI 功能键 */
|
||||||
"ai" = "key_ai";
|
"ai" = "key_ai";
|
||||||
/* Emoji功能键 */
|
/* Emoji功能键 */
|
||||||
"emoji" = "key_emoji";
|
//"emoji" = "key_emoji";
|
||||||
|
"emoji_panel" = "key_emoji";
|
||||||
/* 发送/换行键 */
|
/* 发送/换行键 */
|
||||||
"return" = "key_send";
|
"return" = "key_send";
|
||||||
|
|
||||||
|
|||||||
234454
CustomKeyboard/Resource/kb_words.txt
Normal file
@@ -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 = 26.0;
|
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;
|
||||||
@@ -48,6 +55,9 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
@property (nonatomic, assign) CGPoint backspaceLastTouchPointInSelf;
|
@property (nonatomic, assign) CGPoint backspaceLastTouchPointInSelf;
|
||||||
@property (nonatomic, assign) NSUInteger backspaceClearToken;
|
@property (nonatomic, assign) NSUInteger backspaceClearToken;
|
||||||
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
||||||
|
@property (nonatomic, copy) NSString *pendingClearBefore;
|
||||||
|
@property (nonatomic, copy) NSString *pendingClearAfter;
|
||||||
|
@property (nonatomic, assign) KBClearPhase backspaceClearPhase;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KBBackspaceLongPressHandler
|
@implementation KBBackspaceLongPressHandler
|
||||||
@@ -55,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;
|
||||||
}
|
}
|
||||||
@@ -73,6 +84,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
self.backspaceHasLastTouchPoint = NO;
|
self.backspaceHasLastTouchPoint = NO;
|
||||||
self.backspaceHoldToken += 1;
|
self.backspaceHoldToken += 1;
|
||||||
[self kb_hideBackspaceClearLabel];
|
[self kb_hideBackspaceClearLabel];
|
||||||
|
self.pendingClearBefore = nil;
|
||||||
|
self.pendingClearAfter = nil;
|
||||||
|
|
||||||
if (!button) { return; }
|
if (!button) { return; }
|
||||||
|
|
||||||
@@ -99,7 +112,18 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
}
|
}
|
||||||
switch (gr.state) {
|
switch (gr.state) {
|
||||||
case UIGestureRecognizerStateBegan: {
|
case UIGestureRecognizerStateBegan: {
|
||||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
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) {
|
||||||
|
[self kb_capturePendingClearSnapshotIfNeeded];
|
||||||
|
[[KBInputBufferManager shared] beginPendingClearSnapshot];
|
||||||
|
}
|
||||||
self.backspaceHoldToken += 1;
|
self.backspaceHoldToken += 1;
|
||||||
NSUInteger token = self.backspaceHoldToken;
|
NSUInteger token = self.backspaceHoldToken;
|
||||||
self.backspaceHoldActive = YES;
|
self.backspaceHoldActive = YES;
|
||||||
@@ -134,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) {
|
||||||
@@ -145,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;
|
||||||
@@ -186,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;
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
|
||||||
@@ -222,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];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -303,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;
|
||||||
@@ -310,6 +386,11 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
[self kb_hideBackspaceClearLabel];
|
[self kb_hideBackspaceClearLabel];
|
||||||
if (shouldClear) {
|
if (shouldClear) {
|
||||||
[self kb_clearAllInput];
|
[self kb_clearAllInput];
|
||||||
|
} else {
|
||||||
|
self.pendingClearBefore = nil;
|
||||||
|
self.pendingClearAfter = nil;
|
||||||
|
[[KBInputBufferManager shared] clearPendingClearSnapshot];
|
||||||
|
[[KBInputBufferManager shared] commitLiveToManual];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,9 +482,9 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
- (UILabel *)backspaceClearLabel {
|
- (UILabel *)backspaceClearLabel {
|
||||||
if (!_backspaceClearLabel) {
|
if (!_backspaceClearLabel) {
|
||||||
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
|
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||||
label.text = @"立刻清空";
|
label.text = KBLocalized(@"Clear");
|
||||||
label.textAlignment = NSTextAlignmentCenter;
|
label.textAlignment = NSTextAlignmentCenter;
|
||||||
label.font = [UIFont systemFontOfSize:12 weight:UIFontWeightSemibold];
|
label.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||||
label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor;
|
label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor;
|
||||||
label.backgroundColor = [self kb_backspaceClearLabelNormalColor];
|
label.backgroundColor = [self kb_backspaceClearLabelNormalColor];
|
||||||
label.layer.cornerRadius = kKBBackspaceClearLabelCornerRadius;
|
label.layer.cornerRadius = kKBBackspaceClearLabelCornerRadius;
|
||||||
@@ -421,10 +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 = ivc.textDocumentProxy.documentContextBeforeInput ?: @"";
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
[[KBBackspaceUndoManager shared] recordClearWithContext:before];
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
||||||
}
|
}
|
||||||
|
self.pendingClearBefore = 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];
|
||||||
}
|
}
|
||||||
@@ -437,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)),
|
||||||
@@ -489,4 +635,28 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
return self.backspaceButton.superview;
|
return self.backspaceButton.superview;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)kb_captureDeletionSnapshotIfNeeded {
|
||||||
|
if ([KBBackspaceUndoManager shared].hasUndo) { return; }
|
||||||
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
|
if (!ivc) { return; }
|
||||||
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
|
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:proxy.documentContextBeforeInput
|
||||||
|
after:proxy.documentContextAfterInput];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_capturePendingClearSnapshotIfNeeded {
|
||||||
|
if (self.pendingClearBefore.length > 0 || self.pendingClearAfter.length > 0) { return; }
|
||||||
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
|
if (!ivc) { return; }
|
||||||
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
|
self.pendingClearBefore = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
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
|
||||||
|
|||||||
@@ -15,13 +15,19 @@ extern NSNotificationName const KBBackspaceUndoStateDidChangeNotification;
|
|||||||
|
|
||||||
+ (instancetype)shared;
|
+ (instancetype)shared;
|
||||||
|
|
||||||
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput)
|
/// 记录一次删除前的快照(不改变撤销按钮显示)。
|
||||||
- (void)recordClearWithContext:(NSString *)context;
|
- (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after;
|
||||||
|
|
||||||
|
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput/AfterInput)。
|
||||||
|
- (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;
|
||||||
|
|
||||||
/// 非清空行为触发时,清理撤销状态
|
/// 非删除行为触发时,清理撤销状态
|
||||||
- (void)registerNonClearAction;
|
- (void)registerNonClearAction;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -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, strong) NSMutableArray<NSString *> *segments; // deletion order (last -> first)
|
@property (nonatomic, copy) NSString *undoText;
|
||||||
@property (nonatomic, assign) BOOL lastActionWasClear;
|
@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
|
||||||
@@ -27,42 +52,191 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
|
|||||||
|
|
||||||
- (instancetype)init {
|
- (instancetype)init {
|
||||||
if (self = [super init]) {
|
if (self = [super init]) {
|
||||||
_segments = [NSMutableArray array];
|
_undoText = @"";
|
||||||
|
_undoAfterLength = 0;
|
||||||
|
_snapshotSource = KBUndoSnapshotSourceNone;
|
||||||
|
_undoDeletedPieces = [NSMutableArray array];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)recordClearWithContext:(NSString *)context {
|
- (void)captureAndDeleteBackwardFromProxy:(id<UITextDocumentProxy>)proxy count:(NSUInteger)count {
|
||||||
if (context.length == 0) { return; }
|
if (!proxy || count == 0) { return; }
|
||||||
NSString *segment = [self kb_segmentForClearFromContext:context];
|
|
||||||
if (segment.length == 0) { return; }
|
|
||||||
|
|
||||||
if (!self.lastActionWasClear) {
|
NSString *selected = proxy.selectedText ?: @"";
|
||||||
[self.segments removeAllObjects];
|
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;
|
||||||
}
|
}
|
||||||
[self.segments addObject:segment];
|
|
||||||
self.lastActionWasClear = YES;
|
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 {
|
||||||
|
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 *safeAfter = after ?: @"";
|
||||||
|
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
||||||
|
if (full.length == 0) { return; }
|
||||||
|
self.undoText = full;
|
||||||
|
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 {
|
||||||
|
NSString *pending = [KBInputBufferManager shared].pendingClearSnapshot;
|
||||||
|
NSString *manual = [KBInputBufferManager shared].manualSnapshot;
|
||||||
|
NSString *fallbackText = (pending.length > 0) ? pending : ((manual.length > 0) ? manual : [KBInputBufferManager shared].liveText);
|
||||||
|
|
||||||
|
NSString *safeBefore = before ?: @"";
|
||||||
|
NSString *safeAfter = after ?: @"";
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.segments.count == 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;
|
||||||
NSString *text = [self kb_buildUndoText];
|
NSString *curBefore = proxy.documentContextBeforeInput ?: @"";
|
||||||
if (text.length == 0) { return; }
|
NSString *curAfter = proxy.documentContextAfterInput ?: @"";
|
||||||
[proxy insertText:text];
|
KB_UNDO_LOG(@"performUndo/currentBefore", curBefore);
|
||||||
|
KB_UNDO_LOG(@"performUndo/currentAfter", curAfter);
|
||||||
[self.segments removeAllObjects];
|
NSString *insertText = [self kb_buildUndoInsertTextFromPieces];
|
||||||
self.lastActionWasClear = NO;
|
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.undoAfterLength = 0;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceNone;
|
||||||
|
[self.undoDeletedPieces removeAllObjects];
|
||||||
[self kb_updateHasUndo:NO];
|
[self kb_updateHasUndo:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)registerNonClearAction {
|
- (void)registerNonClearAction {
|
||||||
self.lastActionWasClear = NO;
|
if (!self.hasUndo) { return; }
|
||||||
if (self.segments.count == 0) { return; }
|
if (self.undoText.length > 0) {
|
||||||
[self.segments removeAllObjects];
|
KB_UNDO_LOG(@"registerNonClearAction/clearUndoText", self.undoText);
|
||||||
|
}
|
||||||
|
if (self.undoDeletedPieces.count > 0) {
|
||||||
|
KB_UNDO_LOG(@"registerNonClearAction/clearDeletedPieces", [self kb_buildUndoInsertTextFromPieces]);
|
||||||
|
}
|
||||||
|
self.undoText = @"";
|
||||||
|
self.undoAfterLength = 0;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceNone;
|
||||||
|
[self.undoDeletedPieces removeAllObjects];
|
||||||
[self kb_updateHasUndo:NO];
|
[self kb_updateHasUndo:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,97 +248,57 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
|
|||||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self];
|
[[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString *)kb_segmentForClearFromContext:(NSString *)context {
|
- (NSString *)kb_lastComposedCharacterFromString:(NSString *)text {
|
||||||
NSInteger length = context.length;
|
if (text.length == 0) { return @""; }
|
||||||
if (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 ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
static NSCharacterSet *sentenceBoundarySet = nil;
|
- (NSString *)kb_buildUndoInsertTextFromPieces {
|
||||||
static NSCharacterSet *whitespaceSet = nil;
|
if (self.undoDeletedPieces.count == 0) { return @""; }
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
|
|
||||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
|
||||||
});
|
|
||||||
|
|
||||||
NSInteger end = length;
|
|
||||||
while (end > 0) {
|
|
||||||
unichar ch = [context characterAtIndex:end - 1];
|
|
||||||
if ([whitespaceSet characterIsMember:ch]) {
|
|
||||||
end -= 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NSInteger searchEnd = end;
|
|
||||||
while (searchEnd > 0) {
|
|
||||||
unichar ch = [context characterAtIndex:searchEnd - 1];
|
|
||||||
if ([sentenceBoundarySet characterIsMember:ch]) {
|
|
||||||
searchEnd -= 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NSInteger boundaryIndex = NSNotFound;
|
|
||||||
for (NSInteger i = searchEnd - 1; i >= 0; i--) {
|
|
||||||
unichar ch = [context characterAtIndex:i];
|
|
||||||
if ([sentenceBoundarySet characterIsMember:ch]) {
|
|
||||||
boundaryIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NSInteger start = (boundaryIndex == NSNotFound) ? 0 : (boundaryIndex + 1);
|
|
||||||
if (start >= length) { return @""; }
|
|
||||||
return [context substringFromIndex:start];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSString *)kb_buildUndoText {
|
|
||||||
if (self.segments.count == 0) { return @""; }
|
|
||||||
NSArray<NSString *> *ordered = [[self.segments reverseObjectEnumerator] allObjects];
|
|
||||||
NSMutableString *result = [NSMutableString string];
|
NSMutableString *result = [NSMutableString string];
|
||||||
for (NSInteger i = 0; i < ordered.count; i++) {
|
for (NSInteger i = (NSInteger)self.undoDeletedPieces.count - 1; i >= 0; i--) {
|
||||||
NSString *segment = ordered[i] ?: @"";
|
NSString *piece = self.undoDeletedPieces[(NSUInteger)i] ?: @"";
|
||||||
if (segment.length == 0) { continue; }
|
if (piece.length == 0) { continue; }
|
||||||
if (i < ordered.count - 1) {
|
[result appendString:piece];
|
||||||
segment = [self kb_replaceTrailingBoundaryWithComma:segment];
|
}
|
||||||
}
|
return result;
|
||||||
[result appendString:segment];
|
}
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSString *)kb_replaceTrailingBoundaryWithComma:(NSString *)segment {
|
static const NSInteger kKBUndoClearMaxRounds = 200;
|
||||||
if (segment.length == 0) { return segment; }
|
|
||||||
|
|
||||||
static NSCharacterSet *boundarySet = nil;
|
- (void)kb_clearAllTextForProxy:(id<UITextDocumentProxy>)proxy {
|
||||||
static NSCharacterSet *englishBoundarySet = nil;
|
if (!proxy) { return; }
|
||||||
static NSCharacterSet *whitespaceSet = nil;
|
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
boundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
|
|
||||||
englishBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;"];
|
|
||||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
|
||||||
});
|
|
||||||
|
|
||||||
NSInteger idx = segment.length - 1;
|
if ([proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
|
||||||
while (idx >= 0) {
|
NSInteger guard = 0;
|
||||||
unichar ch = [segment characterAtIndex:idx];
|
NSString *contextAfter = proxy.documentContextAfterInput ?: @"";
|
||||||
if ([whitespaceSet characterIsMember:ch]) {
|
while (contextAfter.length > 0 && guard < kKBUndoClearMaxRounds) {
|
||||||
idx -= 1;
|
NSInteger offset = (NSInteger)contextAfter.length;
|
||||||
continue;
|
[proxy adjustTextPositionByCharacterOffset:offset];
|
||||||
|
for (NSUInteger i = 0; i < contextAfter.length; i++) {
|
||||||
|
[proxy deleteBackward];
|
||||||
|
}
|
||||||
|
guard += 1;
|
||||||
|
contextAfter = proxy.documentContextAfterInput ?: @"";
|
||||||
}
|
}
|
||||||
if (![boundarySet characterIsMember:ch]) {
|
|
||||||
return segment;
|
|
||||||
}
|
|
||||||
NSString *comma = [englishBoundarySet characterIsMember:ch] ? @"," : @",";
|
|
||||||
NSMutableString *mutable = [segment mutableCopy];
|
|
||||||
NSRange r = NSMakeRange(idx, 1);
|
|
||||||
[mutable replaceCharactersInRange:r withString:comma];
|
|
||||||
return mutable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return segment;
|
NSInteger guard = 0;
|
||||||
|
NSString *contextBefore = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
while (contextBefore.length > 0 && guard < kKBUndoClearMaxRounds) {
|
||||||
|
for (NSUInteger i = 0; i < contextBefore.length; i++) {
|
||||||
|
[proxy deleteBackward];
|
||||||
|
}
|
||||||
|
guard += 1;
|
||||||
|
contextBefore = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
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
@@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KBFunctionBarView
|
@implementation KBFunctionBarView
|
||||||
|
static const CGFloat kKBBackButtonWidth = 40;
|
||||||
|
|
||||||
- (instancetype)initWithFrame:(CGRect)frame{
|
- (instancetype)initWithFrame:(CGRect)frame{
|
||||||
if (self = [super initWithFrame:frame]) {
|
if (self = [super initWithFrame:frame]) {
|
||||||
@@ -83,14 +84,14 @@
|
|||||||
UIButton *appButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
UIButton *appButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||||
appButton.tag = 100; // 左侧 index = 0
|
appButton.tag = 100; // 左侧 index = 0
|
||||||
UIImage *appImage = [UIImage imageNamed:@"App_icon"];
|
UIImage *appImage = [UIImage imageNamed:@"App_icon"];
|
||||||
[appButton setImage:appImage forState:UIControlStateNormal];
|
[appButton setBackgroundImage:appImage forState:UIControlStateNormal];
|
||||||
appButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
|
appButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||||
appButton.adjustsImageWhenHighlighted = YES;
|
appButton.adjustsImageWhenHighlighted = YES;
|
||||||
[appButton addTarget:self action:@selector(onLeftTap:) forControlEvents:UIControlEventTouchUpInside];
|
[appButton addTarget:self action:@selector(onLeftTap:) forControlEvents:UIControlEventTouchUpInside];
|
||||||
[self.leftContainer addSubview:appButton];
|
[self.leftContainer addSubview:appButton];
|
||||||
[appButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
[appButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.center.equalTo(self.leftContainer);
|
make.center.equalTo(self.leftContainer);
|
||||||
make.width.height.mas_equalTo(34); // 设计图尺寸
|
make.width.height.mas_equalTo(kKBBackButtonWidth); // 设计图尺寸
|
||||||
}];
|
}];
|
||||||
self.leftButtonsInternal = @[appButton];
|
self.leftButtonsInternal = @[appButton];
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -770,24 +779,45 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
|
|||||||
- (void)kb_fullAccessChanged {
|
- (void)kb_fullAccessChanged {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; });
|
dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; });
|
||||||
}
|
}
|
||||||
- (void)onTapDelete {
|
|
||||||
|
- (void)onTapDelete {
|
||||||
NSLog(@"点击:删除");
|
NSLog(@"点击:删除");
|
||||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
[[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;
|
||||||
[proxy deleteBackward];
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
||||||
|
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:proxy.documentContextBeforeInput
|
||||||
|
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
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
|
|
||||||
/// emoji 面板点击搜索
|
/// emoji 面板点击搜索
|
||||||
- (void)keyBoardMainViewDidTapEmojiSearch:(KBKeyBoardMainView *)keyBoardMainView;
|
- (void)keyBoardMainViewDidTapEmojiSearch:(KBKeyBoardMainView *)keyBoardMainView;
|
||||||
|
|
||||||
|
/// 选择了联想词
|
||||||
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface KBKeyBoardMainView : UIView
|
@interface KBKeyBoardMainView : UIView
|
||||||
@@ -39,6 +42,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
/// 应用当前皮肤(会触发键区重载以应用按键颜色)
|
/// 应用当前皮肤(会触发键区重载以应用按键颜色)
|
||||||
- (void)kb_applyTheme;
|
- (void)kb_applyTheme;
|
||||||
|
|
||||||
|
/// 更新联想候选
|
||||||
|
- (void)kb_setSuggestions:(NSArray<NSString *> *)suggestions;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -11,29 +11,42 @@
|
|||||||
#import "KBFunctionView.h"
|
#import "KBFunctionView.h"
|
||||||
#import "KBKey.h"
|
#import "KBKey.h"
|
||||||
#import "KBEmojiPanelView.h"
|
#import "KBEmojiPanelView.h"
|
||||||
|
#import "KBSuggestionBarView.h"
|
||||||
#import "Masonry.h"
|
#import "Masonry.h"
|
||||||
#import "KBSkinManager.h"
|
#import "KBSkinManager.h"
|
||||||
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
|
||||||
@interface KBKeyBoardMainView ()<KBToolBarDelegate, KBKeyboardViewDelegate, KBEmojiPanelViewDelegate>
|
@interface KBKeyBoardMainView ()<KBToolBarDelegate, KBKeyboardViewDelegate, KBEmojiPanelViewDelegate, KBSuggestionBarViewDelegate>
|
||||||
@property (nonatomic, strong) KBToolBar *topBar;
|
@property (nonatomic, strong) KBToolBar *topBar;
|
||||||
|
@property (nonatomic, strong) KBSuggestionBarView *suggestionBar;
|
||||||
@property (nonatomic, strong) KBKeyboardView *keyboardView;
|
@property (nonatomic, strong) KBKeyboardView *keyboardView;
|
||||||
@property (nonatomic, strong) KBEmojiPanelView *emojiView;
|
@property (nonatomic, strong) KBEmojiPanelView *emojiView;
|
||||||
@property (nonatomic, assign) BOOL emojiPanelVisible;
|
@property (nonatomic, assign) BOOL emojiPanelVisible;
|
||||||
|
@property (nonatomic, assign) BOOL suggestionBarHasItems;
|
||||||
// 注意:功能面板的展示/隐藏由外部控制器决定,此处不再直接管理显隐
|
// 注意:功能面板的展示/隐藏由外部控制器决定,此处不再直接管理显隐
|
||||||
@end
|
@end
|
||||||
@implementation KBKeyBoardMainView
|
@implementation KBKeyBoardMainView
|
||||||
|
|
||||||
- (instancetype)initWithFrame:(CGRect)frame {
|
- (instancetype)initWithFrame:(CGRect)frame {
|
||||||
if (self = [super initWithFrame:frame]) {
|
if (self = [super initWithFrame:frame]) {
|
||||||
self.backgroundColor = [KBSkinManager shared].current.keyboardBackground;
|
// self.backgroundColor = [KBSkinManager shared].current.keyboardBackground;
|
||||||
|
self.backgroundColor = [UIColor colorWithHex:0xD1D3DB];
|
||||||
|
|
||||||
// 顶部栏
|
// 顶部栏
|
||||||
self.topBar = [[KBToolBar alloc] init];
|
self.topBar = [[KBToolBar alloc] init];
|
||||||
self.topBar.delegate = self;
|
self.topBar.delegate = self;
|
||||||
[self addSubview:self.topBar];
|
[self addSubview:self.topBar];
|
||||||
|
|
||||||
|
// 联想栏
|
||||||
|
self.suggestionBar = [[KBSuggestionBarView alloc] init];
|
||||||
|
self.suggestionBar.delegate = self;
|
||||||
|
self.suggestionBar.hidden = YES;
|
||||||
|
[self addSubview:self.suggestionBar];
|
||||||
|
|
||||||
// 键盘区域(高度按照设计值做等比缩放,避免不同机型上按键被压缩/拉伸)
|
// 键盘区域(高度按照设计值做等比缩放,避免不同机型上按键被压缩/拉伸)
|
||||||
CGFloat keyboardAreaHeight = KBFit(200.0f);
|
CGFloat keyboardAreaHeight = KBFit(200.0f);
|
||||||
CGFloat bottomInset = KBFit(4.0f);
|
CGFloat bottomInset = KBFit(4.0f);
|
||||||
|
// CGFloat topBarHeight = KBFit(40.0f);
|
||||||
CGFloat barSpacing = KBFit(6.0f);
|
CGFloat barSpacing = KBFit(6.0f);
|
||||||
|
|
||||||
self.keyboardView = [[KBKeyboardView alloc] init];
|
self.keyboardView = [[KBKeyboardView alloc] init];
|
||||||
@@ -54,16 +67,39 @@
|
|||||||
make.edges.equalTo(self);
|
make.edges.equalTo(self);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
// [self.topBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
// make.left.right.equalTo(self);
|
||||||
|
// make.top.equalTo(self.mas_top).offset(0);
|
||||||
|
// make.height.mas_equalTo(topBarHeight);
|
||||||
|
// }];
|
||||||
[self.topBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.topBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.right.equalTo(self);
|
make.left.right.equalTo(self);
|
||||||
make.top.equalTo(self.mas_top).offset(0);
|
make.top.equalTo(self.mas_top).offset(0);
|
||||||
make.bottom.equalTo(self.keyboardView.mas_top).offset(0);
|
make.bottom.equalTo(self.keyboardView.mas_top).offset(0);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
[self.suggestionBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.right.equalTo(self);
|
||||||
|
make.top.equalTo(self.topBar);
|
||||||
|
make.bottom.equalTo(self.topBar);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.keyboardView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(self.topBar.mas_bottom).offset(barSpacing);
|
||||||
|
}];
|
||||||
// 功能面板切换交由外部控制器处理;此处不直接创建/管理
|
// 功能面板切换交由外部控制器处理;此处不直接创建/管理
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(kb_undoStateChanged)
|
||||||
|
name:KBBackspaceUndoStateDidChangeNotification
|
||||||
|
object:nil];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)dealloc {
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)setEmojiPanelVisible:(BOOL)visible animated:(BOOL)animated {
|
- (void)setEmojiPanelVisible:(BOOL)visible animated:(BOOL)animated {
|
||||||
if (self.emojiPanelVisible == visible) return;
|
if (self.emojiPanelVisible == visible) return;
|
||||||
self.emojiPanelVisible = visible;
|
self.emojiPanelVisible = visible;
|
||||||
@@ -74,17 +110,24 @@
|
|||||||
} else {
|
} else {
|
||||||
self.keyboardView.hidden = NO;
|
self.keyboardView.hidden = NO;
|
||||||
self.topBar.hidden = NO;
|
self.topBar.hidden = NO;
|
||||||
|
self.suggestionBar.hidden = !self.suggestionBarHasItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
void (^changes)(void) = ^{
|
void (^changes)(void) = ^{
|
||||||
self.emojiView.alpha = visible ? 1.0 : 0.0;
|
self.emojiView.alpha = visible ? 1.0 : 0.0;
|
||||||
self.keyboardView.alpha = visible ? 0.0 : 1.0;
|
self.keyboardView.alpha = visible ? 0.0 : 1.0;
|
||||||
self.topBar.alpha = visible ? 0.0 : 1.0;
|
self.topBar.alpha = visible ? 0.0 : 1.0;
|
||||||
|
self.suggestionBar.alpha = visible ? 0.0 : ([self kb_shouldShowSuggestions] ? 1.0 : 0.0);
|
||||||
};
|
};
|
||||||
void (^completion)(BOOL) = ^(BOOL finished) {
|
void (^completion)(BOOL) = ^(BOOL finished) {
|
||||||
self.emojiView.hidden = !visible;
|
self.emojiView.hidden = !visible;
|
||||||
self.keyboardView.hidden = visible;
|
self.keyboardView.hidden = visible;
|
||||||
self.topBar.hidden = visible;
|
self.topBar.hidden = visible;
|
||||||
|
if (visible) {
|
||||||
|
self.suggestionBar.hidden = YES;
|
||||||
|
} else {
|
||||||
|
self.suggestionBar.hidden = ![self kb_shouldShowSuggestions];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (animated) {
|
if (animated) {
|
||||||
@@ -205,16 +248,52 @@
|
|||||||
- (void)kb_applyTheme {
|
- (void)kb_applyTheme {
|
||||||
KBSkinManager *mgr = [KBSkinManager shared];
|
KBSkinManager *mgr = [KBSkinManager shared];
|
||||||
BOOL hasImg = ([mgr currentBackgroundImage] != nil);
|
BOOL hasImg = ([mgr currentBackgroundImage] != nil);
|
||||||
UIColor *bg = mgr.current.keyboardBackground;
|
// UIColor *bg = mgr.current.keyboardBackground;
|
||||||
|
UIColor *bg = [UIColor colorWithHex:0xD1D3DB];
|
||||||
self.backgroundColor = hasImg ? [UIColor clearColor] : bg;
|
self.backgroundColor = hasImg ? [UIColor clearColor] : bg;
|
||||||
self.keyboardView.backgroundColor = hasImg ? [UIColor clearColor] : bg;
|
self.keyboardView.backgroundColor = hasImg ? [UIColor clearColor] : bg;
|
||||||
if ([self.topBar respondsToSelector:@selector(kb_applyTheme)]) {
|
if ([self.topBar respondsToSelector:@selector(kb_applyTheme)]) {
|
||||||
[self.topBar kb_applyTheme];
|
[self.topBar kb_applyTheme];
|
||||||
}
|
}
|
||||||
|
[self.suggestionBar applyTheme:mgr.current];
|
||||||
[self.keyboardView reloadKeys];
|
[self.keyboardView reloadKeys];
|
||||||
if (self.emojiView) {
|
if (self.emojiView) {
|
||||||
[self.emojiView applyTheme:mgr.current];
|
[self.emojiView applyTheme:mgr.current];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - Suggestions
|
||||||
|
|
||||||
|
- (void)kb_setSuggestions:(NSArray<NSString *> *)suggestions {
|
||||||
|
self.suggestionBarHasItems = (suggestions.count > 0);
|
||||||
|
[self.suggestionBar updateSuggestions:suggestions];
|
||||||
|
[self kb_applySuggestionVisibility];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - KBSuggestionBarViewDelegate
|
||||||
|
|
||||||
|
- (void)suggestionBarView:(KBSuggestionBarView *)view didSelectSuggestion:(NSString *)suggestion {
|
||||||
|
if ([self.delegate respondsToSelector:@selector(keyBoardMainView:didSelectSuggestion:)]) {
|
||||||
|
[self.delegate keyBoardMainView:self didSelectSuggestion:suggestion];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_undoStateChanged {
|
||||||
|
[self kb_applySuggestionVisibility];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)kb_shouldShowSuggestions {
|
||||||
|
if (self.emojiPanelVisible) { return NO; }
|
||||||
|
if (![KBBackspaceUndoManager shared].hasUndo && self.suggestionBarHasItems) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_applySuggestionVisibility {
|
||||||
|
BOOL shouldShow = [self kb_shouldShowSuggestions];
|
||||||
|
self.suggestionBar.hidden = !shouldShow;
|
||||||
|
self.suggestionBar.alpha = shouldShow ? 1.0 : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -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 ?: @""];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
CustomKeyboard/View/KBSuggestionBarView.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// KBSuggestionBarView.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@class KBSuggestionBarView;
|
||||||
|
@class KBSkinTheme;
|
||||||
|
|
||||||
|
@protocol KBSuggestionBarViewDelegate <NSObject>
|
||||||
|
- (void)suggestionBarView:(KBSuggestionBarView *)view didSelectSuggestion:(NSString *)suggestion;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface KBSuggestionBarView : UIView
|
||||||
|
|
||||||
|
@property (nonatomic, weak) id<KBSuggestionBarViewDelegate> delegate;
|
||||||
|
|
||||||
|
- (void)updateSuggestions:(NSArray<NSString *> *)suggestions;
|
||||||
|
- (void)applyTheme:(KBSkinTheme *)theme;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
114
CustomKeyboard/View/KBSuggestionBarView.m
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
//
|
||||||
|
// KBSuggestionBarView.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBSuggestionBarView.h"
|
||||||
|
#import "Masonry.h"
|
||||||
|
#import "KBSkinManager.h"
|
||||||
|
|
||||||
|
@interface KBSuggestionBarView ()
|
||||||
|
@property (nonatomic, strong) UIScrollView *scrollView;
|
||||||
|
@property (nonatomic, strong) UIStackView *stackView;
|
||||||
|
@property (nonatomic, copy) NSArray<NSString *> *items;
|
||||||
|
@property (nonatomic, strong) UIColor *pillColor;
|
||||||
|
@property (nonatomic, strong) UIColor *textColor;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBSuggestionBarView
|
||||||
|
|
||||||
|
- (instancetype)initWithFrame:(CGRect)frame {
|
||||||
|
if (self = [super initWithFrame:frame]) {
|
||||||
|
self.backgroundColor = [UIColor clearColor];
|
||||||
|
[self setupUI];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setupUI {
|
||||||
|
[self addSubview:self.scrollView];
|
||||||
|
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.edges.equalTo(self);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.scrollView addSubview:self.stackView];
|
||||||
|
[self.stackView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.edges.equalTo(self.scrollView).insets(UIEdgeInsetsMake(0, 8, 0, 8));
|
||||||
|
make.height.equalTo(self.scrollView);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self applyTheme:[KBSkinManager shared].current];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)updateSuggestions:(NSArray<NSString *> *)suggestions {
|
||||||
|
self.items = suggestions ?: @[];
|
||||||
|
|
||||||
|
for (UIView *view in self.stackView.arrangedSubviews) {
|
||||||
|
[self.stackView removeArrangedSubview:view];
|
||||||
|
[view removeFromSuperview];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (NSString *item in self.items) {
|
||||||
|
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||||
|
btn.layer.cornerRadius = 12.0;
|
||||||
|
btn.layer.masksToBounds = YES;
|
||||||
|
btn.backgroundColor = self.pillColor ?: [UIColor colorWithWhite:1 alpha:0.9];
|
||||||
|
btn.titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
||||||
|
[btn setTitle:item forState:UIControlStateNormal];
|
||||||
|
[btn setTitleColor:self.textColor ?: [UIColor blackColor] forState:UIControlStateNormal];
|
||||||
|
btn.contentEdgeInsets = UIEdgeInsetsMake(4, 10, 4, 10);
|
||||||
|
[btn addTarget:self action:@selector(onTapSuggestion:) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
[self.stackView addArrangedSubview:btn];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.hidden = (self.items.count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)applyTheme:(KBSkinTheme *)theme {
|
||||||
|
UIColor *bg = theme.keyBackground ?: [UIColor whiteColor];
|
||||||
|
UIColor *text = theme.keyTextColor ?: [UIColor blackColor];
|
||||||
|
UIColor *barBg = [UIColor colorWithHex:0xD1D3DB];
|
||||||
|
self.backgroundColor = barBg;
|
||||||
|
self.pillColor = bg;
|
||||||
|
self.textColor = text;
|
||||||
|
|
||||||
|
for (UIView *view in self.stackView.arrangedSubviews) {
|
||||||
|
if (![view isKindOfClass:[UIButton class]]) { continue; }
|
||||||
|
UIButton *btn = (UIButton *)view;
|
||||||
|
btn.backgroundColor = self.pillColor;
|
||||||
|
[btn setTitleColor:self.textColor forState:UIControlStateNormal];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Actions
|
||||||
|
|
||||||
|
- (void)onTapSuggestion:(UIButton *)sender {
|
||||||
|
NSString *title = sender.currentTitle ?: @"";
|
||||||
|
if (title.length == 0) { return; }
|
||||||
|
if ([self.delegate respondsToSelector:@selector(suggestionBarView:didSelectSuggestion:)]) {
|
||||||
|
[self.delegate suggestionBarView:self didSelectSuggestion:title];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Lazy
|
||||||
|
|
||||||
|
- (UIScrollView *)scrollView {
|
||||||
|
if (!_scrollView) {
|
||||||
|
_scrollView = [[UIScrollView alloc] init];
|
||||||
|
_scrollView.showsHorizontalScrollIndicator = NO;
|
||||||
|
_scrollView.alwaysBounceHorizontal = YES;
|
||||||
|
}
|
||||||
|
return _scrollView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIStackView *)stackView {
|
||||||
|
if (!_stackView) {
|
||||||
|
_stackView = [[UIStackView alloc] init];
|
||||||
|
_stackView.axis = UILayoutConstraintAxisHorizontal;
|
||||||
|
_stackView.alignment = UIStackViewAlignmentCenter;
|
||||||
|
_stackView.spacing = 8.0;
|
||||||
|
}
|
||||||
|
return _stackView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
@implementation KBToolBar
|
@implementation KBToolBar
|
||||||
|
|
||||||
static NSString * const kKBAIKeyIdentifier = @"ai";
|
static NSString * const kKBAIKeyIdentifier = @"ai";
|
||||||
|
static NSString * const kKBUndoKeyIdentifier = @"key_revoke";
|
||||||
static const CGFloat kKBAIButtonWidth = 40;
|
static const CGFloat kKBAIButtonWidth = 40;
|
||||||
static const CGFloat kKBAIButtonHeight = 40;
|
static const CGFloat kKBAIButtonHeight = 40;
|
||||||
|
|
||||||
@@ -96,6 +97,7 @@ static const CGFloat kKBAIButtonHeight = 40;
|
|||||||
make.right.equalTo(self.mas_right).offset(-12);
|
make.right.equalTo(self.mas_right).offset(-12);
|
||||||
make.centerY.equalTo(self.mas_centerY);
|
make.centerY.equalTo(self.mas_centerY);
|
||||||
make.height.mas_equalTo(32);
|
make.height.mas_equalTo(32);
|
||||||
|
make.width.mas_equalTo(84);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
[self kb_updateLeftContainerConstraints];
|
[self kb_updateLeftContainerConstraints];
|
||||||
@@ -169,6 +171,7 @@ static const CGFloat kKBAIButtonHeight = 40;
|
|||||||
|
|
||||||
- (void)kb_applyTheme {
|
- (void)kb_applyTheme {
|
||||||
[self kb_updateAIButtonAppearance];
|
[self kb_updateAIButtonAppearance];
|
||||||
|
[self kb_updateUndoButtonAppearance];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)kb_updateAIButtonAppearance {
|
- (void)kb_updateAIButtonAppearance {
|
||||||
@@ -205,6 +208,26 @@ static const CGFloat kKBAIButtonHeight = 40;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)kb_updateUndoButtonAppearance {
|
||||||
|
if (!self.undoButtonInternal) { return; }
|
||||||
|
|
||||||
|
KBSkinManager *skinManager = [KBSkinManager shared];
|
||||||
|
UIImage *icon = [skinManager iconImageForKeyIdentifier:kKBUndoKeyIdentifier caseVariant:0];
|
||||||
|
if (!icon) {
|
||||||
|
icon = [UIImage imageNamed:@"key_revoke"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
[self.undoButtonInternal setImage:icon forState:UIControlStateNormal];
|
||||||
|
[self.undoButtonInternal setImage:icon forState:UIControlStateHighlighted];
|
||||||
|
[self.undoButtonInternal setImage:icon forState:UIControlStateSelected];
|
||||||
|
} else {
|
||||||
|
[self.undoButtonInternal setImage:nil forState:UIControlStateNormal];
|
||||||
|
[self.undoButtonInternal setImage:nil forState:UIControlStateHighlighted];
|
||||||
|
[self.undoButtonInternal setImage:nil forState:UIControlStateSelected];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Actions
|
#pragma mark - Actions
|
||||||
|
|
||||||
- (void)onLeftAction:(UIButton *)sender {
|
- (void)onLeftAction:(UIButton *)sender {
|
||||||
@@ -262,14 +285,15 @@ static const CGFloat kKBAIButtonHeight = 40;
|
|||||||
|
|
||||||
- (UIButton *)undoButtonInternal {
|
- (UIButton *)undoButtonInternal {
|
||||||
if (!_undoButtonInternal) {
|
if (!_undoButtonInternal) {
|
||||||
_undoButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
|
_undoButtonInternal = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||||
_undoButtonInternal.layer.cornerRadius = 16;
|
// _undoButtonInternal.layer.cornerRadius = 16;
|
||||||
_undoButtonInternal.layer.masksToBounds = YES;
|
// _undoButtonInternal.layer.masksToBounds = YES;
|
||||||
_undoButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
|
// _undoButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
|
||||||
_undoButtonInternal.titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
// _undoButtonInternal.titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
||||||
[_undoButtonInternal setTitle:@"撤销删除" forState:UIControlStateNormal];
|
// [_undoButtonInternal setTitle:@"撤销删除" forState:UIControlStateNormal];
|
||||||
[_undoButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
// [_undoButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||||
_undoButtonInternal.contentEdgeInsets = UIEdgeInsetsMake(0, 10, 0, 10);
|
// _undoButtonInternal.contentEdgeInsets = UIEdgeInsetsMake(0, 10, 0, 10);
|
||||||
|
[_undoButtonInternal setImage:[UIImage imageNamed:@"key_revoke"] forState:UIControlStateNormal];
|
||||||
_undoButtonInternal.hidden = YES;
|
_undoButtonInternal.hidden = YES;
|
||||||
_undoButtonInternal.alpha = 0.0;
|
_undoButtonInternal.alpha = 0.0;
|
||||||
[_undoButtonInternal addTarget:self action:@selector(onUndo) forControlEvents:UIControlEventTouchUpInside];
|
[_undoButtonInternal addTarget:self action:@selector(onUndo) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
|||||||
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
@@ -32,6 +32,7 @@
|
|||||||
#define API_UPDATA_INFO @"/user/updateInfo" // 更新用户
|
#define API_UPDATA_INFO @"/user/updateInfo" // 更新用户
|
||||||
|
|
||||||
#define KB_API_USER_DETAIL @"/user/detail" // 用户详情
|
#define KB_API_USER_DETAIL @"/user/detail" // 用户详情
|
||||||
|
#define API_USER_INVITE_CODE @"/user/inviteCode" // 查询邀请码
|
||||||
#define API_CHARACTER_LIST @"/character/list" // 排行榜角色列表(综合)
|
#define API_CHARACTER_LIST @"/character/list" // 排行榜角色列表(综合)
|
||||||
#define API_NOT_LOGIN_CHARACTER_LIST @"/character/listWithNotLogin" //未登录用户人设列表
|
#define API_NOT_LOGIN_CHARACTER_LIST @"/character/listWithNotLogin" //未登录用户人设列表
|
||||||
|
|
||||||
@@ -56,6 +57,10 @@
|
|||||||
#define API_THEME_DOWNLOAD @"/themes/download" // 主题下载信息
|
#define API_THEME_DOWNLOAD @"/themes/download" // 主题下载信息
|
||||||
#define API_THEME_RECOMMENDED @"/themes/recommended" // 推荐主题列表
|
#define API_THEME_RECOMMENDED @"/themes/recommended" // 推荐主题列表
|
||||||
#define API_THEME_SEARCH @"/themes/search" // 搜索主题(themeName)
|
#define API_THEME_SEARCH @"/themes/search" // 搜索主题(themeName)
|
||||||
|
#define API_USER_THEMES_BATCH_DELETE @"/user-themes/batch-delete" // 批量删除用户主题
|
||||||
|
#define API_THEME_PURCHASE_LIST @"/themes/purchase/list" // 查询主题购买记录
|
||||||
|
#define API_THEME_RESTORE @"/themes/restore" // 恢复已删除的主题
|
||||||
|
#define API_WALLET_TRANSACTIONS @"/wallet/transactions" // 分页查询钱包交易记录
|
||||||
|
|
||||||
/// pay
|
/// pay
|
||||||
#define API_VALIDATE_RECEIPT @"/apple/validate-receipt" // 排行榜标签列表
|
#define API_VALIDATE_RECEIPT @"/apple/validate-receipt" // 排行榜标签列表
|
||||||
|
|||||||
@@ -38,7 +38,8 @@
|
|||||||
// 基础baseUrl
|
// 基础baseUrl
|
||||||
#ifndef KB_BASE_URL
|
#ifndef KB_BASE_URL
|
||||||
//#define KB_BASE_URL @"https://m1.apifoxmock.com/m1/5438099-5113192-default/"
|
//#define KB_BASE_URL @"https://m1.apifoxmock.com/m1/5438099-5113192-default/"
|
||||||
#define KB_BASE_URL @"http://192.168.2.21:7529/api"
|
//#define KB_BASE_URL @"http://192.168.2.21:7529/api"
|
||||||
|
#define KB_BASE_URL @"https://devcallback.loveamorkey.com/api"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#import "KBFont.h"
|
#import "KBFont.h"
|
||||||
@@ -87,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
@@ -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
|
||||||
|
|
||||||
87
Shared/KBMaiPointReporter.h
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
//
|
||||||
|
// KBMaiPointReporter.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
#ifndef KB_MAI_POINT_BASE_URL
|
||||||
|
#define KB_MAI_POINT_BASE_URL @"http://192.168.2.21:35310/api"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef KB_MAI_POINT_PATH_NEW_ACCOUNT
|
||||||
|
#define KB_MAI_POINT_PATH_NEW_ACCOUNT @"/newAccount"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef KB_MAI_POINT_PATH_GENERIC_DATA
|
||||||
|
#define KB_MAI_POINT_PATH_GENERIC_DATA @"/genericData"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
extern NSString * const KBMaiPointErrorDomain;
|
||||||
|
extern NSString * const KBMaiPointEventTypePageExposure;
|
||||||
|
extern NSString * const KBMaiPointEventTypeClick;
|
||||||
|
|
||||||
|
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.
|
||||||
|
@interface KBMaiPointReporter : NSObject
|
||||||
|
|
||||||
|
+ (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.
|
||||||
|
- (void)reportNewAccountWithType:(NSString *)type
|
||||||
|
account:(nullable NSString *)account
|
||||||
|
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.
|
||||||
|
- (void)postPath:(NSString *)path
|
||||||
|
parameters:(NSDictionary *)parameters
|
||||||
|
completion:(KBMaiPointReportCompletion _Nullable)completion;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
399
Shared/KBMaiPointReporter.m
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
//
|
||||||
|
// KBMaiPointReporter.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
|
||||||
|
#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 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
|
||||||
|
|
||||||
|
+ (instancetype)sharedReporter {
|
||||||
|
static KBMaiPointReporter *reporter = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
reporter = [[KBMaiPointReporter alloc] init];
|
||||||
|
});
|
||||||
|
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
|
||||||
|
account:(NSString * _Nullable)account
|
||||||
|
completion:(KBMaiPointReportCompletion _Nullable)completion {
|
||||||
|
NSString *trimmedType = [self kb_trimmedStringOrEmpty:type];
|
||||||
|
NSString *trimmedAccount = [self kb_trimmedStringOrEmpty:account];
|
||||||
|
if (trimmedType.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSDictionary *params = @{
|
||||||
|
@"type": trimmedType,
|
||||||
|
@"account": trimmedAccount ?: @"",
|
||||||
|
@"token": [self kb_currentTokenOrEmpty]
|
||||||
|
};
|
||||||
|
[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
|
||||||
|
parameters:(NSDictionary *)parameters
|
||||||
|
completion:(KBMaiPointReportCompletion _Nullable)completion {
|
||||||
|
if (path.length == 0 || ![parameters isKindOfClass:[NSDictionary class]]) {
|
||||||
|
NSError *error = [NSError errorWithDomain:KBMaiPointErrorDomain
|
||||||
|
code:-1
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"Invalid parameter"}];
|
||||||
|
if (completion) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
completion(NO, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *safePath = [path hasPrefix:@"/"] ? path : [@"/" stringByAppendingString:path];
|
||||||
|
NSString *urlString = [NSString stringWithFormat:@"%@%@", KB_MAI_POINT_BASE_URL, safePath];
|
||||||
|
NSURL *url = [NSURL URLWithString:urlString];
|
||||||
|
if (!url) {
|
||||||
|
NSError *error = [NSError errorWithDomain:KBMaiPointErrorDomain
|
||||||
|
code:-2
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"Invalid URL"}];
|
||||||
|
if (completion) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
completion(NO, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSError *jsonError = nil;
|
||||||
|
NSData *body = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:&jsonError];
|
||||||
|
if (jsonError) {
|
||||||
|
if (completion) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
completion(NO, jsonError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
||||||
|
request.HTTPMethod = @"POST";
|
||||||
|
request.timeoutInterval = 10.0;
|
||||||
|
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
||||||
|
request.HTTPBody = body;
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
KBMaiPoint_DebugLogURL(request);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||||
|
config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
||||||
|
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
|
||||||
|
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
|
||||||
|
completionHandler:^(NSData *data,
|
||||||
|
NSURLResponse *response,
|
||||||
|
NSError *error) {
|
||||||
|
BOOL success = NO;
|
||||||
|
NSError *finalError = error;
|
||||||
|
|
||||||
|
if (!finalError) {
|
||||||
|
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||||
|
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
|
||||||
|
success = (statusCode >= 200 && statusCode < 300);
|
||||||
|
if (!success) {
|
||||||
|
finalError = [NSError errorWithDomain:KBMaiPointErrorDomain
|
||||||
|
code:statusCode
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"Invalid response"}];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalError = [NSError errorWithDomain:KBMaiPointErrorDomain
|
||||||
|
code:-3
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"Invalid response"}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
KBMaiPoint_DebugLogError(response, finalError);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (completion) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
completion(success, finalError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
[task resume];
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
@@ -24,6 +24,7 @@ static NSString * const kKBSkinPendingKindKey = @"kind";
|
|||||||
static NSString * const kKBSkinPendingTimestampKey = @"timestamp";
|
static NSString * const kKBSkinPendingTimestampKey = @"timestamp";
|
||||||
static NSString * const kKBSkinPendingIconShortKey = @"iconShortNames";
|
static NSString * const kKBSkinPendingIconShortKey = @"iconShortNames";
|
||||||
static NSString * const kKBSkinMetadataFileName = @"metadata.plist";
|
static NSString * const kKBSkinMetadataFileName = @"metadata.plist";
|
||||||
|
static NSString * const kKBSkinForceDownloadKey = @"force_download";
|
||||||
static NSString * const kKBSkinMetadataNameKey = @"name";
|
static NSString * const kKBSkinMetadataNameKey = @"name";
|
||||||
static NSString * const kKBSkinMetadataPreviewKey = @"preview";
|
static NSString * const kKBSkinMetadataPreviewKey = @"preview";
|
||||||
static NSString * const kKBSkinMetadataZipKey = @"zip_url";
|
static NSString * const kKBSkinMetadataZipKey = @"zip_url";
|
||||||
@@ -220,6 +221,11 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
NSString *skinId = skinJSON[@"id"] ?: @"remote";
|
NSString *skinId = skinJSON[@"id"] ?: @"remote";
|
||||||
NSString *name = skinJSON[@"name"] ?: skinId;
|
NSString *name = skinJSON[@"name"] ?: skinId;
|
||||||
NSString *zipURL = skinJSON[@"zip_url"] ?: @"";
|
NSString *zipURL = skinJSON[@"zip_url"] ?: @"";
|
||||||
|
BOOL forceDownload = NO;
|
||||||
|
id forceValue = skinJSON[kKBSkinForceDownloadKey];
|
||||||
|
if ([forceValue respondsToSelector:@selector(boolValue)]) {
|
||||||
|
forceDownload = [forceValue boolValue];
|
||||||
|
}
|
||||||
|
|
||||||
// key_icons 可选:
|
// key_icons 可选:
|
||||||
// - 若后端提供 key_icons,则优先使用服务端映射;
|
// - 若后端提供 key_icons,则优先使用服务端映射;
|
||||||
@@ -258,6 +264,21 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
BOOL hasCachedAssets = (contents.count > 0);
|
BOOL hasCachedAssets = (contents.count > 0);
|
||||||
|
|
||||||
NSString *bgPath = [skinRoot stringByAppendingPathComponent:@"background.png"];
|
NSString *bgPath = [skinRoot stringByAppendingPathComponent:@"background.png"];
|
||||||
|
BOOL useTempRoot = forceDownload;
|
||||||
|
NSString *tempToken = nil;
|
||||||
|
NSString *workingRoot = skinRoot;
|
||||||
|
NSString *workingIconsDir = iconsDir;
|
||||||
|
NSString *workingBgPath = bgPath;
|
||||||
|
if (useTempRoot) {
|
||||||
|
tempToken = [NSString stringWithFormat:@"%lld", (long long)([[NSDate date] timeIntervalSince1970] * 1000)];
|
||||||
|
NSString *tmpName = [NSString stringWithFormat:@"%@__tmp_%@", skinId, tempToken];
|
||||||
|
workingRoot = [skinsRoot stringByAppendingPathComponent:tmpName];
|
||||||
|
workingIconsDir = [workingRoot stringByAppendingPathComponent:@"icons"];
|
||||||
|
workingBgPath = [workingRoot stringByAppendingPathComponent:@"background.png"];
|
||||||
|
[fm removeItemAtPath:workingRoot error:nil];
|
||||||
|
}
|
||||||
|
NSLog(@"⬇️[SkinBridge] request id=%@ force=%d cached=%d zip=%@",
|
||||||
|
skinId, forceDownload, hasCachedAssets, zipURL);
|
||||||
|
|
||||||
dispatch_group_t group = dispatch_group_create();
|
dispatch_group_t group = dispatch_group_create();
|
||||||
__block BOOL zipOK = YES;
|
__block BOOL zipOK = YES;
|
||||||
@@ -265,8 +286,8 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
__block NSError *innerError = nil;
|
__block NSError *innerError = nil;
|
||||||
|
|
||||||
#if __has_include(<SSZipArchive/SSZipArchive.h>)
|
#if __has_include(<SSZipArchive/SSZipArchive.h>)
|
||||||
// 若本地尚未缓存该皮肤资源且提供了 zip_url,则通过网络下载并解压 Zip 包。
|
// 若需要强制下载,或本地尚未缓存该皮肤资源且提供了 zip_url,则下载并解压 Zip 包。
|
||||||
if (!hasCachedAssets && zipURL.length > 0) {
|
if ((forceDownload || !hasCachedAssets) && zipURL.length > 0) {
|
||||||
dispatch_group_enter(group);
|
dispatch_group_enter(group);
|
||||||
|
|
||||||
void (^handleZipData)(NSData *) = ^(NSData *data) {
|
void (^handleZipData)(NSData *) = ^(NSData *data) {
|
||||||
@@ -277,15 +298,17 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
code:KBSkinBridgeErrorZipMissing
|
code:KBSkinBridgeErrorZipMissing
|
||||||
userInfo:@{NSLocalizedDescriptionKey: @"Zip data is empty"}];
|
userInfo:@{NSLocalizedDescriptionKey: @"Zip data is empty"}];
|
||||||
}
|
}
|
||||||
|
NSLog(@"❌[SkinBridge] zip data empty id=%@", skinId);
|
||||||
dispatch_group_leave(group);
|
dispatch_group_leave(group);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
NSLog(@"📦[SkinBridge] unzip start id=%@ temp=%d", skinId, useTempRoot);
|
||||||
// 将 Zip 写入临时路径再解压
|
// 将 Zip 写入临时路径再解压
|
||||||
[fm createDirectoryAtPath:skinRoot
|
[fm createDirectoryAtPath:workingRoot
|
||||||
withIntermediateDirectories:YES
|
withIntermediateDirectories:YES
|
||||||
attributes:nil
|
attributes:nil
|
||||||
error:NULL];
|
error:NULL];
|
||||||
NSString *zipPath = [skinRoot stringByAppendingPathComponent:@"skin.zip"];
|
NSString *zipPath = [workingRoot stringByAppendingPathComponent:@"skin.zip"];
|
||||||
if (![data writeToFile:zipPath atomically:YES]) {
|
if (![data writeToFile:zipPath atomically:YES]) {
|
||||||
zipOK = NO;
|
zipOK = NO;
|
||||||
if (!innerError) {
|
if (!innerError) {
|
||||||
@@ -293,13 +316,14 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
code:KBSkinBridgeErrorUnzipFailed
|
code:KBSkinBridgeErrorUnzipFailed
|
||||||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to write zip file"}];
|
userInfo:@{NSLocalizedDescriptionKey: @"Failed to write zip file"}];
|
||||||
}
|
}
|
||||||
|
NSLog(@"❌[SkinBridge] zip write failed id=%@", skinId);
|
||||||
dispatch_group_leave(group);
|
dispatch_group_leave(group);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSError *unzipError = nil;
|
NSError *unzipError = nil;
|
||||||
BOOL ok = [SSZipArchive unzipFileAtPath:zipPath
|
BOOL ok = [SSZipArchive unzipFileAtPath:zipPath
|
||||||
toDestination:skinRoot
|
toDestination:workingRoot
|
||||||
overwrite:YES
|
overwrite:YES
|
||||||
password:nil
|
password:nil
|
||||||
error:&unzipError];
|
error:&unzipError];
|
||||||
@@ -311,24 +335,22 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
code:KBSkinBridgeErrorUnzipFailed
|
code:KBSkinBridgeErrorUnzipFailed
|
||||||
userInfo:nil];
|
userInfo:nil];
|
||||||
}
|
}
|
||||||
|
NSLog(@"❌[SkinBridge] unzip failed id=%@ error=%@", skinId, unzipError);
|
||||||
dispatch_group_leave(group);
|
dispatch_group_leave(group);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记已成功解压一次(即使 icons 目录结构需要后续整理)。
|
|
||||||
didUnzip = YES;
|
|
||||||
|
|
||||||
// 兼容“额外包一层目录”的压缩结构:
|
// 兼容“额外包一层目录”的压缩结构:
|
||||||
// 若 Skins/<skinId>/icons 为空,但存在 Skins/<skinId>/<子目录>/icons,
|
// 若 Skins/<skinId>/icons 为空,但存在 Skins/<skinId>/<子目录>/icons,
|
||||||
// 则将实际 icons 与 background.png 上移到预期位置。
|
// 则将实际 icons 与 background.png 上移到预期位置。
|
||||||
BOOL isDir2 = NO;
|
BOOL isDir2 = NO;
|
||||||
NSArray *iconsContent = [fm contentsOfDirectoryAtPath:iconsDir error:NULL];
|
NSArray *iconsContent = [fm contentsOfDirectoryAtPath:workingIconsDir error:NULL];
|
||||||
BOOL iconsValid = ([fm fileExistsAtPath:iconsDir isDirectory:&isDir2] && isDir2 && iconsContent.count > 0);
|
BOOL iconsValid = ([fm fileExistsAtPath:workingIconsDir isDirectory:&isDir2] && isDir2 && iconsContent.count > 0);
|
||||||
if (!iconsValid) {
|
if (!iconsValid) {
|
||||||
NSArray<NSString *> *subItems = [fm contentsOfDirectoryAtPath:skinRoot error:NULL];
|
NSArray<NSString *> *subItems = [fm contentsOfDirectoryAtPath:workingRoot error:NULL];
|
||||||
for (NSString *subName in subItems) {
|
for (NSString *subName in subItems) {
|
||||||
if ([subName isEqualToString:@"icons"] || [subName isEqualToString:@"__MACOSX"]) continue;
|
if ([subName isEqualToString:@"icons"] || [subName isEqualToString:@"__MACOSX"]) continue;
|
||||||
NSString *nestedRoot = [skinRoot stringByAppendingPathComponent:subName];
|
NSString *nestedRoot = [workingRoot stringByAppendingPathComponent:subName];
|
||||||
BOOL isDirNested = NO;
|
BOOL isDirNested = NO;
|
||||||
if (![fm fileExistsAtPath:nestedRoot isDirectory:&isDirNested] || !isDirNested) continue;
|
if (![fm fileExistsAtPath:nestedRoot isDirectory:&isDirNested] || !isDirNested) continue;
|
||||||
|
|
||||||
@@ -338,14 +360,14 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
NSArray *nestedFiles = [fm contentsOfDirectoryAtPath:nestedIcons error:NULL];
|
NSArray *nestedFiles = [fm contentsOfDirectoryAtPath:nestedIcons error:NULL];
|
||||||
if (nestedFiles.count > 0) {
|
if (nestedFiles.count > 0) {
|
||||||
// 确保目标 icons 目录存在
|
// 确保目标 icons 目录存在
|
||||||
[fm createDirectoryAtPath:iconsDir
|
[fm createDirectoryAtPath:workingIconsDir
|
||||||
withIntermediateDirectories:YES
|
withIntermediateDirectories:YES
|
||||||
attributes:nil
|
attributes:nil
|
||||||
error:NULL];
|
error:NULL];
|
||||||
// 将 icons 下所有文件上移一层
|
// 将 icons 下所有文件上移一层
|
||||||
for (NSString *fn in nestedFiles) {
|
for (NSString *fn in nestedFiles) {
|
||||||
NSString *from = [nestedIcons stringByAppendingPathComponent:fn];
|
NSString *from = [nestedIcons stringByAppendingPathComponent:fn];
|
||||||
NSString *to = [iconsDir stringByAppendingPathComponent:fn];
|
NSString *to = [workingIconsDir stringByAppendingPathComponent:fn];
|
||||||
[fm removeItemAtPath:to error:nil];
|
[fm removeItemAtPath:to error:nil];
|
||||||
[fm moveItemAtPath:from toPath:to error:nil];
|
[fm moveItemAtPath:from toPath:to error:nil];
|
||||||
}
|
}
|
||||||
@@ -355,20 +377,65 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
// 处理 background.png:若在子目录下存在,则上移到 skinRoot
|
// 处理 background.png:若在子目录下存在,则上移到 skinRoot
|
||||||
NSString *nestedBg = [nestedRoot stringByAppendingPathComponent:@"background.png"];
|
NSString *nestedBg = [nestedRoot stringByAppendingPathComponent:@"background.png"];
|
||||||
if ([fm fileExistsAtPath:nestedBg]) {
|
if ([fm fileExistsAtPath:nestedBg]) {
|
||||||
[fm removeItemAtPath:bgPath error:nil];
|
[fm removeItemAtPath:workingBgPath error:nil];
|
||||||
[fm moveItemAtPath:nestedBg toPath:bgPath error:nil];
|
[fm moveItemAtPath:nestedBg toPath:workingBgPath error:nil];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (useTempRoot) {
|
||||||
|
NSString *backupName = [NSString stringWithFormat:@"%@__bak_%@", skinId, (tempToken ?: @"0")];
|
||||||
|
NSString *backupRoot = [skinsRoot stringByAppendingPathComponent:backupName];
|
||||||
|
[fm removeItemAtPath:backupRoot error:nil];
|
||||||
|
NSError *swapError = nil;
|
||||||
|
BOOL movedOld = NO;
|
||||||
|
if ([fm fileExistsAtPath:skinRoot]) {
|
||||||
|
movedOld = [fm moveItemAtPath:skinRoot toPath:backupRoot error:&swapError];
|
||||||
|
if (!movedOld && swapError) {
|
||||||
|
zipOK = NO;
|
||||||
|
if (!innerError) {
|
||||||
|
innerError = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
|
code:KBSkinBridgeErrorUnzipFailed
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"Failed to backup old skin"}];
|
||||||
|
}
|
||||||
|
NSLog(@"❌[SkinBridge] backup failed id=%@ error=%@", skinId, swapError);
|
||||||
|
dispatch_group_leave(group);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BOOL movedNew = [fm moveItemAtPath:workingRoot toPath:skinRoot error:&swapError];
|
||||||
|
if (!movedNew || swapError) {
|
||||||
|
zipOK = NO;
|
||||||
|
if (!innerError) {
|
||||||
|
innerError = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
|
code:KBSkinBridgeErrorUnzipFailed
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"Failed to replace skin assets"}];
|
||||||
|
}
|
||||||
|
if (movedOld) {
|
||||||
|
[fm moveItemAtPath:backupRoot toPath:skinRoot error:nil];
|
||||||
|
}
|
||||||
|
NSLog(@"❌[SkinBridge] replace failed id=%@ error=%@", skinId, swapError);
|
||||||
|
dispatch_group_leave(group);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (movedOld) {
|
||||||
|
[fm removeItemAtPath:backupRoot error:nil];
|
||||||
|
}
|
||||||
|
NSLog(@"🧹[SkinBridge] replaced old skin id=%@", skinId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记已成功解压一次(即使 icons 目录结构需要后续整理)。
|
||||||
|
didUnzip = YES;
|
||||||
|
NSLog(@"✅[SkinBridge] unzip done id=%@", skinId);
|
||||||
dispatch_group_leave(group);
|
dispatch_group_leave(group);
|
||||||
};
|
};
|
||||||
|
|
||||||
#if __has_include("KBNetworkManager.h")
|
#if __has_include("KBNetworkManager.h")
|
||||||
// 远程下载(http/https)
|
// 远程下载(http/https)
|
||||||
NSLog(@"[SkinBridge] will GET zip: %@", zipURL);
|
NSLog(@"🌐[SkinBridge] will GET zip: %@", zipURL);
|
||||||
[KBHUD show];
|
[KBHUD showWithStatus:@"正在下载..."];
|
||||||
[[KBNetworkManager shared] GETData:zipURL parameters:nil headers:nil completion:^(NSData *data, NSURLResponse *response, NSError *error) {
|
[[KBNetworkManager shared] GETData:zipURL parameters:nil headers:nil completion:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||||
NSLog(@"[SkinBridge] GET finished, error = %@", error);
|
NSLog(@"🌐[SkinBridge] GET finished id=%@ error=%@", skinId, error);
|
||||||
if (error || data.length == 0) {
|
if (error || data.length == 0) {
|
||||||
zipOK = NO;
|
zipOK = NO;
|
||||||
if (!innerError) {
|
if (!innerError) {
|
||||||
@@ -399,6 +466,9 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
#endif
|
#endif
|
||||||
|
} else {
|
||||||
|
NSLog(@"ℹ️[SkinBridge] skip download id=%@ force=%d cached=%d zip=%@",
|
||||||
|
skinId, forceDownload, hasCachedAssets, zipURL);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
zipOK = NO;
|
zipOK = NO;
|
||||||
@@ -411,11 +481,12 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
||||||
// 若既没有预先存在的缓存资源,也没有在本次流程中成功解压出资源,
|
// 若既没有预先存在的缓存资源,也没有在本次流程中成功解压出资源,
|
||||||
// 说明当前皮肤 B 的资源完全不可用,此时不应覆盖现有皮肤主题。
|
// 说明当前皮肤 B 的资源完全不可用,此时不应覆盖现有皮肤主题。
|
||||||
BOOL hasAssets = (hasCachedAssets || didUnzip);
|
BOOL hasAssets = (didUnzip || (!forceDownload && hasCachedAssets));
|
||||||
if (!hasAssets) {
|
if (!hasAssets) {
|
||||||
NSError *finalError = innerError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
NSError *finalError = innerError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
code:KBSkinBridgeErrorZipMissing
|
code:KBSkinBridgeErrorZipMissing
|
||||||
userInfo:@{NSLocalizedDescriptionKey: @"Zip resource not available"}];
|
userInfo:@{NSLocalizedDescriptionKey: @"Zip resource not available"}];
|
||||||
|
NSLog(@"❌[SkinBridge] apply aborted id=%@ error=%@", skinId, finalError);
|
||||||
if (completion) completion(NO, finalError);
|
if (completion) completion(NO, finalError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -459,6 +530,10 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
userInfo:nil];
|
userInfo:nil];
|
||||||
}
|
}
|
||||||
if (completion) completion(ok, finalError);
|
if (completion) completion(ok, finalError);
|
||||||
|
NSLog(@"%@ [SkinBridge] apply %@ id=%@",
|
||||||
|
(ok ? @"✅" : @"❌"),
|
||||||
|
(ok ? @"ok" : @"failed"),
|
||||||
|
skinId);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
NSString *preview = [skinJSON[@"preview"] isKindOfClass:NSString.class] ? skinJSON[@"preview"] : nil;
|
NSString *preview = [skinJSON[@"preview"] isKindOfClass:NSString.class] ? skinJSON[@"preview"] : nil;
|
||||||
[self recordInstalledSkinWithId:skinId
|
[self recordInstalledSkinWithId:skinId
|
||||||
@@ -766,4 +841,3 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
|||||||
|
|
||||||
- (BOOL)applyTheme:(KBSkinTheme *)theme {
|
- (BOOL)applyTheme:(KBSkinTheme *)theme {
|
||||||
if (!theme) return NO;
|
if (!theme) return NO;
|
||||||
|
NSLog(@"🎨[SkinManager] apply theme id=%@ name=%@", theme.skinId, theme.name);
|
||||||
// 将主题写入 App Group 存储(失败也不影响本次进程内的使用)
|
// 将主题写入 App Group 存储(失败也不影响本次进程内的使用)
|
||||||
[self p_saveToStore:theme];
|
[self p_saveToStore:theme];
|
||||||
// 始终更新当前主题并广播通知,确保当前进程和扩展之间保持同步。
|
// 始终更新当前主题并广播通知,确保当前进程和扩展之间保持同步。
|
||||||
|
|||||||
@@ -19,6 +19,12 @@
|
|||||||
"current_lang" = "Current: %@";
|
"current_lang" = "Current: %@";
|
||||||
"common_back" = "Back";
|
"common_back" = "Back";
|
||||||
|
|
||||||
|
// search
|
||||||
|
"Recommended Skin" = "Recommended Skin";
|
||||||
|
"Historical Search" = "Historical Search";
|
||||||
|
"Search Themes" = "Search Themes";
|
||||||
|
"Search" = "Search";
|
||||||
|
|
||||||
// Login & account
|
// Login & account
|
||||||
"Log In" = "Log In";
|
"Log In" = "Log In";
|
||||||
"Signed in successfully" = "Signed in successfully";
|
"Signed in successfully" = "Signed in successfully";
|
||||||
@@ -31,7 +37,8 @@
|
|||||||
"Invalid login credential" = "Invalid login credential";
|
"Invalid login credential" = "Invalid login credential";
|
||||||
"No token returned" = "No token returned";
|
"No token returned" = "No token returned";
|
||||||
"Failed to save login state" = "Failed to save login state";
|
"Failed to save login state" = "Failed to save login state";
|
||||||
"请切换到主App完成登录" = "Please switch to the main app to finish signing in";
|
"Sign-in canceled" = "Sign-in canceled";
|
||||||
|
"Please switch to the key of love app to finish signing in" = "Please switch to the key of love app to finish signing in";
|
||||||
"Continue Via Email" = "Continue Via Email";
|
"Continue Via Email" = "Continue Via Email";
|
||||||
"Login With Email Password" = "Login With Email Password";
|
"Login With Email Password" = "Login With Email Password";
|
||||||
"Enter Email Address" = "Enter Email Address";
|
"Enter Email Address" = "Enter Email Address";
|
||||||
@@ -119,7 +126,7 @@
|
|||||||
"Personal" = "Personal";
|
"Personal" = "Personal";
|
||||||
"My Keyboard" = "My Keyboard";
|
"My Keyboard" = "My Keyboard";
|
||||||
"Notice" = "Notice";
|
"Notice" = "Notice";
|
||||||
"Share App" = "Share App";
|
"invite" = "invite";
|
||||||
"Feedback" = "Feedback";
|
"Feedback" = "Feedback";
|
||||||
"E-mail" = "E-mail";
|
"E-mail" = "E-mail";
|
||||||
"Agreement" = "Agreement";
|
"Agreement" = "Agreement";
|
||||||
@@ -162,31 +169,7 @@
|
|||||||
"Log Out" = "Log Out";
|
"Log Out" = "Log Out";
|
||||||
"Ranking List" = "Ranking List";
|
"Ranking List" = "Ranking List";
|
||||||
"Persona circle" = "Persona circle";
|
"Persona circle" = "Persona circle";
|
||||||
|
"Clear" = "Clear";
|
||||||
|
|
||||||
|
|
||||||
// Skin sample names
|
|
||||||
"极光" = "Aurora";
|
|
||||||
"雪山" = "Snow Mountain";
|
|
||||||
"湖面" = "Lake";
|
|
||||||
|
|
||||||
// Sample tags & copy
|
|
||||||
"高情商" = "High EQ";
|
|
||||||
"暖味拉扯" = "Ambiguous flirting";
|
|
||||||
"风趣幽默" = "Witty & humorous";
|
|
||||||
"撩女生" = "Flirt with girls";
|
|
||||||
"社交惬匿" = "Relaxed socializing";
|
|
||||||
"情场高手" = "Dating expert";
|
|
||||||
"一枚暖男" = "Warm-hearted guy";
|
|
||||||
"聊天搭子" = "Chat buddy";
|
|
||||||
"表达爱意" = "Express love";
|
|
||||||
"更多话术" = "More prompts";
|
|
||||||
"Tap to paste their message" = "Tap to paste their message";
|
|
||||||
"Tap any conversation to paste, then try any reply style~" = "Tap any conversation to paste, then try any reply style~";
|
|
||||||
"What are you doing?" = "What are you doing?";
|
|
||||||
"I'm going to take a shower." = "I'm going to take a shower.";
|
|
||||||
"Welcome to use the [key of love] keyboard" = "Welcome to use the [key of love] keyboard";
|
|
||||||
"👋 Welcome to Key of Love Keyboard" = "👋 Welcome to Key of Love Keyboard";
|
|
||||||
|
|
||||||
// Payment & IAP
|
// Payment & IAP
|
||||||
"Payment successful" = "Payment successful";
|
"Payment successful" = "Payment successful";
|
||||||
@@ -194,60 +177,12 @@
|
|||||||
"Purchase: %@ Coins %@" = "Purchase: %@ Coins %@";
|
"Purchase: %@ Coins %@" = "Purchase: %@ Coins %@";
|
||||||
"Pay clicked" = "Pay clicked";
|
"Pay clicked" = "Pay clicked";
|
||||||
"Points Recharge" = "Points Recharge";
|
"Points Recharge" = "Points Recharge";
|
||||||
|
"Recharge" = "Recharge";
|
||||||
|
"Consumption Record" = "Consumption Record";
|
||||||
"My Points" = "My Points";
|
"My Points" = "My Points";
|
||||||
|
"Consumption Details" = "Consumption Details";
|
||||||
"No data" = "No data";
|
"No data" = "No data";
|
||||||
|
|
||||||
// Example categories/items
|
|
||||||
"能力" = "Ability";
|
|
||||||
"能力2" = "Ability 2";
|
|
||||||
"爱好" = "Hobby";
|
|
||||||
"爱好2" = "Hobby 2";
|
|
||||||
"队友" = "Teammates";
|
|
||||||
"队友2" = "Teammates 2";
|
|
||||||
"高级能力" = "Advanced abilities";
|
|
||||||
"高级爱好" = "Advanced hobbies";
|
|
||||||
"高级队友" = "Advanced teammates";
|
|
||||||
|
|
||||||
// Example fruits etc.
|
|
||||||
"果冻橙" = "Jelly orange";
|
|
||||||
"芒果" = "Mango";
|
|
||||||
"有机水果卷心菜" = "Organic cabbage";
|
|
||||||
"水果萝卜" = "Fruit radish";
|
|
||||||
"熟冻帝王蟹" = "Cooked king crab";
|
|
||||||
"赣南脐橙" = "Gannan navel orange";
|
|
||||||
"苹果" = "Apple";
|
|
||||||
"胡萝卜" = "Carrot";
|
|
||||||
"葡萄" = "Grape";
|
|
||||||
"西瓜" = "Watermelon";
|
|
||||||
"小龙虾" = "Crawfish";
|
|
||||||
"吃烤肉" = "Eat barbecue";
|
|
||||||
"吃鸡腿肉" = "Eat chicken drumsticks";
|
|
||||||
"吃牛肉" = "Eat beef";
|
|
||||||
"各种肉" = "All kinds of meat";
|
|
||||||
|
|
||||||
// One Piece sample roles
|
|
||||||
"【剑士】罗罗诺亚·索隆" = "[Swordsman] Roronoa Zoro";
|
|
||||||
"【航海士】娜美" = "[Navigator] Nami";
|
|
||||||
"【狙击手】乌索普" = "[Sniper] Usopp";
|
|
||||||
"【厨师】香吉士" = "[Cook] Sanji";
|
|
||||||
"【船医】托尼托尼·乔巴" = "[Doctor] Tony Tony Chopper";
|
|
||||||
"【船匠】 弗兰奇" = "[Shipwright] Franky";
|
|
||||||
"【音乐家】布鲁克" = "[Musician] Brook";
|
|
||||||
"【考古学家】妮可·罗宾" = "[Archaeologist] Nico Robin";
|
|
||||||
|
|
||||||
// Rubber-series sample moves
|
|
||||||
"橡胶火箭" = "Gum-Gum Rocket";
|
|
||||||
"橡胶火箭炮" = "Gum-Gum Bazooka";
|
|
||||||
"橡胶机关枪" = "Gum-Gum Gatling";
|
|
||||||
"橡胶子弹" = "Gum-Gum Bullet";
|
|
||||||
"橡胶攻城炮" = "Gum-Gum Cannon";
|
|
||||||
"橡胶象枪" = "Gum-Gum Elephant Gun";
|
|
||||||
"橡胶象枪乱打" = "Gum-Gum Elephant Gatling";
|
|
||||||
"橡胶灰熊铳" = "Gum-Gum Grizzly Magnum";
|
|
||||||
"橡胶雷神象枪" = "Gum-Gum Thor Elephant Gun";
|
|
||||||
"橡胶猿王枪" = "Gum-Gum King Kong Gun";
|
|
||||||
"橡胶犀·榴弹炮" = "Gum-Gum Rhino Grenade";
|
|
||||||
"橡胶大蛇炮" = "Gum-Gum Great Serpent Cannon";
|
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
"测试" = "Test";
|
"测试" = "Test";
|
||||||
|
|||||||
@@ -19,6 +19,13 @@
|
|||||||
"current_lang" = "当前:%@";
|
"current_lang" = "当前:%@";
|
||||||
"common_back" = "返回";
|
"common_back" = "返回";
|
||||||
|
|
||||||
|
// search
|
||||||
|
"Recommended Skin" = "推荐皮肤";
|
||||||
|
"Historical Search" = "历史搜索";
|
||||||
|
"Search Themes" = "搜索主题";
|
||||||
|
"Search" = "搜索";
|
||||||
|
|
||||||
|
|
||||||
// 登录与账号(以英文 key 为准)
|
// 登录与账号(以英文 key 为准)
|
||||||
"Log In" = "登录";
|
"Log In" = "登录";
|
||||||
"Signed in successfully" = "登录成功";
|
"Signed in successfully" = "登录成功";
|
||||||
@@ -31,7 +38,8 @@
|
|||||||
"Invalid login credential" = "无效的登录凭证";
|
"Invalid login credential" = "无效的登录凭证";
|
||||||
"No token returned" = "未返回 token";
|
"No token returned" = "未返回 token";
|
||||||
"Failed to save login state" = "保存登录态失败";
|
"Failed to save login state" = "保存登录态失败";
|
||||||
"请切换到主App完成登录" = "请切换到主App完成登录";
|
"Sign-in canceled" = "登录已取消";
|
||||||
|
"Please switch to the key of love app to finish signing in" = "请切换到Key of Love App完成登录";
|
||||||
"Continue Via Email" = "通过邮箱登录";
|
"Continue Via Email" = "通过邮箱登录";
|
||||||
"Login With Email Password" = "使用邮箱密码登录";
|
"Login With Email Password" = "使用邮箱密码登录";
|
||||||
"Enter Email Address" = "请输入邮箱地址";
|
"Enter Email Address" = "请输入邮箱地址";
|
||||||
@@ -120,7 +128,7 @@
|
|||||||
"Personal" = "个人";
|
"Personal" = "个人";
|
||||||
"My Keyboard" = "我的键盘";
|
"My Keyboard" = "我的键盘";
|
||||||
"Notice" = "通知";
|
"Notice" = "通知";
|
||||||
"Share App" = "分享app";
|
"invite" = "邀请";
|
||||||
"Feedback" = "反馈";
|
"Feedback" = "反馈";
|
||||||
"E-mail" = "联系我们";
|
"E-mail" = "联系我们";
|
||||||
"Agreement" = "协议";
|
"Agreement" = "协议";
|
||||||
@@ -162,29 +170,9 @@
|
|||||||
"Log Out" = "退出";
|
"Log Out" = "退出";
|
||||||
"Ranking List" = "排行榜";
|
"Ranking List" = "排行榜";
|
||||||
"Persona circle" = "圈子";
|
"Persona circle" = "圈子";
|
||||||
|
"Clear" = "立刻清空";
|
||||||
|
|
||||||
// 皮肤示例名称
|
// 皮肤示例名称
|
||||||
"极光" = "极光";
|
|
||||||
"雪山" = "雪山";
|
|
||||||
"湖面" = "湖面";
|
|
||||||
|
|
||||||
// 示例标签与文案
|
|
||||||
"高情商" = "高情商";
|
|
||||||
"暖味拉扯" = "暖味拉扯";
|
|
||||||
"风趣幽默" = "风趣幽默";
|
|
||||||
"撩女生" = "撩女生";
|
|
||||||
"社交惬匿" = "社交惬匿";
|
|
||||||
"情场高手" = "情场高手";
|
|
||||||
"一枚暖男" = "一枚暖男";
|
|
||||||
"聊天搭子" = "聊天搭子";
|
|
||||||
"表达爱意" = "表达爱意";
|
|
||||||
"更多话术" = "更多话术";
|
|
||||||
"点击粘贴TA的话" = "点击粘贴TA的话";
|
|
||||||
"点击任一对话去粘贴,选择任意回复方式去试用吧~" = "点击任一对话去粘贴,选择任意回复方式去试用吧~";
|
|
||||||
"在干嘛?" = "在干嘛?";
|
|
||||||
"我去洗澡了" = "我去洗澡了";
|
|
||||||
"🎉 如您遇到其他问题,可点击在线客服帮您解决~" = "🎉 如您遇到其他问题,可点击在线客服帮您解决~";
|
|
||||||
"👋 欢迎使用『Lovekey 键盘』" = "👋 欢迎使用『Lovekey 键盘』";
|
|
||||||
|
|
||||||
// 支付与内购(英文 key)
|
// 支付与内购(英文 key)
|
||||||
"Payment successful" = "支付成功";
|
"Payment successful" = "支付成功";
|
||||||
@@ -192,60 +180,12 @@
|
|||||||
"Purchase: %@ Coins %@" = "购买:%@ Coins %@";
|
"Purchase: %@ Coins %@" = "购买:%@ Coins %@";
|
||||||
"Pay clicked" = "点击支付";
|
"Pay clicked" = "点击支付";
|
||||||
"Points Recharge" = "积分充值";
|
"Points Recharge" = "积分充值";
|
||||||
|
"Recharge" = "充值";
|
||||||
|
"Consumption Record" = "消费记录";
|
||||||
"My Points" = "我的积分";
|
"My Points" = "我的积分";
|
||||||
|
"Consumption Details" = "消费明细";
|
||||||
"No data" = "暂无数据";
|
"No data" = "暂无数据";
|
||||||
|
|
||||||
// 示例商品/分类
|
|
||||||
"能力" = "能力";
|
|
||||||
"能力2" = "能力2";
|
|
||||||
"爱好" = "爱好";
|
|
||||||
"爱好2" = "爱好2";
|
|
||||||
"队友" = "队友";
|
|
||||||
"队友2" = "队友2";
|
|
||||||
"高级能力" = "高级能力";
|
|
||||||
"高级爱好" = "高级爱好";
|
|
||||||
"高级队友" = "高级队友";
|
|
||||||
|
|
||||||
// 示例水果等
|
|
||||||
"果冻橙" = "果冻橙";
|
|
||||||
"芒果" = "芒果";
|
|
||||||
"有机水果卷心菜" = "有机水果卷心菜";
|
|
||||||
"水果萝卜" = "水果萝卜";
|
|
||||||
"熟冻帝王蟹" = "熟冻帝王蟹";
|
|
||||||
"赣南脐橙" = "赣南脐橙";
|
|
||||||
"苹果" = "苹果";
|
|
||||||
"胡萝卜" = "胡萝卜";
|
|
||||||
"葡萄" = "葡萄";
|
|
||||||
"西瓜" = "西瓜";
|
|
||||||
"小龙虾" = "小龙虾";
|
|
||||||
"吃烤肉" = "吃烤肉";
|
|
||||||
"吃鸡腿肉" = "吃鸡腿肉";
|
|
||||||
"吃牛肉" = "吃牛肉";
|
|
||||||
"各种肉" = "各种肉";
|
|
||||||
|
|
||||||
// One Piece 示例角色
|
|
||||||
"【剑士】罗罗诺亚·索隆" = "【剑士】罗罗诺亚·索隆";
|
|
||||||
"【航海士】娜美" = "【航海士】娜美";
|
|
||||||
"【狙击手】乌索普" = "【狙击手】乌索普";
|
|
||||||
"【厨师】香吉士" = "【厨师】香吉士";
|
|
||||||
"【船医】托尼托尼·乔巴" = "【船医】托尼托尼·乔巴";
|
|
||||||
"【船匠】 弗兰奇" = "【船匠】 弗兰奇";
|
|
||||||
"【音乐家】布鲁克" = "【音乐家】布鲁克";
|
|
||||||
"【考古学家】妮可·罗宾" = "【考古学家】妮可·罗宾";
|
|
||||||
|
|
||||||
// 橡胶系列示例文案
|
|
||||||
"橡胶火箭" = "橡胶火箭";
|
|
||||||
"橡胶火箭炮" = "橡胶火箭炮";
|
|
||||||
"橡胶机关枪" = "橡胶机关枪";
|
|
||||||
"橡胶子弹" = "橡胶子弹";
|
|
||||||
"橡胶攻城炮" = "橡胶攻城炮";
|
|
||||||
"橡胶象枪" = "橡胶象枪";
|
|
||||||
"橡胶象枪乱打" = "橡胶象枪乱打";
|
|
||||||
"橡胶灰熊铳" = "橡胶灰熊铳";
|
|
||||||
"橡胶雷神象枪" = "橡胶雷神象枪";
|
|
||||||
"橡胶猿王枪" = "橡胶猿王枪";
|
|
||||||
"橡胶犀·榴弹炮" = "橡胶犀·榴弹炮";
|
|
||||||
"橡胶大蛇炮" = "橡胶大蛇炮";
|
|
||||||
|
|
||||||
// 其它
|
// 其它
|
||||||
"Test" = "测试";
|
"Test" = "测试";
|
||||||
|
|||||||
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
@@ -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
@@ -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>
|
||||||
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
@@ -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>
|
||||||
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>
|
||||||
@@ -95,13 +95,11 @@
|
|||||||
048908CE2EBE373500FABA60 /* KBSkinCardCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908C92EBE373500FABA60 /* KBSkinCardCell.m */; };
|
048908CE2EBE373500FABA60 /* KBSkinCardCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908C92EBE373500FABA60 /* KBSkinCardCell.m */; };
|
||||||
048908CF2EBE373500FABA60 /* KBTagCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908CB2EBE373500FABA60 /* KBTagCell.m */; };
|
048908CF2EBE373500FABA60 /* KBTagCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908CB2EBE373500FABA60 /* KBTagCell.m */; };
|
||||||
048908D22EBF611D00FABA60 /* KBHistoryMoreCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908D12EBF611D00FABA60 /* KBHistoryMoreCell.m */; };
|
048908D22EBF611D00FABA60 /* KBHistoryMoreCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908D12EBF611D00FABA60 /* KBHistoryMoreCell.m */; };
|
||||||
048908DA2EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908D82EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout.m */; };
|
048908DA2EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908D82EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout.m */; };
|
||||||
048908DD2EBF67EB00FABA60 /* KBSearchResultVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908DC2EBF67EB00FABA60 /* KBSearchResultVC.m */; };
|
048908DD2EBF67EB00FABA60 /* KBSearchResultVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908DC2EBF67EB00FABA60 /* KBSearchResultVC.m */; };
|
||||||
05A1B2D12F5B1A2B3C4D5E60 /* KBSearchVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A1B2C52F5B1A2B3C4D5E60 /* KBSearchVM.m */; };
|
048908E02EBF73DC00FABA60 /* MySkinVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908DF2EBF73DC00FABA60 /* MySkinVC.m */; };
|
||||||
05A1B2D22F5B1A2B3C4D5E60 /* KBSearchThemeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A1B2C72F5B1A2B3C4D5E60 /* KBSearchThemeModel.m */; };
|
048908E32EBF760000FABA60 /* MySkinCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908E22EBF760000FABA60 /* MySkinCell.m */; };
|
||||||
048908E02EBF73DC00FABA60 /* MySkinVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908DF2EBF73DC00FABA60 /* MySkinVC.m */; };
|
048908E32EBF821700FABA60 /* KBSkinDetailVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908E22EBF821700FABA60 /* KBSkinDetailVC.m */; };
|
||||||
048908E32EBF760000FABA60 /* MySkinCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908E22EBF760000FABA60 /* MySkinCell.m */; };
|
|
||||||
048908E32EBF821700FABA60 /* KBSkinDetailVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908E22EBF821700FABA60 /* KBSkinDetailVC.m */; };
|
|
||||||
048908E62EBF841B00FABA60 /* KBSkinDetailTagCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908E52EBF841B00FABA60 /* KBSkinDetailTagCell.m */; };
|
048908E62EBF841B00FABA60 /* KBSkinDetailTagCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908E52EBF841B00FABA60 /* KBSkinDetailTagCell.m */; };
|
||||||
048908E92EBF843000FABA60 /* KBSkinDetailHeaderCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908E82EBF843000FABA60 /* KBSkinDetailHeaderCell.m */; };
|
048908E92EBF843000FABA60 /* KBSkinDetailHeaderCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908E82EBF843000FABA60 /* KBSkinDetailHeaderCell.m */; };
|
||||||
048908EC2EBF849300FABA60 /* KBSkinTagsContainerCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908EB2EBF849300FABA60 /* KBSkinTagsContainerCell.m */; };
|
048908EC2EBF849300FABA60 /* KBSkinTagsContainerCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908EB2EBF849300FABA60 /* KBSkinTagsContainerCell.m */; };
|
||||||
@@ -131,6 +129,10 @@
|
|||||||
0498BD8C2EE69E15006CC1D5 /* KBTagItemModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD8A2EE69E15006CC1D5 /* KBTagItemModel.m */; };
|
0498BD8C2EE69E15006CC1D5 /* KBTagItemModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD8A2EE69E15006CC1D5 /* KBTagItemModel.m */; };
|
||||||
0498BD8F2EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */; };
|
0498BD8F2EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */; };
|
||||||
0498BD902EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */; };
|
0498BD902EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */; };
|
||||||
|
A1F0C1D22FACAD0012345678 /* KBMaiPointReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */; };
|
||||||
|
A1F0C1D32FACAD0012345678 /* KBMaiPointReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */; };
|
||||||
|
A1F0C1C22FABCDEF12345678 /* KBInviteCodeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */; };
|
||||||
|
A1F0C1C32FABCDEF12345678 /* KBInviteCodeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */; };
|
||||||
0498BDDA2EE7ECEA006CC1D5 /* WJXEventSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BDD82EE7ECEA006CC1D5 /* WJXEventSource.m */; };
|
0498BDDA2EE7ECEA006CC1D5 /* WJXEventSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BDD82EE7ECEA006CC1D5 /* WJXEventSource.m */; };
|
||||||
0498BDDE2EE81508006CC1D5 /* KBShopVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BDDD2EE81508006CC1D5 /* KBShopVM.m */; };
|
0498BDDE2EE81508006CC1D5 /* KBShopVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BDDD2EE81508006CC1D5 /* KBShopVM.m */; };
|
||||||
0498BDE12EEA87C9006CC1D5 /* KBShopStyleModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BDE02EEA87C8006CC1D5 /* KBShopStyleModel.m */; };
|
0498BDE12EEA87C9006CC1D5 /* KBShopStyleModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BDE02EEA87C8006CC1D5 /* KBShopStyleModel.m */; };
|
||||||
@@ -178,8 +180,6 @@
|
|||||||
04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95692EB05497007BD342 /* KBKeyButton.m */; };
|
04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95692EB05497007BD342 /* KBKeyButton.m */; };
|
||||||
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC956C2EB054B7007BD342 /* KBKeyboardView.m */; };
|
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC956C2EB054B7007BD342 /* KBKeyboardView.m */; };
|
||||||
04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC956F2EB09516007BD342 /* KBFunctionView.m */; };
|
04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC956F2EB09516007BD342 /* KBFunctionView.m */; };
|
||||||
A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */; };
|
|
||||||
A1B2C9052FBD000200000001 /* KBBackspaceUndoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */; };
|
|
||||||
04FC95732EB09570007BD342 /* KBFunctionBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95722EB09570007BD342 /* KBFunctionBarView.m */; };
|
04FC95732EB09570007BD342 /* KBFunctionBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95722EB09570007BD342 /* KBFunctionBarView.m */; };
|
||||||
04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95752EB095DE007BD342 /* KBFunctionPasteView.m */; };
|
04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95752EB095DE007BD342 /* KBFunctionPasteView.m */; };
|
||||||
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95782EB09BC8007BD342 /* KBKeyBoardMainView.m */; };
|
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95782EB09BC8007BD342 /* KBKeyBoardMainView.m */; };
|
||||||
@@ -207,20 +207,31 @@
|
|||||||
04FEDC222F00020000999999 /* KBKeyboardSubscriptionProduct.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC212F00020000999999 /* KBKeyboardSubscriptionProduct.m */; };
|
04FEDC222F00020000999999 /* KBKeyboardSubscriptionProduct.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC212F00020000999999 /* KBKeyboardSubscriptionProduct.m */; };
|
||||||
04FEDC322F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC312F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.m */; };
|
04FEDC322F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC312F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.m */; };
|
||||||
04FEDC422F00040000999999 /* KBKeyboardSubscriptionFeatureMarqueeView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC412F00040000999999 /* KBKeyboardSubscriptionFeatureMarqueeView.m */; };
|
04FEDC422F00040000999999 /* KBKeyboardSubscriptionFeatureMarqueeView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC412F00040000999999 /* KBKeyboardSubscriptionFeatureMarqueeView.m */; };
|
||||||
|
05A1B2D12F5B1A2B3C4D5E60 /* KBSearchVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A1B2C52F5B1A2B3C4D5E60 /* KBSearchVM.m */; };
|
||||||
|
05A1B2D22F5B1A2B3C4D5E60 /* KBSearchThemeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A1B2C72F5B1A2B3C4D5E60 /* KBSearchThemeModel.m */; };
|
||||||
471CAD3574798685B72ADD55 /* KBMyTheme.m in Sources */ = {isa = PBXBuildFile; fileRef = 180D662EC4DB3A7FFF83FF18 /* KBMyTheme.m */; };
|
471CAD3574798685B72ADD55 /* KBMyTheme.m in Sources */ = {isa = PBXBuildFile; fileRef = 180D662EC4DB3A7FFF83FF18 /* KBMyTheme.m */; };
|
||||||
49B63DBAEE9076C591E13D68 /* KBShopThemeTagModel.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A844CD2D8584596DBE6316 /* KBShopThemeTagModel.m */; };
|
49B63DBAEE9076C591E13D68 /* KBShopThemeTagModel.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A844CD2D8584596DBE6316 /* KBShopThemeTagModel.m */; };
|
||||||
550CB2630FA4A7B4B9782EFA /* KBMyTheme.m in Sources */ = {isa = PBXBuildFile; fileRef = 180D662EC4DB3A7FFF83FF18 /* KBMyTheme.m */; };
|
550CB2630FA4A7B4B9782EFA /* KBMyTheme.m in Sources */ = {isa = PBXBuildFile; fileRef = 180D662EC4DB3A7FFF83FF18 /* KBMyTheme.m */; };
|
||||||
7A36414DFDA5BEC9B7D2E318 /* Pods_CustomKeyboard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C1092FB2B452F95B15D4263 /* Pods_CustomKeyboard.framework */; };
|
7A36414DFDA5BEC9B7D2E318 /* Pods_CustomKeyboard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C1092FB2B452F95B15D4263 /* Pods_CustomKeyboard.framework */; };
|
||||||
A1B2C3D42EB0A0A100000001 /* KBFunctionTagCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D32EB0A0A100000001 /* KBFunctionTagCell.m */; };
|
A1B2C3D42EB0A0A100000001 /* KBFunctionTagCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D32EB0A0A100000001 /* KBFunctionTagCell.m */; };
|
||||||
A1B2C3E22EB0C0A100000001 /* KBNetworkManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3E12EB0C0A100000001 /* KBNetworkManager.m */; };
|
A1B2C3E22EB0C0A100000001 /* KBNetworkManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3E12EB0C0A100000001 /* KBNetworkManager.m */; };
|
||||||
|
A1B2C3EA2F20000000000001 /* KBSuggestionEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3E72F20000000000001 /* KBSuggestionEngine.m */; };
|
||||||
|
A1B2C3EB2F20000000000001 /* KBSuggestionBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3E92F20000000000001 /* KBSuggestionBarView.m */; };
|
||||||
|
A1B2C3ED2F20000000000001 /* kb_words.txt in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3EC2F20000000000001 /* kb_words.txt */; };
|
||||||
A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */; };
|
A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */; };
|
||||||
A1B2C4002EB4A0A100000003 /* KBAuthManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4002EB4A0A100000002 /* KBAuthManager.m */; };
|
A1B2C4002EB4A0A100000003 /* KBAuthManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4002EB4A0A100000002 /* KBAuthManager.m */; };
|
||||||
A1B2C4002EB4A0A100000004 /* KBAuthManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4002EB4A0A100000002 /* KBAuthManager.m */; };
|
A1B2C4002EB4A0A100000004 /* KBAuthManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4002EB4A0A100000002 /* KBAuthManager.m */; };
|
||||||
A1B2C4202EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */; };
|
A1B2C4202EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */; };
|
||||||
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 */; };
|
||||||
|
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 */; };
|
||||||
|
A1F0C1B12F1234567890ABCD /* KBConsumptionRecord.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1A12F1234567890ABCD /* KBConsumptionRecord.m */; };
|
||||||
|
A1F0C1B22F1234567890ABCD /* KBConsumptionRecordCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1A32F1234567890ABCD /* KBConsumptionRecordCell.m */; };
|
||||||
|
A1F0C1B32F1234567890ABCD /* KBConsumptionRecordVC.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1A52F1234567890ABCD /* KBConsumptionRecordVC.m */; };
|
||||||
EB72B60040437E3C0A4890FC /* KBShopThemeDetailModel.m in Sources */ = {isa = PBXBuildFile; fileRef = B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */; };
|
EB72B60040437E3C0A4890FC /* KBShopThemeDetailModel.m in Sources */ = {isa = PBXBuildFile; fileRef = B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */; };
|
||||||
ECC9EE02174D86E8D792472F /* Pods_keyBoard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 967065BB5230E43F293B3AF9 /* Pods_keyBoard.framework */; };
|
ECC9EE02174D86E8D792472F /* Pods_keyBoard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 967065BB5230E43F293B3AF9 /* Pods_keyBoard.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@@ -389,16 +400,12 @@
|
|||||||
048908D02EBF611D00FABA60 /* KBHistoryMoreCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBHistoryMoreCell.h; sourceTree = "<group>"; };
|
048908D02EBF611D00FABA60 /* KBHistoryMoreCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBHistoryMoreCell.h; sourceTree = "<group>"; };
|
||||||
048908D12EBF611D00FABA60 /* KBHistoryMoreCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBHistoryMoreCell.m; sourceTree = "<group>"; };
|
048908D12EBF611D00FABA60 /* KBHistoryMoreCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBHistoryMoreCell.m; sourceTree = "<group>"; };
|
||||||
048908D72EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UICollectionViewLeftAlignedLayout.h; sourceTree = "<group>"; };
|
048908D72EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UICollectionViewLeftAlignedLayout.h; sourceTree = "<group>"; };
|
||||||
048908D82EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UICollectionViewLeftAlignedLayout.m; sourceTree = "<group>"; };
|
048908D82EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UICollectionViewLeftAlignedLayout.m; sourceTree = "<group>"; };
|
||||||
048908DB2EBF67EB00FABA60 /* KBSearchResultVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSearchResultVC.h; sourceTree = "<group>"; };
|
048908DB2EBF67EB00FABA60 /* KBSearchResultVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSearchResultVC.h; sourceTree = "<group>"; };
|
||||||
048908DC2EBF67EB00FABA60 /* KBSearchResultVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSearchResultVC.m; sourceTree = "<group>"; };
|
048908DC2EBF67EB00FABA60 /* KBSearchResultVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSearchResultVC.m; sourceTree = "<group>"; };
|
||||||
05A1B2C42F5B1A2B3C4D5E60 /* KBSearchVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSearchVM.h; sourceTree = "<group>"; };
|
048908DE2EBF73DC00FABA60 /* MySkinVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MySkinVC.h; sourceTree = "<group>"; };
|
||||||
05A1B2C52F5B1A2B3C4D5E60 /* KBSearchVM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSearchVM.m; sourceTree = "<group>"; };
|
048908DF2EBF73DC00FABA60 /* MySkinVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MySkinVC.m; sourceTree = "<group>"; };
|
||||||
05A1B2C62F5B1A2B3C4D5E60 /* KBSearchThemeModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSearchThemeModel.h; sourceTree = "<group>"; };
|
048908E12EBF760000FABA60 /* MySkinCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MySkinCell.h; sourceTree = "<group>"; };
|
||||||
05A1B2C72F5B1A2B3C4D5E60 /* KBSearchThemeModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSearchThemeModel.m; sourceTree = "<group>"; };
|
|
||||||
048908DE2EBF73DC00FABA60 /* MySkinVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MySkinVC.h; sourceTree = "<group>"; };
|
|
||||||
048908DF2EBF73DC00FABA60 /* MySkinVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MySkinVC.m; sourceTree = "<group>"; };
|
|
||||||
048908E12EBF760000FABA60 /* MySkinCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MySkinCell.h; sourceTree = "<group>"; };
|
|
||||||
048908E12EBF821700FABA60 /* KBSkinDetailVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinDetailVC.h; sourceTree = "<group>"; };
|
048908E12EBF821700FABA60 /* KBSkinDetailVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinDetailVC.h; sourceTree = "<group>"; };
|
||||||
048908E22EBF760000FABA60 /* MySkinCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MySkinCell.m; sourceTree = "<group>"; };
|
048908E22EBF760000FABA60 /* MySkinCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MySkinCell.m; sourceTree = "<group>"; };
|
||||||
048908E22EBF821700FABA60 /* KBSkinDetailVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinDetailVC.m; sourceTree = "<group>"; };
|
048908E22EBF821700FABA60 /* KBSkinDetailVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinDetailVC.m; sourceTree = "<group>"; };
|
||||||
@@ -453,6 +460,10 @@
|
|||||||
0498BD8A2EE69E15006CC1D5 /* KBTagItemModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBTagItemModel.m; sourceTree = "<group>"; };
|
0498BD8A2EE69E15006CC1D5 /* KBTagItemModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBTagItemModel.m; sourceTree = "<group>"; };
|
||||||
0498BD8D2EE6A3BD006CC1D5 /* KBMyMainModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyMainModel.h; sourceTree = "<group>"; };
|
0498BD8D2EE6A3BD006CC1D5 /* KBMyMainModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyMainModel.h; sourceTree = "<group>"; };
|
||||||
0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyMainModel.m; sourceTree = "<group>"; };
|
0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyMainModel.m; sourceTree = "<group>"; };
|
||||||
|
A1F0C1C02FABCDEF12345678 /* KBInviteCodeModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBInviteCodeModel.h; sourceTree = "<group>"; };
|
||||||
|
A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBInviteCodeModel.m; sourceTree = "<group>"; };
|
||||||
|
A1F0C1D02FACAD0012345678 /* KBMaiPointReporter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMaiPointReporter.h; sourceTree = "<group>"; };
|
||||||
|
A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMaiPointReporter.m; sourceTree = "<group>"; };
|
||||||
0498BDD72EE7ECEA006CC1D5 /* WJXEventSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WJXEventSource.h; sourceTree = "<group>"; };
|
0498BDD72EE7ECEA006CC1D5 /* WJXEventSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WJXEventSource.h; sourceTree = "<group>"; };
|
||||||
0498BDD82EE7ECEA006CC1D5 /* WJXEventSource.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WJXEventSource.m; sourceTree = "<group>"; };
|
0498BDD82EE7ECEA006CC1D5 /* WJXEventSource.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WJXEventSource.m; sourceTree = "<group>"; };
|
||||||
0498BDDC2EE81508006CC1D5 /* KBShopVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBShopVM.h; sourceTree = "<group>"; };
|
0498BDDC2EE81508006CC1D5 /* KBShopVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBShopVM.h; sourceTree = "<group>"; };
|
||||||
@@ -540,10 +551,6 @@
|
|||||||
04FC956C2EB054B7007BD342 /* KBKeyboardView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardView.m; sourceTree = "<group>"; };
|
04FC956C2EB054B7007BD342 /* KBKeyboardView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardView.m; sourceTree = "<group>"; };
|
||||||
04FC956E2EB09516007BD342 /* KBFunctionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionView.h; sourceTree = "<group>"; };
|
04FC956E2EB09516007BD342 /* KBFunctionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionView.h; sourceTree = "<group>"; };
|
||||||
04FC956F2EB09516007BD342 /* KBFunctionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionView.m; sourceTree = "<group>"; };
|
04FC956F2EB09516007BD342 /* KBFunctionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionView.m; sourceTree = "<group>"; };
|
||||||
A1B2C9012FBD000100000001 /* KBBackspaceLongPressHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBBackspaceLongPressHandler.h; 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>"; };
|
|
||||||
A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBBackspaceUndoManager.m; sourceTree = "<group>"; };
|
|
||||||
04FC95712EB09570007BD342 /* KBFunctionBarView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionBarView.h; sourceTree = "<group>"; };
|
04FC95712EB09570007BD342 /* KBFunctionBarView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionBarView.h; sourceTree = "<group>"; };
|
||||||
04FC95722EB09570007BD342 /* KBFunctionBarView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionBarView.m; sourceTree = "<group>"; };
|
04FC95722EB09570007BD342 /* KBFunctionBarView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionBarView.m; sourceTree = "<group>"; };
|
||||||
04FC95742EB095DE007BD342 /* KBFunctionPasteView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionPasteView.h; sourceTree = "<group>"; };
|
04FC95742EB095DE007BD342 /* KBFunctionPasteView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionPasteView.h; sourceTree = "<group>"; };
|
||||||
@@ -600,6 +607,10 @@
|
|||||||
04FEDC312F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardSubscriptionFeatureItemView.m; sourceTree = "<group>"; };
|
04FEDC312F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardSubscriptionFeatureItemView.m; sourceTree = "<group>"; };
|
||||||
04FEDC402F00040000999999 /* KBKeyboardSubscriptionFeatureMarqueeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardSubscriptionFeatureMarqueeView.h; sourceTree = "<group>"; };
|
04FEDC402F00040000999999 /* KBKeyboardSubscriptionFeatureMarqueeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardSubscriptionFeatureMarqueeView.h; sourceTree = "<group>"; };
|
||||||
04FEDC412F00040000999999 /* KBKeyboardSubscriptionFeatureMarqueeView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardSubscriptionFeatureMarqueeView.m; sourceTree = "<group>"; };
|
04FEDC412F00040000999999 /* KBKeyboardSubscriptionFeatureMarqueeView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardSubscriptionFeatureMarqueeView.m; sourceTree = "<group>"; };
|
||||||
|
05A1B2C42F5B1A2B3C4D5E60 /* KBSearchVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSearchVM.h; sourceTree = "<group>"; };
|
||||||
|
05A1B2C52F5B1A2B3C4D5E60 /* KBSearchVM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSearchVM.m; sourceTree = "<group>"; };
|
||||||
|
05A1B2C62F5B1A2B3C4D5E60 /* KBSearchThemeModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSearchThemeModel.h; sourceTree = "<group>"; };
|
||||||
|
05A1B2C72F5B1A2B3C4D5E60 /* KBSearchThemeModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSearchThemeModel.m; sourceTree = "<group>"; };
|
||||||
180D662EC4DB3A7FFF83FF18 /* KBMyTheme.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyTheme.m; sourceTree = "<group>"; };
|
180D662EC4DB3A7FFF83FF18 /* KBMyTheme.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyTheme.m; sourceTree = "<group>"; };
|
||||||
2C1092FB2B452F95B15D4263 /* Pods_CustomKeyboard.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CustomKeyboard.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
2C1092FB2B452F95B15D4263 /* Pods_CustomKeyboard.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CustomKeyboard.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
35E2B1C590E060D912A4E7F4 /* KBShopThemeTagModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBShopThemeTagModel.h; sourceTree = "<group>"; };
|
35E2B1C590E060D912A4E7F4 /* KBShopThemeTagModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBShopThemeTagModel.h; sourceTree = "<group>"; };
|
||||||
@@ -614,18 +625,35 @@
|
|||||||
A1B2C3D32EB0A0A100000001 /* KBFunctionTagCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionTagCell.m; sourceTree = "<group>"; };
|
A1B2C3D32EB0A0A100000001 /* KBFunctionTagCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionTagCell.m; sourceTree = "<group>"; };
|
||||||
A1B2C3E02EB0C0A100000001 /* KBNetworkManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBNetworkManager.h; sourceTree = "<group>"; };
|
A1B2C3E02EB0C0A100000001 /* KBNetworkManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBNetworkManager.h; sourceTree = "<group>"; };
|
||||||
A1B2C3E12EB0C0A100000001 /* KBNetworkManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBNetworkManager.m; sourceTree = "<group>"; };
|
A1B2C3E12EB0C0A100000001 /* KBNetworkManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBNetworkManager.m; sourceTree = "<group>"; };
|
||||||
|
A1B2C3E62F20000000000001 /* KBSuggestionEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSuggestionEngine.h; sourceTree = "<group>"; };
|
||||||
|
A1B2C3E72F20000000000001 /* KBSuggestionEngine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSuggestionEngine.m; sourceTree = "<group>"; };
|
||||||
|
A1B2C3E82F20000000000001 /* KBSuggestionBarView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSuggestionBarView.h; sourceTree = "<group>"; };
|
||||||
|
A1B2C3E92F20000000000001 /* KBSuggestionBarView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSuggestionBarView.m; sourceTree = "<group>"; };
|
||||||
|
A1B2C3EC2F20000000000001 /* kb_words.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = kb_words.txt; sourceTree = "<group>"; };
|
||||||
A1B2C3F12EB35A9900000001 /* KBFullAccessGuideView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFullAccessGuideView.h; sourceTree = "<group>"; };
|
A1B2C3F12EB35A9900000001 /* KBFullAccessGuideView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFullAccessGuideView.h; sourceTree = "<group>"; };
|
||||||
A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFullAccessGuideView.m; sourceTree = "<group>"; };
|
A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFullAccessGuideView.m; sourceTree = "<group>"; };
|
||||||
A1B2C4002EB4A0A100000001 /* KBAuthManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAuthManager.h; sourceTree = "<group>"; };
|
A1B2C4002EB4A0A100000001 /* KBAuthManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAuthManager.h; sourceTree = "<group>"; };
|
||||||
A1B2C4002EB4A0A100000002 /* KBAuthManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAuthManager.m; sourceTree = "<group>"; };
|
A1B2C4002EB4A0A100000002 /* KBAuthManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAuthManager.m; sourceTree = "<group>"; };
|
||||||
A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardPermissionManager.m; sourceTree = "<group>"; };
|
A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardPermissionManager.m; sourceTree = "<group>"; };
|
||||||
A1B2C4232EB4B7A100000001 /* KBKeyboardPermissionManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardPermissionManager.h; sourceTree = "<group>"; };
|
A1B2C4232EB4B7A100000001 /* KBKeyboardPermissionManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardPermissionManager.h; sourceTree = "<group>"; };
|
||||||
|
A1B2C9012FBD000100000001 /* KBBackspaceLongPressHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBBackspaceLongPressHandler.h; 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>"; };
|
||||||
|
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>"; };
|
||||||
A1B2E0022EBC7AAA00000001 /* KBTopThreeView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBTopThreeView.m; sourceTree = "<group>"; };
|
A1B2E0022EBC7AAA00000001 /* KBTopThreeView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBTopThreeView.m; sourceTree = "<group>"; };
|
||||||
A1B2E0032EBC7AAA00000001 /* HomeHotCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeHotCell.h; sourceTree = "<group>"; };
|
A1B2E0032EBC7AAA00000001 /* HomeHotCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeHotCell.h; sourceTree = "<group>"; };
|
||||||
A1B2E0042EBC7AAA00000001 /* HomeHotCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HomeHotCell.m; sourceTree = "<group>"; };
|
A1B2E0042EBC7AAA00000001 /* HomeHotCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HomeHotCell.m; sourceTree = "<group>"; };
|
||||||
|
A1F0C1A02F1234567890ABCD /* KBConsumptionRecord.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBConsumptionRecord.h; sourceTree = "<group>"; };
|
||||||
|
A1F0C1A12F1234567890ABCD /* KBConsumptionRecord.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBConsumptionRecord.m; sourceTree = "<group>"; };
|
||||||
|
A1F0C1A22F1234567890ABCD /* KBConsumptionRecordCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBConsumptionRecordCell.h; sourceTree = "<group>"; };
|
||||||
|
A1F0C1A32F1234567890ABCD /* KBConsumptionRecordCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBConsumptionRecordCell.m; sourceTree = "<group>"; };
|
||||||
|
A1F0C1A42F1234567890ABCD /* KBConsumptionRecordVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBConsumptionRecordVC.h; sourceTree = "<group>"; };
|
||||||
|
A1F0C1A52F1234567890ABCD /* KBConsumptionRecordVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBConsumptionRecordVC.m; sourceTree = "<group>"; };
|
||||||
B12EC429812407B9F0E67565 /* Pods-CustomKeyboard.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CustomKeyboard.release.xcconfig"; path = "Target Support Files/Pods-CustomKeyboard/Pods-CustomKeyboard.release.xcconfig"; sourceTree = "<group>"; };
|
B12EC429812407B9F0E67565 /* Pods-CustomKeyboard.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CustomKeyboard.release.xcconfig"; path = "Target Support Files/Pods-CustomKeyboard/Pods-CustomKeyboard.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
B8CA018AB878499327504AAD /* Pods-CustomKeyboard.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CustomKeyboard.debug.xcconfig"; path = "Target Support Files/Pods-CustomKeyboard/Pods-CustomKeyboard.debug.xcconfig"; sourceTree = "<group>"; };
|
B8CA018AB878499327504AAD /* Pods-CustomKeyboard.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CustomKeyboard.debug.xcconfig"; path = "Target Support Files/Pods-CustomKeyboard/Pods-CustomKeyboard.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeDetailModel.m; sourceTree = "<group>"; };
|
B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeDetailModel.m; sourceTree = "<group>"; };
|
||||||
@@ -657,6 +685,7 @@
|
|||||||
041007D02ECE010100D203BB /* Resource */ = {
|
041007D02ECE010100D203BB /* Resource */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A1B2C3EC2F20000000000001 /* kb_words.txt */,
|
||||||
0498BDF42EEC50EE006CC1D5 /* emoji_categories.json */,
|
0498BDF42EEC50EE006CC1D5 /* emoji_categories.json */,
|
||||||
041007D12ECE012000D203BB /* KBSkinIconMap.strings */,
|
041007D12ECE012000D203BB /* KBSkinIconMap.strings */,
|
||||||
041007D32ECE012500D203BB /* 002.zip */,
|
041007D32ECE012500D203BB /* 002.zip */,
|
||||||
@@ -815,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>";
|
||||||
@@ -942,15 +973,15 @@
|
|||||||
path = Common;
|
path = Common;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
048908BD2EBE329D00FABA60 /* M */ = {
|
048908BD2EBE329D00FABA60 /* M */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
05A1B2C62F5B1A2B3C4D5E60 /* KBSearchThemeModel.h */,
|
05A1B2C62F5B1A2B3C4D5E60 /* KBSearchThemeModel.h */,
|
||||||
05A1B2C72F5B1A2B3C4D5E60 /* KBSearchThemeModel.m */,
|
05A1B2C72F5B1A2B3C4D5E60 /* KBSearchThemeModel.m */,
|
||||||
);
|
);
|
||||||
path = M;
|
path = M;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
048908BE2EBE329D00FABA60 /* V */ = {
|
048908BE2EBE329D00FABA60 /* V */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1086,6 +1117,8 @@
|
|||||||
children = (
|
children = (
|
||||||
04A9FE102EB4D0D20020DB6D /* KBFullAccessManager.h */,
|
04A9FE102EB4D0D20020DB6D /* KBFullAccessManager.h */,
|
||||||
04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */,
|
04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */,
|
||||||
|
A1B2C3E62F20000000000001 /* KBSuggestionEngine.h */,
|
||||||
|
A1B2C3E72F20000000000001 /* KBSuggestionEngine.m */,
|
||||||
04FEDA9F2EEDB00100123456 /* KBEmojiDataProvider.h */,
|
04FEDA9F2EEDB00100123456 /* KBEmojiDataProvider.h */,
|
||||||
04FEDAA02EEDB00100123456 /* KBEmojiDataProvider.m */,
|
04FEDAA02EEDB00100123456 /* KBEmojiDataProvider.m */,
|
||||||
);
|
);
|
||||||
@@ -1154,6 +1187,8 @@
|
|||||||
046131132ECF454500A6FADF /* KBKeyPreviewView.m */,
|
046131132ECF454500A6FADF /* KBKeyPreviewView.m */,
|
||||||
04FC95772EB09BC8007BD342 /* KBKeyBoardMainView.h */,
|
04FC95772EB09BC8007BD342 /* KBKeyBoardMainView.h */,
|
||||||
04FC95782EB09BC8007BD342 /* KBKeyBoardMainView.m */,
|
04FC95782EB09BC8007BD342 /* KBKeyBoardMainView.m */,
|
||||||
|
A1B2C3E82F20000000000001 /* KBSuggestionBarView.h */,
|
||||||
|
A1B2C3E92F20000000000001 /* KBSuggestionBarView.m */,
|
||||||
04FC956E2EB09516007BD342 /* KBFunctionView.h */,
|
04FC956E2EB09516007BD342 /* KBFunctionView.h */,
|
||||||
04FC956F2EB09516007BD342 /* KBFunctionView.m */,
|
04FC956F2EB09516007BD342 /* KBFunctionView.m */,
|
||||||
04FC95712EB09570007BD342 /* KBFunctionBarView.h */,
|
04FC95712EB09570007BD342 /* KBFunctionBarView.h */,
|
||||||
@@ -1197,15 +1232,15 @@
|
|||||||
path = Buy;
|
path = Buy;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
04DC5CED2EF2D2C400F1AC80 /* VM */ = {
|
04DC5CED2EF2D2C400F1AC80 /* VM */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
05A1B2C42F5B1A2B3C4D5E60 /* KBSearchVM.h */,
|
05A1B2C42F5B1A2B3C4D5E60 /* KBSearchVM.h */,
|
||||||
05A1B2C52F5B1A2B3C4D5E60 /* KBSearchVM.m */,
|
05A1B2C52F5B1A2B3C4D5E60 /* KBSearchVM.m */,
|
||||||
);
|
);
|
||||||
path = VM;
|
path = VM;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
04FC95662EB0546C007BD342 /* Model */ = {
|
04FC95662EB0546C007BD342 /* Model */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1320,8 +1355,12 @@
|
|||||||
0498BD8A2EE69E15006CC1D5 /* KBTagItemModel.m */,
|
0498BD8A2EE69E15006CC1D5 /* KBTagItemModel.m */,
|
||||||
0498BD8D2EE6A3BD006CC1D5 /* KBMyMainModel.h */,
|
0498BD8D2EE6A3BD006CC1D5 /* KBMyMainModel.h */,
|
||||||
0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */,
|
0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */,
|
||||||
|
A1F0C1C02FABCDEF12345678 /* KBInviteCodeModel.h */,
|
||||||
|
A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */,
|
||||||
7ECBD0E320F971D0FBEDD7BC /* KBMyTheme.h */,
|
7ECBD0E320F971D0FBEDD7BC /* KBMyTheme.h */,
|
||||||
180D662EC4DB3A7FFF83FF18 /* KBMyTheme.m */,
|
180D662EC4DB3A7FFF83FF18 /* KBMyTheme.m */,
|
||||||
|
A1F0C1A02F1234567890ABCD /* KBConsumptionRecord.h */,
|
||||||
|
A1F0C1A12F1234567890ABCD /* KBConsumptionRecord.m */,
|
||||||
);
|
);
|
||||||
path = M;
|
path = M;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1333,6 +1372,8 @@
|
|||||||
049FB31C2EC21BCD00FAB05D /* KBMyKeyboardCell.m */,
|
049FB31C2EC21BCD00FAB05D /* KBMyKeyboardCell.m */,
|
||||||
048908E12EBF760000FABA60 /* MySkinCell.h */,
|
048908E12EBF760000FABA60 /* MySkinCell.h */,
|
||||||
048908E22EBF760000FABA60 /* MySkinCell.m */,
|
048908E22EBF760000FABA60 /* MySkinCell.m */,
|
||||||
|
A1F0C1A22F1234567890ABCD /* KBConsumptionRecordCell.h */,
|
||||||
|
A1F0C1A32F1234567890ABCD /* KBConsumptionRecordCell.m */,
|
||||||
048908E42EBF841B00FABA60 /* KBSkinDetailTagCell.h */,
|
048908E42EBF841B00FABA60 /* KBSkinDetailTagCell.h */,
|
||||||
048908E52EBF841B00FABA60 /* KBSkinDetailTagCell.m */,
|
048908E52EBF841B00FABA60 /* KBSkinDetailTagCell.m */,
|
||||||
048908E72EBF843000FABA60 /* KBSkinDetailHeaderCell.h */,
|
048908E72EBF843000FABA60 /* KBSkinDetailHeaderCell.h */,
|
||||||
@@ -1366,6 +1407,8 @@
|
|||||||
049FB2192EC20A9E00FAB05D /* KBMyKeyBoardVC.m */,
|
049FB2192EC20A9E00FAB05D /* KBMyKeyBoardVC.m */,
|
||||||
048908DE2EBF73DC00FABA60 /* MySkinVC.h */,
|
048908DE2EBF73DC00FABA60 /* MySkinVC.h */,
|
||||||
048908DF2EBF73DC00FABA60 /* MySkinVC.m */,
|
048908DF2EBF73DC00FABA60 /* MySkinVC.m */,
|
||||||
|
A1F0C1A42F1234567890ABCD /* KBConsumptionRecordVC.h */,
|
||||||
|
A1F0C1A52F1234567890ABCD /* KBConsumptionRecordVC.m */,
|
||||||
049FB2212EC311F900FAB05D /* KBPersonInfoVC.h */,
|
049FB2212EC311F900FAB05D /* KBPersonInfoVC.h */,
|
||||||
049FB2222EC311F900FAB05D /* KBPersonInfoVC.m */,
|
049FB2222EC311F900FAB05D /* KBPersonInfoVC.m */,
|
||||||
04791F902ED48010004E8522 /* KBNoticeVC.h */,
|
04791F902ED48010004E8522 /* KBNoticeVC.h */,
|
||||||
@@ -1612,6 +1655,8 @@
|
|||||||
0498BD842EE1B255006CC1D5 /* KBSignUtils.m */,
|
0498BD842EE1B255006CC1D5 /* KBSignUtils.m */,
|
||||||
047920482EDDCE25004E8522 /* KBUserSessionManager.h */,
|
047920482EDDCE25004E8522 /* KBUserSessionManager.h */,
|
||||||
047920492EDDCE25004E8522 /* KBUserSessionManager.m */,
|
047920492EDDCE25004E8522 /* KBUserSessionManager.m */,
|
||||||
|
A1F0C1D02FACAD0012345678 /* KBMaiPointReporter.h */,
|
||||||
|
A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */,
|
||||||
);
|
);
|
||||||
path = Shared;
|
path = Shared;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1773,6 +1818,7 @@
|
|||||||
04A9FE202EB893F10020DB6D /* Localizable.strings in Resources */,
|
04A9FE202EB893F10020DB6D /* Localizable.strings in Resources */,
|
||||||
041007D42ECE012500D203BB /* 002.zip in Resources */,
|
041007D42ECE012500D203BB /* 002.zip in Resources */,
|
||||||
041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */,
|
041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */,
|
||||||
|
A1B2C3ED2F20000000000001 /* kb_words.txt in Resources */,
|
||||||
04791FFB2ED5EAB8004E8522 /* fense.zip in Resources */,
|
04791FFB2ED5EAB8004E8522 /* fense.zip in Resources */,
|
||||||
0498BDF52EEC50EE006CC1D5 /* emoji_categories.json in Resources */,
|
0498BDF52EEC50EE006CC1D5 /* emoji_categories.json in Resources */,
|
||||||
04791FF72ED5B985004E8522 /* Christmas.zip in Resources */,
|
04791FF72ED5B985004E8522 /* Christmas.zip in Resources */,
|
||||||
@@ -1877,6 +1923,7 @@
|
|||||||
0450AC4A2EF2C3ED00B6AF06 /* KBKeyboardSubscriptionOptionCell.m in Sources */,
|
0450AC4A2EF2C3ED00B6AF06 /* KBKeyboardSubscriptionOptionCell.m in Sources */,
|
||||||
04A9FE0F2EB481100020DB6D /* KBHUD.m in Sources */,
|
04A9FE0F2EB481100020DB6D /* KBHUD.m in Sources */,
|
||||||
04C6EADD2EAF8CEB0089C901 /* KBToolBar.m in Sources */,
|
04C6EADD2EAF8CEB0089C901 /* KBToolBar.m in Sources */,
|
||||||
|
A1B2C3EB2F20000000000001 /* KBSuggestionBarView.m in Sources */,
|
||||||
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */,
|
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */,
|
||||||
04FEDAB32EEDB05000123456 /* KBEmojiPanelView.m in Sources */,
|
04FEDAB32EEDB05000123456 /* KBEmojiPanelView.m in Sources */,
|
||||||
04FEDB032EFE000000123456 /* KBEmojiBottomBarView.m in Sources */,
|
04FEDB032EFE000000123456 /* KBEmojiBottomBarView.m in Sources */,
|
||||||
@@ -1889,6 +1936,7 @@
|
|||||||
04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */,
|
04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */,
|
||||||
A1B2C3D42EB0A0A100000001 /* KBFunctionTagCell.m in Sources */,
|
A1B2C3D42EB0A0A100000001 /* KBFunctionTagCell.m in Sources */,
|
||||||
04A9FE1A2EB892460020DB6D /* KBLocalizationManager.m in Sources */,
|
04A9FE1A2EB892460020DB6D /* KBLocalizationManager.m in Sources */,
|
||||||
|
A1B2C3EA2F20000000000001 /* KBSuggestionEngine.m in Sources */,
|
||||||
A1B2C3E22EB0C0A100000001 /* KBNetworkManager.m in Sources */,
|
A1B2C3E22EB0C0A100000001 /* KBNetworkManager.m in Sources */,
|
||||||
049FB2352EC45C6A00FAB05D /* NetworkStreamHandler.m in Sources */,
|
049FB2352EC45C6A00FAB05D /* NetworkStreamHandler.m in Sources */,
|
||||||
04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */,
|
04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */,
|
||||||
@@ -1904,12 +1952,15 @@
|
|||||||
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 */,
|
||||||
04FC95672EB0546C007BD342 /* KBKey.m in Sources */,
|
04FC95672EB0546C007BD342 /* KBKey.m in Sources */,
|
||||||
A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */,
|
A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */,
|
||||||
0498BD8F2EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */,
|
0498BD8F2EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */,
|
||||||
|
A1F0C1C22FABCDEF12345678 /* KBInviteCodeModel.m in Sources */,
|
||||||
|
A1F0C1D22FACAD0012345678 /* KBMaiPointReporter.m in Sources */,
|
||||||
04FEDC222F00020000999999 /* KBKeyboardSubscriptionProduct.m in Sources */,
|
04FEDC222F00020000999999 /* KBKeyboardSubscriptionProduct.m in Sources */,
|
||||||
0450AA742EF013D000B6AF06 /* KBEmojiCollectionCell.m in Sources */,
|
0450AA742EF013D000B6AF06 /* KBEmojiCollectionCell.m in Sources */,
|
||||||
550CB2630FA4A7B4B9782EFA /* KBMyTheme.m in Sources */,
|
550CB2630FA4A7B4B9782EFA /* KBMyTheme.m in Sources */,
|
||||||
@@ -1973,7 +2024,12 @@
|
|||||||
04A9FE1B2EB892460020DB6D /* KBLocalizationManager.m in Sources */,
|
04A9FE1B2EB892460020DB6D /* KBLocalizationManager.m in Sources */,
|
||||||
048908BC2EBE1FCB00FABA60 /* BaseViewController.m in Sources */,
|
048908BC2EBE1FCB00FABA60 /* BaseViewController.m in Sources */,
|
||||||
0498BD902EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */,
|
0498BD902EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */,
|
||||||
|
A1F0C1C32FABCDEF12345678 /* KBInviteCodeModel.m in Sources */,
|
||||||
|
A1F0C1D32FACAD0012345678 /* KBMaiPointReporter.m in Sources */,
|
||||||
471CAD3574798685B72ADD55 /* KBMyTheme.m in Sources */,
|
471CAD3574798685B72ADD55 /* KBMyTheme.m in Sources */,
|
||||||
|
A1F0C1B12F1234567890ABCD /* KBConsumptionRecord.m in Sources */,
|
||||||
|
A1F0C1B22F1234567890ABCD /* KBConsumptionRecordCell.m in Sources */,
|
||||||
|
A1F0C1B32F1234567890ABCD /* KBConsumptionRecordVC.m in Sources */,
|
||||||
04FC95D72EB1EA16007BD342 /* BaseTableView.m in Sources */,
|
04FC95D72EB1EA16007BD342 /* BaseTableView.m in Sources */,
|
||||||
0498BD712EE02A41006CC1D5 /* KBForgetPwdNewPwdVC.m in Sources */,
|
0498BD712EE02A41006CC1D5 /* KBForgetPwdNewPwdVC.m in Sources */,
|
||||||
048908EF2EBF861800FABA60 /* KBSkinSectionTitleCell.m in Sources */,
|
048908EF2EBF861800FABA60 /* KBSkinSectionTitleCell.m in Sources */,
|
||||||
@@ -1988,13 +2044,13 @@
|
|||||||
0477BDF72EBC63A80055D639 /* KBTestVC.m in Sources */,
|
0477BDF72EBC63A80055D639 /* KBTestVC.m in Sources */,
|
||||||
04122F7E2EC5FC5500EF7AB3 /* KBJfPayCell.m in Sources */,
|
04122F7E2EC5FC5500EF7AB3 /* KBJfPayCell.m in Sources */,
|
||||||
049FB2402EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */,
|
049FB2402EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */,
|
||||||
04FC95C92EB1E4C9007BD342 /* BaseNavigationController.m in Sources */,
|
04FC95C92EB1E4C9007BD342 /* BaseNavigationController.m in Sources */,
|
||||||
048908DD2EBF67EB00FABA60 /* KBSearchResultVC.m in Sources */,
|
048908DD2EBF67EB00FABA60 /* KBSearchResultVC.m in Sources */,
|
||||||
05A1B2D12F5B1A2B3C4D5E60 /* KBSearchVM.m in Sources */,
|
05A1B2D12F5B1A2B3C4D5E60 /* KBSearchVM.m in Sources */,
|
||||||
05A1B2D22F5B1A2B3C4D5E60 /* KBSearchThemeModel.m in Sources */,
|
05A1B2D22F5B1A2B3C4D5E60 /* KBSearchThemeModel.m in Sources */,
|
||||||
047C65102EBCA8DD0035E841 /* HomeRankContentVC.m in Sources */,
|
047C65102EBCA8DD0035E841 /* HomeRankContentVC.m in Sources */,
|
||||||
047C655C2EBCD0F80035E841 /* UIView+KBShadow.m in Sources */,
|
047C655C2EBCD0F80035E841 /* UIView+KBShadow.m in Sources */,
|
||||||
049FB2262EC3136D00FAB05D /* KBPersonInfoItemCell.m in Sources */,
|
049FB2262EC3136D00FAB05D /* KBPersonInfoItemCell.m in Sources */,
|
||||||
048908C32EBE32B800FABA60 /* KBSearchVC.m in Sources */,
|
048908C32EBE32B800FABA60 /* KBSearchVC.m in Sources */,
|
||||||
049FB20B2EC1C13800FAB05D /* KBSkinBottomActionView.m in Sources */,
|
049FB20B2EC1C13800FAB05D /* KBSkinBottomActionView.m in Sources */,
|
||||||
047C655E2EBCD5B20035E841 /* UIImage+KBColor.m in Sources */,
|
047C655E2EBCD5B20035E841 /* UIImage+KBColor.m in Sources */,
|
||||||
|
|||||||
22
keyBoard/Assets.xcassets/My/my_chongzhi_bg.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "my_chongzhi_bg@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "my_chongzhi_bg@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
keyBoard/Assets.xcassets/My/my_chongzhi_bg.imageset/my_chongzhi_bg@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
keyBoard/Assets.xcassets/My/my_chongzhi_bg.imageset/my_chongzhi_bg@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
22
keyBoard/Assets.xcassets/My/my_record_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "my_record_icon@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "my_record_icon@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
keyBoard/Assets.xcassets/My/my_record_icon.imageset/my_record_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
keyBoard/Assets.xcassets/My/my_record_icon.imageset/my_record_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
@@ -5,12 +5,12 @@
|
|||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "placeholder_image_icon@2x.png",
|
"filename" : "切图 232@2x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "placeholder_image_icon@3x.png",
|
"filename" : "切图 232@3x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 121 KiB |
BIN
keyBoard/Assets.xcassets/Ohter/placeholder_image_icon.imageset/切图 232@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
keyBoard/Assets.xcassets/Ohter/placeholder_image_icon.imageset/切图 232@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 82 KiB |
22
keyBoard/Assets.xcassets/Shop/shop_goumai_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "shop_goumai_icon@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "shop_goumai_icon@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
keyBoard/Assets.xcassets/Shop/shop_goumai_icon.imageset/shop_goumai_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
keyBoard/Assets.xcassets/Shop/shop_goumai_icon.imageset/shop_goumai_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
@@ -37,6 +37,10 @@
|
|||||||
[self kb_updateBackButtonVisibility];
|
[self kb_updateBackButtonVisibility];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)dealloc{
|
||||||
|
KBLOG(@"页面销毁 -- 💥💥 -- %@",[self class]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Custom NavBar
|
#pragma mark - Custom NavBar
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
|
|
||||||
@interface UIColor (Extension)
|
@interface UIColor (Extension)
|
||||||
+ (UIColor *)colorWithHex:(int)hexValue;
|
+ (UIColor *)colorWithHex:(int)hexValue;
|
||||||
|
+ (nullable UIColor *)colorWithHexString:(NSString *)hexString;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -20,4 +20,25 @@
|
|||||||
{
|
{
|
||||||
return [UIColor colorWithHex:hexValue alpha:1.0];
|
return [UIColor colorWithHex:hexValue alpha:1.0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ (UIColor *)colorWithHexString:(NSString *)hexString
|
||||||
|
{
|
||||||
|
if (hexString.length == 0) { return nil; }
|
||||||
|
NSString *clean = [[hexString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] uppercaseString];
|
||||||
|
if ([clean hasPrefix:@"#"]) {
|
||||||
|
clean = [clean substringFromIndex:1];
|
||||||
|
} else if ([clean hasPrefix:@"0X"]) {
|
||||||
|
clean = [clean substringFromIndex:2];
|
||||||
|
}
|
||||||
|
if (clean.length != 6 && clean.length != 8) { return nil; }
|
||||||
|
unsigned long long value = 0;
|
||||||
|
NSScanner *scanner = [NSScanner scannerWithString:clean];
|
||||||
|
if (![scanner scanHexLongLong:&value]) { return nil; }
|
||||||
|
if (clean.length == 6) {
|
||||||
|
return [UIColor colorWithHex:(int)value];
|
||||||
|
}
|
||||||
|
CGFloat alpha = ((value & 0xFF000000) >> 24) / 255.0;
|
||||||
|
int rgb = (int)(value & 0xFFFFFF);
|
||||||
|
return [UIColor colorWithHex:rgb alpha:alpha];
|
||||||
|
}
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ static inline SDWebImageOptions KBWebImageDefaultOptions(void) {
|
|||||||
SDWebImageProgressiveLoad; // 渐进式
|
SDWebImageProgressiveLoad; // 渐进式
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline void KBWebImageEnsureIndicator(UIImageView *imageView) {
|
||||||
|
if (!imageView.sd_imageIndicator) {
|
||||||
|
imageView.sd_imageIndicator = SDWebImageActivityIndicator.mediumIndicator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static inline NSURL *_KBURL(id url) {
|
static inline NSURL *_KBURL(id url) {
|
||||||
if (!url) return nil;
|
if (!url) return nil;
|
||||||
if ([url isKindOfClass:NSURL.class]) return url;
|
if ([url isKindOfClass:NSURL.class]) return url;
|
||||||
@@ -32,6 +38,7 @@ static inline NSURL *_KBURL(id url) {
|
|||||||
NSURL *u = _KBURL(url);
|
NSURL *u = _KBURL(url);
|
||||||
// 默认渐隐动画
|
// 默认渐隐动画
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
|
KBWebImageEnsureIndicator(self);
|
||||||
[self sd_setImageWithURL:u
|
[self sd_setImageWithURL:u
|
||||||
placeholderImage:placeholder
|
placeholderImage:placeholder
|
||||||
options:options
|
options:options
|
||||||
@@ -66,6 +73,7 @@ static inline NSURL *_KBURL(id url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
|
KBWebImageEnsureIndicator(self);
|
||||||
[self sd_setImageWithURL:u
|
[self sd_setImageWithURL:u
|
||||||
placeholderImage:placeholder
|
placeholderImage:placeholder
|
||||||
options:options
|
options:options
|
||||||
@@ -106,6 +114,7 @@ static inline NSURL *_KBURL(id url) {
|
|||||||
NSDictionary *context = @{ SDWebImageContextImageTransformer : pipeline };
|
NSDictionary *context = @{ SDWebImageContextImageTransformer : pipeline };
|
||||||
|
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
|
KBWebImageEnsureIndicator(self);
|
||||||
[self sd_setImageWithURL:u
|
[self sd_setImageWithURL:u
|
||||||
placeholderImage:placeholder
|
placeholderImage:placeholder
|
||||||
options:options
|
options:options
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ static __weak UIView *sContainerView = nil; // 缺省承载视图(
|
|||||||
if (!hostView) { return nil; }
|
if (!hostView) { return nil; }
|
||||||
|
|
||||||
MBProgressHUD *hud = sHUD;
|
MBProgressHUD *hud = sHUD;
|
||||||
|
if (hud && hud.superview != hostView) {
|
||||||
|
// Host view changed or HUD was removed; discard and recreate.
|
||||||
|
[hud removeFromSuperview];
|
||||||
|
hud = nil;
|
||||||
|
sHUD = nil;
|
||||||
|
}
|
||||||
if (!hud) {
|
if (!hud) {
|
||||||
hud = [MBProgressHUD showHUDAddedTo:hostView animated:YES];
|
hud = [MBProgressHUD showHUDAddedTo:hostView animated:YES];
|
||||||
sHUD = hud;
|
sHUD = hud;
|
||||||
@@ -70,6 +76,9 @@ static __weak UIView *sContainerView = nil; // 缺省承载视图(
|
|||||||
hud.margin = 16;
|
hud.margin = 16;
|
||||||
hud.label.numberOfLines = 0;
|
hud.label.numberOfLines = 0;
|
||||||
hud.detailsLabel.numberOfLines = 0;
|
hud.detailsLabel.numberOfLines = 0;
|
||||||
|
} else {
|
||||||
|
// If a previous hide is in progress, bring it back.
|
||||||
|
[hud showAnimated:YES];
|
||||||
}
|
}
|
||||||
// 遮罩与交互(按本次 show 的入参应用)
|
// 遮罩与交互(按本次 show 的入参应用)
|
||||||
[self applyMaskType:mask hud:hud];
|
[self applyMaskType:mask hud:hud];
|
||||||
|
|||||||
@@ -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,6 +48,11 @@
|
|||||||
|
|
||||||
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];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,11 +226,17 @@
|
|||||||
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;
|
||||||
[self.loginVM emailLoginEmail:email password:password WithCompletion:^(BOOL success, NSError * _Nullable error) {
|
[self.loginVM emailLoginEmail:email password:password WithCompletion:^(BOOL success, NSError * _Nullable error) {
|
||||||
if (success) {
|
if (success) {
|
||||||
|
[weakSelf report:email];
|
||||||
[[NSUserDefaults standardUserDefaults] setValue:email forKey:KBUserEmailKey];
|
[[NSUserDefaults standardUserDefaults] setValue:email forKey:KBUserEmailKey];
|
||||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||||
id<UIApplicationDelegate> appDelegate = UIApplication.sharedApplication.delegate;
|
id<UIApplicationDelegate> appDelegate = UIApplication.sharedApplication.delegate;
|
||||||
@@ -248,6 +254,11 @@
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
- (void)report:(NSString *)account{
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportNewAccountWithType:@"登录用户" account:account completion:^(BOOL success, NSError * _Nullable error) {
|
||||||
|
KBLOG(@"====");
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)onTapForgotPassword {
|
- (void)onTapForgotPassword {
|
||||||
KBLOG(@"KBEmailLoginVC onTapForgotPassword");
|
KBLOG(@"KBEmailLoginVC onTapForgotPassword");
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -114,6 +114,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_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")];
|
||||||
return;
|
return;
|
||||||
@@ -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;
|
||||||
@@ -100,6 +105,7 @@
|
|||||||
self.params[@"gender"] = genderNumber;
|
self.params[@"gender"] = genderNumber;
|
||||||
}
|
}
|
||||||
NSString *email = self.params[@"mailAddress"] ? self.params[@"mailAddress"] : @"";
|
NSString *email = self.params[@"mailAddress"] ? self.params[@"mailAddress"] : @"";
|
||||||
|
KBWeakSelf
|
||||||
[self.loginVM emailRegisterParams:self.params withCompletion:^(BOOL success, NSError * _Nullable error) {
|
[self.loginVM emailRegisterParams:self.params withCompletion:^(BOOL success, NSError * _Nullable error) {
|
||||||
if (success) {
|
if (success) {
|
||||||
[KBHUD showInfo:KBLocalized(@"Signed in successfully")];
|
[KBHUD showInfo:KBLocalized(@"Signed in successfully")];
|
||||||
@@ -107,6 +113,7 @@
|
|||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[[NSUserDefaults standardUserDefaults] setValue:email forKey:KBUserEmailKey];
|
[[NSUserDefaults standardUserDefaults] setValue:email forKey:KBUserEmailKey];
|
||||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||||
|
[weakSelf report:email];
|
||||||
KBEmailLoginVC *vc = [[KBEmailLoginVC alloc] init];
|
KBEmailLoginVC *vc = [[KBEmailLoginVC alloc] init];
|
||||||
[KB_CURRENT_NAV pushViewController:vc animated:true];
|
[KB_CURRENT_NAV pushViewController:vc animated:true];
|
||||||
// id<UIApplicationDelegate> appDelegate = UIApplication.sharedApplication.delegate;
|
// id<UIApplicationDelegate> appDelegate = UIApplication.sharedApplication.delegate;
|
||||||
@@ -129,6 +136,12 @@
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)report:(NSString *)account{
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportNewAccountWithType:@"注册账号" account:account completion:^(BOOL success, NSError * _Nullable error) {
|
||||||
|
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
- (nullable NSNumber *)kb_localGenderParamIfAvailable {
|
- (nullable NSNumber *)kb_localGenderParamIfAvailable {
|
||||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||||||
// 只有在用户真正看过性别选择页后,才认为本地的性别有效
|
// 只有在用户真正看过性别选择页后,才认为本地的性别有效
|
||||||
|
|||||||
@@ -106,7 +106,16 @@ static NSString * const kKBAppleUserIdentifierKey = @"com.company.keyboard.apple
|
|||||||
|
|
||||||
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)) {
|
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)) {
|
||||||
if (self.completion) {
|
if (self.completion) {
|
||||||
self.completion(nil, error);
|
NSError *finalError = error;
|
||||||
|
if (@available(iOS 13.0, *)) {
|
||||||
|
if ([error.domain isEqualToString:ASAuthorizationErrorDomain] && error.code == ASAuthorizationErrorCanceled) {
|
||||||
|
finalError = [NSError errorWithDomain:error.domain
|
||||||
|
code:error.code
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Sign-in canceled")
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// self.completion(nil, finalError);
|
||||||
}
|
}
|
||||||
self.completion = nil;
|
self.completion = nil;
|
||||||
self.presentingVC = nil;
|
self.presentingVC = nil;
|
||||||
|
|||||||
@@ -42,6 +42,13 @@
|
|||||||
if (completion) completion(NO);
|
if (completion) completion(NO);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
BOOL forceDownload = NO;
|
||||||
|
id forceValue = skinJSON[@"force_download"];
|
||||||
|
if ([forceValue respondsToSelector:@selector(boolValue)]) {
|
||||||
|
forceDownload = [forceValue boolValue];
|
||||||
|
}
|
||||||
|
NSLog(@"🧩[SkinService] apply mode=%lu id=%@ force=%d",
|
||||||
|
(unsigned long)mode, skinJSON[@"id"], forceDownload);
|
||||||
|
|
||||||
// // 1. 点击应用皮肤时,检查键盘启用 & 完全访问状态,并尽量给出友好提示。
|
// // 1. 点击应用皮肤时,检查键盘启用 & 完全访问状态,并尽量给出友好提示。
|
||||||
// KBKeyboardPermissionManager *perm = [KBKeyboardPermissionManager shared];
|
// KBKeyboardPermissionManager *perm = [KBKeyboardPermissionManager shared];
|
||||||
@@ -282,7 +289,7 @@
|
|||||||
zipURL:zipName
|
zipURL:zipName
|
||||||
themeJSON:themeJSON];
|
themeJSON:themeJSON];
|
||||||
}
|
}
|
||||||
[KBHUD showInfo:(ok ? KBLocalized(@"已应用,切到键盘查看") : KBLocalized(@"应用皮肤失败"))];
|
[KBHUD showInfo:(ok ? KBLocalized(@"Applied. Switch to the keyboard to view.") : KBLocalized(@"Application to the skin failed"))];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
25
keyBoard/Class/Me/M/KBConsumptionRecord.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// KBConsumptionRecord.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// 消费/购买记录模型 1 消费 2充值
|
||||||
|
typedef NS_ENUM(NSInteger, KBConsumptionRecordType) {
|
||||||
|
KBConsumptionRecordTypeConsumption = 1,
|
||||||
|
KBConsumptionRecordTypeRecharge = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
@interface KBConsumptionRecord : NSObject
|
||||||
|
@property (nonatomic, strong, nullable) NSNumber *amount;
|
||||||
|
@property (nonatomic, assign) KBConsumptionRecordType type;
|
||||||
|
@property (nonatomic, strong, nullable) NSNumber *beforeBalance;
|
||||||
|
@property (nonatomic, strong, nullable) NSNumber *afterBalance;
|
||||||
|
@property (nonatomic, copy, nullable) NSString *kbdescription;
|
||||||
|
@property (nonatomic, copy, nullable) NSString *createdAt;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
16
keyBoard/Class/Me/M/KBConsumptionRecord.m
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// KBConsumptionRecord.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBConsumptionRecord.h"
|
||||||
|
|
||||||
|
@implementation KBConsumptionRecord
|
||||||
|
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
|
||||||
|
// JSON: { "id": 0, "tagName": "xxx" }
|
||||||
|
// Model: tagId / tagName
|
||||||
|
return @{
|
||||||
|
@"kbdescription" : @"description",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@end
|
||||||
20
keyBoard/Class/Me/M/KBInviteCodeModel.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// KBInviteCodeModel.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// 邀请码信息
|
||||||
|
@interface KBInviteCodeModel : NSObject
|
||||||
|
@property (nonatomic, copy, nullable) NSString *inviteCode;
|
||||||
|
@property (nonatomic, copy, nullable) NSString *h5Link;
|
||||||
|
@property (nonatomic, assign) NSInteger status;
|
||||||
|
@property (nonatomic, assign) NSInteger usedCount;
|
||||||
|
@property (nonatomic, strong, nullable) NSNumber *maxUses;
|
||||||
|
@property (nonatomic, copy, nullable) NSString *expiresAt;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
17
keyBoard/Class/Me/M/KBInviteCodeModel.m
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// KBInviteCodeModel.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBInviteCodeModel.h"
|
||||||
|
#import <MJExtension/MJExtension.h>
|
||||||
|
|
||||||
|
@implementation KBInviteCodeModel
|
||||||
|
|
||||||
|
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
|
||||||
|
return @{
|
||||||
|
@"inviteCode": @"code"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
16
keyBoard/Class/Me/V/KBConsumptionRecordCell.h
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// KBConsumptionRecordCell.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "BaseCell.h"
|
||||||
|
@class KBConsumptionRecord;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface KBConsumptionRecordCell : BaseCell
|
||||||
|
+ (NSString *)reuseId;
|
||||||
|
- (void)configWithRecord:(KBConsumptionRecord *)record;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
123
keyBoard/Class/Me/V/KBConsumptionRecordCell.m
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
//
|
||||||
|
// KBConsumptionRecordCell.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBConsumptionRecordCell.h"
|
||||||
|
#import "KBConsumptionRecord.h"
|
||||||
|
#import <Masonry/Masonry.h>
|
||||||
|
#import "UIColor+Extension.h"
|
||||||
|
|
||||||
|
@interface KBConsumptionRecordCell ()
|
||||||
|
@property (nonatomic, strong) UILabel *titleLabel;
|
||||||
|
@property (nonatomic, strong) UILabel *timeLabel;
|
||||||
|
@property (nonatomic, strong) UILabel *amountLabel;
|
||||||
|
@property (nonatomic, strong) UIView *lineView;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBConsumptionRecordCell
|
||||||
|
|
||||||
|
- (void)setupUI {
|
||||||
|
self.contentView.backgroundColor = [UIColor whiteColor];
|
||||||
|
[self.contentView addSubview:self.titleLabel];
|
||||||
|
[self.contentView addSubview:self.timeLabel];
|
||||||
|
[self.contentView addSubview:self.amountLabel];
|
||||||
|
[self.contentView addSubview:self.lineView];
|
||||||
|
|
||||||
|
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(self.contentView).offset(16);
|
||||||
|
make.top.equalTo(self.contentView).offset(14);
|
||||||
|
make.right.lessThanOrEqualTo(self.amountLabel.mas_left).offset(-12);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(self.titleLabel);
|
||||||
|
make.top.equalTo(self.titleLabel.mas_bottom).offset(6);
|
||||||
|
make.bottom.equalTo(self.contentView).offset(-14);
|
||||||
|
make.right.lessThanOrEqualTo(self.amountLabel.mas_left).offset(-12);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.amountLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.right.equalTo(self.contentView).offset(-16);
|
||||||
|
make.centerY.equalTo(self.titleLabel);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.lineView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(self.contentView).offset(16);
|
||||||
|
make.right.equalTo(self.contentView).offset(-16);
|
||||||
|
make.bottom.equalTo(self.contentView);
|
||||||
|
make.height.mas_equalTo(KB_ONE_PIXEL);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)configWithRecord:(KBConsumptionRecord *)record {
|
||||||
|
NSString *title = record.kbdescription.length ? record.kbdescription : KBLocalized(@"Consumption");
|
||||||
|
self.titleLabel.text = title;
|
||||||
|
|
||||||
|
self.timeLabel.text = record.createdAt.length ? record.createdAt : @"--";
|
||||||
|
|
||||||
|
NSString *displayAmount = [self kb_formatAmountText:record.amount];
|
||||||
|
self.amountLabel.text = displayAmount;
|
||||||
|
|
||||||
|
UIColor *incomeColor = [UIColor colorWithHex:0x66CD7C];
|
||||||
|
UIColor *expenseColor = [UIColor colorWithHex:0xCD2853];
|
||||||
|
self.amountLabel.textColor = (record.type == KBConsumptionRecordTypeRecharge) ? incomeColor : expenseColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_formatAmountText:(NSNumber *)amount {
|
||||||
|
if (![amount isKindOfClass:NSNumber.class]) { return @"--"; }
|
||||||
|
static NSNumberFormatter *formatter;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
formatter = [[NSNumberFormatter alloc] init];
|
||||||
|
formatter.numberStyle = NSNumberFormatterDecimalStyle;
|
||||||
|
formatter.minimumFractionDigits = 2;
|
||||||
|
formatter.maximumFractionDigits = 2;
|
||||||
|
formatter.minimumIntegerDigits = 1;
|
||||||
|
formatter.positivePrefix = @"+";
|
||||||
|
});
|
||||||
|
return [formatter stringFromNumber:amount] ?: @"--";
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Lazy
|
||||||
|
|
||||||
|
- (UILabel *)titleLabel {
|
||||||
|
if (!_titleLabel) {
|
||||||
|
_titleLabel = [UILabel new];
|
||||||
|
_titleLabel.font = [KBFont medium:15];
|
||||||
|
_titleLabel.textColor = [UIColor colorWithHex:KBBlackValue];
|
||||||
|
_titleLabel.text = KBLocalized(@"Consumption");
|
||||||
|
}
|
||||||
|
return _titleLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UILabel *)timeLabel {
|
||||||
|
if (!_timeLabel) {
|
||||||
|
_timeLabel = [UILabel new];
|
||||||
|
_timeLabel.font = [KBFont regular:12];
|
||||||
|
_timeLabel.textColor = [UIColor colorWithHex:0x9FA5B5];
|
||||||
|
_timeLabel.text = @"--";
|
||||||
|
}
|
||||||
|
return _timeLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UILabel *)amountLabel {
|
||||||
|
if (!_amountLabel) {
|
||||||
|
_amountLabel = [UILabel new];
|
||||||
|
_amountLabel.font = [KBFont medium:16];
|
||||||
|
_amountLabel.textColor = [UIColor colorWithHex:0xE36464];
|
||||||
|
_amountLabel.textAlignment = NSTextAlignmentRight;
|
||||||
|
_amountLabel.text = @"--";
|
||||||
|
}
|
||||||
|
return _amountLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIView *)lineView {
|
||||||
|
if (!_lineView) {
|
||||||
|
_lineView = [UIView new];
|
||||||
|
_lineView.backgroundColor = [UIColor colorWithHex:0xEFEFF1];
|
||||||
|
}
|
||||||
|
return _lineView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
@property (nonatomic, strong) UIImageView *cardRight;
|
@property (nonatomic, strong) UIImageView *cardRight;
|
||||||
@property (nonatomic, strong) UIImageView *avatarEditIcon; // 头像右下角的编辑图标
|
@property (nonatomic, strong) UIImageView *avatarEditIcon; // 头像右下角的编辑图标
|
||||||
@property (nonatomic, strong) KBUser *userModel;
|
@property (nonatomic, strong) KBUser *userModel;
|
||||||
|
@property (nonatomic, assign) NSInteger kb_vipLayoutState; // -1: unknown, 0: non-vip, 1: vip
|
||||||
//@property (nonatomic, strong) MASConstraint *vipIconWidthConstraint;
|
//@property (nonatomic, strong) MASConstraint *vipIconWidthConstraint;
|
||||||
//@property (nonatomic, strong) MASConstraint *nameRightToVipConstraint;
|
//@property (nonatomic, strong) MASConstraint *nameRightToVipConstraint;
|
||||||
//@property (nonatomic, strong) MASConstraint *nameRightToSuperviewConstraint;
|
//@property (nonatomic, strong) MASConstraint *nameRightToSuperviewConstraint;
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
|
|
||||||
- (instancetype)initWithFrame:(CGRect)frame {
|
- (instancetype)initWithFrame:(CGRect)frame {
|
||||||
if (self = [super initWithFrame:frame]) {
|
if (self = [super initWithFrame:frame]) {
|
||||||
|
self.kb_vipLayoutState = -1;
|
||||||
self.backgroundColor = [UIColor clearColor]; // 透明,露出底部背景图
|
self.backgroundColor = [UIColor clearColor]; // 透明,露出底部背景图
|
||||||
|
|
||||||
[self addSubview:self.titleLabel];
|
[self addSubview:self.titleLabel];
|
||||||
@@ -131,11 +133,54 @@
|
|||||||
[self.avatarView kb_setAvatarURL:user.avatarUrl placeholder:KBAvatarPlaceholderImage];
|
[self.avatarView kb_setAvatarURL:user.avatarUrl placeholder:KBAvatarPlaceholderImage];
|
||||||
|
|
||||||
BOOL isVip = user.isVip;
|
BOOL isVip = user.isVip;
|
||||||
|
[self kb_applyVipLayout:isVip];
|
||||||
self.vipIconView.hidden = !isVip;
|
self.vipIconView.hidden = !isVip;
|
||||||
self.vipExpiryLabel.hidden = !isVip;
|
self.vipExpiryLabel.hidden = !isVip;
|
||||||
self.vipExpiryLabel.text = isVip ? [self vipExpiryDisplayTextFrom:user.vipExpiry] : nil;
|
self.vipExpiryLabel.text = isVip ? [self vipExpiryDisplayTextFrom:user.vipExpiry] : nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)kb_applyVipLayout:(BOOL)isVip {
|
||||||
|
NSInteger state = isVip ? 1 : 0;
|
||||||
|
if (self.kb_vipLayoutState == state) { return; }
|
||||||
|
self.kb_vipLayoutState = state;
|
||||||
|
|
||||||
|
if (isVip) {
|
||||||
|
[self.nameLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(self.avatarView.mas_right).offset(10);
|
||||||
|
make.bottom.equalTo(self.avatarView.mas_centerY).offset(-4);
|
||||||
|
}];
|
||||||
|
[self.vipIconView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.centerY.equalTo(self.nameLabel);
|
||||||
|
make.left.equalTo(self.nameLabel.mas_right).offset(10);
|
||||||
|
make.right.lessThanOrEqualTo(self).offset(-16);
|
||||||
|
make.width.mas_equalTo(51);
|
||||||
|
make.height.mas_equalTo(20);
|
||||||
|
}];
|
||||||
|
[self.vipExpiryLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(self.avatarView.mas_centerY).offset(4);
|
||||||
|
make.left.equalTo(self.nameLabel);
|
||||||
|
make.right.lessThanOrEqualTo(self).offset(-16);
|
||||||
|
}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.nameLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(self.avatarView.mas_right).offset(10);
|
||||||
|
make.centerY.equalTo(self.avatarView);
|
||||||
|
make.right.lessThanOrEqualTo(self).offset(-16);
|
||||||
|
}];
|
||||||
|
[self.vipIconView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(self.nameLabel.mas_right);
|
||||||
|
make.centerY.equalTo(self.nameLabel);
|
||||||
|
make.width.height.mas_equalTo(0);
|
||||||
|
}];
|
||||||
|
[self.vipExpiryLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(self.nameLabel.mas_bottom);
|
||||||
|
make.left.equalTo(self.nameLabel);
|
||||||
|
make.width.height.mas_equalTo(0);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
- (NSString *)vipExpiryDisplayTextFrom:(NSString *)rawDate {
|
- (NSString *)vipExpiryDisplayTextFrom:(NSString *)rawDate {
|
||||||
NSString *prefix = KBLocalized(@"Due On");
|
NSString *prefix = KBLocalized(@"Due On");
|
||||||
if (rawDate.length == 0) {
|
if (rawDate.length == 0) {
|
||||||
@@ -254,7 +299,7 @@
|
|||||||
- (UILabel *)nameLabel {
|
- (UILabel *)nameLabel {
|
||||||
if (!_nameLabel) {
|
if (!_nameLabel) {
|
||||||
_nameLabel = [UILabel new];
|
_nameLabel = [UILabel new];
|
||||||
_nameLabel.text = @"Notice";
|
_nameLabel.text = @"--";
|
||||||
_nameLabel.font = [KBFont medium:20];
|
_nameLabel.font = [KBFont medium:20];
|
||||||
_nameLabel.textColor = [UIColor colorWithHex:KBBlackValue];
|
_nameLabel.textColor = [UIColor colorWithHex:KBBlackValue];
|
||||||
_nameLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
_nameLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@class KBShopThemeDetailModel;
|
@class KBShopThemeDetailModel;
|
||||||
|
|
||||||
@interface KBSkinDetailHeaderCell : UICollectionViewCell
|
@interface KBSkinDetailHeaderCell : UICollectionViewCell
|
||||||
@property (nonatomic, strong) UIImageView *coverView; // 顶部大图
|
|
||||||
@property (nonatomic, strong) UILabel *leftLabel; // 下方左侧文案(#1B1F1A)
|
|
||||||
@property (nonatomic, strong) UILabel *rightLabel; // 下方右侧文案(#02BEAC)
|
|
||||||
- (void)configWithDetail:(nullable KBShopThemeDetailModel *)detail;
|
- (void)configWithDetail:(nullable KBShopThemeDetailModel *)detail;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,11 @@
|
|||||||
#import "KBSkinDetailHeaderCell.h"
|
#import "KBSkinDetailHeaderCell.h"
|
||||||
#import "UIImageView+KBWebImage.h"
|
#import "UIImageView+KBWebImage.h"
|
||||||
#import "KBShopThemeDetailModel.h"
|
#import "KBShopThemeDetailModel.h"
|
||||||
|
@interface KBSkinDetailHeaderCell()
|
||||||
|
@property (nonatomic, strong) UIImageView *coverView; // 顶部大图
|
||||||
|
@property (nonatomic, strong) UILabel *leftLabel; // 下方左侧文案(#1B1F1A)
|
||||||
|
@property (nonatomic, strong) UILabel *rightLabel; // 下方右侧文案(#02BEAC)
|
||||||
|
@end
|
||||||
@implementation KBSkinDetailHeaderCell
|
@implementation KBSkinDetailHeaderCell
|
||||||
- (instancetype)initWithFrame:(CGRect)frame {
|
- (instancetype)initWithFrame:(CGRect)frame {
|
||||||
if (self = [super initWithFrame:frame]) {
|
if (self = [super initWithFrame:frame]) {
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@class KBShopThemeTagModel;
|
||||||
|
|
||||||
@interface KBSkinDetailTagCell : UICollectionViewCell
|
@interface KBSkinDetailTagCell : UICollectionViewCell
|
||||||
- (void)config:(NSString *)text;
|
- (void)configWithTag:(KBShopThemeTagModel *)tag;
|
||||||
/// 根据文案计算自适应宽度(外部布局用)
|
/// 根据标签计算自适应宽度(外部布局用)
|
||||||
+ (CGSize)sizeForText:(NSString *)text;
|
+ (CGSize)sizeForTag:(KBShopThemeTagModel *)tag;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "KBSkinDetailTagCell.h"
|
#import "KBSkinDetailTagCell.h"
|
||||||
|
#import "KBShopThemeTagModel.h"
|
||||||
|
#import "UIColor+Extension.h"
|
||||||
@interface KBSkinDetailTagCell ()
|
@interface KBSkinDetailTagCell ()
|
||||||
@property (nonatomic, strong) UILabel *titleLabel;
|
@property (nonatomic, strong) UILabel *titleLabel;
|
||||||
@end
|
@end
|
||||||
@@ -23,11 +25,14 @@
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)config:(NSString *)text {
|
- (void)configWithTag:(KBShopThemeTagModel *)tag {
|
||||||
self.titleLabel.text = text ?: @"";
|
NSString *text = tag.label ?: @"";
|
||||||
|
self.titleLabel.text = text;
|
||||||
|
self.contentView.backgroundColor = [UIColor colorWithHexString:tag.color];
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (CGSize)sizeForText:(NSString *)text {
|
+ (CGSize)sizeForTag:(KBShopThemeTagModel *)tag {
|
||||||
|
NSString *text = tag.label ?: @"";
|
||||||
if (text.length == 0) { return CGSizeMake(40, 24); }
|
if (text.length == 0) { return CGSizeMake(40, 24); }
|
||||||
CGSize s = [text sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:14]}];
|
CGSize s = [text sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:14]}];
|
||||||
// 两侧内边距 12 + 12,高度固定 32
|
// 两侧内边距 12 + 12,高度固定 32
|
||||||
|
|||||||