From 8dbaa9dcf6d9314f380328e8614e3dc7d85edb01 Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Wed, 19 Nov 2025 20:16:19 +0800 Subject: [PATCH] =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8C=96json=20strings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyBoard.xcodeproj/project.pbxproj | 8 + .../Home/VC/FunctionTest/KBSkinCenterVC.m | 6 +- keyBoard/Class/Manager/KBSkinService.h | 11 +- keyBoard/Class/Manager/KBSkinService.m | 365 ++++++++++-------- keyBoard/Class/Resource/KBSkinIconMap.strings | 242 ++++++++++++ 5 files changed, 478 insertions(+), 154 deletions(-) create mode 100644 keyBoard/Class/Resource/KBSkinIconMap.strings diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index ee0b2c8..daad1c6 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -29,6 +29,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 */; }; 043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EAE12EAF940F0089C901 /* KBPermissionViewController.m */; }; 0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */; }; 0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; }; @@ -216,6 +217,7 @@ 04286A052ECC81B200CE730C /* KBSkinService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinService.m; sourceTree = ""; }; 04286A0A2ECD88B400CE730C /* KeyboardAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = KeyboardAssets.xcassets; sourceTree = ""; }; 04286A0E2ECDA71B00CE730C /* 001.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = 001.zip; sourceTree = ""; }; + 04286A122ECDEBF900CE730C /* KBSkinIconMap.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = KBSkinIconMap.strings; sourceTree = ""; }; 0459D1B22EBA284C00F2D189 /* KBSkinCenterVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinCenterVC.h; sourceTree = ""; }; 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinCenterVC.m; sourceTree = ""; }; 0459D1B52EBA287900F2D189 /* KBSkinManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinManager.h; sourceTree = ""; }; @@ -591,6 +593,7 @@ 047C652C2EBCAAAC0035E841 /* Resource */ = { isa = PBXGroup; children = ( + 04286A122ECDEBF900CE730C /* KBSkinIconMap.strings */, 047C652B2EBCAAAC0035E841 /* Images */, 04286A0E2ECDA71B00CE730C /* 001.zip */, ); @@ -1411,6 +1414,7 @@ 04C6EABA2EAF86530089C901 /* Assets.xcassets in Resources */, 04A9FE212EB893F10020DB6D /* Localizable.strings in Resources */, 04C6EABC2EAF86530089C901 /* LaunchScreen.storyboard in Resources */, + 04286A132ECDEBF900CE730C /* KBSkinIconMap.strings in Resources */, 04C6EABD2EAF86530089C901 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1448,10 +1452,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-keyBoard/Pods-keyBoard-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-keyBoard/Pods-keyBoard-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-keyBoard/Pods-keyBoard-frameworks.sh\"\n"; diff --git a/keyBoard/Class/Home/VC/FunctionTest/KBSkinCenterVC.m b/keyBoard/Class/Home/VC/FunctionTest/KBSkinCenterVC.m index 9f13755..05eaca4 100644 --- a/keyBoard/Class/Home/VC/FunctionTest/KBSkinCenterVC.m +++ b/keyBoard/Class/Home/VC/FunctionTest/KBSkinCenterVC.m @@ -188,7 +188,11 @@ if (idx < 0 || idx >= self.skins.count) return; NSDictionary *skin = self.skins[idx]; if (!skin) return; - [[KBSkinService shared] applySkinWithJSON:skin fromViewController:self completion:nil]; + // 默认按远程 Zip 模式测试;如需本地 bundle,请改为 KBSkinSourceModeLocalBundleZip。 + [[KBSkinService shared] applySkinWithJSON:skin + fromViewController:self + mode:KBSkinSourceModeLocalBundleZip + completion:nil]; } @end diff --git a/keyBoard/Class/Manager/KBSkinService.h b/keyBoard/Class/Manager/KBSkinService.h index 7cb5fc8..cef0f49 100644 --- a/keyBoard/Class/Manager/KBSkinService.h +++ b/keyBoard/Class/Manager/KBSkinService.h @@ -14,6 +14,14 @@ NS_ASSUME_NONNULL_BEGIN typedef void(^KBSkinApplyCompletion)(BOOL success); +/// 皮肤资源来源 +typedef NS_ENUM(NSUInteger, KBSkinSourceMode) { + /// 远程 Zip:通过网络下载 zip_url 并解压到 App Group + KBSkinSourceModeRemoteZip = 0, + /// 本地 bundle Zip:从主 App bundle 中读取 zip_url 指定的 zip 文件并解压到 App Group + KBSkinSourceModeLocalBundleZip = 1, +}; + /// 皮肤下载与应用服务(仅主 App 使用) @interface KBSkinService : NSObject @@ -23,12 +31,13 @@ typedef void(^KBSkinApplyCompletion)(BOOL success); /// /// @param skinJSON 与后端约定的皮肤结构(包含 id/name/background_image/hidden_keys/key_icons 等) /// @param presenting 用于弹出“键盘权限引导页”的控制器,可为 nil +/// @param mode 资源来源模式:远程 / 本地 bundle /// @param completion 应用完成回调(下载/写入全部结束后调用,success 表示是否成功) - (void)applySkinWithJSON:(NSDictionary *)skinJSON fromViewController:(nullable UIViewController *)presenting + mode:(KBSkinSourceMode)mode completion:(nullable KBSkinApplyCompletion)completion; @end NS_ASSUME_NONNULL_END - diff --git a/keyBoard/Class/Manager/KBSkinService.m b/keyBoard/Class/Manager/KBSkinService.m index 6cc9d8f..8aab7d7 100644 --- a/keyBoard/Class/Manager/KBSkinService.m +++ b/keyBoard/Class/Manager/KBSkinService.m @@ -25,126 +25,18 @@ static NSDictionary *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" // 发送/换行键 - }; + // 从配置文件加载映射,避免在代码里维护大段字面量。 + // 使用 .strings 形式,便于为每个键添加注释。 + NSString *path = [[NSBundle mainBundle] pathForResource:@"KBSkinIconMap" ofType:@"strings"]; + if (path.length > 0) { + NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path]; + if ([dict isKindOfClass:NSDictionary.class]) { + map = dict; + } + } + if (!map) { + map = @{}; // 防御:配置缺失时返回空表,避免崩溃 + } }); return map; } @@ -157,6 +49,7 @@ - (void)applySkinWithJSON:(NSDictionary *)skinJSON fromViewController:(UIViewController *)presenting + mode:(KBSkinSourceMode)mode completion:(KBSkinApplyCompletion)completion { if (skinJSON.count == 0) { if (completion) completion(NO); @@ -177,10 +70,17 @@ // [KBHUD showInfo:KBLocalized(@"皮肤已应用,键盘需开启“允许完全访问”后才能显示图片")]; // } -#if KB_SKIN_ICON_USE_REMOTE - [self kb_applySkinUsingRemoteIcons:skinJSON completion:completion]; -#else -#endif + switch (mode) { + case KBSkinSourceModeLocalBundleZip: + // 本地 bundle 模式:zip_url 为 bundle 内的 zip 文件名 + [self kb_applySkinUsingLocalBundle:skinJSON completion:completion]; + break; + case KBSkinSourceModeRemoteZip: + default: + // 远程模式:zip_url 为 http/https 地址 + [self kb_applySkinUsingRemoteIcons:skinJSON completion:completion]; + break; + } } /// 远程 Zip 模式:skinJSON 提供 zip_url,一套皮肤一个压缩包。 @@ -231,10 +131,7 @@ __block BOOL zipOK = YES; #if __has_include() - // 若本地尚未缓存该皮肤资源且提供了 zip_url,则下载并解压 Zip 包。 - // 支持两种来源: - // 1) 线上 URL(http/https):通过 KBNetworkManager 下载; - // 2) 本地测试:zip_url 以 "bundle://" 前缀开头,例如 "bundle://001.zip"。 + // 若本地尚未缓存该皮肤资源且提供了 zip_url,则通过网络下载并解压 Zip 包。 if (!hasCachedAssets && zipURL.length > 0) { dispatch_group_enter(group); @@ -314,32 +211,16 @@ 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]; + // 远程下载(http/https) + [[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; } - 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; @@ -383,4 +264,184 @@ }); } +/// 本地 bundle 模式:不走网络,skin[@"zip_url"] 直接为 bundle 内 zip 文件名(可带/不带扩展名)。 +/// - 仍然解压到 AppGroup/Skins//...,方便键盘扩展通过 App Group 读取。 +- (void)kb_applySkinUsingLocalBundle:(NSDictionary *)skin completion:(KBSkinApplyCompletion)completion { + NSString *skinId = skin[@"id"] ?: @"local"; + NSString *name = skin[@"name"] ?: skinId; + NSString *zipName = skin[@"zip_url"] ?: @""; // 本地 bundle 内的 zip 文件名 + + // key_icons 逻辑与远程模式保持一致 + 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() + // 若本地尚未缓存该皮肤资源且提供了 zipName,则从 bundle 读取 zip 并解压。 + if (!hasCachedAssets && zipName.length > 0) { + dispatch_group_enter(group); + + // 兼容旧协议:zipName 可能形如 "bundle://001.zip" + NSString *fileName = zipName ?: @""; + if ([fileName hasPrefix:@"bundle://"]) { + fileName = [fileName substringFromIndex:@"bundle://".length]; + } + + // 支持带子路径,例如 Images/001.zip + NSString *dir = [fileName stringByDeletingLastPathComponent]; + NSString *last = fileName.lastPathComponent; + + NSString *ext = last.pathExtension; + NSString *base = last; + if (ext.length == 0) { + ext = @"zip"; + } else { + base = [last stringByDeletingPathExtension]; + } + + NSString *path = nil; + if (dir.length > 0) { + path = [[NSBundle mainBundle] pathForResource:base ofType:ext inDirectory:dir]; + } else { + path = [[NSBundle mainBundle] pathForResource:base ofType:ext]; + } + + NSData *data = (path.length > 0) ? [NSData dataWithContentsOfFile:path] : nil; + + if (data.length == 0) { + zipOK = NO; + dispatch_group_leave(group); + } else { + // 将 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); + } else { + 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); + } else { + // 兼容“额外包一层目录”的压缩结构,与远程模式保持一致。 + BOOL isDir2 = NO; + NSArray *iconsContent = [fm contentsOfDirectoryAtPath:iconsDir error:NULL]; + BOOL iconsValid = ([fm fileExistsAtPath:iconsDir isDirectory:&isDir2] && isDir2 && iconsContent.count > 0); + if (!iconsValid) { + NSArray *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) { + [fm createDirectoryAtPath:iconsDir + withIntermediateDirectories:YES + attributes:nil + error:NULL]; + 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]; + } + } + } + + 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); + } + } + } + } + #else + zipOK = NO; + #endif + + dispatch_group_notify(group, dispatch_get_main_queue(), ^{ + // 构造 key_icons -> App Group 相对路径 映射 + NSMutableDictionary *iconPathMap = [NSMutableDictionary dictionary]; + [iconShortNames enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, NSString *shortName, BOOL *stop) { + if (![shortName isKindOfClass:NSString.class] || shortName.length == 0) return; + NSString *fileName = shortName; + 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]; + + 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 diff --git a/keyBoard/Class/Resource/KBSkinIconMap.strings b/keyBoard/Class/Resource/KBSkinIconMap.strings new file mode 100644 index 0000000..b750723 --- /dev/null +++ b/keyBoard/Class/Resource/KBSkinIconMap.strings @@ -0,0 +1,242 @@ +/* 字母 q(小写) */ +"letter_q_lower" = "key_q"; +/* 字母 Q(大写) */ +"letter_q_upper" = "key_q"; + +/* 字母 w(小写) */ +"letter_w_lower" = "key_w"; +/* 字母 W(大写) */ +"letter_w_upper" = "key_w"; + +/* 字母 e(小写) */ +"letter_e_lower" = "key_e"; +/* 字母 E(大写) */ +"letter_e_upper" = "key_e"; + +/* 字母 r(小写) */ +"letter_r_lower" = "key_r"; +/* 字母 R(大写) */ +"letter_r_upper" = "key_r"; + +/* 字母 t(小写) */ +"letter_t_lower" = "key_t"; +/* 字母 T(大写) */ +"letter_t_upper" = "key_t"; + +/* 字母 y(小写) */ +"letter_y_lower" = "key_y"; +/* 字母 Y(大写) */ +"letter_y_upper" = "key_y"; + +/* 字母 u(小写) */ +"letter_u_lower" = "key_u"; +/* 字母 U(大写) */ +"letter_u_upper" = "key_u"; + +/* 字母 i(小写) */ +"letter_i_lower" = "key_i"; +/* 字母 I(大写) */ +"letter_i_upper" = "key_i"; + +/* 字母 o(小写) */ +"letter_o_lower" = "key_o"; +/* 字母 O(大写) */ +"letter_o_upper" = "key_o"; + +/* 字母 p(小写) */ +"letter_p_lower" = "key_p"; +/* 字母 P(大写) */ +"letter_p_upper" = "key_p"; + +/* 字母 a(小写) */ +"letter_a_lower" = "key_a"; +/* 字母 A(大写) */ +"letter_a_upper" = "key_a"; + +/* 字母 s(小写) */ +"letter_s_lower" = "key_s"; +/* 字母 S(大写) */ +"letter_s_upper" = "key_s"; + +/* 字母 d(小写) */ +"letter_d_lower" = "key_d"; +/* 字母 D(大写) */ +"letter_d_upper" = "key_d"; + +/* 字母 f(小写) */ +"letter_f_lower" = "key_f"; +/* 字母 F(大写) */ +"letter_f_upper" = "key_f"; + +/* 字母 g(小写) */ +"letter_g_lower" = "key_g"; +/* 字母 G(大写) */ +"letter_g_upper" = "key_g"; + +/* 字母 h(小写) */ +"letter_h_lower" = "key_h"; +/* 字母 H(大写) */ +"letter_h_upper" = "key_h"; + +/* 字母 j(小写) */ +"letter_j_lower" = "key_j"; +/* 字母 J(大写) */ +"letter_j_upper" = "key_j"; + +/* 字母 k(小写) */ +"letter_k_lower" = "key_k"; +/* 字母 K(大写) */ +"letter_k_upper" = "key_k"; + +/* 字母 l(小写) */ +"letter_l_lower" = "key_l"; +/* 字母 L(大写) */ +"letter_l_upper" = "key_l"; + +/* 字母 z(小写) */ +"letter_z_lower" = "key_z"; +/* 字母 Z(大写) */ +"letter_z_upper" = "key_z"; + +/* 字母 x(小写) */ +"letter_x_lower" = "key_x"; +/* 字母 X(大写) */ +"letter_x_upper" = "key_x"; + +/* 字母 c(小写) */ +"letter_c_lower" = "key_c"; +/* 字母 C(大写) */ +"letter_c_upper" = "key_c"; + +/* 字母 v(小写) */ +"letter_v_lower" = "key_v"; +/* 字母 V(大写) */ +"letter_v_upper" = "key_v"; + +/* 字母 b(小写) */ +"letter_b_lower" = "key_b"; +/* 字母 B(大写) */ +"letter_b_upper" = "key_b"; + +/* 字母 n(小写) */ +"letter_n_lower" = "key_n"; +/* 字母 N(大写) */ +"letter_n_upper" = "key_n"; + +/* 字母 m(小写) */ +"letter_m_lower" = "key_m"; +/* 字母 M(大写) */ +"letter_m_upper" = "key_m"; + +/* 数字 1 */ +"digit_1" = "key_1"; +/* 数字 2 */ +"digit_2" = "key_2"; +/* 数字 3 */ +"digit_3" = "key_3"; +/* 数字 4 */ +"digit_4" = "key_4"; +/* 数字 5 */ +"digit_5" = "key_5"; +/* 数字 6 */ +"digit_6" = "key_6"; +/* 数字 7 */ +"digit_7" = "key_7"; +/* 数字 8 */ +"digit_8" = "key_8"; +/* 数字 9 */ +"digit_9" = "key_9"; +/* 数字 0 */ +"digit_0" = "key_0"; + +/* '-' */ +"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(⇧) */ +"shift" = "key_up"; +/* 字母面板左下角 "123" */ +"mode_123" = "key_123"; +/* 数字面板左下角 "abc" */ +"mode_abc" = "key_abc"; +/* 数字面板内 "123 -> #+=" */ +"symbols_toggle_more" = "key_symbols_more"; +/* 数字面板内 "#+= -> 123" */ +"symbols_toggle_123" = "key_symbols_123"; +/* 自定义 AI 功能键 */ +"ai" = "key_ai"; +/* 发送/换行键 */ +"return" = "key_send"; +