添加皮肤
@@ -54,93 +54,109 @@
|
||||
// - key_icons:
|
||||
// * 当 KB_SKIN_ICON_USE_REMOTE==0 时,value 写本地 Assets 名称(如 "kb_space_melon")
|
||||
// * 当 KB_SKIN_ICON_USE_REMOTE==1 时,value 写 Zip 包内的图标“短文件名”(如 "key_space";解压后路径为 Skins/<skinId>/icons/key_space.png)
|
||||
// self.skins = @[
|
||||
// @{
|
||||
// @"id": @"melon",
|
||||
// @"name": KBLocalized(@"蜜瓜主题"),
|
||||
// @"background_image": @"https://picsum.photos/id/1018/800/450.jpg",
|
||||
// @"background": @"#F5FFE8",
|
||||
// @"key_bg": @"#FFFFFF",
|
||||
// @"key_text": @"#4A4A4A",
|
||||
// @"key_highlight": @"#D9F4C4",
|
||||
// @"accent": @"#A4D68A",
|
||||
// @"hidden_keys": @[
|
||||
// @"letter_q",@"letter_w",@"letter_e",@"letter_r",@"letter_t",
|
||||
// @"letter_y",@"letter_u",@"letter_i",@"letter_o",@"letter_p",
|
||||
// @"letter_a",@"letter_s",@"letter_d",@"letter_f",@"letter_g",
|
||||
// @"letter_h",@"letter_j",@"letter_k",@"letter_l",
|
||||
// @"letter_z",@"letter_x",@"letter_c",@"letter_v",
|
||||
// @"letter_b",@"letter_n",@"letter_m",
|
||||
// @"space"
|
||||
// ],
|
||||
// // 默认假设本地测试:这里的值写 Assets 名称。
|
||||
// // 如果开启远程 Zip 模式,可改为 Zip 内对应的图标短文件名(如 "key_space")。
|
||||
// @"key_icons": @{
|
||||
// // 字母键:大小写共用一套本地图(演示用)
|
||||
// // 若后续需要不同图,只需改为 *_lower / *_upper 对应不同资源名即可。
|
||||
// @"letter_q_lower": @"key_q",
|
||||
// @"letter_q_upper": @"key_q",
|
||||
// @"letter_w_lower": @"key_w",
|
||||
// @"letter_w_upper": @"key_w",
|
||||
// @"letter_e_lower": @"key_e",
|
||||
// @"letter_e_upper": @"key_e",
|
||||
// @"letter_r_lower": @"key_r",
|
||||
// @"letter_r_upper": @"key_r",
|
||||
// @"letter_t_lower": @"key_t",
|
||||
// @"letter_t_upper": @"key_t",
|
||||
// @"letter_y_lower": @"key_y",
|
||||
// @"letter_y_upper": @"key_y",
|
||||
// @"letter_u_lower": @"key_u",
|
||||
// @"letter_u_upper": @"key_u",
|
||||
// @"letter_i_lower": @"key_i",
|
||||
// @"letter_i_upper": @"key_i",
|
||||
// @"letter_o_lower": @"key_o",
|
||||
// @"letter_o_upper": @"key_o",
|
||||
// @"letter_p_lower": @"key_p",
|
||||
// @"letter_p_upper": @"key_p",
|
||||
//
|
||||
// @"letter_a_lower": @"key_a",
|
||||
// @"letter_a_upper": @"key_a",
|
||||
// @"letter_s_lower": @"key_s",
|
||||
// @"letter_s_upper": @"key_s",
|
||||
// @"letter_d_lower": @"key_d",
|
||||
// @"letter_d_upper": @"key_d",
|
||||
// @"letter_f_lower": @"key_f",
|
||||
// @"letter_f_upper": @"key_f",
|
||||
// @"letter_g_lower": @"key_g",
|
||||
// @"letter_g_upper": @"key_g",
|
||||
// @"letter_h_lower": @"key_h",
|
||||
// @"letter_h_upper": @"key_h",
|
||||
// @"letter_j_lower": @"key_j",
|
||||
// @"letter_j_upper": @"key_j",
|
||||
// @"letter_k_lower": @"key_k",
|
||||
// @"letter_k_upper": @"key_k",
|
||||
// @"letter_l_lower": @"key_l",
|
||||
// @"letter_l_upper": @"key_l",
|
||||
//
|
||||
// @"letter_z_lower": @"key_z",
|
||||
// @"letter_z_upper": @"key_z",
|
||||
// @"letter_x_lower": @"key_x",
|
||||
// @"letter_x_upper": @"key_x",
|
||||
// @"letter_c_lower": @"key_c",
|
||||
// @"letter_c_upper": @"key_c",
|
||||
// @"letter_v_lower": @"key_v",
|
||||
// @"letter_v_upper": @"key_v",
|
||||
// @"letter_b_lower": @"key_b",
|
||||
// @"letter_b_upper": @"key_b",
|
||||
// @"letter_n_lower": @"key_n",
|
||||
// @"letter_n_upper": @"key_n",
|
||||
// @"letter_m_lower": @"key_m",
|
||||
// @"letter_m_upper": @"key_m",
|
||||
//
|
||||
// // 功能键(无大小写变体)
|
||||
// @"space": @"key_space", // 空格键
|
||||
// @"backspace": @"key_del", // 删除键
|
||||
// @"shift": @"key_up", // Shift(上箭头)
|
||||
// @"mode_123": @"key_123", // 字母面板左下角 "123"
|
||||
// @"ai": @"key_ai", // 自定义 AI 键
|
||||
// @"return": @"key_send" // 发送/换行键
|
||||
// }
|
||||
// }
|
||||
// ];
|
||||
self.skins = @[
|
||||
@{
|
||||
@"id": @"melon",
|
||||
@"name": KBLocalized(@"蜜瓜主题"),
|
||||
@"background_image": @"https://picsum.photos/id/1018/800/450.jpg",
|
||||
@"id": @"local001",
|
||||
@"name": @"本地001皮肤",
|
||||
// 关键:zip_url 写成 bundle:// 前缀 + 文件名
|
||||
@"zip_url": @"bundle://001.zip",
|
||||
|
||||
// 颜色你可以先随便写一套,或者继承默认
|
||||
@"background": @"#F5FFE8",
|
||||
@"key_bg": @"#FFFFFF",
|
||||
@"key_text": @"#4A4A4A",
|
||||
@"key_highlight": @"#D9F4C4",
|
||||
@"accent": @"#A4D68A",
|
||||
@"hidden_keys": @[
|
||||
@"letter_q",@"letter_w",@"letter_e",@"letter_r",@"letter_t",
|
||||
@"letter_y",@"letter_u",@"letter_i",@"letter_o",@"letter_p",
|
||||
@"letter_a",@"letter_s",@"letter_d",@"letter_f",@"letter_g",
|
||||
@"letter_h",@"letter_j",@"letter_k",@"letter_l",
|
||||
@"letter_z",@"letter_x",@"letter_c",@"letter_v",
|
||||
@"letter_b",@"letter_n",@"letter_m",
|
||||
@"space"
|
||||
],
|
||||
// 默认假设本地测试:这里的值写 Assets 名称。
|
||||
// 如果开启远程 Zip 模式,可改为 Zip 内对应的图标短文件名(如 "key_space")。
|
||||
@"key_icons": @{
|
||||
// 字母键:大小写共用一套本地图(演示用)
|
||||
// 若后续需要不同图,只需改为 *_lower / *_upper 对应不同资源名即可。
|
||||
@"letter_q_lower": @"key_q",
|
||||
@"letter_q_upper": @"key_q",
|
||||
@"letter_w_lower": @"key_w",
|
||||
@"letter_w_upper": @"key_w",
|
||||
@"letter_e_lower": @"key_e",
|
||||
@"letter_e_upper": @"key_e",
|
||||
@"letter_r_lower": @"key_r",
|
||||
@"letter_r_upper": @"key_r",
|
||||
@"letter_t_lower": @"key_t",
|
||||
@"letter_t_upper": @"key_t",
|
||||
@"letter_y_lower": @"key_y",
|
||||
@"letter_y_upper": @"key_y",
|
||||
@"letter_u_lower": @"key_u",
|
||||
@"letter_u_upper": @"key_u",
|
||||
@"letter_i_lower": @"key_i",
|
||||
@"letter_i_upper": @"key_i",
|
||||
@"letter_o_lower": @"key_o",
|
||||
@"letter_o_upper": @"key_o",
|
||||
@"letter_p_lower": @"key_p",
|
||||
@"letter_p_upper": @"key_p",
|
||||
|
||||
@"letter_a_lower": @"key_a",
|
||||
@"letter_a_upper": @"key_a",
|
||||
@"letter_s_lower": @"key_s",
|
||||
@"letter_s_upper": @"key_s",
|
||||
@"letter_d_lower": @"key_d",
|
||||
@"letter_d_upper": @"key_d",
|
||||
@"letter_f_lower": @"key_f",
|
||||
@"letter_f_upper": @"key_f",
|
||||
@"letter_g_lower": @"key_g",
|
||||
@"letter_g_upper": @"key_g",
|
||||
@"letter_h_lower": @"key_h",
|
||||
@"letter_h_upper": @"key_h",
|
||||
@"letter_j_lower": @"key_j",
|
||||
@"letter_j_upper": @"key_j",
|
||||
@"letter_k_lower": @"key_k",
|
||||
@"letter_k_upper": @"key_k",
|
||||
@"letter_l_lower": @"key_l",
|
||||
@"letter_l_upper": @"key_l",
|
||||
|
||||
@"letter_z_lower": @"key_z",
|
||||
@"letter_z_upper": @"key_z",
|
||||
@"letter_x_lower": @"key_x",
|
||||
@"letter_x_upper": @"key_x",
|
||||
@"letter_c_lower": @"key_c",
|
||||
@"letter_c_upper": @"key_c",
|
||||
@"letter_v_lower": @"key_v",
|
||||
@"letter_v_upper": @"key_v",
|
||||
@"letter_b_lower": @"key_b",
|
||||
@"letter_b_upper": @"key_b",
|
||||
@"letter_n_lower": @"key_n",
|
||||
@"letter_n_upper": @"key_n",
|
||||
@"letter_m_lower": @"key_m",
|
||||
@"letter_m_upper": @"key_m",
|
||||
|
||||
// 功能键(无大小写变体)
|
||||
@"space": @"key_space", // 空格键
|
||||
@"backspace": @"key_del", // 删除键
|
||||
@"shift": @"key_up", // Shift(上箭头)
|
||||
@"mode_123": @"key_123", // 字母面板左下角 "123"
|
||||
@"ai": @"key_ai", // 自定义 AI 键
|
||||
@"return": @"key_send" // 发送/换行键
|
||||
}
|
||||
@"accent": @"#A4D68A"
|
||||
// 不写 key_icons,代码会自动用本地那份映射表
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -339,16 +339,18 @@
|
||||
|
||||
#if __has_include(<SSZipArchive/SSZipArchive.h>)
|
||||
// 若本地尚未缓存该皮肤资源且提供了 zip_url,则下载并解压 Zip 包。
|
||||
// 支持两种来源:
|
||||
// 1) 线上 URL(http/https):通过 KBNetworkManager 下载;
|
||||
// 2) 本地测试:zip_url 以 "bundle://" 前缀开头,例如 "bundle://001.zip"。
|
||||
if (!hasCachedAssets && zipURL.length > 0) {
|
||||
dispatch_group_enter(group);
|
||||
[[KBNetworkManager shared] GET:zipURL parameters:nil headers:nil completion:^(id jsonOrData, NSURLResponse *response, NSError *error) {
|
||||
NSData *data = ([jsonOrData isKindOfClass:NSData.class] ? (NSData *)jsonOrData : nil);
|
||||
if (error || data.length == 0) {
|
||||
|
||||
void (^handleZipData)(NSData *) = ^(NSData *data) {
|
||||
if (data.length == 0) {
|
||||
zipOK = NO;
|
||||
dispatch_group_leave(group);
|
||||
return;
|
||||
}
|
||||
|
||||
// 将 Zip 写入临时路径再解压
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:skinRoot
|
||||
withIntermediateDirectories:YES
|
||||
@@ -370,9 +372,81 @@
|
||||
[fm removeItemAtPath:zipPath error:nil];
|
||||
if (!ok || unzipError) {
|
||||
zipOK = NO;
|
||||
dispatch_group_leave(group);
|
||||
return;
|
||||
}
|
||||
|
||||
// 兼容“额外包一层目录”的压缩结构:
|
||||
// 若 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);
|
||||
if (!iconsValid) {
|
||||
NSArray<NSString *> *subItems = [fm contentsOfDirectoryAtPath:skinRoot error:NULL];
|
||||
for (NSString *name in subItems) {
|
||||
if ([name isEqualToString:@"icons"] || [name isEqualToString:@"__MACOSX"]) continue;
|
||||
NSString *nestedRoot = [skinRoot stringByAppendingPathComponent:name];
|
||||
BOOL isDirNested = NO;
|
||||
if (![fm fileExistsAtPath:nestedRoot isDirectory:&isDirNested] || !isDirNested) continue;
|
||||
|
||||
NSString *nestedIcons = [nestedRoot stringByAppendingPathComponent:@"icons"];
|
||||
BOOL isDirNestedIcons = NO;
|
||||
if ([fm fileExistsAtPath:nestedIcons isDirectory:&isDirNestedIcons] && isDirNestedIcons) {
|
||||
NSArray *nestedFiles = [fm contentsOfDirectoryAtPath:nestedIcons error:NULL];
|
||||
if (nestedFiles.count > 0) {
|
||||
// 确保目标 icons 目录存在
|
||||
[fm createDirectoryAtPath:iconsDir
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:NULL];
|
||||
// 将 icons 下所有文件上移一层
|
||||
for (NSString *fn in nestedFiles) {
|
||||
NSString *from = [nestedIcons stringByAppendingPathComponent:fn];
|
||||
NSString *to = [iconsDir stringByAppendingPathComponent:fn];
|
||||
[fm removeItemAtPath:to error:nil];
|
||||
[fm moveItemAtPath:from toPath:to error:nil];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
};
|
||||
|
||||
// 本地 bundle 测试:zip_url 形如 "bundle://001.zip"
|
||||
if ([zipURL hasPrefix:@"bundle://"]) {
|
||||
NSString *name = [zipURL substringFromIndex:@"bundle://".length];
|
||||
NSString *fileName = name ?: @"";
|
||||
NSString *ext = fileName.pathExtension;
|
||||
NSString *base = fileName;
|
||||
if (ext.length == 0) {
|
||||
ext = @"zip";
|
||||
} else {
|
||||
base = [fileName stringByDeletingPathExtension];
|
||||
}
|
||||
NSString *path = [[NSBundle mainBundle] pathForResource:base ofType:ext];
|
||||
NSData *data = (path.length > 0) ? [NSData dataWithContentsOfFile:path] : nil;
|
||||
handleZipData(data);
|
||||
} else {
|
||||
// 正常远程下载
|
||||
[[KBNetworkManager shared] GET:zipURL parameters:nil headers:nil completion:^(id jsonOrData, NSURLResponse *response, NSError *error) {
|
||||
NSData *data = ([jsonOrData isKindOfClass:NSData.class] ? (NSData *)jsonOrData : nil);
|
||||
if (error || data.length == 0) {
|
||||
zipOK = NO;
|
||||
dispatch_group_leave(group);
|
||||
return;
|
||||
}
|
||||
handleZipData(data);
|
||||
}];
|
||||
}
|
||||
}
|
||||
#else
|
||||
zipOK = NO;
|
||||
|
||||
BIN
keyBoard/Class/Resource/001.zip
Normal file
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 155 B |
|
Before Width: | Height: | Size: 156 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |