diff --git a/CustomKeyboard/Resource/Christmas.zip b/CustomKeyboard/Resource/Christmas.zip index cf469e8..e9d8ea4 100644 Binary files a/CustomKeyboard/Resource/Christmas.zip and b/CustomKeyboard/Resource/Christmas.zip differ diff --git a/Shared/KBSkinInstallBridge.h b/Shared/KBSkinInstallBridge.h index 657c45c..2df14fc 100644 --- a/Shared/KBSkinInstallBridge.h +++ b/Shared/KBSkinInstallBridge.h @@ -10,6 +10,18 @@ NS_ASSUME_NONNULL_BEGIN /// 跨进程通知:主 App 请求键盘扩展安装皮肤。 extern NSString * const KBDarwinSkinInstallRequestNotification; +/// 皮肤安装桥接错误域。 +extern NSErrorDomain const KBSkinBridgeErrorDomain; + +/// 皮肤安装桥接错误码。 +typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) { + KBSkinBridgeErrorInvalidPayload = 1, + KBSkinBridgeErrorContainerUnavailable, + KBSkinBridgeErrorZipMissing, + KBSkinBridgeErrorUnzipFailed, + KBSkinBridgeErrorApplyFailed, +}; + typedef void (^KBSkinInstallConsumeCompletion)(BOOL success, NSError * _Nullable error); @interface KBSkinInstallBridge : NSObject @@ -17,6 +29,17 @@ typedef void (^KBSkinInstallConsumeCompletion)(BOOL success, NSError * _Nullable /// 默认图标短文件名映射(从 KBSkinIconMap.strings 读取)。 + (NSDictionary *)defaultIconShortNames; +/// 主 App / 键盘扩展:通过远程 zip_url 下载并安装一套皮肤。 +/// - skinJSON 结构与后端约定一致,至少包含: +/// * id: 皮肤唯一标识 +/// * name: 展示名称(可选,缺省为 id) +/// * zip_url: 远程 Zip 地址(http/https) +/// * key_icons: 按键 -> 图标“短文件名”映射(可选,不传则使用 defaultIconShortNames) +/// - 内部会将 Zip 解压到 App Group/Skins//...,并使用 KBSkinManager 应用主题与背景图。 +/// - 应用成功后,KBSkinManager 会广播皮肤变更通知,键盘扩展可立即感知。 ++ (void)installRemoteSkinWithJSON:(NSDictionary *)skinJSON + completion:(nullable KBSkinInstallConsumeCompletion)completion; + /// 主 App 侧:记录一个“从 bundle 解压皮肤”的请求,写入 App Group 并广播 Darwin 通知。 + (void)publishBundleSkinRequestWithId:(NSString *)skinId name:(NSString *)name @@ -36,4 +59,3 @@ typedef void (^KBSkinInstallConsumeCompletion)(BOOL success, NSError * _Nullable @end NS_ASSUME_NONNULL_END - diff --git a/Shared/KBSkinInstallBridge.m b/Shared/KBSkinInstallBridge.m index fd900eb..1681d0d 100644 --- a/Shared/KBSkinInstallBridge.m +++ b/Shared/KBSkinInstallBridge.m @@ -6,11 +6,15 @@ #import "KBConfig.h" #import "KBSkinManager.h" +#if __has_include("KBNetworkManager.h") +#import "KBNetworkManager.h" +#endif #if __has_include() #import #endif NSString * const KBDarwinSkinInstallRequestNotification = @"com.loveKey.nyx.skin.install.request"; +NSErrorDomain const KBSkinBridgeErrorDomain = @"com.loveKey.nyx.skin.bridge"; static NSString * const kKBSkinPendingRequestKey = @"com.loveKey.nyx.skin.pending"; static NSString * const kKBSkinPendingSkinIdKey = @"skinId"; @@ -20,16 +24,6 @@ static NSString * const kKBSkinPendingKindKey = @"kind"; static NSString * const kKBSkinPendingTimestampKey = @"timestamp"; static NSString * const kKBSkinPendingIconShortKey = @"iconShortNames"; -static NSString * const kKBSkinBridgeErrorDomain = @"com.loveKey.nyx.skin.bridge"; - -typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) { - KBSkinBridgeErrorInvalidPayload = 1, - KBSkinBridgeErrorContainerUnavailable, - KBSkinBridgeErrorZipMissing, - KBSkinBridgeErrorUnzipFailed, - KBSkinBridgeErrorApplyFailed, -}; - @implementation KBSkinInstallBridge + (NSDictionary *)defaultIconShortNames { @@ -54,6 +48,245 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) { return [[NSUserDefaults alloc] initWithSuiteName:AppGroup]; } ++ (void)installRemoteSkinWithJSON:(NSDictionary *)skinJSON + completion:(KBSkinInstallConsumeCompletion)completion { + if (![skinJSON isKindOfClass:NSDictionary.class] || skinJSON.count == 0) { + if (completion) { + NSError *err = [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorInvalidPayload + userInfo:nil]; + dispatch_async(dispatch_get_main_queue(), ^{ completion(NO, err); }); + } + return; + } + + NSString *skinId = skinJSON[@"id"] ?: @"remote"; + NSString *name = skinJSON[@"name"] ?: skinId; + NSString *zipURL = skinJSON[@"zip_url"] ?: @""; + + // key_icons 可选: + // - 若后端提供 key_icons,则优先使用服务端映射; + // - 若未提供,则回退到本地默认映射,这样后端只需返回 id/name/zip_url。 + NSDictionary *iconShortNames = nil; + if ([skinJSON[@"key_icons"] isKindOfClass:NSDictionary.class]) { + iconShortNames = skinJSON[@"key_icons"]; + } else { + iconShortNames = [self defaultIconShortNames]; + } + + NSFileManager *fm = [NSFileManager defaultManager]; + NSURL *containerURL = [fm containerURLForSecurityApplicationGroupIdentifier:AppGroup]; + if (!containerURL) { + if (completion) { + NSError *err = [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorContainerUnavailable + userInfo:@{NSLocalizedDescriptionKey: @"Shared container unavailable"}]; + dispatch_async(dispatch_get_main_queue(), ^{ completion(NO, err); }); + } + return; + } + + NSString *skinsRoot = [containerURL.path stringByAppendingPathComponent:@"Skins"]; + NSString *skinRoot = [skinsRoot stringByAppendingPathComponent:skinId]; + NSString *iconsDir = [skinRoot stringByAppendingPathComponent:@"icons"]; + [fm createDirectoryAtPath:iconsDir + withIntermediateDirectories:YES + attributes:nil + error:NULL]; + + 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; + __block NSError *innerError = nil; + +#if __has_include() + // 若本地尚未缓存该皮肤资源且提供了 zip_url,则通过网络下载并解压 Zip 包。 + if (!hasCachedAssets && zipURL.length > 0) { + dispatch_group_enter(group); + + void (^handleZipData)(NSData *) = ^(NSData *data) { + if (data.length == 0) { + zipOK = NO; + if (!innerError) { + innerError = [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorZipMissing + userInfo:@{NSLocalizedDescriptionKey: @"Zip data is empty"}]; + } + dispatch_group_leave(group); + return; + } + // 将 Zip 写入临时路径再解压 + [fm createDirectoryAtPath:skinRoot + withIntermediateDirectories:YES + attributes:nil + error:NULL]; + NSString *zipPath = [skinRoot stringByAppendingPathComponent:@"skin.zip"]; + if (![data writeToFile:zipPath atomically:YES]) { + zipOK = NO; + if (!innerError) { + innerError = [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorUnzipFailed + userInfo:@{NSLocalizedDescriptionKey: @"Failed to write zip file"}]; + } + 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; + if (!innerError) { + innerError = unzipError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorUnzipFailed + userInfo:nil]; + } + dispatch_group_leave(group); + return; + } + + // 兼容“额外包一层目录”的压缩结构: + // 若 Skins//icons 为空,但存在 Skins//<子目录>/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 *subItems = [fm contentsOfDirectoryAtPath:skinRoot error:NULL]; + for (NSString *subName in subItems) { + if ([subName isEqualToString:@"icons"] || [subName isEqualToString:@"__MACOSX"]) continue; + NSString *nestedRoot = [skinRoot stringByAppendingPathComponent:subName]; + 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); + }; + +#if __has_include("KBNetworkManager.h") + // 远程下载(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; + if (!innerError) { + innerError = error ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorZipMissing + userInfo:@{NSLocalizedDescriptionKey: @"Failed to download zip"}]; + } + dispatch_group_leave(group); + return; + } + handleZipData(data); + }]; +#else + // 无 KBNetworkManager 时,退回到简单的 dataWithContentsOfURL 下载(阻塞当前线程) + dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + NSURL *url = [NSURL URLWithString:zipURL]; + NSData *data = url ? [NSData dataWithContentsOfURL:url] : nil; + if (!data) { + zipOK = NO; + if (!innerError) { + innerError = [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorZipMissing + userInfo:@{NSLocalizedDescriptionKey: @"Failed to download zip"}]; + } + dispatch_group_leave(group); + } else { + handleZipData(data); + } + }); +#endif + } +#else + zipOK = NO; + innerError = [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorUnzipFailed + userInfo:@{NSLocalizedDescriptionKey: @"SSZipArchive not available"}]; +#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; + // 若未带扩展名,默认按 .png 处理 + if (fileName.pathExtension.length == 0) { + fileName = [fileName stringByAppendingPathExtension:@"png"]; + } + NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@", skinId, fileName]; + iconPathMap[identifier] = relative; + }]; + + NSMutableDictionary *themeJSON = [skinJSON 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; + } + + NSError *finalError = nil; + if (!ok) { + finalError = innerError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorApplyFailed + userInfo:nil]; + } + if (completion) completion(ok, finalError); + }); +} + + (void)publishBundleSkinRequestWithId:(NSString *)skinId name:(NSString *)name zipName:(NSString *)zipName @@ -115,7 +348,7 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) { error:(NSError * __autoreleasing *)error { #if !__has_include() if (error) { - *error = [NSError errorWithDomain:kKBSkinBridgeErrorDomain + *error = [NSError errorWithDomain:KBSkinBridgeErrorDomain code:KBSkinBridgeErrorUnzipFailed userInfo:@{NSLocalizedDescriptionKey: @"SSZipArchive not available"}]; } @@ -126,7 +359,7 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) { NSString *zipName = payload[kKBSkinPendingZipKey]; if (skinId.length == 0 || zipName.length == 0) { if (error) { - *error = [NSError errorWithDomain:kKBSkinBridgeErrorDomain + *error = [NSError errorWithDomain:KBSkinBridgeErrorDomain code:KBSkinBridgeErrorInvalidPayload userInfo:nil]; } @@ -192,9 +425,9 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) { } if (zipPath.length == 0) { if (error) { - *error = [NSError errorWithDomain:kKBSkinBridgeErrorDomain - code:KBSkinBridgeErrorZipMissing - userInfo:@{NSLocalizedDescriptionKey: @"Zip resource not found"}]; + *error = [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorZipMissing + userInfo:@{NSLocalizedDescriptionKey: @"Zip resource not found"}]; } return NO; } @@ -207,7 +440,7 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) { error:&unzipError]; if (!ok || unzipError) { if (error) { - *error = unzipError ?: [NSError errorWithDomain:kKBSkinBridgeErrorDomain + *error = unzipError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain code:KBSkinBridgeErrorUnzipFailed userInfo:nil]; } @@ -283,7 +516,7 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) { } if (!ok && error) { - *error = [NSError errorWithDomain:kKBSkinBridgeErrorDomain + *error = [NSError errorWithDomain:KBSkinBridgeErrorDomain code:KBSkinBridgeErrorApplyFailed userInfo:nil]; } diff --git a/Shared/Localization/en.lproj/Localizable.strings b/Shared/Localization/en.lproj/Localizable.strings index c25c1c0..078ec6b 100644 --- a/Shared/Localization/en.lproj/Localizable.strings +++ b/Shared/Localization/en.lproj/Localizable.strings @@ -106,6 +106,9 @@ "Notification Setting" = "Notification Setting"; "Please Enter The Content" = "Please Enter The Content"; "Commit" = "Commit"; +"Nickname" = "Nickname"; +"Gender" = "Gender"; +"User ID" = "User ID"; // Search & history diff --git a/Shared/Localization/zh-Hans.lproj/Localizable.strings b/Shared/Localization/zh-Hans.lproj/Localizable.strings index 7ebc6aa..96b32af 100644 --- a/Shared/Localization/zh-Hans.lproj/Localizable.strings +++ b/Shared/Localization/zh-Hans.lproj/Localizable.strings @@ -107,6 +107,9 @@ "Notification Setting" = "通知设置"; "Please Enter The Content" = "请输入反馈内容"; "Commit" = "提交"; +"Nickname" = "用户名"; +"Gender" = "性别"; +"User ID" = "用户ID"; // 搜索与历史(英文 key) "Clear history" = "清空历史"; diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index fc9fdb9..e039137 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -52,7 +52,7 @@ 04791F952ED48028004E8522 /* KBFeedBackVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F942ED48028004E8522 /* KBFeedBackVC.m */; }; 04791F982ED49CE7004E8522 /* KBFont.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F972ED49CE7004E8522 /* KBFont.m */; }; 04791F992ED49CE7004E8522 /* KBFont.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F972ED49CE7004E8522 /* KBFont.m */; }; - 04791FF52ED5A487004E8522 /* Christmas.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04791FF42ED5A487004E8522 /* Christmas.zip */; }; + 04791FF72ED5B985004E8522 /* Christmas.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04791FF62ED5B985004E8522 /* Christmas.zip */; }; 047C650D2EBC8A840035E841 /* KBPanModalView.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650C2EBC8A840035E841 /* KBPanModalView.m */; }; 047C65102EBCA8DD0035E841 /* HomeRankContentVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650F2EBCA8DD0035E841 /* HomeRankContentVC.m */; }; 047C65502EBCBA9E0035E841 /* KBShopVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C654F2EBCBA9E0035E841 /* KBShopVC.m */; }; @@ -263,7 +263,7 @@ 04791F942ED48028004E8522 /* KBFeedBackVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFeedBackVC.m; sourceTree = ""; }; 04791F962ED49CE7004E8522 /* KBFont.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFont.h; sourceTree = ""; }; 04791F972ED49CE7004E8522 /* KBFont.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFont.m; sourceTree = ""; }; - 04791FF42ED5A487004E8522 /* Christmas.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = Christmas.zip; sourceTree = ""; }; + 04791FF62ED5B985004E8522 /* Christmas.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = Christmas.zip; sourceTree = ""; }; 047C650B2EBC8A840035E841 /* KBPanModalView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPanModalView.h; sourceTree = ""; }; 047C650C2EBC8A840035E841 /* KBPanModalView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPanModalView.m; sourceTree = ""; }; 047C650E2EBCA8DD0035E841 /* HomeRankContentVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeRankContentVC.h; sourceTree = ""; }; @@ -501,7 +501,7 @@ 041007D12ECE012000D203BB /* KBSkinIconMap.strings */, 041007D32ECE012500D203BB /* 002.zip */, 046131102ECF3A6E00A6FADF /* fense.zip */, - 04791FF42ED5A487004E8522 /* Christmas.zip */, + 04791FF62ED5B985004E8522 /* Christmas.zip */, ); path = Resource; sourceTree = ""; @@ -1453,7 +1453,7 @@ 046131112ECF3A6E00A6FADF /* fense.zip in Resources */, 041007D42ECE012500D203BB /* 002.zip in Resources */, 041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */, - 04791FF52ED5A487004E8522 /* Christmas.zip in Resources */, + 04791FF72ED5B985004E8522 /* Christmas.zip in Resources */, 04286A0B2ECD88B400CE730C /* KeyboardAssets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/keyBoard/Class/Manager/KBSkinService.m b/keyBoard/Class/Manager/KBSkinService.m index c34ac3e..2b8dbcf 100644 --- a/keyBoard/Class/Manager/KBSkinService.m +++ b/keyBoard/Class/Manager/KBSkinService.m @@ -77,178 +77,25 @@ /// - skinJSON.key_icons 的 value 填写 Zip 内的图标“短文件名”(不含路径,可不含扩展名),例如 "key_a" /// 应用时会被转换为相对 App Group 根目录的路径:Skins//icons/.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,则优先使用服务端映射; - // - 若未提供,则回退到本地默认映射(KBSkinInstallBridge.defaultIconShortNames),这样后端只需返回 id/name/zip_url。 - NSDictionary *iconShortNames = nil; - if ([skin[@"key_icons"] isKindOfClass:NSDictionary.class]) { - iconShortNames = skin[@"key_icons"]; - } else { - iconShortNames = [KBSkinInstallBridge 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() - // 若本地尚未缓存该皮肤资源且提供了 zip_url,则通过网络下载并解压 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//icons 为空,但存在 Skins//<子目录>/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 *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); - }; - - // 远程下载(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; - } - handleZipData(data); - }]; - } -#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; - // 若未带扩展名,默认按 .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; + [KBSkinInstallBridge installRemoteSkinWithJSON:skin + completion:^(BOOL success, NSError * _Nullable error) { + if (completion) { + completion(success); } - - 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 (!success && error) { + NSLog(@"[KBSkinService] remote skin install failed: %@", error); } - - if (!zipOK && !hasCachedAssets) { - ok = NO; + NSString *message = nil; + if (success) { + message = KBLocalized(@"已应用,切到键盘查看"); + } else if ([error.domain isEqualToString:KBSkinBridgeErrorDomain] && + error.code == KBSkinBridgeErrorContainerUnavailable) { + message = KBLocalized(@"无法访问共享容器,应用皮肤失败"); + } else { + message = KBLocalized(@"应用皮肤失败"); } - - if (completion) completion(ok); - [KBHUD showInfo:(ok ? KBLocalized(@"已应用,切到键盘查看") : KBLocalized(@"应用皮肤失败"))]; - }); + [KBHUD showInfo:message]; + }]; } /// 本地 bundle 模式:不走网络,skin[@"zip_url"] 直接为 bundle 内 zip 文件名(可带/不带扩展名)。 diff --git a/keyBoard/Class/Me/VC/KBPersonInfoVC.m b/keyBoard/Class/Me/VC/KBPersonInfoVC.m index 3781f98..60e9cdb 100644 --- a/keyBoard/Class/Me/VC/KBPersonInfoVC.m +++ b/keyBoard/Class/Me/VC/KBPersonInfoVC.m @@ -43,15 +43,15 @@ - (void)viewDidLoad { [super viewDidLoad]; - self.kb_titleLabel.text = @"Settings"; // 导航标题 + self.kb_titleLabel.text = KBLocalized(@"Settings"); // 导航标题 self.kb_navView.backgroundColor = [UIColor clearColor]; self.view.backgroundColor = [UIColor colorWithHex:0xF8F8F8]; // 构造数据 self.items = @[ - @{ @"title": @"Nickname", @"value": @"Nickname", @"arrow": @YES, @"copy": @NO }, - @{ @"title": @"Gender", @"value": @"Choose", @"arrow": @YES, @"copy": @NO }, - @{ @"title": @"User ID", @"value": @"8888888", @"arrow": @NO, @"copy": @YES }, + @{ @"title": KBLocalized(@"Nickname"), @"value": @"Nickname", @"arrow": @YES, @"copy": @NO }, + @{ @"title": KBLocalized(@"Gender"), @"value": @"Choose", @"arrow": @YES, @"copy": @NO }, + @{ @"title": KBLocalized(@"User ID"), @"value": @"8888888", @"arrow": @NO, @"copy": @YES }, ]; [self.view addSubview:self.tableView];