56 Commits

Author SHA1 Message Date
85dcd72a5d 删除测试精灵3d相关代码
修改上报数据
2026-01-13 16:55:24 +08:00
21fcbe3665 修改暗黑模式功能UI 2026-01-09 19:35:36 +08:00
1b6724f043 1 2026-01-09 19:13:35 +08:00
ef332ecaa1 默认皮肤适配暗黑模式 2026-01-09 13:07:11 +08:00
3d6d673c0b 添加注释 2026-01-09 12:56:53 +08:00
674f09d5b6 处理按钮之间点不到的问题 HitInside 2026-01-08 20:18:37 +08:00
11d8f78b1b 2 2026-01-08 20:04:23 +08:00
bbacef4ff7 1 2026-01-08 18:57:17 +08:00
8e692647d3 2 2026-01-08 17:23:22 +08:00
6f80f969a4 1 2026-01-08 16:54:38 +08:00
bdf2a9af80 修改人设长按排序后,键盘人设要同步问题 2026-01-07 19:05:52 +08:00
e858d35722 修改首页pop对钩问题 2026-01-07 15:45:01 +08:00
f2d5210313 1 2026-01-07 14:32:57 +08:00
1b0af3e2d6 修改颜色 2026-01-07 14:32:49 +08:00
0965cd3c7e 修改了横屏键盘不居中为题 2026-01-07 13:11:23 +08:00
c3909d63da 添加埋点 2026-01-06 19:25:34 +08:00
1096f24c57 修改hud 2025-12-31 17:32:39 +08:00
7ed84fd445 修改分享方式 2025-12-30 20:41:30 +08:00
4e2d7d2908 添加部分通用上报
修改bug 未登录在键盘点击充值要先去跳转登录
2025-12-30 15:27:35 +08:00
34089ddeea 1 2025-12-26 15:51:27 +08:00
6ec98468de Merge branch 'dev_处理立刻清空和撤销删除' into dev_st
# Conflicts:
#	CustomKeyboard/Utils/KBBackspaceLongPressHandler.m
解决冲突
2025-12-26 15:08:14 +08:00
2d5919016f 测试上报 接口报错 2025-12-26 15:02:48 +08:00
c0fa51bb2e 修改用户名UI 2025-12-26 14:48:24 +08:00
6713f36387 修改国际化 2025-12-26 14:38:29 +08:00
f24750458a 修改立刻清空按钮 2025-12-26 14:19:39 +08:00
510a2f4d66 添加邀请链接 2025-12-26 14:07:21 +08:00
ae37730da6 修改 我已经退出界面,然后从新进入界面弹起键盘,为什么撤销删除按钮显示? 2025-12-26 13:55:07 +08:00
203f104ece 修改键盘长按立即清空和撤销删除 2025-12-26 11:47:44 +08:00
8e934dd83a 1 2025-12-25 17:58:33 +08:00
1676916a5c 1 2025-12-25 17:32:34 +08:00
1af5a0e849 添加部分埋点 2025-12-25 17:20:24 +08:00
5b6e0a8fbf 修改key 2025-12-25 13:07:28 +08:00
9968883bab 修改邀请 2025-12-25 13:01:14 +08:00
af5f637d31 1 2025-12-24 20:17:37 +08:00
0a725e845e 处理详情tag的背景色 2025-12-23 20:56:00 +08:00
6a539dc3c5 处理键盘长按删除 撤销出现的bug 2025-12-23 18:05:01 +08:00
73d6ec933a 修改金币超出问题 2025-12-23 15:33:14 +08:00
000d603241 优化下载皮肤❤️ 2025-12-23 15:26:32 +08:00
fbf9fe9f2a 2 2025-12-23 14:37:11 +08:00
8e4d7e1ee8 处理按钮 2025-12-23 14:15:27 +08:00
262eb57b36 修改接口 2025-12-23 14:07:53 +08:00
2e1c261775 修改UI 2025-12-23 14:02:44 +08:00
6ad2079351 修改emoji 更换api 2025-12-23 13:27:25 +08:00
a477592f5d 新增按钮 2025-12-22 21:16:38 +08:00
6f336e8368 国际化 2025-12-22 20:53:24 +08:00
17e038beb1 修改接口 2025-12-22 19:22:12 +08:00
4e6fd90668 1 2025-12-22 19:19:28 +08:00
5cfc76e6c5 修改我的皮肤逻辑 2025-12-22 16:42:56 +08:00
9e33c93763 修改弹窗 2025-12-22 15:49:28 +08:00
1c9ae7bc06 1 2025-12-22 15:37:22 +08:00
472e9ad341 修改联想背景色 2025-12-22 14:02:43 +08:00
19c69f4f6f Merge branch 'dev_联想' into dev_st 2025-12-22 13:47:23 +08:00
8788cbb105 处理UI 2025-12-22 13:46:45 +08:00
ea77e9a5f8 处理苹果bug 默认键盘颜色改为 2025-12-22 13:29:00 +08:00
eaaf0e1ed6 修改UI 2025-12-22 13:08:59 +08:00
8a344b293d 添加联想 2025-12-22 12:54:28 +08:00
252 changed files with 242784 additions and 3337 deletions

View File

@@ -5,12 +5,12 @@
"scale" : "1x"
},
{
"filename" : "切图 270@2x.png",
"filename" : "切图 271@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "切图 270@3x.png",
"filename" : "切图 271@3x.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -4,15 +4,79 @@
"idiom" : "universal",
"scale" : "1x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "light"
}
],
"idiom" : "universal",
"scale" : "1x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "kb_del_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "light"
}
],
"filename" : "kb_del_icon@2x 1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "切图 256@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "kb_del_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "light"
}
],
"filename" : "kb_del_icon@3x 1.png",
"idiom" : "universal",
"scale" : "3x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "切图 256@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -5,12 +5,12 @@
"scale" : "1x"
},
{
"filename" : "key_123@2x.png",
"filename" : "key_revoke@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "key_123@3x.png",
"filename" : "key_revoke@3x.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

File diff suppressed because it is too large Load Diff

View 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

View 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

View File

@@ -0,0 +1,96 @@
//
// KBKeyboardLayoutConfig.h
// CustomKeyboard
//
// 键盘布局配置模型(由 JSON 驱动)
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBKeyboardLayoutMetrics : NSObject
@property (nonatomic, strong, nullable) NSNumber *rowSpacing;
@property (nonatomic, strong, nullable) NSNumber *topInset;
@property (nonatomic, strong, nullable) NSNumber *bottomInset;
@property (nonatomic, strong, nullable) NSNumber *keyHeight;
@property (nonatomic, strong, nullable) NSNumber *edgeInset;
@property (nonatomic, strong, nullable) NSNumber *gap;
@property (nonatomic, strong, nullable) NSNumber *letterWidth;
@property (nonatomic, strong, nullable) NSNumber *controlWidth;
@property (nonatomic, strong, nullable) NSNumber *sendWidth;
@property (nonatomic, strong, nullable) NSNumber *symbolsWideWidth;
@property (nonatomic, strong, nullable) NSNumber *symbolsSideWidth;
@end
@interface KBKeyboardLayoutFonts : NSObject
@property (nonatomic, strong, nullable) NSNumber *letter;
@property (nonatomic, strong, nullable) NSNumber *digit;
@property (nonatomic, strong, nullable) NSNumber *symbol;
@property (nonatomic, strong, nullable) NSNumber *mode;
@property (nonatomic, strong, nullable) NSNumber *space;
@property (nonatomic, strong, nullable) NSNumber *send;
@end
@interface KBKeyboardKeyDef : NSObject
@property (nonatomic, copy, nullable) NSString *type;
@property (nonatomic, copy, nullable) NSString *title;
@property (nonatomic, copy, nullable) NSString *selectedTitle;
@property (nonatomic, copy, nullable) NSString *symbolName;
@property (nonatomic, copy, nullable) NSString *selectedSymbolName;
@property (nonatomic, copy, nullable) NSString *font;
@property (nonatomic, copy, nullable) NSString *width;
@property (nonatomic, strong, nullable) NSNumber *widthValue;
@property (nonatomic, copy, nullable) NSString *backgroundColor;
@end
@interface KBKeyboardRowItem : NSObject
@property (nonatomic, copy, nullable) NSString *itemId;
@property (nonatomic, copy, nullable) NSString *width;
@property (nonatomic, strong, nullable) NSNumber *widthValue;
+ (NSArray<KBKeyboardRowItem *> *)itemsFromRawArray:(NSArray *)raw;
@end
@interface KBKeyboardRowSegments : NSObject
@property (nonatomic, strong, nullable) NSArray *left;
@property (nonatomic, strong, nullable) NSArray *center;
@property (nonatomic, strong, nullable) NSArray *right;
- (NSArray<KBKeyboardRowItem *> *)leftItems;
- (NSArray<KBKeyboardRowItem *> *)centerItems;
- (NSArray<KBKeyboardRowItem *> *)rightItems;
@end
@interface KBKeyboardRowConfig : NSObject
@property (nonatomic, strong, nullable) NSNumber *height;
@property (nonatomic, strong, nullable) NSNumber *insetLeft;
@property (nonatomic, strong, nullable) NSNumber *insetRight;
@property (nonatomic, strong, nullable) NSNumber *gap;
@property (nonatomic, copy, nullable) NSString *align;
@property (nonatomic, strong, nullable) NSArray *items;
@property (nonatomic, strong, nullable) KBKeyboardRowSegments *segments;
- (NSArray<KBKeyboardRowItem *> *)resolvedItems;
@end
@interface KBKeyboardLayout : NSObject
@property (nonatomic, strong, nullable) NSArray<KBKeyboardRowConfig *> *rows;
@end
@interface KBKeyboardLayoutConfig : NSObject
@property (nonatomic, assign) CGFloat designWidth;
@property (nonatomic, strong, nullable) KBKeyboardLayoutMetrics *metrics;
@property (nonatomic, strong, nullable) KBKeyboardLayoutFonts *fonts;
@property (nonatomic, copy, nullable) NSString *defaultKeyBackground;
@property (nonatomic, strong, nullable) NSDictionary<NSString *, KBKeyboardKeyDef *> *keyDefs;
@property (nonatomic, strong, nullable) NSDictionary<NSString *, KBKeyboardLayout *> *layouts;
+ (nullable instancetype)sharedConfig;
+ (nullable instancetype)configFromJSONData:(NSData *)data;
- (CGFloat)scaledValue:(CGFloat)designValue;
- (CGFloat)keyboardAreaDesignHeight;
- (CGFloat)keyboardAreaScaledHeight;
- (nullable KBKeyboardLayout *)layoutForName:(NSString *)name;
- (nullable KBKeyboardKeyDef *)keyDefForIdentifier:(NSString *)identifier;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,187 @@
//
// KBKeyboardLayoutConfig.m
// CustomKeyboard
//
#import "KBKeyboardLayoutConfig.h"
#import <MJExtension/MJExtension.h>
#import "KBConfig.h"
static NSString * const kKBKeyboardLayoutConfigFileName = @"kb_keyboard_layout_config";
@implementation KBKeyboardLayoutMetrics
@end
@implementation KBKeyboardLayoutFonts
@end
@implementation KBKeyboardKeyDef
@end
@implementation KBKeyboardRowItem
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
return @{ @"itemId": @"id" };
}
+ (NSArray<KBKeyboardRowItem *> *)itemsFromRawArray:(NSArray *)raw {
if (![raw isKindOfClass:[NSArray class]] || raw.count == 0) {
return @[];
}
NSMutableArray<KBKeyboardRowItem *> *items = [NSMutableArray arrayWithCapacity:raw.count];
for (id obj in raw) {
if ([obj isKindOfClass:[NSString class]]) {
KBKeyboardRowItem *item = [KBKeyboardRowItem new];
item.itemId = (NSString *)obj;
[items addObject:item];
continue;
}
if ([obj isKindOfClass:[NSDictionary class]]) {
KBKeyboardRowItem *item = [KBKeyboardRowItem mj_objectWithKeyValues:obj];
if (item.itemId.length == 0) {
NSString *fallback = ((NSDictionary *)obj)[@"id"];
if ([fallback isKindOfClass:[NSString class]]) {
item.itemId = fallback;
}
}
if (item.itemId.length > 0) {
[items addObject:item];
}
}
}
return items.copy;
}
@end
@implementation KBKeyboardRowSegments
- (NSArray<KBKeyboardRowItem *> *)leftItems {
return [KBKeyboardRowItem itemsFromRawArray:self.left ?: @[]];
}
- (NSArray<KBKeyboardRowItem *> *)centerItems {
return [KBKeyboardRowItem itemsFromRawArray:self.center ?: @[]];
}
- (NSArray<KBKeyboardRowItem *> *)rightItems {
return [KBKeyboardRowItem itemsFromRawArray:self.right ?: @[]];
}
@end
@implementation KBKeyboardRowConfig
- (NSArray<KBKeyboardRowItem *> *)resolvedItems {
return [KBKeyboardRowItem itemsFromRawArray:self.items ?: @[]];
}
@end
@implementation KBKeyboardLayout
+ (NSDictionary *)mj_objectClassInArray {
return @{ @"rows": [KBKeyboardRowConfig class] };
}
@end
@implementation KBKeyboardLayoutConfig
+ (instancetype)sharedConfig {
static KBKeyboardLayoutConfig *config = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *path = [[NSBundle mainBundle] pathForResource:kKBKeyboardLayoutConfigFileName ofType:@"json"];
NSData *data = path.length ? [NSData dataWithContentsOfFile:path] : nil;
config = data ? [KBKeyboardLayoutConfig configFromJSONData:data] : nil;
});
return config;
}
+ (instancetype)configFromJSONData:(NSData *)data {
if (data.length == 0) { return nil; }
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error || ![json isKindOfClass:[NSDictionary class]]) {
return nil;
}
NSDictionary *dict = (NSDictionary *)json;
KBKeyboardLayoutConfig *config = [KBKeyboardLayoutConfig mj_objectWithKeyValues:dict];
NSDictionary *keyDefsRaw = dict[@"keyDefs"];
if ([keyDefsRaw isKindOfClass:[NSDictionary class]]) {
NSMutableDictionary<NSString *, KBKeyboardKeyDef *> *defs = [NSMutableDictionary dictionaryWithCapacity:keyDefsRaw.count];
[keyDefsRaw enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if (![key isKindOfClass:[NSString class]] || ![obj isKindOfClass:[NSDictionary class]]) {
return;
}
KBKeyboardKeyDef *def = [KBKeyboardKeyDef mj_objectWithKeyValues:obj];
if (def) {
defs[key] = def;
}
}];
config.keyDefs = defs.copy;
}
NSDictionary *layoutsRaw = dict[@"layouts"];
if ([layoutsRaw isKindOfClass:[NSDictionary class]]) {
NSMutableDictionary<NSString *, KBKeyboardLayout *> *layouts = [NSMutableDictionary dictionaryWithCapacity:layoutsRaw.count];
[layoutsRaw enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if (![key isKindOfClass:[NSString class]] || ![obj isKindOfClass:[NSDictionary class]]) {
return;
}
KBKeyboardLayout *layout = [KBKeyboardLayout mj_objectWithKeyValues:obj];
if (layout) {
layouts[key] = layout;
}
}];
config.layouts = layouts.copy;
}
return config;
}
- (CGFloat)scaledValue:(CGFloat)designValue {
CGFloat baseWidth = (self.designWidth > 0.0) ? self.designWidth : KB_DESIGN_WIDTH;
CGFloat scale = KBScreenWidth() / baseWidth;
return designValue * scale;
}
- (CGFloat)keyboardAreaDesignHeight {
KBKeyboardLayout *layout = [self layoutForName:@"letters"] ?: self.layouts.allValues.firstObject;
NSUInteger rowCount = layout.rows.count;
if (rowCount == 0) { return 0.0; }
CGFloat rowSpacing = self.metrics.rowSpacing.doubleValue;
CGFloat topInset = self.metrics.topInset.doubleValue;
CGFloat bottomInset = self.metrics.bottomInset.doubleValue;
CGFloat total = topInset + bottomInset + rowSpacing * (rowCount - 1);
for (KBKeyboardRowConfig *row in layout.rows) {
CGFloat height = row.height.doubleValue;
if (height <= 0.0) {
height = self.metrics.keyHeight.doubleValue;
}
if (height <= 0.0) { height = 40.0; }
total += height;
}
return total;
}
- (CGFloat)keyboardAreaScaledHeight {
CGFloat designHeight = [self keyboardAreaDesignHeight];
return designHeight > 0.0 ? [self scaledValue:designHeight] : 0.0;
}
- (KBKeyboardLayout *)layoutForName:(NSString *)name {
if (name.length == 0) { return nil; }
return self.layouts[name];
}
- (KBKeyboardKeyDef *)keyDefForIdentifier:(NSString *)identifier {
if (identifier.length == 0) { return nil; }
return self.keyDefs[identifier];
}
@end

View File

@@ -8,7 +8,7 @@
// - 兼容后端“/t”作为分段标记可自动替换为制表符“\t”
// - 首段去首个“\t”若首次正文以一个制表符起始允许前导空白可只移除“一个”\t
//
// 暂未使用
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN

View File

@@ -4,7 +4,7 @@
//
// Created by Mac on 2025/11/12.
//
// 暂未使用
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN

View File

@@ -21,6 +21,9 @@
#import "Masonry.h"
#import "KBHUD.h" // 复用 App 内的 HUD 封装
#import "KBLocalizationManager.h" // 复用多语言封装(可在扩展内使用)
#import "KBMaiPointReporter.h"
//#import "KBLog.h"
// 通用链接Universal Links统一配置
// 配置好 AASA 与 Associated Domains 后,只需修改这里即可切换域名/path。

View File

@@ -71,7 +71,7 @@
/* 字母 g小写 */
"letter_g_lower" = "key_g";
/* 字母 G大写 */
"letter_g_upper" = "key_f_up";
"letter_g_upper" = "key_g_up";
/* 字母 h小写 */
"letter_h_lower" = "key_h";
@@ -242,7 +242,7 @@
/* 自定义 AI 功能键 */
"ai" = "key_ai";
/* Emoji功能键 */
"emoji" = "key_emoji";
//"emoji" = "key_emoji";
"emoji_panel" = "key_emoji";
/* 发送/换行键 */
"return" = "key_send";

Binary file not shown.

View File

@@ -0,0 +1,414 @@
{
"__comment": "键盘布局配置:所有尺寸为设计稿值(会按 designWidth 等比缩放)",
"designWidth": 375,
"__comment_designWidth": "设计稿宽度(如 375用于计算缩放比例",
"defaultKeyBackground": "#FFFFFF",
"__comment_defaultKeyBackground": "无皮肤时按键默认背景色",
"metrics": {
"__comment": "全局尺寸参数单位pt按 designWidth 缩放)",
"rowSpacing": 8,
"__comment_rowSpacing": "行间距(垂直)",
"topInset": 8,
"__comment_topInset": "键盘顶部内边距",
"bottomInset": 6,
"__comment_bottomInset": "键盘底部内边距",
"keyHeight": 41,
"__comment_keyHeight": "默认按键高度",
"edgeInset": 4,
"__comment_edgeInset": "行左右内边距(默认)",
"gap": 5,
"__comment_gap": "按键之间水平间距",
"letterWidth": 32,
"__comment_letterWidth": "字母键默认宽度",
"controlWidth": 41,
"__comment_controlWidth": "控制键宽度(如 shift/backspace/123",
"sendWidth": 88,
"__comment_sendWidth": "send 键宽度",
"symbolsWideWidth": 47,
"__comment_symbolsWideWidth": "符号第3行中间大键宽度",
"symbolsSideWidth": 41,
"__comment_symbolsSideWidth": "符号第3行左右控制键宽度"
},
"fonts": {
"__comment": "字体大小pt",
"letter": 20,
"__comment_letter": "字母键字体大小",
"digit": 20,
"__comment_digit": "数字键字体大小",
"symbol": 18,
"__comment_symbol": "符号键字体大小",
"mode": 14,
"__comment_mode": "模式切换键字体大小ABC/#+=/123",
"space": 18,
"__comment_space": "空格键字体大小",
"send": 18,
"__comment_send": "发送键字体大小"
},
"keyDefs": {
"__comment": "特殊功能键配置id 对应布局中的 item",
"shift": {
"__comment": "大小写切换键",
"type": "shift",
"__comment_type": "类型shift/backspace/mode/symbolsToggle/space/return/custom",
"title": "⇧",
"__comment_title": "按钮文本(无皮肤时显示)",
"symbolName": "shift",
"__comment_symbolName": "无皮肤时使用 SF Symbol 名称",
"selectedSymbolName": "shift.fill",
"__comment_selectedSymbolName": "选中态 SF Symbol 名称",
"font": "symbol",
"__comment_font": "使用 fonts 中哪一类字号",
"width": "controlWidth",
"__comment_width": "宽度:引用 metrics 中字段或具体数值",
"backgroundColor": "#B7BBC4",
"__comment_backgroundColor": "按键背景色"
},
"backspace": {
"__comment": "删除键",
"type": "backspace",
"__comment_type": "类型shift/backspace/mode/symbolsToggle/space/return/custom",
"title": "⌫",
"__comment_title": "按钮文本(无皮肤时显示)",
"font": "symbol",
"__comment_font": "使用 fonts 中哪一类字号",
"width": "controlWidth",
"__comment_width": "宽度:引用 metrics 中字段或具体数值",
"backgroundColor": "#B7BBC4",
"__comment_backgroundColor": "按键背景色"
},
"mode_123": {
"__comment": "字母面板左下角 123",
"type": "mode",
"__comment_type": "类型shift/backspace/mode/symbolsToggle/space/return/custom",
"title": "123",
"__comment_title": "按钮文本(无皮肤时显示)",
"font": "mode",
"__comment_font": "使用 fonts 中哪一类字号",
"width": "controlWidth",
"__comment_width": "宽度:引用 metrics 中字段或具体数值",
"backgroundColor": "#B7BBC4",
"__comment_backgroundColor": "按键背景色"
},
"mode_abc": {
"__comment": "数字面板左下角 ABC",
"type": "mode",
"__comment_type": "类型shift/backspace/mode/symbolsToggle/space/return/custom",
"title": "ABC",
"__comment_title": "按钮文本(无皮肤时显示)",
"font": "mode",
"__comment_font": "使用 fonts 中哪一类字号",
"width": "controlWidth",
"__comment_width": "宽度:引用 metrics 中字段或具体数值",
"backgroundColor": "#B7BBC4",
"__comment_backgroundColor": "按键背景色"
},
"symbols_toggle_more": {
"__comment": "数字面板内 123 -> #+=",
"type": "symbolsToggle",
"__comment_type": "类型shift/backspace/mode/symbolsToggle/space/return/custom",
"title": "#+=",
"__comment_title": "按钮文本(无皮肤时显示)",
"font": "mode",
"__comment_font": "使用 fonts 中哪一类字号",
"width": "symbolsSideWidth",
"__comment_width": "宽度:引用 metrics 中字段或具体数值",
"backgroundColor": "#B7BBC4",
"__comment_backgroundColor": "按键背景色"
},
"symbols_toggle_123": {
"__comment": "数字面板内 #+= -> 123",
"type": "symbolsToggle",
"__comment_type": "类型shift/backspace/mode/symbolsToggle/space/return/custom",
"title": "123",
"__comment_title": "按钮文本(无皮肤时显示)",
"font": "mode",
"__comment_font": "使用 fonts 中哪一类字号",
"width": "symbolsSideWidth",
"__comment_width": "宽度:引用 metrics 中字段或具体数值",
"backgroundColor": "#B7BBC4",
"__comment_backgroundColor": "按键背景色"
},
"emoji": {
"__comment": "emoji 功能键",
"type": "custom",
"__comment_type": "类型shift/backspace/mode/symbolsToggle/space/return/custom",
"title": "😁",
"__comment_title": "按钮文本(无皮肤时显示)",
"font": "symbol",
"__comment_font": "使用 fonts 中哪一类字号",
"width": "controlWidth",
"__comment_width": "宽度:引用 metrics 中字段或具体数值",
"backgroundColor": "#B7BBC4",
"__comment_backgroundColor": "按键背景色"
},
"space": {
"__comment": "空格键",
"type": "space",
"__comment_type": "类型shift/backspace/mode/symbolsToggle/space/return/custom",
"title": "space",
"__comment_title": "按钮文本(无皮肤时显示)",
"font": "space",
"__comment_font": "使用 fonts 中哪一类字号",
"width": "flex",
"__comment_width": "flex 表示自动占满剩余空间"
},
"send": {
"__comment": "发送键",
"type": "return",
"__comment_type": "类型shift/backspace/mode/symbolsToggle/space/return/custom",
"title": "send",
"__comment_title": "按钮文本(无皮肤时显示)",
"font": "send",
"__comment_font": "使用 fonts 中哪一类字号",
"width": "sendWidth",
"__comment_width": "宽度:引用 metrics 中字段或具体数值",
"backgroundColor": "#B7BBC4",
"__comment_backgroundColor": "按键背景色"
}
},
"layouts": {
"__comment": "布局集合letters/numbers/symbolsMore",
"letters": {
"__comment": "字母布局(小写/大写共用)",
"rows": [
{
"__comment": "字母第一行 qwertyuiop",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"letter:q", "letter:w", "letter:e", "letter:r", "letter:t",
"letter:y", "letter:u", "letter:i", "letter:o", "letter:p"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "字母第二行 asdfghjkl",
"align": "center",
"__comment_align": "对齐方式left/center",
"insetLeft": 0,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 0,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"letter:a", "letter:s", "letter:d", "letter:f", "letter:g",
"letter:h", "letter:j", "letter:k", "letter:l"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "字母第三行:左 shift中间字母右 backspace",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"segments": {
"__comment": "分段布局left/center/right",
"left": [
{ "id": "shift", "width": "controlWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.controlWidth" }
],
"__comment_left": "左侧固定按钮",
"center": [
"letter:z", "letter:x", "letter:c", "letter:v", "letter:b", "letter:n", "letter:m"
],
"__comment_center": "中间字母键集合,整体居中",
"right": [
{ "id": "backspace", "width": "controlWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.controlWidth" }
],
"__comment_right": "右侧固定按钮"
}
},
{
"__comment": "字母第四行123/emoji/space/send",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"mode_123", "emoji", "space", "send"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
}
]
},
"numbers": {
"__comment": "数字面板布局123 页)",
"rows": [
{
"__comment": "数字第一行 1234567890",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"digit:1", "digit:2", "digit:3", "digit:4", "digit:5",
"digit:6", "digit:7", "digit:8", "digit:9", "digit:0"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "数字第二行 - / : ; ( ) ¥ & @ “",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"sym:-", "sym:/", "sym::", "sym:;", "sym:(",
"sym:)", "sym:¥", "sym:&", "sym:@", "sym:“"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "数字第三行:#+= / 中间符号 / 删除",
"align": "center",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"segments": {
"__comment": "分段布局left/center/right",
"left": [
{ "id": "symbols_toggle_more", "width": "symbolsSideWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.symbolsSideWidth" }
],
"__comment_left": "左侧切换按钮",
"center": [
{ "id": "sym:.", "width": "symbolsWideWidth", "__comment_id": "符号键 id", "__comment_width": "宽度引用 metrics.symbolsWideWidth" },
{ "id": "sym:,", "width": "symbolsWideWidth", "__comment_id": "符号键 id", "__comment_width": "宽度引用 metrics.symbolsWideWidth" },
{ "id": "sym:?", "width": "symbolsWideWidth", "__comment_id": "符号键 id", "__comment_width": "宽度引用 metrics.symbolsWideWidth" },
{ "id": "sym:!", "width": "symbolsWideWidth", "__comment_id": "符号键 id", "__comment_width": "宽度引用 metrics.symbolsWideWidth" },
{ "id": "sym:", "width": "symbolsWideWidth", "__comment_id": "符号键 id", "__comment_width": "宽度引用 metrics.symbolsWideWidth" }
],
"__comment_center": "中间符号键集合,整体居中",
"right": [
{ "id": "backspace", "width": "symbolsSideWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.symbolsSideWidth" }
],
"__comment_right": "右侧删除键"
}
},
{
"__comment": "数字第四行ABC/emoji/space/send",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"mode_abc", "emoji", "space", "send"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
}
]
},
"symbolsMore": {
"__comment": "符号面板布局(#+= 页)",
"rows": [
{
"__comment": "符号第一行 [ ] { } # % ^ * + =",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"sym:[", "sym:]", "sym:{", "sym:}", "sym:#",
"sym:%", "sym:^", "sym:*", "sym:+", "sym:="
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "符号第二行 _ \\ | ~ < > € ¥ $ ·",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"sym:_", "sym:\\", "sym:|", "sym:~", "sym:<",
"sym:>", "sym:€", "sym:¥", "sym:$", "sym:·"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
},
{
"__comment": "符号第三行123 / 中间符号 / 删除",
"align": "center",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"segments": {
"__comment": "分段布局left/center/right",
"left": [
{ "id": "symbols_toggle_123", "width": "symbolsSideWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.symbolsSideWidth" }
],
"__comment_left": "左侧切换按钮",
"center": [
{ "id": "sym:.", "width": "symbolsWideWidth", "__comment_id": "符号键 id", "__comment_width": "宽度引用 metrics.symbolsWideWidth" },
{ "id": "sym:,", "width": "symbolsWideWidth", "__comment_id": "符号键 id", "__comment_width": "宽度引用 metrics.symbolsWideWidth" },
{ "id": "sym:?", "width": "symbolsWideWidth", "__comment_id": "符号键 id", "__comment_width": "宽度引用 metrics.symbolsWideWidth" },
{ "id": "sym:!", "width": "symbolsWideWidth", "__comment_id": "符号键 id", "__comment_width": "宽度引用 metrics.symbolsWideWidth" },
{ "id": "sym:", "width": "symbolsWideWidth", "__comment_id": "符号键 id", "__comment_width": "宽度引用 metrics.symbolsWideWidth" }
],
"__comment_center": "中间符号键集合,整体居中",
"right": [
{ "id": "backspace", "width": "symbolsSideWidth", "__comment_id": "引用 keyDefs 的 id", "__comment_width": "宽度引用 metrics.symbolsSideWidth" }
],
"__comment_right": "右侧删除键"
}
},
{
"__comment": "符号第四行ABC/emoji/space/send",
"align": "left",
"__comment_align": "对齐方式left/center",
"insetLeft": 4,
"__comment_insetLeft": "本行左边距(覆盖 metrics.edgeInset",
"insetRight": 4,
"__comment_insetRight": "本行右边距(覆盖 metrics.edgeInset",
"gap": 5,
"__comment_gap": "本行按键间距(覆盖 metrics.gap",
"items": [
"mode_abc", "emoji", "space", "send"
],
"__comment_items": "本行按键列表letter:x/digit:x/sym:x 或 keyDefs 中的 id"
}
]
}
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -11,10 +11,10 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithContainerView:(UIView *)containerView;
/// 配置删除按钮(包含长按删除;可选是否显示“立刻清空”提示)
/// 配置删除按钮(包含长按删除;可选是否显示“上滑清空”提示)
- (void)bindDeleteButton:(nullable UIView *)button showClearLabel:(BOOL)showClearLabel;
/// 触发“立刻清空”逻辑(可用于功能面板的清空按钮)
/// 触发“上滑清空”逻辑(可用于功能面板的清空按钮)
- (void)performClearAction;
@end

View File

@@ -7,24 +7,25 @@
#import "KBResponderUtils.h"
#import "KBSkinManager.h"
#import "KBBackspaceUndoManager.h"
#import "KBInputBufferManager.h"
static const NSTimeInterval kKBBackspaceLongPressMinDuration = 0.35;
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 kKBBackspaceChunkFastDelay = 1.4;
static const NSInteger kKBBackspaceChunkSize = 6;
static const NSInteger kKBBackspaceChunkSizeFast = 12;
static const NSTimeInterval kKBBackspaceChunkFastDelay = 1.2;
static const NSInteger kKBBackspaceChunkSize = 8;
static const NSInteger kKBBackspaceChunkSizeFast = 16;
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 kKBBackspaceClearLabelTopGap = 6.0;
static const CGFloat kKBBackspaceClearLabelHorizontalInset = 6.0;
static const NSInteger kKBBackspaceClearBatchSize = 24;
static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.005;
static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.02;
static const NSInteger kKBBackspaceClearMaxDeletes = 10000;
static const NSInteger kKBBackspaceClearEmptyContextMaxRounds = 40;
static const NSInteger kKBBackspaceClearMaxStep = 80;
static const NSInteger kKBBackspaceClearDeletesPerTick = 10;
typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
KBBackspaceChunkClassUnknown = 0,
@@ -34,6 +35,12 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
KBBackspaceChunkClassOther
};
typedef NS_ENUM(NSInteger, KBClearPhase) {
KBClearPhaseSkipWhitespace = 0,
KBClearPhaseSkipTrailingBoundary,
KBClearPhaseDeleteUntilBoundary
};
@interface KBBackspaceLongPressHandler ()
@property (nonatomic, weak) UIView *containerView;
@property (nonatomic, weak) UIView *backspaceButton;
@@ -48,6 +55,9 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
@property (nonatomic, assign) CGPoint backspaceLastTouchPointInSelf;
@property (nonatomic, assign) NSUInteger backspaceClearToken;
@property (nonatomic, strong) UILabel *backspaceClearLabel;
@property (nonatomic, copy) NSString *pendingClearBefore;
@property (nonatomic, copy) NSString *pendingClearAfter;
@property (nonatomic, assign) KBClearPhase backspaceClearPhase;
@end
@implementation KBBackspaceLongPressHandler
@@ -55,6 +65,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
- (instancetype)initWithContainerView:(UIView *)containerView {
if (self = [super init]) {
_containerView = containerView;
_backspaceClearPhase = KBClearPhaseSkipWhitespace;
}
return self;
}
@@ -73,6 +84,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
self.backspaceHasLastTouchPoint = NO;
self.backspaceHoldToken += 1;
[self kb_hideBackspaceClearLabel];
self.pendingClearBefore = nil;
self.pendingClearAfter = nil;
if (!button) { return; }
@@ -99,7 +112,18 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
}
switch (gr.state) {
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;
NSUInteger token = self.backspaceHoldToken;
self.backspaceHoldActive = YES;
@@ -134,6 +158,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
if (!ivc) { self.backspaceHoldActive = NO; return; }
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
NSString *before = proxy.documentContextBeforeInput ?: @"";
if (before.length == 0) { before = [KBInputBufferManager shared].liveText ?: @""; }
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
NSInteger deleteCount = 1;
if (before.length > 0) {
@@ -145,9 +170,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
[self kb_showBackspaceClearLabelIfNeeded];
}
}
for (NSInteger i = 0; i < deleteCount; i++) {
[proxy deleteBackward];
}
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:(NSUInteger)deleteCount];
[[KBInputBufferManager shared] applyHoldDeleteCount:(NSUInteger)deleteCount];
NSTimeInterval interval = [self kb_backspaceRepeatIntervalForElapsed:elapsed];
__weak typeof(self) weakSelf = self;
@@ -186,34 +210,77 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
asciiWordSet = [NSCharacterSet characterSetWithCharactersInString:
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"];
punctuationSet = [NSCharacterSet punctuationCharacterSet];
NSMutableCharacterSet *punct = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
// / chunk 1
[punct addCharactersInString:@",。!?;:、()【】《》“”‘’·…—"];
punctuationSet = [punct copy];
});
__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)
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
if (substring.length == 0) { return; }
KBBackspaceChunkClass currentClass = KBBackspaceChunkClassOther;
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) {
if (deleteCount >= maxCount) {
*stop = YES;
return;
}
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 == CoreASCII /
if (coreClass == KBBackspaceChunkClassUnknown) {
coreClass = currentClass;
}
if (currentClass != coreClass) {
*stop = YES;
consumed = YES;
continue;
}
deleteCount += 1;
consumed = YES;
}
if (deleteCount >= maxCount) {
*stop = YES;
return;
}
}];
@@ -222,13 +289,16 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
- (NSInteger)kb_clearDeleteCountForContext:(NSString *)context
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 *whitespaceSet = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"];
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
});
@@ -303,6 +373,12 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
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.backspaceChunkModeActive = NO;
self.backspaceHoldToken += 1;
@@ -310,6 +386,11 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
[self kb_hideBackspaceClearLabel];
if (shouldClear) {
[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 {
if (!_backspaceClearLabel) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
label.text = @"立刻清空";
label.text = KBLocalized(@"Clear");
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.backgroundColor = [self kb_backspaceClearLabelNormalColor];
label.layer.cornerRadius = kKBBackspaceClearLabelCornerRadius;
@@ -421,10 +502,14 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
UIInputViewController *ivc = KBFindInputViewController(start);
if (ivc) {
NSString *before = ivc.textDocumentProxy.documentContextBeforeInput ?: @"";
[[KBBackspaceUndoManager shared] recordClearWithContext:before];
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
}
self.pendingClearBefore = nil;
self.pendingClearAfter = nil;
[[KBInputBufferManager shared] clearPendingClearSnapshot];
self.backspaceClearToken += 1;
self.backspaceClearPhase = KBClearPhaseSkipWhitespace;
NSUInteger token = self.backspaceClearToken;
[self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0];
}
@@ -437,40 +522,101 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
UIInputViewController *ivc = KBFindInputViewController(start);
if (!ivc) { return; }
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
NSString *before = proxy.documentContextBeforeInput ?: @"";
NSInteger count = before.length;
NSInteger batch = 0;
NSInteger nextEmptyRounds = emptyRounds;
BOOL hitBoundary = NO;
if (count > 0) {
batch = [self kb_clearDeleteCountForContext:before hitBoundary:&hitBoundary];
nextEmptyRounds = 0;
} else {
batch = kKBBackspaceClearBatchSize;
nextEmptyRounds = emptyRounds + 1;
}
if (batch <= 0) { batch = 1; }
static NSCharacterSet *stopBoundarySet = nil;
static NSCharacterSet *trailingBoundarySet = nil;
static NSCharacterSet *trailingWhitespaceSet = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// stopBoundary:
// - . ! ?
// - /
// - \n \r
stopBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?…\n\r\u2028\u2029"];
if (guard >= kKBBackspaceClearMaxDeletes ||
nextEmptyRounds > kKBBackspaceClearEmptyContextMaxRounds) {
// trailingBoundary:
// //
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;
}
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;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(kKBBackspaceClearBatchInterval * NSEC_PER_SEC)),
@@ -489,4 +635,28 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
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

View File

@@ -15,13 +15,19 @@ extern NSNotificationName const KBBackspaceUndoStateDidChangeNotification;
+ (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 处执行撤销(向光标处插回删除的内容)
- (void)performUndoFromResponder:(UIResponder *)responder;
/// 非清空行为触发时,清理撤销状态
/// 非删除行为触发时,清理撤销状态
- (void)registerNonClearAction;
@end

View File

@@ -5,13 +5,38 @@
#import "KBBackspaceUndoManager.h"
#import "KBResponderUtils.h"
#import "KBInputBufferManager.h"
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 ()
@property (nonatomic, strong) NSMutableArray<NSString *> *segments; // deletion order (last -> first)
@property (nonatomic, assign) BOOL lastActionWasClear;
@property (nonatomic, copy) NSString *undoText;
@property (nonatomic, assign) NSInteger undoAfterLength;
@property (nonatomic, assign) BOOL hasUndo;
@property (nonatomic, assign) KBUndoSnapshotSource snapshotSource;
@property (nonatomic, strong) NSMutableArray<NSString *> *undoDeletedPieces;
@end
@implementation KBBackspaceUndoManager
@@ -27,42 +52,191 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
- (instancetype)init {
if (self = [super init]) {
_segments = [NSMutableArray array];
_undoText = @"";
_undoAfterLength = 0;
_snapshotSource = KBUndoSnapshotSourceNone;
_undoDeletedPieces = [NSMutableArray array];
}
return self;
}
- (void)recordClearWithContext:(NSString *)context {
if (context.length == 0) { return; }
NSString *segment = [self kb_segmentForClearFromContext:context];
if (segment.length == 0) { return; }
- (void)captureAndDeleteBackwardFromProxy:(id<UITextDocumentProxy>)proxy count:(NSUInteger)count {
if (!proxy || count == 0) { return; }
if (!self.lastActionWasClear) {
[self.segments removeAllObjects];
NSString *selected = proxy.selectedText ?: @"";
NSString *ctxBefore = proxy.documentContextBeforeInput ?: @"";
NSString *ctxAfter = proxy.documentContextAfterInput ?: @"";
NSUInteger ctxLen = ctxBefore.length + ctxAfter.length;
BOOL isSelectAllLike = (selected.length > 0 &&
(ctxLen == 0 || selected.length >= MAX((NSUInteger)40, ctxLen * 2)));
if (isSelectAllLike) {
// /QQ/
if (self.hasUndo) {
[self registerNonClearAction];
}
[self.segments addObject:segment];
self.lastActionWasClear = YES;
#if DEBUG
KB_UNDO_LOG(@"captureAndDelete/selectAllDisableUndo", selected);
#endif
[proxy deleteBackward];
[[KBInputBufferManager shared] resetWithText:@""];
return;
}
if (!self.hasUndo) {
[self.undoDeletedPieces removeAllObjects];
self.undoText = @"";
self.undoAfterLength = 0;
self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot;
[self kb_updateHasUndo:YES];
}
BOOL didAppend = NO;
NSString *lastObservedBefore = nil;
for (NSUInteger i = 0; i < count; i++) {
NSString *before = proxy.documentContextBeforeInput ?: @"";
if (before.length > 0) {
// 宿 runloop context
if (lastObservedBefore && [before isEqualToString:lastObservedBefore]) {
// still delete, but don't record
} else {
NSString *piece = [self kb_lastComposedCharacterFromString:before];
if (piece.length > 0) {
[self.undoDeletedPieces addObject:piece];
didAppend = YES;
}
lastObservedBefore = before;
}
}
[proxy deleteBackward];
}
#if DEBUG
if (didAppend) {
NSUInteger piecesCount = self.undoDeletedPieces.count;
if (piecesCount <= 20) {
KB_UNDO_LOG(@"captureAndDelete/undoInsertTextNow", [self kb_buildUndoInsertTextFromPieces]);
} else if (piecesCount % 50 == 0) {
NSString *lastPiece = self.undoDeletedPieces.lastObject ?: @"";
NSLog(@"[captureAndDelete/undoPieces] pieces=%lu lastPiece=%@",
(unsigned long)piecesCount,
lastPiece);
}
}
#endif
}
- (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after {
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];
}
- (void)performUndoFromResponder:(UIResponder *)responder {
if (self.segments.count == 0) { return; }
if (!self.hasUndo) { return; }
UIInputViewController *ivc = KBFindInputViewController(responder);
if (!ivc) { return; }
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
NSString *text = [self kb_buildUndoText];
if (text.length == 0) { return; }
[proxy insertText:text];
[self.segments removeAllObjects];
self.lastActionWasClear = NO;
NSString *curBefore = proxy.documentContextBeforeInput ?: @"";
NSString *curAfter = proxy.documentContextAfterInput ?: @"";
KB_UNDO_LOG(@"performUndo/currentBefore", curBefore);
KB_UNDO_LOG(@"performUndo/currentAfter", curAfter);
NSString *insertText = [self kb_buildUndoInsertTextFromPieces];
if (insertText.length > 0) {
KB_UNDO_LOG(@"performUndo/insertDeletedText", insertText);
[proxy insertText:insertText];
[[KBInputBufferManager shared] appendText:insertText];
} else if (self.undoText.length > 0) {
KB_UNDO_LOG(@"performUndo/fallbackUndoText", self.undoText);
[self kb_clearAllTextForProxy:proxy];
[proxy insertText:self.undoText];
if (self.undoAfterLength > 0 &&
[proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
[proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength];
}
[[KBInputBufferManager shared] resetWithText:self.undoText];
} else {
return;
}
self.undoText = @"";
self.undoAfterLength = 0;
self.snapshotSource = KBUndoSnapshotSourceNone;
[self.undoDeletedPieces removeAllObjects];
[self kb_updateHasUndo:NO];
}
- (void)registerNonClearAction {
self.lastActionWasClear = NO;
if (self.segments.count == 0) { return; }
[self.segments removeAllObjects];
if (!self.hasUndo) { return; }
if (self.undoText.length > 0) {
KB_UNDO_LOG(@"registerNonClearAction/clearUndoText", self.undoText);
}
if (self.undoDeletedPieces.count > 0) {
KB_UNDO_LOG(@"registerNonClearAction/clearDeletedPieces", [self kb_buildUndoInsertTextFromPieces]);
}
self.undoText = @"";
self.undoAfterLength = 0;
self.snapshotSource = KBUndoSnapshotSourceNone;
[self.undoDeletedPieces removeAllObjects];
[self kb_updateHasUndo:NO];
}
@@ -74,97 +248,57 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
[[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self];
}
- (NSString *)kb_segmentForClearFromContext:(NSString *)context {
NSInteger length = context.length;
if (length == 0) { return @""; }
static NSCharacterSet *sentenceBoundarySet = nil;
static NSCharacterSet *whitespaceSet = nil;
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;
}
- (NSString *)kb_lastComposedCharacterFromString:(NSString *)text {
if (text.length == 0) { return @""; }
__block NSString *last = @"";
[text enumerateSubstringsInRange:NSMakeRange(0, text.length)
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
last = substring ?: @"";
*stop = YES;
}];
return last ?: @"";
}
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];
- (NSString *)kb_buildUndoInsertTextFromPieces {
if (self.undoDeletedPieces.count == 0) { return @""; }
NSMutableString *result = [NSMutableString string];
for (NSInteger i = 0; i < ordered.count; i++) {
NSString *segment = ordered[i] ?: @"";
if (segment.length == 0) { continue; }
if (i < ordered.count - 1) {
segment = [self kb_replaceTrailingBoundaryWithComma:segment];
}
[result appendString:segment];
for (NSInteger i = (NSInteger)self.undoDeletedPieces.count - 1; i >= 0; i--) {
NSString *piece = self.undoDeletedPieces[(NSUInteger)i] ?: @"";
if (piece.length == 0) { continue; }
[result appendString:piece];
}
return result;
}
- (NSString *)kb_replaceTrailingBoundaryWithComma:(NSString *)segment {
if (segment.length == 0) { return segment; }
static NSCharacterSet *boundarySet = nil;
static NSCharacterSet *englishBoundarySet = nil;
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;
while (idx >= 0) {
unichar ch = [segment characterAtIndex:idx];
if ([whitespaceSet characterIsMember:ch]) {
idx -= 1;
continue;
}
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;
static const NSInteger kKBUndoClearMaxRounds = 200;
- (void)kb_clearAllTextForProxy:(id<UITextDocumentProxy>)proxy {
if (!proxy) { return; }
if ([proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
NSInteger guard = 0;
NSString *contextAfter = proxy.documentContextAfterInput ?: @"";
while (contextAfter.length > 0 && guard < kKBUndoClearMaxRounds) {
NSInteger offset = (NSInteger)contextAfter.length;
[proxy adjustTextPositionByCharacterOffset:offset];
for (NSUInteger i = 0; i < contextAfter.length; i++) {
[proxy deleteBackward];
}
guard += 1;
contextAfter = proxy.documentContextAfterInput ?: @"";
}
}
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

View 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

View 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

View File

@@ -4,6 +4,7 @@
#import "KBFunctionTagListView.h"
#import "KBFunctionTagCell.h"
#import "KBMaiPointReporter.h"
static NSString * const kKBFunctionTagCellId2 = @"KBFunctionTagCellId2";
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; }
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
if ([self.delegate respondsToSelector:@selector(tagListView:didSelectIndex:title:)]) {
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:)]) {
[self.delegate tagListView:self didSelectIndex:indexPath.item title:model.characterName];
}
}

View File

@@ -18,6 +18,7 @@
@end
@implementation KBFunctionBarView
static const CGFloat kKBBackButtonWidth = 40;
- (instancetype)initWithFrame:(CGRect)frame{
if (self = [super initWithFrame:frame]) {
@@ -83,14 +84,14 @@
UIButton *appButton = [UIButton buttonWithType:UIButtonTypeCustom];
appButton.tag = 100; // index = 0
UIImage *appImage = [UIImage imageNamed:@"App_icon"];
[appButton setImage:appImage forState:UIControlStateNormal];
[appButton setBackgroundImage:appImage forState:UIControlStateNormal];
appButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
appButton.adjustsImageWhenHighlighted = YES;
[appButton addTarget:self action:@selector(onLeftTap:) forControlEvents:UIControlEventTouchUpInside];
[self.leftContainer addSubview:appButton];
[appButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.leftContainer);
make.width.height.mas_equalTo(34); //
make.width.height.mas_equalTo(kKBBackButtonWidth); //
}];
self.leftButtonsInternal = @[appButton];

View File

@@ -6,19 +6,20 @@
//
#import "KBFunctionTagCell.h"
#import "KBFunctionView.h"
#import "Masonry.h"
@interface KBFunctionTagCell ()
@property (nonatomic, strong) UILabel *emojiLabel;
@property (nonatomic, strong) UILabel *titleLabelInternal;
@property (nonatomic, strong) UIActivityIndicatorView *loadingView;
@property(nonatomic, strong) UILabel *emojiLabel;
@property(nonatomic, strong) UILabel *titleLabelInternal;
@property(nonatomic, strong) UIActivityIndicatorView *loadingView;
@end
@implementation KBFunctionTagCell
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.contentView.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
self.contentView.backgroundColor = [KBFunctionView kb_cellBackgroundColor];
self.contentView.layer.cornerRadius = 12;
self.contentView.layer.masksToBounds = YES;
@@ -59,7 +60,7 @@
return self;
}
- (void)setItemModel:(KBTagItemModel *)itemModel{
- (void)setItemModel:(KBTagItemModel *)itemModel {
_itemModel = itemModel;
self.emojiLabel.text = itemModel.emoji;
self.titleLabelInternal.text = itemModel.characterName;
@@ -73,7 +74,6 @@
_emojiLabel.textAlignment = NSTextAlignmentCenter;
_emojiLabel.font = [KBFont medium:20];
_emojiLabel.adjustsFontSizeToFitWidth = YES;
}
return _emojiLabel;
}
@@ -82,7 +82,7 @@
if (!_titleLabelInternal) {
_titleLabelInternal = [[UILabel alloc] init];
_titleLabelInternal.font = [KBFont medium:10];
_titleLabelInternal.textColor = [UIColor colorWithHex:0x1B1F1A];
_titleLabelInternal.textColor = [KBFunctionView kb_cellTextColor];
//
_titleLabelInternal.numberOfLines = 2;
_titleLabelInternal.lineBreakMode = NSLineBreakByTruncatingTail;
@@ -91,14 +91,19 @@
}
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
static UIActivityIndicatorViewStyle KBSpinnerStyle(void) { return UIActivityIndicatorViewStyleMedium; }
static UIActivityIndicatorViewStyle KBSpinnerStyle(void) {
return UIActivityIndicatorViewStyleMedium;
}
#else
static UIActivityIndicatorViewStyle KBSpinnerStyle(void) { return UIActivityIndicatorViewStyleGray; }
static UIActivityIndicatorViewStyle KBSpinnerStyle(void) {
return UIActivityIndicatorViewStyleGray;
}
#endif
- (UIActivityIndicatorView *)loadingView {
if (!_loadingView) {
_loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:KBSpinnerStyle()];
_loadingView = [[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:KBSpinnerStyle()];
_loadingView.hidesWhenStopped = YES;
_loadingView.color = [UIColor grayColor];
_loadingView.hidden = YES;
@@ -108,7 +113,9 @@ static UIActivityIndicatorViewStyle KBSpinnerStyle(void) { return UIActivityIndi
#pragma mark - Expose
- (UILabel *)titleLabel { return self.titleLabelInternal; }
- (UILabel *)titleLabel {
return self.titleLabelInternal;
}
- (void)setLoading:(BOOL)loading {
if (loading) {

View File

@@ -6,13 +6,16 @@
//
#import <UIKit/UIKit.h>
@class KBFunctionBarView, KBFunctionPasteView,KBFunctionView;
@class KBFunctionBarView, KBFunctionPasteView, KBFunctionView;
@protocol KBFunctionViewDelegate <NSObject>
@optional
- (void)functionView:(KBFunctionView *_Nullable)functionView didTapToolActionAtIndex:(NSInteger)index;
- (void)functionView:(KBFunctionView *_Nullable)functionView didRightTapToolActionAtIndex:(NSInteger)index;
- (void)functionViewDidRequestSubscription:(KBFunctionView *_Nullable)functionView;
- (void)functionView:(KBFunctionView *_Nullable)functionView
didTapToolActionAtIndex:(NSInteger)index;
- (void)functionView:(KBFunctionView *_Nullable)functionView
didRightTapToolActionAtIndex:(NSInteger)index;
- (void)functionViewDidRequestSubscription:
(KBFunctionView *_Nullable)functionView;
@end
@@ -21,24 +24,33 @@ NS_ASSUME_NONNULL_BEGIN
/// 整个功能面板视图顶部Bar + 粘贴区 + 标签列表 + 右侧操作按钮
@interface KBFunctionView : UIView
@property (nonatomic, weak) id<KBFunctionViewDelegate> delegate;
@property(nonatomic, weak) id<KBFunctionViewDelegate> delegate;
@property (nonatomic, strong, readonly) UICollectionView *collectionView; // 话术分类/标签列表
@property (nonatomic, strong, readonly) NSArray<NSString *> *items; // 简单数据源(演示用)
@property(nonatomic, strong, readonly)
UICollectionView *collectionView; // 话术分类/标签列表
@property(nonatomic, strong, readonly)
NSArray<NSString *> *items; // 简单数据源(演示用)
// 子视图暴露,便于外部接入事件
@property (nonatomic, strong, readonly) KBFunctionBarView *barView;
@property (nonatomic, strong, readonly) KBFunctionPasteView *pasteView;
@property(nonatomic, strong, readonly) KBFunctionBarView *barView;
@property(nonatomic, strong, readonly) KBFunctionPasteView *pasteView;
@property (nonatomic, strong, readonly) UIButton *pasteButton; // 右侧-粘贴
@property (nonatomic, strong, readonly) UIButton *deleteButton; // 右侧-删除
@property (nonatomic, strong, readonly) UIButton *clearButton; // 右侧-清空
@property (nonatomic, strong, readonly) UIButton *sendButton; // 右侧-发送
@property(nonatomic, strong, readonly) UIButton *pasteButton; // 右侧-粘贴
@property(nonatomic, strong, readonly) UIButton *deleteButton; // 右侧-删除
@property(nonatomic, strong, readonly) UIButton *clearButton; // 右侧-清空
@property(nonatomic, strong, readonly) UIButton *sendButton; // 右侧-发送
/// 应用当前皮肤(更新背景/强调色)
- (void)kb_applyTheme;
#pragma mark - Theme Colors (用于 Cell 获取暗黑模式颜色)
/// Cell 背景色:暗黑 #707070浅色 白色90%透明度
+ (UIColor *)kb_cellBackgroundColor;
/// Cell 文字颜色:暗黑 #FFFFFF浅色 #1B1F1A
+ (UIColor *)kb_cellTextColor;
@end
NS_ASSUME_NONNULL_END

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,9 @@ NS_ASSUME_NONNULL_BEGIN
/// emoji 面板点击搜索
- (void)keyBoardMainViewDidTapEmojiSearch:(KBKeyBoardMainView *)keyBoardMainView;
/// 选择了联想词
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion;
@end
@interface KBKeyBoardMainView : UIView
@@ -39,6 +42,9 @@ NS_ASSUME_NONNULL_BEGIN
/// 应用当前皮肤(会触发键区重载以应用按键颜色)
- (void)kb_applyTheme;
/// 更新联想候选
- (void)kb_setSuggestions:(NSArray<NSString *> *)suggestions;
@end
NS_ASSUME_NONNULL_END

View File

@@ -11,29 +11,50 @@
#import "KBFunctionView.h"
#import "KBKey.h"
#import "KBEmojiPanelView.h"
#import "KBSuggestionBarView.h"
#import "Masonry.h"
#import "KBSkinManager.h"
#import "KBBackspaceUndoManager.h"
#import "KBKeyboardLayoutConfig.h"
@interface KBKeyBoardMainView ()<KBToolBarDelegate, KBKeyboardViewDelegate, KBEmojiPanelViewDelegate>
@interface KBKeyBoardMainView ()<KBToolBarDelegate, KBKeyboardViewDelegate, KBEmojiPanelViewDelegate, KBSuggestionBarViewDelegate>
@property (nonatomic, strong) KBToolBar *topBar;
@property (nonatomic, strong) KBSuggestionBarView *suggestionBar;
@property (nonatomic, strong) KBKeyboardView *keyboardView;
@property (nonatomic, strong) KBEmojiPanelView *emojiView;
@property (nonatomic, assign) BOOL emojiPanelVisible;
@property (nonatomic, assign) BOOL suggestionBarHasItems;
// /
@end
@implementation KBKeyBoardMainView
- (instancetype)initWithFrame:(CGRect)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.delegate = self;
[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);
KBKeyboardLayoutConfig *layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
if (layoutConfig) {
CGFloat configHeight = [layoutConfig keyboardAreaScaledHeight];
if (configHeight > 0.0) {
keyboardAreaHeight = configHeight;
}
}
CGFloat bottomInset = KBFit(4.0f);
// CGFloat topBarHeight = KBFit(40.0f);
CGFloat barSpacing = KBFit(6.0f);
self.keyboardView = [[KBKeyboardView alloc] init];
@@ -54,16 +75,39 @@
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) {
make.left.right.equalTo(self);
make.top.equalTo(self.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;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)setEmojiPanelVisible:(BOOL)visible animated:(BOOL)animated {
if (self.emojiPanelVisible == visible) return;
self.emojiPanelVisible = visible;
@@ -74,17 +118,24 @@
} else {
self.keyboardView.hidden = NO;
self.topBar.hidden = NO;
self.suggestionBar.hidden = !self.suggestionBarHasItems;
}
void (^changes)(void) = ^{
self.emojiView.alpha = visible ? 1.0 : 0.0;
self.keyboardView.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) {
self.emojiView.hidden = !visible;
self.keyboardView.hidden = visible;
self.topBar.hidden = visible;
if (visible) {
self.suggestionBar.hidden = YES;
} else {
self.suggestionBar.hidden = ![self kb_shouldShowSuggestions];
}
};
if (animated) {
@@ -204,17 +255,50 @@
- (void)kb_applyTheme {
KBSkinManager *mgr = [KBSkinManager shared];
BOOL hasImg = ([mgr currentBackgroundImage] != nil);
UIColor *bg = mgr.current.keyboardBackground;
self.backgroundColor = hasImg ? [UIColor clearColor] : bg;
self.keyboardView.backgroundColor = hasImg ? [UIColor clearColor] : bg;
self.backgroundColor = [UIColor clearColor];
self.keyboardView.backgroundColor = [UIColor clearColor];
if ([self.topBar respondsToSelector:@selector(kb_applyTheme)]) {
[self.topBar kb_applyTheme];
}
[self.suggestionBar applyTheme:mgr.current];
[self.keyboardView reloadKeys];
if (self.emojiView) {
[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

View File

@@ -11,6 +11,7 @@
@property (nonatomic, strong) KBKey *key;
@property (nonatomic, strong) UIImageView *iconView;
@property (nonatomic, strong, nullable) UIColor *customBackgroundColor;
/// 配置基础样式(背景、圆角等)。创建按钮时调用。
- (void)applyDefaultStyle;

View File

@@ -6,12 +6,14 @@
#import "KBKeyButton.h"
#import "KBKey.h"
#import "KBSkinManager.h"
#import <QuartzCore/QuartzCore.h>
@interface KBKeyButton ()
// 便 KBKeyboardView
@property (nonatomic, weak, readonly) UIView *kb_keyboardContainer;
@property (nonatomic, strong) UIImageView *normalImageView; ///
@property (nonatomic, strong) UIColor *baseBackgroundColor; /// / normalImageView
@property (nonatomic, strong) CAGradientLayer *bottomShadowLayer;
@end
@@ -24,8 +26,8 @@
[NSLayoutConstraint activateConstraints:@[
[self.normalImageView.topAnchor constraintEqualToAnchor:self.topAnchor],
[self.normalImageView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[self.normalImageView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:2],
[self.normalImageView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-2],
[self.normalImageView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[self.normalImageView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
]];
[self applyDefaultStyle];
}
@@ -48,6 +50,7 @@
// 使
[self refreshStateAppearance];
[self kb_setupBottomShadowIfNeeded];
//
if (!self.iconView) {
@@ -61,8 +64,8 @@
[NSLayoutConstraint activateConstraints:@[
[iv.topAnchor constraintEqualToAnchor:self.topAnchor],
[iv.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[iv.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:2],
[iv.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-2],
[iv.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[iv.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
]];
self.iconView = iv;
@@ -72,6 +75,24 @@
}
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!self.bottomShadowLayer) { return; }
CGRect bounds = self.normalImageView.bounds;
CGFloat shadowHeight = 2;
if (CGRectGetHeight(bounds) <= 0 || CGRectGetWidth(bounds) <= 0) {
return;
}
//
if (self.iconView.image != nil) {
self.titleLabel.hidden = YES;
}
self.bottomShadowLayer.frame = CGRectMake(0,
CGRectGetHeight(bounds) - shadowHeight,
CGRectGetWidth(bounds),
shadowHeight);
}
- (void)setKey:(KBKey *)key {
_key = key;
}
@@ -121,14 +142,25 @@
[self refreshStateAppearance];
}
- (void)setCustomBackgroundColor:(UIColor *)customBackgroundColor {
_customBackgroundColor = customBackgroundColor;
[self refreshStateAppearance];
}
- (void)refreshStateAppearance {
// Shift/CapsLock
KBSkinTheme *t = [KBSkinManager shared].current;
UIColor *base = nil;
if (self.isSelected) {
base = t.keyHighlightBackground ?: t.keyBackground;
if (self.customBackgroundColor) {
base = t.keyHighlightBackground ?: self.customBackgroundColor;
}
} else {
base = t.keyBackground;
base = self.customBackgroundColor ?: t.keyBackground;
}
if (self.customBackgroundColor && self.key.type == KBKeyTypeShift) {
base = self.customBackgroundColor;
}
if (!base) {
base = [UIColor whiteColor];
@@ -138,6 +170,13 @@
// normalImageView
self.backgroundColor = [UIColor clearColor];
if (self.key.type == KBKeyTypeShift) {
UIColor *textColor = self.isSelected ? [UIColor blackColor] : (t.keyTextColor ?: [UIColor blackColor]);
[self setTitleColor:textColor forState:UIControlStateNormal];
[self setTitleColor:textColor forState:UIControlStateHighlighted];
[self setTitleColor:textColor forState:UIControlStateSelected];
}
// icon
if (self.iconView.image != nil || self.normalImageView.hidden) {
return;
@@ -169,6 +208,7 @@
BOOL hasIcon = (iconImg != nil);
self.normalImageView.hidden = hasIcon;
self.bottomShadowLayer.hidden = hasIcon;
if (hasIcon) {
//
[self setTitle:@"" forState:UIControlStateNormal];
@@ -184,6 +224,19 @@
}
}
- (void)kb_setupBottomShadowIfNeeded {
if (self.bottomShadowLayer) { return; }
CAGradientLayer *layer = [CAGradientLayer layer];
layer.startPoint = CGPointMake(0.5, 0.0);
layer.endPoint = CGPointMake(0.5, 1.0);
layer.colors = @[
(id)[UIColor colorWithWhite:0 alpha:0.5].CGColor,
(id)[UIColor colorWithWhite:0 alpha:0.7].CGColor
];
[self.normalImageView.layer addSublayer:layer];
// self.bottomShadowLayer = layer;
}
- (UIImageView *)normalImageView{
if (!_normalImageView) {
_normalImageView = [[UIImageView alloc] init];

View File

@@ -9,6 +9,7 @@
#import "KBSkinManager.h"
#import "KBKeyPreviewView.h"
#import "KBBackspaceLongPressHandler.h"
#import "KBKeyboardLayoutConfig.h"
// UI 便 375 稿 KBFit
#define kKBRowVerticalSpacing KBFit(8.0f)
@@ -33,17 +34,19 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
@property (nonatomic, strong) NSArray<NSArray<KBKey *> *> *keysForRows;
@property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler;
@property (nonatomic, strong) KBKeyPreviewView *previewView;
@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig;
@end
@implementation KBKeyboardView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [KBSkinManager shared].current.keyboardBackground;
self.backgroundColor = [UIColor clearColor];
_layoutStyle = KBKeyboardLayoutStyleLetters;
// Shift
_shiftOn = NO;
_symbolsMoreOn = NO; // 123
self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self];
[self buildBase];
[self reloadKeys];
@@ -67,26 +70,39 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
[self addSubview:self.row3];
[self addSubview:self.row4];
KBKeyboardLayoutConfig *config = [self kb_layoutConfig];
KBKeyboardLayout *layout = [self kb_layoutForName:@"letters"];
NSArray<KBKeyboardRowConfig *> *rows = layout.rows ?: @[];
CGFloat rowSpacing = [self kb_metricValue:config.metrics.rowSpacing fallback:nil defaultValue:8.0];
CGFloat topInset = [self kb_metricValue:config.metrics.topInset fallback:nil defaultValue:8.0];
CGFloat bottomInset = [self kb_metricValue:config.metrics.bottomInset fallback:nil defaultValue:6.0];
CGFloat row1Height = [self kb_rowHeightForRow:(rows.count > 0 ? rows[0] : nil)];
CGFloat row2Height = [self kb_rowHeightForRow:(rows.count > 1 ? rows[1] : nil)];
CGFloat row3Height = [self kb_rowHeightForRow:(rows.count > 2 ? rows[2] : nil)];
CGFloat row4Height = [self kb_rowHeightForRow:(rows.count > 3 ? rows[3] : nil)];
[self.row1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.mas_top).offset(kKBRowVerticalSpacing);
make.top.equalTo(self.mas_top).offset(topInset);
make.left.right.equalTo(self);
make.height.mas_equalTo(kKBRowHeight);
make.height.mas_equalTo(row1Height);
}];
[self.row2 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.row1.mas_bottom).offset(kKBRowVerticalSpacing);
make.top.equalTo(self.row1.mas_bottom).offset(rowSpacing);
make.left.right.equalTo(self);
make.height.equalTo(self.row1);
make.height.mas_equalTo(row2Height);
}];
[self.row3 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.row2.mas_bottom).offset(kKBRowVerticalSpacing);
make.top.equalTo(self.row2.mas_bottom).offset(rowSpacing);
make.left.right.equalTo(self);
make.height.equalTo(self.row1);
make.height.mas_equalTo(row3Height);
}];
[self.row4 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.row3.mas_bottom).offset(kKBRowVerticalSpacing);
make.top.equalTo(self.row3.mas_bottom).offset(rowSpacing);
make.left.right.equalTo(self);
make.height.equalTo(self.row1);
make.bottom.equalTo(self.mas_bottom).offset(-6);
make.height.mas_equalTo(row4Height);
make.bottom.equalTo(self.mas_bottom).offset(-bottomInset);
}];
}
@@ -99,18 +115,92 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
[row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
}
self.keysForRows = [self buildKeysForCurrentLayout];
if (self.keysForRows.count < 4) return;
KBKeyboardLayout *layout = [self kb_currentLayout];
NSArray<KBKeyboardRowConfig *> *rows = layout.rows ?: @[];
if (rows.count < 4) {
[self kb_buildLegacyLayout];
return;
}
[self buildRow:self.row1 withKeys:self.keysForRows[0]];
[self buildRow:self.row1 withRowConfig:rows[0]];
[self buildRow:self.row2 withRowConfig:rows[1]];
[self buildRow:self.row3 withRowConfig:rows[2]];
[self buildRow:self.row4 withRowConfig:rows[3]];
}
//
CGFloat row2Spacer = (self.layoutStyle == KBKeyboardLayoutStyleLetters)
? kKBLettersRow2EdgeSpacerMultiplier : 0.0;
[self buildRow:self.row2 withKeys:self.keysForRows[1] edgeSpacerMultiplier:row2Spacer];
#pragma mark - Hit Test
[self buildRow:self.row3 withKeys:self.keysForRows[2]];
[self buildRow:self.row4 withKeys:self.keysForRows[3]];
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hit = [super hitTest:point withEvent:event];
if ([hit isKindOfClass:[KBKeyButton class]]) {
return hit;
}
if ([self kb_isHitInsideKeyRows:hit]) {
KBKeyButton *btn = [self kb_nearestKeyButtonForPoint:point];
if (btn) { return btn; }
}
return hit;
}
- (BOOL)kb_isHitInsideKeyRows:(UIView *)hitView {
if (!hitView) { return NO; }
if (hitView == self) { return YES; }
if ([hitView isDescendantOfView:self.row1]) { return YES; }
if ([hitView isDescendantOfView:self.row2]) { return YES; }
if ([hitView isDescendantOfView:self.row3]) { return YES; }
if ([hitView isDescendantOfView:self.row4]) { return YES; }
return NO;
}
- (KBKeyButton *)kb_nearestKeyButtonForPoint:(CGPoint)point {
KBKeyButton *best = nil;
CGFloat bestDistance = CGFLOAT_MAX;
NSArray<UIView *> *rows = @[self.row1, self.row2, self.row3, self.row4];
UIView *targetRow = nil;
for (UIView *row in rows) {
CGRect rowFrame = [self convertRect:row.bounds fromView:row];
if (CGRectContainsPoint(rowFrame, point)) {
targetRow = row;
break;
}
}
NSArray<UIView *> *candidateRows = targetRow ? @[targetRow] : rows;
for (UIView *row in candidateRows) {
NSArray<KBKeyButton *> *buttons = [self kb_collectKeyButtonsInView:row];
for (KBKeyButton *btn in buttons) {
CGRect frame = [self convertRect:btn.frame fromView:btn.superview];
CGFloat dx = point.x - CGRectGetMidX(frame);
CGFloat dy = point.y - CGRectGetMidY(frame);
CGFloat dist = (dx * dx) + (dy * dy);
if (dist < bestDistance) {
bestDistance = dist;
best = btn;
}
}
}
return best;
}
- (NSArray<KBKeyButton *> *)kb_collectKeyButtonsInView:(UIView *)view {
if (!view) { return @[]; }
NSMutableArray<KBKeyButton *> *buttons = [NSMutableArray array];
[self kb_collectKeyButtonsInView:view into:buttons];
return buttons.copy;
}
- (void)kb_collectKeyButtonsInView:(UIView *)view
into:(NSMutableArray<KBKeyButton *> *)buttons {
for (UIView *sub in view.subviews) {
if ([sub isKindOfClass:[KBKeyButton class]]) {
[buttons addObject:(KBKeyButton *)sub];
continue;
}
if (sub.subviews.count > 0) {
[self kb_collectKeyButtonsInView:sub into:buttons];
}
}
}
#pragma mark - Key Model Construction
@@ -315,6 +405,152 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
#pragma mark - Row Building
- (void)buildRow:(UIView *)row withRowConfig:(KBKeyboardRowConfig *)rowConfig {
if (!row || !rowConfig) { return; }
CGFloat gap = [self kb_gapForRow:rowConfig];
CGFloat insetLeft = [self kb_insetLeftForRow:rowConfig];
CGFloat insetRight = [self kb_insetRightForRow:rowConfig];
if (rowConfig.segments) {
KBKeyboardRowSegments *segments = rowConfig.segments;
NSArray<KBKeyboardRowItem *> *leftItems = [segments leftItems];
NSArray<KBKeyboardRowItem *> *centerItems = [segments centerItems];
NSArray<KBKeyboardRowItem *> *rightItems = [segments rightItems];
UIView *leftContainer = [UIView new];
UIView *centerContainer = [UIView new];
UIView *rightContainer = [UIView new];
[row addSubview:leftContainer];
[row addSubview:centerContainer];
[row addSubview:rightContainer];
[leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(row.mas_left).offset(insetLeft);
make.top.bottom.equalTo(row);
}];
[rightContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(row.mas_right).offset(-insetRight);
make.top.bottom.equalTo(row);
}];
[centerContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(row);
make.top.bottom.equalTo(row);
make.left.greaterThanOrEqualTo(leftContainer.mas_right).offset(gap);
make.right.lessThanOrEqualTo(rightContainer.mas_left).offset(-gap);
}];
if (leftItems.count == 0) {
[leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(0);
}];
}
if (centerItems.count == 0) {
[centerContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(0);
}];
}
if (rightItems.count == 0) {
[rightContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(0);
}];
}
[self kb_buildButtonsInContainer:leftContainer
items:leftItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO];
[self kb_buildButtonsInContainer:centerContainer
items:centerItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO];
[self kb_buildButtonsInContainer:rightContainer
items:rightItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO];
return;
}
BOOL alignCenter = [rowConfig.align.lowercaseString isEqualToString:@"center"];
[self kb_buildButtonsInContainer:row
items:[rowConfig resolvedItems]
gap:gap
insetLeft:insetLeft
insetRight:insetRight
alignCenter:alignCenter];
}
- (void)kb_buildButtonsInContainer:(UIView *)container
items:(NSArray<KBKeyboardRowItem *> *)items
gap:(CGFloat)gap
insetLeft:(CGFloat)insetLeft
insetRight:(CGFloat)insetRight
alignCenter:(BOOL)alignCenter {
if (items.count == 0) { return; }
UIView *leftSpacer = nil;
UIView *rightSpacer = nil;
if (alignCenter) {
leftSpacer = [UIView new];
rightSpacer = [UIView new];
[container addSubview:leftSpacer];
[container addSubview:rightSpacer];
[leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(container.mas_left).offset(insetLeft);
make.top.bottom.equalTo(container);
}];
[rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(container.mas_right).offset(-insetRight);
make.top.bottom.equalTo(container);
}];
[leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(rightSpacer);
}];
}
KBKeyButton *previous = nil;
for (KBKeyboardRowItem *item in items) {
KBKeyButton *btn = [self kb_buttonForItem:item];
if (!btn) { continue; }
[container addSubview:btn];
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.equalTo(container);
if (previous) {
make.left.equalTo(previous.mas_right).offset(gap);
} else {
if (leftSpacer) {
make.left.equalTo(leftSpacer.mas_right).offset(gap);
} else {
make.left.equalTo(container.mas_left).offset(insetLeft);
}
}
}];
CGFloat width = [self kb_widthForItem:item key:btn.key];
if (width > 0.0) {
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(width);
}];
}
previous = btn;
}
if (!previous) { return; }
[previous mas_makeConstraints:^(MASConstraintMaker *make) {
if (rightSpacer) {
make.right.equalTo(rightSpacer.mas_left).offset(-gap);
} else {
make.right.equalTo(container.mas_right).offset(-insetRight);
}
}];
}
- (void)buildRow:(UIView *)row withKeys:(NSArray<KBKey *> *)keys {
[self buildRow:row withKeys:keys edgeSpacerMultiplier:0.0];
}
@@ -581,6 +817,386 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier {
// Space
}
#pragma mark - Config Helpers
- (KBKeyboardLayoutConfig *)kb_layoutConfig {
if (!self.layoutConfig) {
self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
}
return self.layoutConfig;
}
- (KBKeyboardLayout *)kb_layoutForName:(NSString *)name {
return [[self kb_layoutConfig] layoutForName:name];
}
- (KBKeyboardLayout *)kb_currentLayout {
if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) {
return [self kb_layoutForName:(self.symbolsMoreOn ? @"symbolsMore" : @"numbers")];
}
return [self kb_layoutForName:@"letters"];
}
- (void)kb_buildLegacyLayout {
self.keysForRows = [self buildKeysForCurrentLayout];
if (self.keysForRows.count < 4) { return; }
[self buildRow:self.row1 withKeys:self.keysForRows[0]];
CGFloat row2Spacer = (self.layoutStyle == KBKeyboardLayoutStyleLetters)
? kKBLettersRow2EdgeSpacerMultiplier : 0.0;
[self buildRow:self.row2 withKeys:self.keysForRows[1] edgeSpacerMultiplier:row2Spacer];
[self buildRow:self.row3 withKeys:self.keysForRows[2]];
[self buildRow:self.row4 withKeys:self.keysForRows[3]];
}
- (CGFloat)kb_scaledValue:(CGFloat)designValue {
KBKeyboardLayoutConfig *config = [self kb_layoutConfig];
if (config) {
return [config scaledValue:designValue];
}
return KBFit(designValue);
}
- (CGFloat)kb_numberValue:(NSNumber *)value defaultValue:(CGFloat)defaultValue {
if ([value isKindOfClass:[NSNumber class]]) {
return value.doubleValue;
}
return defaultValue;
}
- (CGFloat)kb_metricValue:(NSNumber *)value fallback:(NSNumber *)fallback defaultValue:(CGFloat)defaultValue {
CGFloat v = [self kb_numberValue:value defaultValue:-1.0];
if (v < 0.0) {
v = [self kb_numberValue:fallback defaultValue:defaultValue];
}
if (v < 0.0) {
v = defaultValue;
}
return [self kb_scaledValue:v];
}
- (CGFloat)kb_rowHeightForRow:(KBKeyboardRowConfig *)row {
KBKeyboardLayoutConfig *config = [self kb_layoutConfig];
NSNumber *height = row.height ?: config.metrics.keyHeight;
CGFloat value = [self kb_numberValue:height defaultValue:40.0];
return [self kb_scaledValue:value];
}
- (CGFloat)kb_gapForRow:(KBKeyboardRowConfig *)row {
KBKeyboardLayoutConfig *config = [self kb_layoutConfig];
return [self kb_metricValue:row.gap fallback:config.metrics.gap defaultValue:5.0];
}
- (CGFloat)kb_insetLeftForRow:(KBKeyboardRowConfig *)row {
KBKeyboardLayoutConfig *config = [self kb_layoutConfig];
return [self kb_metricValue:row.insetLeft fallback:config.metrics.edgeInset defaultValue:0.0];
}
- (CGFloat)kb_insetRightForRow:(KBKeyboardRowConfig *)row {
KBKeyboardLayoutConfig *config = [self kb_layoutConfig];
return [self kb_metricValue:row.insetRight fallback:config.metrics.edgeInset defaultValue:0.0];
}
- (KBKeyButton *)kb_buttonForItem:(KBKeyboardRowItem *)item {
if (item.itemId.length == 0) { return nil; }
KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:item.itemId];
KBKey *key = [self kb_keyForItemId:item.itemId];
if (!key) { return nil; }
KBKeyButton *btn = [[KBKeyButton alloc] init];
btn.key = key;
[btn setTitle:key.title forState:UIControlStateNormal];
UIColor *bgColor = [self kb_backgroundColorForItem:item keyDef:def];
if (bgColor) {
btn.customBackgroundColor = bgColor;
}
CGFloat fontSize = [self kb_fontSizeForItem:item key:key];
if (fontSize > 0.0) {
btn.titleLabel.font = [UIFont systemFontOfSize:fontSize weight:UIFontWeightSemibold];
}
[btn applyThemeForCurrentKey];
[btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside];
if (key.type == KBKeyTypeBackspace) {
[self.backspaceHandler bindDeleteButton:btn showClearLabel:YES];
}
if (key.type == KBKeyTypeShift) {
btn.selected = self.shiftOn;
}
[self kb_applySymbolIfNeededForButton:btn keyDef:def fontSize:fontSize];
return btn;
}
- (void)kb_applySymbolIfNeededForButton:(KBKeyButton *)button
keyDef:(KBKeyboardKeyDef *)def
fontSize:(CGFloat)fontSize {
if (!button || !def) { return; }
if (button.iconView.image != nil) { return; }
NSString *symbolName = button.isSelected ? def.selectedSymbolName : def.symbolName;
if (symbolName.length == 0) { return; }
UIImage *image = [UIImage systemImageNamed:symbolName];
if (!image) { return; }
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:fontSize weight:UIFontWeightSemibold];
image = [image imageWithConfiguration:config];
button.iconView.image = image;
button.iconView.hidden = NO;
button.iconView.contentMode = UIViewContentModeCenter;
button.titleLabel.hidden = YES;
UIColor *textColor = [KBSkinManager shared].current.keyTextColor ?: [UIColor blackColor];
button.iconView.tintColor = button.isSelected ? [UIColor blackColor] : textColor;
}
- (UIColor *)kb_backgroundColorForItem:(KBKeyboardRowItem *)item keyDef:(KBKeyboardKeyDef *)def {
NSString *hex = def.backgroundColor;
if (hex.length == 0) {
hex = [self kb_layoutConfig].defaultKeyBackground;
}
if (hex.length == 0) { return nil; }
return [KBSkinManager colorFromHexString:hex defaultColor:nil];
}
- (CGFloat)kb_metricWidthForKey:(NSString *)key {
KBKeyboardLayoutMetrics *m = [self kb_layoutConfig].metrics;
if ([key isEqualToString:@"letterWidth"]) { return m.letterWidth.doubleValue; }
if ([key isEqualToString:@"controlWidth"]) { return m.controlWidth.doubleValue; }
if ([key isEqualToString:@"sendWidth"]) { return m.sendWidth.doubleValue; }
if ([key isEqualToString:@"symbolsWideWidth"]) { return m.symbolsWideWidth.doubleValue; }
if ([key isEqualToString:@"symbolsSideWidth"]) { return m.symbolsSideWidth.doubleValue; }
return 0.0;
}
- (CGFloat)kb_widthForItem:(KBKeyboardRowItem *)item key:(KBKey *)key {
CGFloat width = 0.0;
if (item.widthValue.doubleValue > 0.0) {
width = item.widthValue.doubleValue;
} else if (item.width.length > 0) {
if ([item.width.lowercaseString isEqualToString:@"flex"]) {
return 0.0;
}
width = [self kb_metricWidthForKey:item.width];
if (width <= 0.0) {
width = item.width.doubleValue;
}
}
if (width <= 0.0) {
KBKeyboardLayoutMetrics *m = [self kb_layoutConfig].metrics;
if ([item.itemId hasPrefix:@"letter:"] ||
[item.itemId hasPrefix:@"digit:"] ||
[item.itemId hasPrefix:@"sym:"]) {
width = m.letterWidth.doubleValue;
} else if (key.type == KBKeyTypeReturn) {
width = m.sendWidth.doubleValue;
} else if (key.type == KBKeyTypeSpace) {
return 0.0;
} else {
width = m.controlWidth.doubleValue;
}
}
if (width <= 0.0) {
if ([item.itemId hasPrefix:@"letter:"] ||
[item.itemId hasPrefix:@"digit:"] ||
[item.itemId hasPrefix:@"sym:"]) {
width = 32.0;
} else if (key.type == KBKeyTypeReturn) {
width = 88.0;
} else if (key.type == KBKeyTypeSpace) {
return 0.0;
} else {
width = 41.0;
}
}
return width > 0.0 ? [self kb_scaledValue:width] : 0.0;
}
- (CGFloat)kb_fontSizeForItem:(KBKeyboardRowItem *)item key:(KBKey *)key {
NSString *fontKey = nil;
if ([item.itemId hasPrefix:@"letter:"]) {
fontKey = @"letter";
} else if ([item.itemId hasPrefix:@"digit:"]) {
fontKey = @"digit";
} else if ([item.itemId hasPrefix:@"sym:"]) {
fontKey = @"symbol";
} else {
KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:item.itemId];
fontKey = def.font;
}
if (fontKey.length == 0) {
switch (key.type) {
case KBKeyTypeModeChange:
case KBKeyTypeSymbolsToggle:
fontKey = @"mode";
break;
case KBKeyTypeSpace:
fontKey = @"space";
break;
case KBKeyTypeReturn:
fontKey = @"send";
break;
default:
fontKey = @"symbol";
break;
}
}
return [self kb_fontSizeForFontKey:fontKey];
}
- (CGFloat)kb_fontSizeForFontKey:(NSString *)fontKey {
KBKeyboardLayoutFonts *fonts = [self kb_layoutConfig].fonts;
CGFloat size = 0.0;
if ([fontKey isEqualToString:@"letter"]) { size = fonts.letter.doubleValue; }
else if ([fontKey isEqualToString:@"digit"]) { size = fonts.digit.doubleValue; }
else if ([fontKey isEqualToString:@"symbol"]) { size = fonts.symbol.doubleValue; }
else if ([fontKey isEqualToString:@"mode"]) { size = fonts.mode.doubleValue; }
else if ([fontKey isEqualToString:@"space"]) { size = fonts.space.doubleValue; }
else if ([fontKey isEqualToString:@"send"]) { size = fonts.send.doubleValue; }
if (size <= 0.0) { size = 18.0; }
return [self kb_scaledValue:size];
}
- (KBKey *)kb_keyForItemId:(NSString *)itemId {
if (itemId.length == 0) { return nil; }
KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:itemId];
if (def) {
return [self kb_keyFromDef:def identifier:itemId];
}
NSRange range = [itemId rangeOfString:@":"];
if (range.location != NSNotFound) {
NSString *prefix = [itemId substringToIndex:range.location];
NSString *value = [itemId substringFromIndex:range.location + 1];
if ([prefix isEqualToString:@"letter"]) {
if (value.length == 1) {
return [self kb_letterKeyWithChar:value];
}
return nil;
}
if ([prefix isEqualToString:@"digit"]) {
NSString *identifier = [NSString stringWithFormat:@"digit_%@", value];
KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter];
k.caseVariant = KBKeyCaseVariantNone;
return k;
}
if ([prefix isEqualToString:@"sym"]) {
NSString *identifier = [self kb_identifierForSymbol:value];
KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter];
k.caseVariant = KBKeyCaseVariantNone;
return k;
}
}
return nil;
}
- (KBKey *)kb_keyFromDef:(KBKeyboardKeyDef *)def identifier:(NSString *)identifier {
KBKeyType type = [self kb_keyTypeForDef:def];
NSString *title = def.title ?: @"";
if (type == KBKeyTypeShift && self.shiftOn && def.selectedTitle.length > 0) {
title = def.selectedTitle;
}
NSString *output = @"";
switch (type) {
case KBKeyTypeSpace:
output = @" ";
break;
case KBKeyTypeReturn:
output = @"\n";
break;
default:
output = @"";
break;
}
NSString *finalId = identifier;
if ([identifier isEqualToString:@"emoji"]) {
finalId = KBKeyIdentifierEmojiPanel;
} else if ([identifier isEqualToString:@"send"]) {
finalId = @"return";
}
KBKey *k = [KBKey keyWithIdentifier:finalId title:title output:output type:type];
if (type == KBKeyTypeShift) {
k.caseVariant = self.shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower;
} else {
k.caseVariant = KBKeyCaseVariantNone;
}
return k;
}
- (KBKeyType)kb_keyTypeForDef:(KBKeyboardKeyDef *)def {
NSString *type = def.type.lowercaseString;
if ([type isEqualToString:@"shift"]) return KBKeyTypeShift;
if ([type isEqualToString:@"backspace"]) return KBKeyTypeBackspace;
if ([type isEqualToString:@"mode"]) return KBKeyTypeModeChange;
if ([type isEqualToString:@"symbolstoggle"]) return KBKeyTypeSymbolsToggle;
if ([type isEqualToString:@"space"]) return KBKeyTypeSpace;
if ([type isEqualToString:@"return"]) return KBKeyTypeReturn;
if ([type isEqualToString:@"globe"]) return KBKeyTypeGlobe;
return KBKeyTypeCustom;
}
- (NSString *)kb_identifierForSymbol:(NSString *)symbol {
if (symbol.length == 0) { return nil; }
static NSDictionary<NSString *, NSString *> *map = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
map = @{
@"-": @"sym_minus",
@"/": @"sym_slash",
@":": @"sym_colon",
@";": @"sym_semicolon",
@"(": @"sym_paren_l",
@")": @"sym_paren_r",
@"¥": @"sym_money",
@"¥": @"sym_money",
@"&": @"sym_amp",
@"@": @"sym_at",
@"\"": @"sym_quote_double",
@"“": @"sym_quote_double",
@"”": @"sym_quote_double",
@".": @"sym_dot",
@",": @"sym_comma",
@"?": @"sym_question",
@"!": @"sym_exclam",
@"'": @"sym_quote_single",
@"": @"sym_quote_single",
@"": @"sym_quote_single",
@"[": @"sym_bracket_l",
@"]": @"sym_bracket_r",
@"{": @"sym_brace_l",
@"}": @"sym_brace_r",
@"#": @"sym_hash",
@"%": @"sym_percent",
@"^": @"sym_caret",
@"*": @"sym_asterisk",
@"+": @"sym_plus",
@"=": @"sym_equal",
@"_": @"sym_underscore",
@"\\": @"sym_backslash",
@"|": @"sym_pipe",
@"~": @"sym_tilde",
@"<": @"sym_lt",
@">": @"sym_gt",
@"€": @"sym_euro",
@"$": @"sym_dollar",
@"·": @"sym_bullet"
};
});
return map[symbol];
}
#pragma mark - Actions
- (void)onKeyTapped:(KBKeyButton *)sender {

View File

@@ -9,6 +9,7 @@
#import "KBStreamTextView.h"
#import "KBResponderUtils.h" // UIInputViewController宿
#import "KBInputBufferManager.h"
@interface KBStreamTextView ()
@@ -371,6 +372,7 @@ static inline NSString *KBTrimRight(NSString *s) {
if (rawText.length > 0) {
[proxy insertText:rawText];
}
[[KBInputBufferManager shared] resetWithText:rawText ?: @""];
}
}
}

View 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

View 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

View File

@@ -23,6 +23,7 @@
@implementation KBToolBar
static NSString * const kKBAIKeyIdentifier = @"ai";
static NSString * const kKBUndoKeyIdentifier = @"key_revoke";
static const CGFloat kKBAIButtonWidth = 40;
static const CGFloat kKBAIButtonHeight = 40;
@@ -96,6 +97,7 @@ static const CGFloat kKBAIButtonHeight = 40;
make.right.equalTo(self.mas_right).offset(-12);
make.centerY.equalTo(self.mas_centerY);
make.height.mas_equalTo(32);
make.width.mas_equalTo(84);
}];
[self kb_updateLeftContainerConstraints];
@@ -169,6 +171,7 @@ static const CGFloat kKBAIButtonHeight = 40;
- (void)kb_applyTheme {
[self kb_updateAIButtonAppearance];
[self kb_updateUndoButtonAppearance];
}
- (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
- (void)onLeftAction:(UIButton *)sender {
@@ -262,14 +285,15 @@ static const CGFloat kKBAIButtonHeight = 40;
- (UIButton *)undoButtonInternal {
if (!_undoButtonInternal) {
_undoButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
_undoButtonInternal.layer.cornerRadius = 16;
_undoButtonInternal.layer.masksToBounds = YES;
_undoButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
_undoButtonInternal.titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
[_undoButtonInternal setTitle:@"撤销删除" forState:UIControlStateNormal];
[_undoButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
_undoButtonInternal.contentEdgeInsets = UIEdgeInsetsMake(0, 10, 0, 10);
_undoButtonInternal = [UIButton buttonWithType:UIButtonTypeCustom];
// _undoButtonInternal.layer.cornerRadius = 16;
// _undoButtonInternal.layer.masksToBounds = YES;
// _undoButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
// _undoButtonInternal.titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
// [_undoButtonInternal setTitle:@"撤销删除" forState:UIControlStateNormal];
// [_undoButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
// _undoButtonInternal.contentEdgeInsets = UIEdgeInsetsMake(0, 10, 0, 10);
[_undoButtonInternal setImage:[UIImage imageNamed:@"key_revoke"] forState:UIControlStateNormal];
_undoButtonInternal.hidden = YES;
_undoButtonInternal.alpha = 0.0;
[_undoButtonInternal addTarget:self action:@selector(onUndo) forControlEvents:UIControlEventTouchUpInside];

148
KBMaiPointEventTable.md Normal file
View 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

Binary file not shown.

View File

@@ -96,6 +96,6 @@ SPEC CHECKSUMS:
SDWebImage: f29024626962457f3470184232766516dee8dfea
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
PODFILE CHECKSUM: acf7541bd40dd969fa4950d6c000005b2889c85b
PODFILE CHECKSUM: 3b9d37a9d2c323afb33b6389f3c70184f53ea313
COCOAPODS: 1.16.2

2
Pods/Manifest.lock generated
View File

@@ -96,6 +96,6 @@ SPEC CHECKSUMS:
SDWebImage: f29024626962457f3470184232766516dee8dfea
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
PODFILE CHECKSUM: acf7541bd40dd969fa4950d6c000005b2889c85b
PODFILE CHECKSUM: 3b9d37a9d2c323afb33b6389f3c70184f53ea313
COCOAPODS: 1.16.2

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,7 @@
#define API_UPDATA_INFO @"/user/updateInfo" // 更新用户
#define KB_API_USER_DETAIL @"/user/detail" // 用户详情
#define API_USER_INVITE_CODE @"/user/inviteCode" // 查询邀请码
#define API_CHARACTER_LIST @"/character/list" // 排行榜角色列表(综合)
#define API_NOT_LOGIN_CHARACTER_LIST @"/character/listWithNotLogin" //未登录用户人设列表
@@ -56,6 +57,10 @@
#define API_THEME_DOWNLOAD @"/themes/download" // 主题下载信息
#define API_THEME_RECOMMENDED @"/themes/recommended" // 推荐主题列表
#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
#define API_VALIDATE_RECEIPT @"/apple/validate-receipt" // 排行榜标签列表
@@ -63,7 +68,7 @@
#define API_SUBSCRIPTION_PRODUCT_LIST @"/products/subscription/list" // 查询订阅商品列表
/// AI
#define API_AI_TALK @"/chat/talk" // 排行榜标签列表
#define API_AI_TALK @"/chat/talk"

View File

@@ -38,7 +38,8 @@
// 基础baseUrl
#ifndef KB_BASE_URL
//#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
#import "KBFont.h"
@@ -87,7 +88,8 @@
#if __OBJC__
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) {

20
Shared/KBLog.h Normal file
View 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

View 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
View 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 {
// tokennil -> @"" / 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

View File

@@ -24,6 +24,7 @@ static NSString * const kKBSkinPendingKindKey = @"kind";
static NSString * const kKBSkinPendingTimestampKey = @"timestamp";
static NSString * const kKBSkinPendingIconShortKey = @"iconShortNames";
static NSString * const kKBSkinMetadataFileName = @"metadata.plist";
static NSString * const kKBSkinForceDownloadKey = @"force_download";
static NSString * const kKBSkinMetadataNameKey = @"name";
static NSString * const kKBSkinMetadataPreviewKey = @"preview";
static NSString * const kKBSkinMetadataZipKey = @"zip_url";
@@ -220,6 +221,17 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
NSString *skinId = skinJSON[@"id"] ?: @"remote";
NSString *name = skinJSON[@"name"] ?: skinId;
NSString *zipURL = skinJSON[@"zip_url"] ?: @"";
BOOL forceDownload = NO;
id forceValue = skinJSON[kKBSkinForceDownloadKey];
if ([forceValue respondsToSelector:@selector(boolValue)]) {
forceDownload = [forceValue boolValue];
}
id serverIcons = skinJSON[@"key_icons"];
NSUInteger serverIconCount = [serverIcons isKindOfClass:NSDictionary.class] ? ((NSDictionary *)serverIcons).count : 0;
NSLog(@"[SkinBridge] request id=%@ zip=%@ force=%d key_icons_class=%@ count=%tu",
skinId, zipURL, forceDownload,
serverIcons ? NSStringFromClass([serverIcons class]) : @"nil",
serverIconCount);
// key_icons
// - key_icons使
@@ -230,6 +242,9 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
} else {
iconShortNames = [self defaultIconShortNames];
}
NSLog(@"[SkinBridge] iconShortNames source=%@ count=%tu",
[skinJSON[@"key_icons"] isKindOfClass:NSDictionary.class] ? @"server" : @"default",
iconShortNames.count);
NSFileManager *fm = [NSFileManager defaultManager];
NSURL *containerURL = [fm containerURLForSecurityApplicationGroupIdentifier:AppGroup];
@@ -256,8 +271,24 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
NSArray *contents = hasIconsDir ? [fm contentsOfDirectoryAtPath:iconsDir error:NULL] : nil;
//
BOOL hasCachedAssets = (contents.count > 0);
NSLog(@"[SkinBridge] assets cache id=%@ cached=%d iconsDir=%@", skinId, hasCachedAssets, iconsDir);
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();
__block BOOL zipOK = YES;
@@ -265,8 +296,8 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
__block NSError *innerError = nil;
#if __has_include(<SSZipArchive/SSZipArchive.h>)
// zip_url Zip
if (!hasCachedAssets && zipURL.length > 0) {
// zip_url Zip
if ((forceDownload || !hasCachedAssets) && zipURL.length > 0) {
dispatch_group_enter(group);
void (^handleZipData)(NSData *) = ^(NSData *data) {
@@ -277,15 +308,17 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
code:KBSkinBridgeErrorZipMissing
userInfo:@{NSLocalizedDescriptionKey: @"Zip data is empty"}];
}
NSLog(@"❌[SkinBridge] zip data empty id=%@", skinId);
dispatch_group_leave(group);
return;
}
NSLog(@"📦[SkinBridge] unzip start id=%@ temp=%d", skinId, useTempRoot);
// Zip
[fm createDirectoryAtPath:skinRoot
[fm createDirectoryAtPath:workingRoot
withIntermediateDirectories:YES
attributes:nil
error:NULL];
NSString *zipPath = [skinRoot stringByAppendingPathComponent:@"skin.zip"];
NSString *zipPath = [workingRoot stringByAppendingPathComponent:@"skin.zip"];
if (![data writeToFile:zipPath atomically:YES]) {
zipOK = NO;
if (!innerError) {
@@ -293,13 +326,14 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
code:KBSkinBridgeErrorUnzipFailed
userInfo:@{NSLocalizedDescriptionKey: @"Failed to write zip file"}];
}
NSLog(@"❌[SkinBridge] zip write failed id=%@", skinId);
dispatch_group_leave(group);
return;
}
NSError *unzipError = nil;
BOOL ok = [SSZipArchive unzipFileAtPath:zipPath
toDestination:skinRoot
toDestination:workingRoot
overwrite:YES
password:nil
error:&unzipError];
@@ -311,24 +345,22 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
code:KBSkinBridgeErrorUnzipFailed
userInfo:nil];
}
NSLog(@"❌[SkinBridge] unzip failed id=%@ error=%@", skinId, unzipError);
dispatch_group_leave(group);
return;
}
// 使 icons
didUnzip = YES;
//
// Skins/<skinId>/icons Skins/<skinId>/<>/icons
// icons background.png
BOOL isDir2 = NO;
NSArray *iconsContent = [fm contentsOfDirectoryAtPath:iconsDir error:NULL];
BOOL iconsValid = ([fm fileExistsAtPath:iconsDir isDirectory:&isDir2] && isDir2 && iconsContent.count > 0);
NSArray *iconsContent = [fm contentsOfDirectoryAtPath:workingIconsDir error:NULL];
BOOL iconsValid = ([fm fileExistsAtPath:workingIconsDir isDirectory:&isDir2] && isDir2 && iconsContent.count > 0);
if (!iconsValid) {
NSArray<NSString *> *subItems = [fm contentsOfDirectoryAtPath:skinRoot error:NULL];
NSArray<NSString *> *subItems = [fm contentsOfDirectoryAtPath:workingRoot error:NULL];
for (NSString *subName in subItems) {
if ([subName isEqualToString:@"icons"] || [subName isEqualToString:@"__MACOSX"]) continue;
NSString *nestedRoot = [skinRoot stringByAppendingPathComponent:subName];
NSString *nestedRoot = [workingRoot stringByAppendingPathComponent:subName];
BOOL isDirNested = NO;
if (![fm fileExistsAtPath:nestedRoot isDirectory:&isDirNested] || !isDirNested) continue;
@@ -338,14 +370,14 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
NSArray *nestedFiles = [fm contentsOfDirectoryAtPath:nestedIcons error:NULL];
if (nestedFiles.count > 0) {
// icons
[fm createDirectoryAtPath:iconsDir
[fm createDirectoryAtPath:workingIconsDir
withIntermediateDirectories:YES
attributes:nil
error:NULL];
// icons
for (NSString *fn in nestedFiles) {
NSString *from = [nestedIcons stringByAppendingPathComponent:fn];
NSString *to = [iconsDir stringByAppendingPathComponent:fn];
NSString *to = [workingIconsDir stringByAppendingPathComponent:fn];
[fm removeItemAtPath:to error:nil];
[fm moveItemAtPath:from toPath:to error:nil];
}
@@ -355,20 +387,65 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
// background.png skinRoot
NSString *nestedBg = [nestedRoot stringByAppendingPathComponent:@"background.png"];
if ([fm fileExistsAtPath:nestedBg]) {
[fm removeItemAtPath:bgPath error:nil];
[fm moveItemAtPath:nestedBg toPath:bgPath error:nil];
[fm removeItemAtPath:workingBgPath 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);
};
#if __has_include("KBNetworkManager.h")
// http/https
NSLog(@"[SkinBridge] will GET zip: %@", zipURL);
[KBHUD show];
NSLog(@"🌐[SkinBridge] will GET zip: %@", zipURL);
[KBHUD showWithStatus:@"正在下载..."];
[[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) {
zipOK = NO;
if (!innerError) {
@@ -399,6 +476,9 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
}
});
#endif
} else {
NSLog(@"[SkinBridge] skip download id=%@ force=%d cached=%d zip=%@",
skinId, forceDownload, hasCachedAssets, zipURL);
}
#else
zipOK = NO;
@@ -411,16 +491,21 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//
// B
BOOL hasAssets = (hasCachedAssets || didUnzip);
BOOL hasAssets = (didUnzip || (!forceDownload && hasCachedAssets));
NSLog(@"[SkinBridge] apply check id=%@ hasAssets=%d didUnzip=%d cached=%d",
skinId, hasAssets, didUnzip, hasCachedAssets);
if (!hasAssets) {
NSError *finalError = innerError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain
code:KBSkinBridgeErrorZipMissing
userInfo:@{NSLocalizedDescriptionKey: @"Zip resource not available"}];
NSLog(@"❌[SkinBridge] apply aborted id=%@ error=%@", skinId, finalError);
if (completion) completion(NO, finalError);
return;
}
// key_icons -> App Group
// key_icons -> App Group
NSString *iconsDirFinal = [skinRoot stringByAppendingPathComponent:@"icons"];
__block NSUInteger missingCount = 0;
NSMutableDictionary<NSString *, NSString *> *iconPathMap = [NSMutableDictionary dictionary];
[iconShortNames enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, NSString *shortName, BOOL *stop) {
if (![shortName isKindOfClass:NSString.class] || shortName.length == 0) return;
@@ -429,9 +514,27 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
if (fileName.pathExtension.length == 0) {
fileName = [fileName stringByAppendingPathExtension:@"png"];
}
NSString *fullPath = [iconsDirFinal stringByAppendingPathComponent:fileName];
if (![fm fileExistsAtPath:fullPath]) {
missingCount += 1;
if (missingCount <= 5) {
NSLog(@"[SkinBridge] icon missing id=%@ short=%@", identifier, fileName);
}
return;
}
NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@", skinId, fileName];
iconPathMap[identifier] = relative;
}];
if (missingCount > 0) {
NSLog(@"[SkinBridge] icon missing count=%tu total=%tu", missingCount, iconShortNames.count);
}
NSLog(@"[SkinBridge] iconPathMap count=%tu shift=%@ shift_upper=%@ backspace=%@ mode_123=%@ return=%@",
iconPathMap.count,
iconPathMap[@"shift"],
iconPathMap[@"shift_upper"],
iconPathMap[@"backspace"],
iconPathMap[@"mode_123"],
iconPathMap[@"return"]);
NSMutableDictionary *themeJSON = [skinJSON mutableCopy];
themeJSON[@"id"] = skinId;
@@ -444,6 +547,8 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
// Zip background.png
NSData *bgData = [NSData dataWithContentsOfFile:bgPath];
BOOL ok = themeOK;
NSLog(@"[SkinBridge] theme apply id=%@ themeOK=%d bg=%d",
skinId, themeOK, (bgData.length > 0));
if (bgData.length > 0) {
ok = [[KBSkinManager shared] applyImageSkinWithData:bgData skinId:skinId name:name];
}
@@ -459,6 +564,10 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
userInfo:nil];
}
if (completion) completion(ok, finalError);
NSLog(@"%@ [SkinBridge] apply %@ id=%@",
(ok ? @"✅" : @"❌"),
(ok ? @"ok" : @"failed"),
skinId);
if (ok) {
NSString *preview = [skinJSON[@"preview"] isKindOfClass:NSString.class] ? skinJSON[@"preview"] : nil;
[self recordInstalledSkinWithId:skinId
@@ -673,6 +782,8 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
shortNames = [self defaultIconShortNames];
}
NSString *iconsDirFinal = iconsDir;
__block NSUInteger missingCount = 0;
NSMutableDictionary<NSString *, NSString *> *iconPathMap = [NSMutableDictionary dictionary];
[shortNames enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, NSString *shortName, BOOL *stop) {
if (identifier.length == 0 || ![shortName isKindOfClass:NSString.class] || shortName.length == 0) return;
@@ -680,9 +791,20 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
if (fileName.pathExtension.length == 0) {
fileName = [fileName stringByAppendingPathExtension:@"png"];
}
NSString *fullPath = [iconsDirFinal stringByAppendingPathComponent:fileName];
if (![fm fileExistsAtPath:fullPath]) {
missingCount += 1;
if (missingCount <= 5) {
NSLog(@"[SkinBridge] icon missing(bundle) id=%@ short=%@", identifier, fileName);
}
return;
}
NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@", skinId, fileName];
iconPathMap[identifier] = relative;
}];
if (missingCount > 0) {
NSLog(@"[SkinBridge] icon missing(bundle) count=%tu total=%tu", missingCount, shortNames.count);
}
NSMutableDictionary *themeJSON = [NSMutableDictionary dictionary];
themeJSON[@"id"] = skinId;
@@ -766,4 +888,3 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
}
@end

View File

@@ -152,11 +152,24 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
if ([icons isKindOfClass:NSDictionary.class]) {
t.keyIconMap = icons;
}
NSUInteger iconCount = [t.keyIconMap isKindOfClass:NSDictionary.class] ? t.keyIconMap.count : 0;
NSUInteger hiddenCount = t.hiddenKeyTextIdentifiers.count;
NSLog(@"[SkinManager] applyThemeFromJSON id=%@ name=%@ iconMap=%tu hiddenKeys=%tu",
t.skinId, t.name, iconCount, hiddenCount);
if (iconCount > 0) {
NSLog(@"[SkinManager] iconMap sample shift=%@ shift_upper=%@ backspace=%@ mode_123=%@ return=%@",
t.keyIconMap[@"shift"],
t.keyIconMap[@"shift_upper"],
t.keyIconMap[@"backspace"],
t.keyIconMap[@"mode_123"],
t.keyIconMap[@"return"]);
}
return [self applyTheme:t];
}
- (BOOL)applyTheme:(KBSkinTheme *)theme {
if (!theme) return NO;
NSLog(@"🎨[SkinManager] apply theme id=%@ name=%@", theme.skinId, theme.name);
// App Group 使
[self p_saveToStore:theme];
// 广
@@ -248,6 +261,19 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
}
- (UIImage *)iconImageForKeyIdentifier:(NSString *)identifier caseVariant:(NSInteger)caseVariant {
#if DEBUG
static NSSet<NSString *> *kb_debugIconIds;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
kb_debugIconIds = [NSSet setWithObjects:
@"shift", @"backspace", @"mode_123", @"mode_abc",
@"symbols_toggle_more", @"symbols_toggle_123",
@"return", @"space", @"emoji_panel", @"letter_q",
nil];
});
BOOL shouldLog = [kb_debugIconIds containsObject:identifier];
#endif
NSDictionary<NSString *, NSString *> *map = self.current.keyIconMap;
NSString *value = nil;
@@ -291,10 +317,23 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
UIImage *img = [UIImage imageWithContentsOfFile:fullPath];
if (img) return img;
}
#if DEBUG
if (shouldLog) {
NSLog(@"[SkinManager] icon file missing id=%@ value=%@ skin=%@",
identifier, value, self.current.skinId ?: @"");
}
#endif
return nil;
}
// Assets
return [UIImage imageNamed:value];
UIImage *img = [UIImage imageNamed:value];
#if DEBUG
if (!img && shouldLog) {
NSLog(@"[SkinManager] icon asset missing id=%@ value=%@ skin=%@",
identifier, value, self.current.skinId ?: @"");
}
#endif
return img;
}
// keyIconMap App Group
@@ -328,6 +367,12 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
if (img) return img;
}
}
#if DEBUG
if (shouldLog) {
NSLog(@"[SkinManager] icon fallback missing id=%@ variant=%ld skin=%@",
identifier, (long)caseVariant, self.current.skinId ?: @"");
}
#endif
return nil;
}

View File

@@ -19,6 +19,12 @@
"current_lang" = "Current: %@";
"common_back" = "Back";
// search
"Recommended Skin" = "Recommended Skin";
"Historical Search" = "Historical Search";
"Search Themes" = "Search Themes";
"Search" = "Search";
// Login & account
"Log In" = "Log In";
"Signed in successfully" = "Signed in successfully";
@@ -31,7 +37,8 @@
"Invalid login credential" = "Invalid login credential";
"No token returned" = "No token returned";
"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";
"Login With Email Password" = "Login With Email Password";
"Enter Email Address" = "Enter Email Address";
@@ -119,7 +126,7 @@
"Personal" = "Personal";
"My Keyboard" = "My Keyboard";
"Notice" = "Notice";
"Share App" = "Share App";
"invite" = "invite";
"Feedback" = "Feedback";
"E-mail" = "E-mail";
"Agreement" = "Agreement";
@@ -162,31 +169,7 @@
"Log Out" = "Log Out";
"Ranking List" = "Ranking List";
"Persona circle" = "Persona circle";
// 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";
"Clear" = "Clear";
// Payment & IAP
"Payment successful" = "Payment successful";
@@ -194,60 +177,12 @@
"Purchase: %@ Coins %@" = "Purchase: %@ Coins %@";
"Pay clicked" = "Pay clicked";
"Points Recharge" = "Points Recharge";
"Recharge" = "Recharge";
"Consumption Record" = "Consumption Record";
"My Points" = "My Points";
"Consumption Details" = "Consumption Details";
"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
"测试" = "Test";

View File

@@ -19,6 +19,13 @@
"current_lang" = "当前:%@";
"common_back" = "返回";
// search
"Recommended Skin" = "推荐皮肤";
"Historical Search" = "历史搜索";
"Search Themes" = "搜索主题";
"Search" = "搜索";
// 登录与账号(以英文 key 为准)
"Log In" = "登录";
"Signed in successfully" = "登录成功";
@@ -31,7 +38,8 @@
"Invalid login credential" = "无效的登录凭证";
"No token returned" = "未返回 token";
"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" = "通过邮箱登录";
"Login With Email Password" = "使用邮箱密码登录";
"Enter Email Address" = "请输入邮箱地址";
@@ -120,7 +128,7 @@
"Personal" = "个人";
"My Keyboard" = "我的键盘";
"Notice" = "通知";
"Share App" = "分享app";
"invite" = "邀请";
"Feedback" = "反馈";
"E-mail" = "联系我们";
"Agreement" = "协议";
@@ -162,29 +170,9 @@
"Log Out" = "退出";
"Ranking List" = "排行榜";
"Persona circle" = "圈子";
"Clear" = "立刻清空";
// 皮肤示例名称
"极光" = "极光";
"雪山" = "雪山";
"湖面" = "湖面";
// 示例标签与文案
"高情商" = "高情商";
"暖味拉扯" = "暖味拉扯";
"风趣幽默" = "风趣幽默";
"撩女生" = "撩女生";
"社交惬匿" = "社交惬匿";
"情场高手" = "情场高手";
"一枚暖男" = "一枚暖男";
"聊天搭子" = "聊天搭子";
"表达爱意" = "表达爱意";
"更多话术" = "更多话术";
"点击粘贴TA的话" = "点击粘贴TA的话";
"点击任一对话去粘贴,选择任意回复方式去试用吧~" = "点击任一对话去粘贴,选择任意回复方式去试用吧~";
"在干嘛?" = "在干嘛?";
"我去洗澡了" = "我去洗澡了";
"🎉 如您遇到其他问题,可点击在线客服帮您解决~" = "🎉 如您遇到其他问题,可点击在线客服帮您解决~";
"👋 欢迎使用『Lovekey 键盘』" = "👋 欢迎使用『Lovekey 键盘』";
// 支付与内购(英文 key
"Payment successful" = "支付成功";
@@ -192,60 +180,12 @@
"Purchase: %@ Coins %@" = "购买:%@ Coins %@";
"Pay clicked" = "点击支付";
"Points Recharge" = "积分充值";
"Recharge" = "充值";
"Consumption Record" = "消费记录";
"My Points" = "我的积分";
"Consumption Details" = "消费明细";
"No data" = "暂无数据";
// 示例商品/分类
"能力" = "能力";
"能力2" = "能力2";
"爱好" = "爱好";
"爱好2" = "爱好2";
"队友" = "队友";
"队友2" = "队友2";
"高级能力" = "高级能力";
"高级爱好" = "高级爱好";
"高级队友" = "高级队友";
// 示例水果等
"果冻橙" = "果冻橙";
"芒果" = "芒果";
"有机水果卷心菜" = "有机水果卷心菜";
"水果萝卜" = "水果萝卜";
"熟冻帝王蟹" = "熟冻帝王蟹";
"赣南脐橙" = "赣南脐橙";
"苹果" = "苹果";
"胡萝卜" = "胡萝卜";
"葡萄" = "葡萄";
"西瓜" = "西瓜";
"小龙虾" = "小龙虾";
"吃烤肉" = "吃烤肉";
"吃鸡腿肉" = "吃鸡腿肉";
"吃牛肉" = "吃牛肉";
"各种肉" = "各种肉";
// One Piece 示例角色
"【剑士】罗罗诺亚·索隆" = "【剑士】罗罗诺亚·索隆";
"【航海士】娜美" = "【航海士】娜美";
"【狙击手】乌索普" = "【狙击手】乌索普";
"【厨师】香吉士" = "【厨师】香吉士";
"【船医】托尼托尼·乔巴" = "【船医】托尼托尼·乔巴";
"【船匠】 弗兰奇" = "【船匠】 弗兰奇";
"【音乐家】布鲁克" = "【音乐家】布鲁克";
"【考古学家】妮可·罗宾" = "【考古学家】妮可·罗宾";
// 橡胶系列示例文案
"橡胶火箭" = "橡胶火箭";
"橡胶火箭炮" = "橡胶火箭炮";
"橡胶机关枪" = "橡胶机关枪";
"橡胶子弹" = "橡胶子弹";
"橡胶攻城炮" = "橡胶攻城炮";
"橡胶象枪" = "橡胶象枪";
"橡胶象枪乱打" = "橡胶象枪乱打";
"橡胶灰熊铳" = "橡胶灰熊铳";
"橡胶雷神象枪" = "橡胶雷神象枪";
"橡胶猿王枪" = "橡胶猿王枪";
"橡胶犀·榴弹炮" = "橡胶犀·榴弹炮";
"橡胶大蛇炮" = "橡胶大蛇炮";
// 其它
"Test" = "测试";

View 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>

View 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>

View 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>

View 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>&lt;nil&gt;</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>

View 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>

View 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>

View File

@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
04050ECB2F10FB8F008051EB /* UIImage+KBColor.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C655D2EBCD5B20035E841 /* UIImage+KBColor.m */; };
041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */ = {isa = PBXBuildFile; fileRef = 041007D12ECE012000D203BB /* KBSkinIconMap.strings */; };
041007D42ECE012500D203BB /* 002.zip in Resources */ = {isa = PBXBuildFile; fileRef = 041007D32ECE012500D203BB /* 002.zip */; };
04122F5D2EC5E5A900EF7AB3 /* KBLoginVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F5B2EC5E5A900EF7AB3 /* KBLoginVM.m */; };
@@ -27,7 +28,7 @@
04286A062ECC81B200CE730C /* KBSkinService.m in Sources */ = {isa = PBXBuildFile; fileRef = 04286A052ECC81B200CE730C /* KBSkinService.m */; };
04286A0B2ECD88B400CE730C /* KeyboardAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 04286A0A2ECD88B400CE730C /* KeyboardAssets.xcassets */; };
04286A0F2ECDA71B00CE730C /* 001.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04286A0E2ECDA71B00CE730C /* 001.zip */; };
04286A132ECDEBF900CE730C /* KBSkinIconMap.strings in Resources */ = {isa = PBXBuildFile; fileRef = 04286A122ECDEBF900CE730C /* KBSkinIconMap.strings */; };
04286A132ECDEBF900CE730C /* KBSkinIconMap.strings in Resources */ = {isa = PBXBuildFile; fileRef = 041007D12ECE012000D203BB /* KBSkinIconMap.strings */; };
043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EAE12EAF940F0089C901 /* KBPermissionViewController.m */; };
0450AA742EF013D000B6AF06 /* KBEmojiCollectionCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 0450AA732EF013D000B6AF06 /* KBEmojiCollectionCell.m */; };
0450AAE22EF03D5100B6AF06 /* KBPerson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450AAE12EF03D5100B6AF06 /* KBPerson.swift */; };
@@ -67,7 +68,6 @@
04791F982ED49CE7004E8522 /* KBFont.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F972ED49CE7004E8522 /* KBFont.m */; };
04791F992ED49CE7004E8522 /* KBFont.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F972ED49CE7004E8522 /* KBFont.m */; };
04791FF72ED5B985004E8522 /* Christmas.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04791FF62ED5B985004E8522 /* Christmas.zip */; };
04791FFB2ED5EAB8004E8522 /* fense.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04791FFA2ED5EAB8004E8522 /* fense.zip */; };
04791FFC2ED71D17004E8522 /* UIColor+Extension.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95E42EB220B5007BD342 /* UIColor+Extension.m */; };
04791FFF2ED830FA004E8522 /* KBKeyboardMaskView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791FFE2ED830FA004E8522 /* KBKeyboardMaskView.m */; };
047920072ED86ABC004E8522 /* kb_guide_keyboard.gif in Resources */ = {isa = PBXBuildFile; fileRef = 047920062ED86ABC004E8522 /* kb_guide_keyboard.gif */; };
@@ -97,8 +97,6 @@
048908D22EBF611D00FABA60 /* KBHistoryMoreCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908D12EBF611D00FABA60 /* KBHistoryMoreCell.m */; };
048908DA2EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908D82EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout.m */; };
048908DD2EBF67EB00FABA60 /* KBSearchResultVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908DC2EBF67EB00FABA60 /* KBSearchResultVC.m */; };
05A1B2D12F5B1A2B3C4D5E60 /* KBSearchVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A1B2C52F5B1A2B3C4D5E60 /* KBSearchVM.m */; };
05A1B2D22F5B1A2B3C4D5E60 /* KBSearchThemeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A1B2C72F5B1A2B3C4D5E60 /* KBSearchThemeModel.m */; };
048908E02EBF73DC00FABA60 /* MySkinVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908DF2EBF73DC00FABA60 /* MySkinVC.m */; };
048908E32EBF760000FABA60 /* MySkinCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908E22EBF760000FABA60 /* MySkinCell.m */; };
048908E32EBF821700FABA60 /* KBSkinDetailVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908E22EBF821700FABA60 /* KBSkinDetailVC.m */; };
@@ -147,8 +145,6 @@
049FB2292EC31BB000FAB05D /* KBChangeNicknamePopView.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB2282EC31BB000FAB05D /* KBChangeNicknamePopView.m */; };
049FB22C2EC31F8800FAB05D /* KBGenderPickerPopView.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB22B2EC31F8800FAB05D /* KBGenderPickerPopView.m */; };
049FB22F2EC34EB900FAB05D /* KBStreamTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB22E2EC34EB900FAB05D /* KBStreamTextView.m */; };
049FB2322EC45A0000FAB05D /* KBStreamFetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB2312EC45A0000FAB05D /* KBStreamFetcher.m */; };
049FB2352EC45C6A00FAB05D /* NetworkStreamHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB2342EC45C6A00FAB05D /* NetworkStreamHandler.m */; };
049FB23B2EC4766700FAB05D /* KBFunctionTagListView.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB2372EC4766700FAB05D /* KBFunctionTagListView.m */; };
049FB23C2EC4766700FAB05D /* KBStreamOverlayView.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB2392EC4766700FAB05D /* KBStreamOverlayView.m */; };
049FB23F2EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB23E2EC4B6EF00FAB05D /* KBULBridgeNotification.m */; };
@@ -174,12 +170,12 @@
04C6EADD2EAF8CEB0089C901 /* KBToolBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EADC2EAF8CEB0089C901 /* KBToolBar.m */; };
04D1F6B22EDFF10A00B12345 /* KBSkinInstallBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D1F6B12EDFF10A00B12345 /* KBSkinInstallBridge.m */; };
04D1F6B32EDFF10A00B12345 /* KBSkinInstallBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D1F6B12EDFF10A00B12345 /* KBSkinInstallBridge.m */; };
04E161832F10E6470022C23B /* normal_hei_them.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04E161812F10E6470022C23B /* normal_hei_them.zip */; };
04E161842F10E6470022C23B /* normal_them.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04E161822F10E6470022C23B /* normal_them.zip */; };
04FC95672EB0546C007BD342 /* KBKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95652EB0546C007BD342 /* KBKey.m */; };
04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95692EB05497007BD342 /* KBKeyButton.m */; };
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC956C2EB054B7007BD342 /* KBKeyboardView.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 */; };
04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95752EB095DE007BD342 /* KBFunctionPasteView.m */; };
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95782EB09BC8007BD342 /* KBKeyBoardMainView.m */; };
@@ -205,22 +201,39 @@
04FEDB032EFE000000123456 /* KBEmojiBottomBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDB022EFE000000123456 /* KBEmojiBottomBarView.m */; };
04FEDC122F00010000999999 /* KBKeyboardSubscriptionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC112F00010000999999 /* KBKeyboardSubscriptionView.m */; };
04FEDC222F00020000999999 /* KBKeyboardSubscriptionProduct.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC212F00020000999999 /* KBKeyboardSubscriptionProduct.m */; };
04FEDC252F10000100000001 /* KBKeyboardLayoutConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC242F10000100000001 /* KBKeyboardLayoutConfig.m */; };
04FEDC322F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC312F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.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 */; };
49B63DBAEE9076C591E13D68 /* KBShopThemeTagModel.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A844CD2D8584596DBE6316 /* KBShopThemeTagModel.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 */; };
A1B2C3D42EB0A0A100000001 /* KBFunctionTagCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D32EB0A0A100000001 /* KBFunctionTagCell.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 */; };
A1B2C3F12F20000000000002 /* kb_keyboard_layout_config.json in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3F02F20000000000002 /* kb_keyboard_layout_config.json */; };
A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */; };
A1B2C4002EB4A0A100000003 /* 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 */; };
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 */; };
A1B2E1012EBC7AAA00000001 /* KBTopThreeView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E0022EBC7AAA00000001 /* KBTopThreeView.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 */; };
A1F0C1C22FABCDEF12345678 /* KBInviteCodeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */; };
A1F0C1C32FABCDEF12345678 /* KBInviteCodeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */; };
A1F0C1D22FACAD0012345678 /* KBMaiPointReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */; };
A1F0C1D32FACAD0012345678 /* KBMaiPointReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.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 */; };
/* End PBXBuildFile section */
@@ -285,7 +298,6 @@
04286A052ECC81B200CE730C /* KBSkinService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinService.m; sourceTree = "<group>"; };
04286A0A2ECD88B400CE730C /* KeyboardAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = KeyboardAssets.xcassets; sourceTree = "<group>"; };
04286A0E2ECDA71B00CE730C /* 001.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = 001.zip; sourceTree = "<group>"; };
04286A122ECDEBF900CE730C /* KBSkinIconMap.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = KBSkinIconMap.strings; sourceTree = "<group>"; };
0450AA722EF013D000B6AF06 /* KBEmojiCollectionCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBEmojiCollectionCell.h; sourceTree = "<group>"; };
0450AA732EF013D000B6AF06 /* KBEmojiCollectionCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBEmojiCollectionCell.m; sourceTree = "<group>"; };
0450AAE02EF03D5100B6AF06 /* keyBoard-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "keyBoard-Bridging-Header.h"; sourceTree = "<group>"; };
@@ -338,7 +350,6 @@
04791F962ED49CE7004E8522 /* KBFont.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFont.h; sourceTree = "<group>"; };
04791F972ED49CE7004E8522 /* KBFont.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFont.m; sourceTree = "<group>"; };
04791FF62ED5B985004E8522 /* Christmas.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = Christmas.zip; sourceTree = "<group>"; };
04791FFA2ED5EAB8004E8522 /* fense.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = fense.zip; sourceTree = "<group>"; };
04791FFD2ED830FA004E8522 /* KBKeyboardMaskView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardMaskView.h; sourceTree = "<group>"; };
04791FFE2ED830FA004E8522 /* KBKeyboardMaskView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardMaskView.m; sourceTree = "<group>"; };
047920062ED86ABC004E8522 /* kb_guide_keyboard.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = kb_guide_keyboard.gif; sourceTree = "<group>"; };
@@ -392,10 +403,6 @@
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>"; };
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>"; };
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>"; };
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>"; };
@@ -486,10 +493,6 @@
049FB22B2EC31F8800FAB05D /* KBGenderPickerPopView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBGenderPickerPopView.m; sourceTree = "<group>"; };
049FB22D2EC34EB900FAB05D /* KBStreamTextView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBStreamTextView.h; sourceTree = "<group>"; };
049FB22E2EC34EB900FAB05D /* KBStreamTextView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBStreamTextView.m; sourceTree = "<group>"; };
049FB2302EC45A0000FAB05D /* KBStreamFetcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBStreamFetcher.h; sourceTree = "<group>"; };
049FB2312EC45A0000FAB05D /* KBStreamFetcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBStreamFetcher.m; sourceTree = "<group>"; };
049FB2332EC45C6A00FAB05D /* NetworkStreamHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NetworkStreamHandler.h; sourceTree = "<group>"; };
049FB2342EC45C6A00FAB05D /* NetworkStreamHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NetworkStreamHandler.m; sourceTree = "<group>"; };
049FB2362EC4766700FAB05D /* KBFunctionTagListView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionTagListView.h; sourceTree = "<group>"; };
049FB2372EC4766700FAB05D /* KBFunctionTagListView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionTagListView.m; sourceTree = "<group>"; };
049FB2382EC4766700FAB05D /* KBStreamOverlayView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBStreamOverlayView.h; sourceTree = "<group>"; };
@@ -531,6 +534,8 @@
04C6EAE12EAF940F0089C901 /* KBPermissionViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPermissionViewController.m; sourceTree = "<group>"; };
04D1F6B02EDFF10A00B12345 /* KBSkinInstallBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinInstallBridge.h; sourceTree = "<group>"; };
04D1F6B12EDFF10A00B12345 /* KBSkinInstallBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinInstallBridge.m; sourceTree = "<group>"; };
04E161812F10E6470022C23B /* normal_hei_them.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = normal_hei_them.zip; sourceTree = "<group>"; };
04E161822F10E6470022C23B /* normal_them.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = normal_them.zip; sourceTree = "<group>"; };
04FC953A2EAFAE56007BD342 /* KeyBoardPrefixHeader.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyBoardPrefixHeader.pch; sourceTree = "<group>"; };
04FC95642EB0546C007BD342 /* KBKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKey.h; sourceTree = "<group>"; };
04FC95652EB0546C007BD342 /* KBKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKey.m; sourceTree = "<group>"; };
@@ -540,10 +545,6 @@
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>"; };
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>"; };
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>"; };
@@ -596,10 +597,16 @@
04FEDC112F00010000999999 /* KBKeyboardSubscriptionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardSubscriptionView.m; sourceTree = "<group>"; };
04FEDC202F00020000999999 /* KBKeyboardSubscriptionProduct.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardSubscriptionProduct.h; sourceTree = "<group>"; };
04FEDC212F00020000999999 /* KBKeyboardSubscriptionProduct.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardSubscriptionProduct.m; sourceTree = "<group>"; };
04FEDC232F10000100000001 /* KBKeyboardLayoutConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardLayoutConfig.h; sourceTree = "<group>"; };
04FEDC242F10000100000001 /* KBKeyboardLayoutConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardLayoutConfig.m; sourceTree = "<group>"; };
04FEDC302F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardSubscriptionFeatureItemView.h; 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>"; };
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>"; };
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>"; };
@@ -614,18 +621,40 @@
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>"; };
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>"; };
A1B2C3F02F20000000000002 /* kb_keyboard_layout_config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = kb_keyboard_layout_config.json; 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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeDetailModel.m; sourceTree = "<group>"; };
@@ -657,10 +686,13 @@
041007D02ECE010100D203BB /* Resource */ = {
isa = PBXGroup;
children = (
04E161812F10E6470022C23B /* normal_hei_them.zip */,
04E161822F10E6470022C23B /* normal_them.zip */,
A1B2C3EC2F20000000000001 /* kb_words.txt */,
A1B2C3F02F20000000000002 /* kb_keyboard_layout_config.json */,
0498BDF42EEC50EE006CC1D5 /* emoji_categories.json */,
041007D12ECE012000D203BB /* KBSkinIconMap.strings */,
041007D32ECE012500D203BB /* 002.zip */,
04791FFA2ED5EAB8004E8522 /* fense.zip */,
04791FF62ED5B985004E8522 /* Christmas.zip */,
);
path = Resource;
@@ -815,6 +847,8 @@
A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */,
A1B2C9032FBD000200000001 /* KBBackspaceUndoManager.h */,
A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */,
A1B2C9072FBD000200000003 /* KBInputBufferManager.h */,
A1B2C9082FBD000200000004 /* KBInputBufferManager.m */,
);
path = Utils;
sourceTree = "<group>";
@@ -859,7 +893,6 @@
0479200A2ED87CEE004E8522 /* permiss_video.mp4 */,
047920102ED98E7D004E8522 /* permiss_video_2.mp4 */,
047920062ED86ABC004E8522 /* kb_guide_keyboard.gif */,
04286A122ECDEBF900CE730C /* KBSkinIconMap.strings */,
04286A0E2ECDA71B00CE730C /* 001.zip */,
);
path = Resource;
@@ -1086,6 +1119,8 @@
children = (
04A9FE102EB4D0D20020DB6D /* KBFullAccessManager.h */,
04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */,
A1B2C3E62F20000000000001 /* KBSuggestionEngine.h */,
A1B2C3E72F20000000000001 /* KBSuggestionEngine.m */,
04FEDA9F2EEDB00100123456 /* KBEmojiDataProvider.h */,
04FEDAA02EEDB00100123456 /* KBEmojiDataProvider.m */,
);
@@ -1154,6 +1189,8 @@
046131132ECF454500A6FADF /* KBKeyPreviewView.m */,
04FC95772EB09BC8007BD342 /* KBKeyBoardMainView.h */,
04FC95782EB09BC8007BD342 /* KBKeyBoardMainView.m */,
A1B2C3E82F20000000000001 /* KBSuggestionBarView.h */,
A1B2C3E92F20000000000001 /* KBSuggestionBarView.m */,
04FC956E2EB09516007BD342 /* KBFunctionView.h */,
04FC956F2EB09516007BD342 /* KBFunctionView.m */,
04FC95712EB09570007BD342 /* KBFunctionBarView.h */,
@@ -1213,6 +1250,8 @@
04FC95652EB0546C007BD342 /* KBKey.m */,
04FEDC202F00020000999999 /* KBKeyboardSubscriptionProduct.h */,
04FEDC212F00020000999999 /* KBKeyboardSubscriptionProduct.m */,
04FEDC232F10000100000001 /* KBKeyboardLayoutConfig.h */,
04FEDC242F10000100000001 /* KBKeyboardLayoutConfig.m */,
);
path = Model;
sourceTree = "<group>";
@@ -1320,8 +1359,12 @@
0498BD8A2EE69E15006CC1D5 /* KBTagItemModel.m */,
0498BD8D2EE6A3BD006CC1D5 /* KBMyMainModel.h */,
0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */,
A1F0C1C02FABCDEF12345678 /* KBInviteCodeModel.h */,
A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */,
7ECBD0E320F971D0FBEDD7BC /* KBMyTheme.h */,
180D662EC4DB3A7FFF83FF18 /* KBMyTheme.m */,
A1F0C1A02F1234567890ABCD /* KBConsumptionRecord.h */,
A1F0C1A12F1234567890ABCD /* KBConsumptionRecord.m */,
);
path = M;
sourceTree = "<group>";
@@ -1333,6 +1376,8 @@
049FB31C2EC21BCD00FAB05D /* KBMyKeyboardCell.m */,
048908E12EBF760000FABA60 /* MySkinCell.h */,
048908E22EBF760000FABA60 /* MySkinCell.m */,
A1F0C1A22F1234567890ABCD /* KBConsumptionRecordCell.h */,
A1F0C1A32F1234567890ABCD /* KBConsumptionRecordCell.m */,
048908E42EBF841B00FABA60 /* KBSkinDetailTagCell.h */,
048908E52EBF841B00FABA60 /* KBSkinDetailTagCell.m */,
048908E72EBF843000FABA60 /* KBSkinDetailHeaderCell.h */,
@@ -1366,6 +1411,8 @@
049FB2192EC20A9E00FAB05D /* KBMyKeyBoardVC.m */,
048908DE2EBF73DC00FABA60 /* MySkinVC.h */,
048908DF2EBF73DC00FABA60 /* MySkinVC.m */,
A1F0C1A42F1234567890ABCD /* KBConsumptionRecordVC.h */,
A1F0C1A52F1234567890ABCD /* KBConsumptionRecordVC.m */,
049FB2212EC311F900FAB05D /* KBPersonInfoVC.h */,
049FB2222EC311F900FAB05D /* KBPersonInfoVC.m */,
04791F902ED48010004E8522 /* KBNoticeVC.h */,
@@ -1612,6 +1659,8 @@
0498BD842EE1B255006CC1D5 /* KBSignUtils.m */,
047920482EDDCE25004E8522 /* KBUserSessionManager.h */,
047920492EDDCE25004E8522 /* KBUserSessionManager.m */,
A1F0C1D02FACAD0012345678 /* KBMaiPointReporter.h */,
A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */,
);
path = Shared;
sourceTree = "<group>";
@@ -1672,10 +1721,6 @@
0498BDD92EE7ECEA006CC1D5 /* WJXEventSource */,
A1B2C3E02EB0C0A100000001 /* KBNetworkManager.h */,
A1B2C3E12EB0C0A100000001 /* KBNetworkManager.m */,
049FB2302EC45A0000FAB05D /* KBStreamFetcher.h */,
049FB2312EC45A0000FAB05D /* KBStreamFetcher.m */,
049FB2332EC45C6A00FAB05D /* NetworkStreamHandler.h */,
049FB2342EC45C6A00FAB05D /* NetworkStreamHandler.m */,
);
path = Network;
sourceTree = "<group>";
@@ -1770,10 +1815,13 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
04E161832F10E6470022C23B /* normal_hei_them.zip in Resources */,
04E161842F10E6470022C23B /* normal_them.zip in Resources */,
04A9FE202EB893F10020DB6D /* Localizable.strings in Resources */,
041007D42ECE012500D203BB /* 002.zip in Resources */,
041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */,
04791FFB2ED5EAB8004E8522 /* fense.zip in Resources */,
A1B2C3ED2F20000000000001 /* kb_words.txt in Resources */,
A1B2C3F12F20000000000002 /* kb_keyboard_layout_config.json in Resources */,
0498BDF52EEC50EE006CC1D5 /* emoji_categories.json in Resources */,
04791FF72ED5B985004E8522 /* Christmas.zip in Resources */,
04286A0B2ECD88B400CE730C /* KeyboardAssets.xcassets in Resources */,
@@ -1873,12 +1921,13 @@
files = (
0498BD862EE1BEC9006CC1D5 /* KBSignUtils.m in Sources */,
04791FFC2ED71D17004E8522 /* UIColor+Extension.m in Sources */,
049FB2322EC45A0000FAB05D /* KBStreamFetcher.m in Sources */,
0450AC4A2EF2C3ED00B6AF06 /* KBKeyboardSubscriptionOptionCell.m in Sources */,
04A9FE0F2EB481100020DB6D /* KBHUD.m in Sources */,
04C6EADD2EAF8CEB0089C901 /* KBToolBar.m in Sources */,
A1B2C3EB2F20000000000001 /* KBSuggestionBarView.m in Sources */,
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */,
04FEDAB32EEDB05000123456 /* KBEmojiPanelView.m in Sources */,
04050ECB2F10FB8F008051EB /* UIImage+KBColor.m in Sources */,
04FEDB032EFE000000123456 /* KBEmojiBottomBarView.m in Sources */,
0498BD8C2EE69E15006CC1D5 /* KBTagItemModel.m in Sources */,
046131142ECF454500A6FADF /* KBKeyPreviewView.m in Sources */,
@@ -1889,8 +1938,8 @@
04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */,
A1B2C3D42EB0A0A100000001 /* KBFunctionTagCell.m in Sources */,
04A9FE1A2EB892460020DB6D /* KBLocalizationManager.m in Sources */,
A1B2C3EA2F20000000000001 /* KBSuggestionEngine.m in Sources */,
A1B2C3E22EB0C0A100000001 /* KBNetworkManager.m in Sources */,
049FB2352EC45C6A00FAB05D /* NetworkStreamHandler.m in Sources */,
04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */,
04FEDAA12EEDB00100123456 /* KBEmojiDataProvider.m in Sources */,
04FC95B22EB0B2CC007BD342 /* KBSettingView.m in Sources */,
@@ -1904,13 +1953,17 @@
04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */,
A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */,
A1B2C9052FBD000200000001 /* KBBackspaceUndoManager.m in Sources */,
A1B2C9092FBD000200000005 /* KBInputBufferManager.m in Sources */,
049FB23F2EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */,
04791F992ED49CE7004E8522 /* KBFont.m in Sources */,
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */,
04FC95672EB0546C007BD342 /* KBKey.m in Sources */,
A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */,
0498BD8F2EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */,
A1F0C1C22FABCDEF12345678 /* KBInviteCodeModel.m in Sources */,
A1F0C1D22FACAD0012345678 /* KBMaiPointReporter.m in Sources */,
04FEDC222F00020000999999 /* KBKeyboardSubscriptionProduct.m in Sources */,
04FEDC252F10000100000001 /* KBKeyboardLayoutConfig.m in Sources */,
0450AA742EF013D000B6AF06 /* KBEmojiCollectionCell.m in Sources */,
550CB2630FA4A7B4B9782EFA /* KBMyTheme.m in Sources */,
0498BDDA2EE7ECEA006CC1D5 /* WJXEventSource.m in Sources */,
@@ -1973,7 +2026,12 @@
04A9FE1B2EB892460020DB6D /* KBLocalizationManager.m in Sources */,
048908BC2EBE1FCB00FABA60 /* BaseViewController.m in Sources */,
0498BD902EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */,
A1F0C1C32FABCDEF12345678 /* KBInviteCodeModel.m in Sources */,
A1F0C1D32FACAD0012345678 /* KBMaiPointReporter.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 */,
0498BD712EE02A41006CC1D5 /* KBForgetPwdNewPwdVC.m in Sources */,
048908EF2EBF861800FABA60 /* KBSkinSectionTitleCell.m in Sources */,

View File

@@ -65,7 +65,7 @@
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "disable"
isEnabled = "YES">
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>

View File

@@ -5,12 +5,12 @@
"scale" : "1x"
},
{
"filename" : "key_a@2x.png",
"filename" : "my_chongzhi_bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "key_a@3x.png",
"filename" : "my_chongzhi_bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -5,12 +5,12 @@
"scale" : "1x"
},
{
"filename" : "key_ai@2x.png",
"filename" : "my_record_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "key_ai@3x.png",
"filename" : "my_record_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -5,12 +5,12 @@
"scale" : "1x"
},
{
"filename" : "placeholder_image_icon@2x.png",
"filename" : "切图 232@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "placeholder_image_icon@3x.png",
"filename" : "切图 232@3x.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -5,12 +5,12 @@
"scale" : "1x"
},
{
"filename" : "key_b@2x.png",
"filename" : "shop_goumai_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "key_b@3x.png",
"filename" : "shop_goumai_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -1,16 +1,15 @@
{
"images" : [
{
"filename" : "ChatGPT Image 2026年1月12日 14_07_17.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "key_c@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "key_c@3x.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Some files were not shown because too many files have changed in this diff Show More