494 lines
24 KiB
Objective-C
494 lines
24 KiB
Objective-C
//
|
||
// KBSkinService.m
|
||
// keyBoard
|
||
//
|
||
|
||
#import "KBSkinService.h"
|
||
|
||
#import "KBSkinManager.h"
|
||
#import "KBConfig.h"
|
||
#import "KBKeyboardPermissionManager.h"
|
||
#import "KBNetworkManager.h"
|
||
#import "KBHUD.h"
|
||
|
||
#if __has_include(<SSZipArchive/SSZipArchive.h>)
|
||
#import <SSZipArchive/SSZipArchive.h>
|
||
#endif
|
||
|
||
@implementation KBSkinService
|
||
|
||
#pragma mark - Icon short-name mapping (local default)
|
||
|
||
/// 本地维护的一份“逻辑按键标识 -> 图标短文件名”映射表。
|
||
/// - 若后端 skinJSON 未提供 key_icons,则远程 Zip 模式会回退使用本表;仅依赖 zip_url + 命名规范即可。
|
||
+ (NSDictionary<NSString *, NSString *> *)kb_defaultIconShortNames {
|
||
static NSDictionary<NSString *, NSString *> *map;
|
||
static dispatch_once_t onceToken;
|
||
dispatch_once(&onceToken, ^{
|
||
map = @{
|
||
// 字母键(大小写共用)
|
||
@"letter_q_lower": @"key_q", // 字母 q(小写)
|
||
@"letter_q_upper": @"key_q", // 字母 Q(大写)
|
||
@"letter_w_lower": @"key_w", // 字母 w(小写)
|
||
@"letter_w_upper": @"key_w", // 字母 W(大写)
|
||
@"letter_e_lower": @"key_e", // 字母 e(小写)
|
||
@"letter_e_upper": @"key_e", // 字母 E(大写)
|
||
@"letter_r_lower": @"key_r", // 字母 r(小写)
|
||
@"letter_r_upper": @"key_r", // 字母 R(大写)
|
||
@"letter_t_lower": @"key_t", // 字母 t(小写)
|
||
@"letter_t_upper": @"key_t", // 字母 T(大写)
|
||
@"letter_y_lower": @"key_y", // 字母 y(小写)
|
||
@"letter_y_upper": @"key_y", // 字母 Y(大写)
|
||
@"letter_u_lower": @"key_u", // 字母 u(小写)
|
||
@"letter_u_upper": @"key_u", // 字母 U(大写)
|
||
@"letter_i_lower": @"key_i", // 字母 i(小写)
|
||
@"letter_i_upper": @"key_i", // 字母 I(大写)
|
||
@"letter_o_lower": @"key_o", // 字母 o(小写)
|
||
@"letter_o_upper": @"key_o", // 字母 O(大写)
|
||
@"letter_p_lower": @"key_p", // 字母 p(小写)
|
||
@"letter_p_upper": @"key_p", // 字母 P(大写)
|
||
|
||
@"letter_a_lower": @"key_a", // 字母 a(小写)
|
||
@"letter_a_upper": @"key_a", // 字母 A(大写)
|
||
@"letter_s_lower": @"key_s", // 字母 s(小写)
|
||
@"letter_s_upper": @"key_s", // 字母 S(大写)
|
||
@"letter_d_lower": @"key_d", // 字母 d(小写)
|
||
@"letter_d_upper": @"key_d", // 字母 D(大写)
|
||
@"letter_f_lower": @"key_f", // 字母 f(小写)
|
||
@"letter_f_upper": @"key_f", // 字母 F(大写)
|
||
@"letter_g_lower": @"key_g", // 字母 g(小写)
|
||
@"letter_g_upper": @"key_g", // 字母 G(大写)
|
||
@"letter_h_lower": @"key_h", // 字母 h(小写)
|
||
@"letter_h_upper": @"key_h", // 字母 H(大写)
|
||
@"letter_j_lower": @"key_j", // 字母 j(小写)
|
||
@"letter_j_upper": @"key_j", // 字母 J(大写)
|
||
@"letter_k_lower": @"key_k", // 字母 k(小写)
|
||
@"letter_k_upper": @"key_k", // 字母 K(大写)
|
||
@"letter_l_lower": @"key_l", // 字母 l(小写)
|
||
@"letter_l_upper": @"key_l", // 字母 L(大写)
|
||
|
||
@"letter_z_lower": @"key_z", // 字母 z(小写)
|
||
@"letter_z_upper": @"key_z", // 字母 Z(大写)
|
||
@"letter_x_lower": @"key_x", // 字母 x(小写)
|
||
@"letter_x_upper": @"key_x", // 字母 X(大写)
|
||
@"letter_c_lower": @"key_c", // 字母 c(小写)
|
||
@"letter_c_upper": @"key_c", // 字母 C(大写)
|
||
@"letter_v_lower": @"key_v", // 字母 v(小写)
|
||
@"letter_v_upper": @"key_v", // 字母 V(大写)
|
||
@"letter_b_lower": @"key_b", // 字母 b(小写)
|
||
@"letter_b_upper": @"key_b", // 字母 B(大写)
|
||
@"letter_n_lower": @"key_n", // 字母 n(小写)
|
||
@"letter_n_upper": @"key_n", // 字母 N(大写)
|
||
@"letter_m_lower": @"key_m", // 字母 m(小写)
|
||
@"letter_m_upper": @"key_m", // 字母 M(大写)
|
||
|
||
// 数字键(数字面板顶行 1~0)
|
||
@"digit_1": @"key_1", // 数字 1
|
||
@"digit_2": @"key_2", // 数字 2
|
||
@"digit_3": @"key_3", // 数字 3
|
||
@"digit_4": @"key_4", // 数字 4
|
||
@"digit_5": @"key_5", // 数字 5
|
||
@"digit_6": @"key_6", // 数字 6
|
||
@"digit_7": @"key_7", // 数字 7
|
||
@"digit_8": @"key_8", // 数字 8
|
||
@"digit_9": @"key_9", // 数字 9
|
||
@"digit_0": @"key_0", // 数字 0
|
||
|
||
// 常用符号(123 页第二行 + 第三行)
|
||
@"sym_minus": @"key_minus", // '-'
|
||
@"sym_slash": @"key_slash", // '/'
|
||
@"sym_colon": @"key_colon", // ':'
|
||
@"sym_semicolon": @"key_semicolon",// ';'
|
||
@"sym_paren_l": @"key_paren_l", // '('
|
||
@"sym_paren_r": @"key_paren_r", // ')'
|
||
@"sym_dollar": @"key_dollar", // '$'
|
||
@"sym_amp": @"key_amp", // '&'
|
||
@"sym_at": @"key_at", // '@'
|
||
@"sym_quote_double": @"key_quote_d", // 双引号 "
|
||
|
||
@"sym_comma": @"key_comma", // ','
|
||
@"sym_dot": @"key_dot", // '.'
|
||
@"sym_question": @"key_question", // '?'
|
||
@"sym_exclam": @"key_exclam", // '!'
|
||
@"sym_quote_single": @"key_quote", // 单引号 '
|
||
|
||
// #+= 页额外符号(包括中括号/大括号等)
|
||
@"sym_bracket_l": @"key_bracket_l", // '['
|
||
@"sym_bracket_r": @"key_bracket_r", // ']'
|
||
@"sym_brace_l": @"key_brace_l", // '{'
|
||
@"sym_brace_r": @"key_brace_r", // '}'
|
||
@"sym_hash": @"key_hash", // '#'
|
||
@"sym_percent": @"key_percent", // '%'
|
||
@"sym_caret": @"key_caret", // '^'
|
||
@"sym_asterisk": @"key_asterisk", // '*'
|
||
@"sym_plus": @"key_plus", // '+'
|
||
@"sym_equal": @"key_equal", // '='
|
||
|
||
@"sym_underscore": @"key_underscore", // '_'
|
||
@"sym_backslash": @"key_backslash", // '\'
|
||
@"sym_pipe": @"key_pipe", // '|'
|
||
@"sym_tilde": @"key_tilde", // '~'
|
||
@"sym_lt": @"key_lt", // '<'
|
||
@"sym_gt": @"key_gt", // '>'
|
||
@"sym_euro": @"key_euro", // '€'
|
||
@"sym_pound": @"key_pound", // '£'
|
||
@"sym_bullet": @"key_bullet", // '•'
|
||
|
||
// 功能键(非字符输出)
|
||
@"space": @"key_space", // 空格键
|
||
@"backspace": @"key_del", // 删除键(⌫)
|
||
@"shift": @"key_up", // Shift(⇧)
|
||
@"mode_123": @"key_123", // 字母面板左下角 "123"
|
||
@"mode_abc": @"key_abc", // 数字面板左下角 "abc"
|
||
@"symbols_toggle_more": @"key_symbols_more", // 数字面板内 "123 -> #+="
|
||
@"symbols_toggle_123": @"key_symbols_123", // 数字面板内 "#+= -> 123"
|
||
@"ai": @"key_ai", // 自定义 AI 功能键
|
||
@"return": @"key_send" // 发送/换行键
|
||
};
|
||
});
|
||
return map;
|
||
}
|
||
|
||
+ (instancetype)shared {
|
||
static KBSkinService *s; static dispatch_once_t onceToken;
|
||
dispatch_once(&onceToken, ^{ s = [KBSkinService new]; });
|
||
return s;
|
||
}
|
||
|
||
- (void)applySkinWithJSON:(NSDictionary *)skinJSON
|
||
fromViewController:(UIViewController *)presenting
|
||
completion:(KBSkinApplyCompletion)completion {
|
||
if (skinJSON.count == 0) {
|
||
if (completion) completion(NO);
|
||
return;
|
||
}
|
||
|
||
// 1. 点击应用皮肤时,检查键盘启用 & 完全访问状态,并尽量给出友好提示。
|
||
KBKeyboardPermissionManager *perm = [KBKeyboardPermissionManager shared];
|
||
BOOL enabled = [perm isKeyboardEnabled];
|
||
KBFARecord fa = [perm lastKnownFullAccess];
|
||
BOOL hasFullAccess = (fa == KBFARecordGranted);
|
||
|
||
if (!enabled || !hasFullAccess) {
|
||
// 引导页(内部有自己的展示策略,避免过度打扰)
|
||
[perm presentPermissionIfNeededFrom:presenting];
|
||
|
||
// 简单提示:皮肤可以应用,但未开启完全访问时扩展无法读取 App Group 中的图片。
|
||
[KBHUD showInfo:KBLocalized(@"皮肤已应用,键盘需开启“允许完全访问”后才能显示图片")];
|
||
}
|
||
|
||
#if KB_SKIN_ICON_USE_REMOTE
|
||
[self kb_applySkinUsingRemoteIcons:skinJSON completion:completion];
|
||
#else
|
||
[self kb_applySkinUsingLocalIcons:skinJSON completion:completion];
|
||
#endif
|
||
}
|
||
|
||
#pragma mark - Internal helpers
|
||
|
||
/// 本地模式:key_icons 的 value 为 Assets 名称(主 App bundle 内),导出到 App Group 后再写入主题。
|
||
- (void)kb_applySkinUsingLocalIcons:(NSDictionary *)skin completion:(KBSkinApplyCompletion)completion {
|
||
NSString *skinId = skin[@"id"] ?: @"local";
|
||
NSString *name = skin[@"name"] ?: skinId;
|
||
|
||
NSDictionary *iconNames = [skin[@"key_icons"] isKindOfClass:NSDictionary.class] ? skin[@"key_icons"] : @{};
|
||
NSMutableDictionary<NSString *, NSString *> *iconPathMap = [NSMutableDictionary dictionary];
|
||
|
||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||
if (containerURL && iconNames.count > 0) {
|
||
NSString *skinsRoot = [containerURL.path stringByAppendingPathComponent:@"Skins"];
|
||
NSString *skinRoot = [skinsRoot stringByAppendingPathComponent:skinId];
|
||
NSString *iconsDir = [skinRoot stringByAppendingPathComponent:@"icons"];
|
||
[[NSFileManager defaultManager] createDirectoryAtPath:iconsDir
|
||
withIntermediateDirectories:YES
|
||
attributes:nil
|
||
error:NULL];
|
||
|
||
[iconNames enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, NSString *imageName, BOOL *stop) {
|
||
if (![imageName isKindOfClass:NSString.class] || imageName.length == 0) return;
|
||
UIImage *img = [UIImage imageNamed:imageName];
|
||
if (!img) return;
|
||
NSData *data = UIImagePNGRepresentation(img);
|
||
if (data.length == 0) return;
|
||
NSString *fileName = [NSString stringWithFormat:@"%@.png", identifier];
|
||
NSString *fullPath = [iconsDir stringByAppendingPathComponent:fileName];
|
||
if ([data writeToFile:fullPath atomically:YES]) {
|
||
NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@", skinId, fileName];
|
||
iconPathMap[identifier] = relative;
|
||
}
|
||
}];
|
||
}
|
||
|
||
NSMutableDictionary *themeJSON = [skin mutableCopy];
|
||
if (iconPathMap.count > 0) {
|
||
themeJSON[@"key_icons"] = iconPathMap.copy; // value 改为 App Group 相对路径
|
||
}
|
||
|
||
// 先应用颜色 / hidden_keys / key_icons(此时为相对路径,扩展从 App Group 读取)
|
||
BOOL themeOK = [[KBSkinManager shared] applyThemeFromJSON:themeJSON];
|
||
|
||
// 再处理背景图片(background_image),与原逻辑一致
|
||
NSString *bgURL = skin[@"background_image"] ?: @"";
|
||
if (bgURL.length == 0) {
|
||
if (completion) completion(themeOK);
|
||
if (themeOK) {
|
||
[KBHUD showInfo:KBLocalized(@"已应用皮肤(无背景图)")];
|
||
} else {
|
||
[KBHUD showInfo:KBLocalized(@"应用皮肤失败")];
|
||
}
|
||
return;
|
||
}
|
||
|
||
[[KBNetworkManager shared] GET:bgURL parameters:nil headers:nil completion:^(id jsonOrData, NSURLResponse *response, NSError *error) {
|
||
NSData *data = ([jsonOrData isKindOfClass:NSData.class] ? (NSData *)jsonOrData : nil);
|
||
// 压缩尺寸,避免 Keychain 过大
|
||
if (data && data.length > 0) {
|
||
UIImage *img = [UIImage imageWithData:data];
|
||
if (img) {
|
||
CGFloat maxW = 1500.0;
|
||
if (img.size.width > maxW) {
|
||
CGFloat scale = maxW / img.size.width;
|
||
CGSize newSize = CGSizeMake(maxW, floor(img.size.height * scale));
|
||
UIGraphicsBeginImageContextWithOptions(newSize, YES, 1.0);
|
||
[img drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
|
||
UIImage *resized = UIGraphicsGetImageFromCurrentImageContext();
|
||
UIGraphicsEndImageContext();
|
||
img = resized ?: img;
|
||
}
|
||
data = UIImageJPEGRepresentation(img, 0.85) ?: data;
|
||
}
|
||
}
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
NSData *payload = data;
|
||
if (payload.length == 0) {
|
||
// 兜底:生成一张简单渐变图片
|
||
CGSize size = CGSizeMake(1200, 600);
|
||
UIGraphicsBeginImageContextWithOptions(size, YES, 1.0);
|
||
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
||
UIColor *c1 = [UIColor colorWithRed:0.76 green:0.91 blue:0.86 alpha:1];
|
||
UIColor *c2 = [UIColor colorWithRed:0.93 green:0.97 blue:0.91 alpha:1];
|
||
if ([skin[@"id"] hasPrefix:@"dark"]) {
|
||
c1 = [UIColor colorWithRed:0.1 green:0.12 blue:0.16 alpha:1];
|
||
c2 = [UIColor colorWithRed:0.22 green:0.24 blue:0.28 alpha:1];
|
||
}
|
||
CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
|
||
NSArray *colors = @[(__bridge id)c1.CGColor, (__bridge id)c2.CGColor];
|
||
CGFloat locs[] = {0,1};
|
||
CGGradientRef grad = CGGradientCreateWithColors(space, (__bridge CFArrayRef)colors, locs);
|
||
CGContextDrawLinearGradient(ctx, grad, CGPointZero, CGPointMake(size.width, size.height), 0);
|
||
CGGradientRelease(grad); CGColorSpaceRelease(space);
|
||
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
|
||
UIGraphicsEndImageContext();
|
||
payload = UIImageJPEGRepresentation(img, 0.9);
|
||
}
|
||
BOOL ok = (payload.length > 0)
|
||
? [[KBSkinManager shared] applyImageSkinWithData:payload skinId:skinId name:name]
|
||
: themeOK;
|
||
if (completion) completion(ok);
|
||
[KBHUD showInfo:(ok ? KBLocalized(@"已应用,切到键盘查看") : KBLocalized(@"应用皮肤失败"))];
|
||
});
|
||
}];
|
||
}
|
||
|
||
/// 远程 Zip 模式:skinJSON 提供 zip_url,一套皮肤一个压缩包。
|
||
/// - Zip 解压路径:AppGroup/Skins/<skinId>/...
|
||
/// * 图标:icons/<shortName>.png,例如 icons/key_a.png
|
||
/// * 背景:background.png(可选)
|
||
/// - skinJSON.key_icons 的 value 填写 Zip 内的图标“短文件名”(不含路径,可不含扩展名),例如 "key_a"
|
||
/// 应用时会被转换为相对 App Group 根目录的路径:Skins/<skinId>/icons/<shortName>.png
|
||
- (void)kb_applySkinUsingRemoteIcons:(NSDictionary *)skin completion:(KBSkinApplyCompletion)completion {
|
||
NSString *skinId = skin[@"id"] ?: @"remote";
|
||
NSString *name = skin[@"name"] ?: skinId;
|
||
NSString *zipURL = skin[@"zip_url"] ?: @""; // 新协议:远程 Zip 包地址
|
||
|
||
// key_icons 可选:
|
||
// - 若后端提供 key_icons,则优先使用服务端映射;
|
||
// - 若未提供,则回退到本地默认映射(kb_defaultIconShortNames),这样后端只需返回 id/name/zip_url。
|
||
NSDictionary *iconShortNames = nil;
|
||
if ([skin[@"key_icons"] isKindOfClass:NSDictionary.class]) {
|
||
iconShortNames = skin[@"key_icons"];
|
||
} else {
|
||
iconShortNames = [self.class kb_defaultIconShortNames];
|
||
}
|
||
|
||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||
if (!containerURL) {
|
||
if (completion) completion(NO);
|
||
[KBHUD showInfo:KBLocalized(@"无法访问共享容器,应用皮肤失败")];
|
||
return;
|
||
}
|
||
|
||
NSString *skinsRoot = [containerURL.path stringByAppendingPathComponent:@"Skins"];
|
||
NSString *skinRoot = [skinsRoot stringByAppendingPathComponent:skinId];
|
||
NSString *iconsDir = [skinRoot stringByAppendingPathComponent:@"icons"];
|
||
[[NSFileManager defaultManager] createDirectoryAtPath:iconsDir
|
||
withIntermediateDirectories:YES
|
||
attributes:nil
|
||
error:NULL];
|
||
|
||
NSFileManager *fm = [NSFileManager defaultManager];
|
||
BOOL isDir = NO;
|
||
BOOL hasIconsDir = [fm fileExistsAtPath:iconsDir isDirectory:&isDir] && isDir;
|
||
NSArray *contents = hasIconsDir ? [fm contentsOfDirectoryAtPath:iconsDir error:NULL] : nil;
|
||
BOOL hasCachedAssets = (contents.count > 0);
|
||
|
||
NSString *bgPath = [skinRoot stringByAppendingPathComponent:@"background.png"];
|
||
|
||
dispatch_group_t group = dispatch_group_create();
|
||
__block BOOL zipOK = YES;
|
||
|
||
#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);
|
||
|
||
void (^handleZipData)(NSData *) = ^(NSData *data) {
|
||
if (data.length == 0) {
|
||
zipOK = NO;
|
||
dispatch_group_leave(group);
|
||
return;
|
||
}
|
||
// 将 Zip 写入临时路径再解压
|
||
[[NSFileManager defaultManager] createDirectoryAtPath:skinRoot
|
||
withIntermediateDirectories:YES
|
||
attributes:nil
|
||
error:NULL];
|
||
NSString *zipPath = [skinRoot stringByAppendingPathComponent:@"skin.zip"];
|
||
if (![data writeToFile:zipPath atomically:YES]) {
|
||
zipOK = NO;
|
||
dispatch_group_leave(group);
|
||
return;
|
||
}
|
||
|
||
NSError *unzipError = nil;
|
||
BOOL ok = [SSZipArchive unzipFileAtPath:zipPath
|
||
toDestination:skinRoot
|
||
overwrite:YES
|
||
password:nil
|
||
error:&unzipError];
|
||
[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;
|
||
#endif
|
||
|
||
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
||
// 构造 key_icons -> App Group 相对路径 映射
|
||
NSMutableDictionary<NSString *, NSString *> *iconPathMap = [NSMutableDictionary dictionary];
|
||
[iconShortNames enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, NSString *shortName, BOOL *stop) {
|
||
if (![shortName isKindOfClass:NSString.class] || shortName.length == 0) return;
|
||
NSString *fileName = shortName;
|
||
// 若未带扩展名,默认按 .png 处理
|
||
if (fileName.pathExtension.length == 0) {
|
||
fileName = [fileName stringByAppendingPathExtension:@"png"];
|
||
}
|
||
NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@", skinId, fileName];
|
||
iconPathMap[identifier] = relative;
|
||
}];
|
||
|
||
NSMutableDictionary *themeJSON = [skin mutableCopy];
|
||
themeJSON[@"id"] = skinId;
|
||
if (iconPathMap.count > 0) {
|
||
themeJSON[@"key_icons"] = iconPathMap.copy;
|
||
}
|
||
|
||
BOOL themeOK = [[KBSkinManager shared] applyThemeFromJSON:themeJSON];
|
||
|
||
// 背景图优先从 Zip 解压出的 background.png 读取
|
||
NSData *bgData = [NSData dataWithContentsOfFile:bgPath];
|
||
BOOL ok = themeOK;
|
||
if (bgData.length > 0) {
|
||
ok = [[KBSkinManager shared] applyImageSkinWithData:bgData skinId:skinId name:name];
|
||
}
|
||
|
||
if (!zipOK && !hasCachedAssets) {
|
||
ok = NO;
|
||
}
|
||
|
||
if (completion) completion(ok);
|
||
[KBHUD showInfo:(ok ? KBLocalized(@"已应用,切到键盘查看") : KBLocalized(@"应用皮肤失败"))];
|
||
});
|
||
}
|
||
|
||
@end
|