// // KBSkinInstallBridge.m // #import "KBSkinInstallBridge.h" #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"; static NSString * const kKBSkinPendingSkinNameKey = @"name"; static NSString * const kKBSkinPendingZipKey = @"zipName"; static NSString * const kKBSkinPendingKindKey = @"kind"; static NSString * const kKBSkinPendingTimestampKey = @"timestamp"; static NSString * const kKBSkinPendingIconShortKey = @"iconShortNames"; static NSString * const kKBSkinMetadataFileName = @"metadata.plist"; static NSString * const kKBSkinMetadataNameKey = @"name"; static NSString * const kKBSkinMetadataPreviewKey = @"preview"; static NSString * const kKBSkinMetadataZipKey = @"zip_url"; static NSString * const kKBSkinMetadataInstalledKey = @"installed_at"; @interface KBSkinDownloadRecord () - (instancetype)initWithSkinId:(NSString *)skinId metadata:(NSDictionary *)metadata; @end @implementation KBSkinDownloadRecord - (instancetype)initWithSkinId:(NSString *)skinId metadata:(NSDictionary *)metadata { if (self = [super init]) { _skinId = skinId.length ? skinId : @""; NSString *name = [metadata[kKBSkinMetadataNameKey] isKindOfClass:NSString.class] ? metadata[kKBSkinMetadataNameKey] : nil; _name = name.length > 0 ? name : (_skinId.length ? _skinId : @""); NSString *preview = [metadata[kKBSkinMetadataPreviewKey] isKindOfClass:NSString.class] ? metadata[kKBSkinMetadataPreviewKey] : nil; _previewImage = preview.length > 0 ? preview : nil; NSString *zip = [metadata[kKBSkinMetadataZipKey] isKindOfClass:NSString.class] ? metadata[kKBSkinMetadataZipKey] : nil; _zipURL = zip.length > 0 ? zip : nil; NSTimeInterval installed = 0; id installObj = metadata[kKBSkinMetadataInstalledKey]; if ([installObj respondsToSelector:@selector(doubleValue)]) { installed = [installObj doubleValue]; } if (installed <= 0) { installed = [[NSDate date] timeIntervalSince1970]; } _installedAt = installed; } return self; } @end @implementation KBSkinInstallBridge + (NSString *)kb_skinsRootPath { NSFileManager *fm = [NSFileManager defaultManager]; NSURL *containerURL = [fm containerURLForSecurityApplicationGroupIdentifier:AppGroup]; NSString *root = containerURL.path; if (root.length == 0) { NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); root = dirs.firstObject ?: NSTemporaryDirectory(); } return [root stringByAppendingPathComponent:@"Skins"]; } + (NSString *)kb_metadataPathForSkinId:(NSString *)skinId { if (skinId.length == 0) { return nil; } NSString *skinRoot = [[self kb_skinsRootPath] stringByAppendingPathComponent:skinId]; return [skinRoot stringByAppendingPathComponent:kKBSkinMetadataFileName]; } + (void)kb_storeMetadataForSkinId:(NSString *)skinId name:(NSString *)name preview:(NSString *)preview zipURL:(NSString *)zipURL { if (skinId.length == 0) { return; } dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ NSString *metaPath = [self kb_metadataPathForSkinId:skinId]; if (metaPath.length == 0) { return; } NSFileManager *fm = [NSFileManager defaultManager]; NSString *dir = [metaPath stringByDeletingLastPathComponent]; [fm createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil]; NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; NSMutableDictionary *dict = [NSMutableDictionary dictionary]; dict[@"id"] = skinId; if (name.length > 0) { dict[kKBSkinMetadataNameKey] = name; } if (preview.length > 0) { dict[kKBSkinMetadataPreviewKey] = preview; } if (zipURL.length > 0) { dict[kKBSkinMetadataZipKey] = zipURL; } dict[kKBSkinMetadataInstalledKey] = @(now); [dict writeToFile:metaPath atomically:YES]; }); } + (NSArray *)installedSkinRecords { NSString *root = [self kb_skinsRootPath]; NSFileManager *fm = [NSFileManager defaultManager]; BOOL isDir = NO; if (![fm fileExistsAtPath:root isDirectory:&isDir] || !isDir) { return @[]; } NSArray *entries = [fm contentsOfDirectoryAtPath:root error:NULL] ?: @[]; NSMutableArray *records = [NSMutableArray array]; for (NSString *entry in entries) { if (entry.length == 0 || [entry hasPrefix:@"."]) { continue; } NSString *path = [root stringByAppendingPathComponent:entry]; BOOL isSubDir = NO; if (![fm fileExistsAtPath:path isDirectory:&isSubDir] || !isSubDir) { continue; } NSString *metaPath = [path stringByAppendingPathComponent:kKBSkinMetadataFileName]; NSDictionary *meta = [NSDictionary dictionaryWithContentsOfFile:metaPath] ?: @{}; KBSkinDownloadRecord *record = [[KBSkinDownloadRecord alloc] initWithSkinId:entry metadata:meta]; [records addObject:record]; } [records sortUsingComparator:^NSComparisonResult(KBSkinDownloadRecord *obj1, KBSkinDownloadRecord *obj2) { if (obj1.installedAt == obj2.installedAt) { return NSOrderedSame; } return (obj1.installedAt > obj2.installedAt) ? NSOrderedAscending : NSOrderedDescending; }]; return records.copy; } + (BOOL)removeInstalledSkinWithId:(NSString *)skinId error:(NSError * __autoreleasing *)error { if (skinId.length == 0) { return YES; } NSString *root = [self kb_skinsRootPath]; NSString *skinPath = [root stringByAppendingPathComponent:skinId]; NSFileManager *fm = [NSFileManager defaultManager]; BOOL isDir = NO; if (![fm fileExistsAtPath:skinPath isDirectory:&isDir] || !isDir) { return YES; } BOOL ok = [fm removeItemAtPath:skinPath error:error]; if (ok) { NSString *currentId = [KBSkinManager shared].current.skinId; if ([currentId isKindOfClass:NSString.class] && [currentId isEqualToString:skinId]) { [[KBSkinManager shared] resetToDefault]; } } return ok; } + (void)recordInstalledSkinWithId:(NSString *)skinId name:(NSString *)name preview:(NSString *)preview zipURL:(NSString *)zipURL { [self kb_storeMetadataForSkinId:skinId name:name preview:preview zipURL:zipURL]; } + (NSDictionary *)defaultIconShortNames { static NSDictionary *map; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ 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; } + (NSUserDefaults *)sharedDefaults { 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 BOOL didUnzip = NO; // 标记本次流程中是否成功解压过 Zip __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; } // 标记已成功解压一次(即使 icons 目录结构需要后续整理)。 didUnzip = YES; // 兼容“额外包一层目录”的压缩结构: // 若 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) NSLog(@"[SkinBridge] will GET zip: %@", zipURL); [KBHUD show]; [[KBNetworkManager shared] GETData:zipURL parameters:nil headers:nil completion:^(NSData *data, NSURLResponse *response, NSError *error) { NSLog(@"[SkinBridge] GET finished, error = %@", error); 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(), ^{ // 若既没有预先存在的缓存资源,也没有在本次流程中成功解压出资源, // 说明当前皮肤 B 的资源完全不可用,此时不应覆盖现有皮肤主题。 BOOL hasAssets = (hasCachedAssets || didUnzip); if (!hasAssets) { NSError *finalError = innerError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain code:KBSkinBridgeErrorZipMissing userInfo:@{NSLocalizedDescriptionKey: @"Zip resource not available"}]; if (completion) completion(NO, finalError); return; } // 构造 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); if (ok) { NSString *preview = [skinJSON[@"preview"] isKindOfClass:NSString.class] ? skinJSON[@"preview"] : nil; [self recordInstalledSkinWithId:skinId name:name ?: skinId preview:preview zipURL:zipURL]; } }); } + (void)publishBundleSkinRequestWithId:(NSString *)skinId name:(NSString *)name zipName:(NSString *)zipName iconShortNames:(NSDictionary *)iconShortNames { if (skinId.length == 0 || zipName.length == 0) { return; } NSMutableDictionary *payload = [NSMutableDictionary dictionary]; payload[kKBSkinPendingSkinIdKey] = skinId; payload[kKBSkinPendingSkinNameKey] = name.length > 0 ? name : skinId; payload[kKBSkinPendingZipKey] = zipName; payload[kKBSkinPendingKindKey] = @"bundle"; payload[kKBSkinPendingTimestampKey] = @([[NSDate date] timeIntervalSince1970]); if (iconShortNames.count > 0) { payload[kKBSkinPendingIconShortKey] = iconShortNames; } [[self sharedDefaults] setObject:payload forKey:kKBSkinPendingRequestKey]; [[self sharedDefaults] synchronize]; CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)KBDarwinSkinInstallRequestNotification, NULL, NULL, true); } + (NSDictionary *)pendingRequestPayload { id payload = [[self sharedDefaults] objectForKey:kKBSkinPendingRequestKey]; if ([payload isKindOfClass:NSDictionary.class]) { return payload; } return nil; } + (void)clearPendingRequest { [[self sharedDefaults] removeObjectForKey:kKBSkinPendingRequestKey]; [[self sharedDefaults] synchronize]; } + (void)consumePendingRequestFromBundle:(NSBundle *)bundle completion:(KBSkinInstallConsumeCompletion)completion { NSDictionary *payload = [self pendingRequestPayload]; if (payload.count == 0) { if (completion) completion(NO, nil); return; } dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ NSError *error = nil; BOOL ok = [self processPayload:payload bundle:bundle ?: [NSBundle mainBundle] error:&error]; if (ok) { [self clearPendingRequest]; } dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(ok, error); }); }); } + (BOOL)processPayload:(NSDictionary *)payload bundle:(NSBundle *)bundle error:(NSError * __autoreleasing *)error { #if !__has_include() if (error) { *error = [NSError errorWithDomain:KBSkinBridgeErrorDomain code:KBSkinBridgeErrorUnzipFailed userInfo:@{NSLocalizedDescriptionKey: @"SSZipArchive not available"}]; } return NO; #else NSString *skinId = payload[kKBSkinPendingSkinIdKey]; NSString *name = payload[kKBSkinPendingSkinNameKey] ?: skinId; NSString *zipName = payload[kKBSkinPendingZipKey]; if (skinId.length == 0 || zipName.length == 0) { if (error) { *error = [NSError errorWithDomain:KBSkinBridgeErrorDomain code:KBSkinBridgeErrorInvalidPayload userInfo:nil]; } return NO; } // 皮肤根目录:优先 App Group,若不可写则退回当前进程的 Caches 目录。 NSFileManager *fm = [NSFileManager defaultManager]; NSString *baseRoot = nil; NSURL *containerURL = [fm containerURLForSecurityApplicationGroupIdentifier:AppGroup]; if (containerURL.path.length > 0) { // 探测写权限:在 Skins/.kb_write_test 下创建临时目录 NSString *testDir = [[containerURL.path stringByAppendingPathComponent:@"Skins"] stringByAppendingPathComponent:@".kb_write_test"]; NSError *probeError = nil; BOOL canWrite = [fm createDirectoryAtPath:testDir withIntermediateDirectories:YES attributes:nil error:&probeError]; if (canWrite) { baseRoot = containerURL.path; [fm removeItemAtPath:testDir error:NULL]; } } if (baseRoot.length == 0) { NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); baseRoot = dirs.firstObject ?: NSTemporaryDirectory(); } NSString *skinsRoot = [baseRoot 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"]; if (!hasCachedAssets) { NSString *fileName = zipName; if ([fileName hasPrefix:@"bundle://"]) { fileName = [fileName substringFromIndex:[@"bundle://" length]]; } 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 *zipPath = nil; if (dir.length > 0) { zipPath = [bundle pathForResource:base ofType:ext inDirectory:dir]; } else { zipPath = [bundle pathForResource:base ofType:ext]; } if (zipPath.length == 0) { if (error) { *error = [NSError errorWithDomain:KBSkinBridgeErrorDomain code:KBSkinBridgeErrorZipMissing userInfo:@{NSLocalizedDescriptionKey: @"Zip resource not found"}]; } return NO; } NSError *unzipError = nil; BOOL ok = [SSZipArchive unzipFileAtPath:zipPath toDestination:skinRoot overwrite:YES password:nil error:&unzipError]; if (!ok || unzipError) { if (error) { *error = unzipError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain code:KBSkinBridgeErrorUnzipFailed userInfo:nil]; } return NO; } 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) { [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]; } } } } NSDictionary *shortNames = payload[kKBSkinPendingIconShortKey]; if (![shortNames isKindOfClass:NSDictionary.class] || shortNames.count == 0) { shortNames = [self defaultIconShortNames]; } NSMutableDictionary *iconPathMap = [NSMutableDictionary dictionary]; [shortNames enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, NSString *shortName, BOOL *stop) { if (identifier.length == 0 || ![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 = [NSMutableDictionary dictionary]; themeJSON[@"id"] = skinId; themeJSON[@"name"] = name ?: 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 ?: skinId]; } if (!ok && error) { *error = [NSError errorWithDomain:KBSkinBridgeErrorDomain code:KBSkinBridgeErrorApplyFailed userInfo:nil]; } if (ok) { [self recordInstalledSkinWithId:skinId name:name ?: skinId preview:nil zipURL:zipName]; } return ok; #endif } @end