// // KBSkinInstallBridge.m // #import "KBSkinInstallBridge.h" #import "KBConfig.h" #import "KBSkinManager.h" #if __has_include() #import #endif NSString * const KBDarwinSkinInstallRequestNotification = @"com.loveKey.nyx.skin.install.request"; 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 kKBSkinBridgeErrorDomain = @"com.loveKey.nyx.skin.bridge"; typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) { KBSkinBridgeErrorInvalidPayload = 1, KBSkinBridgeErrorContainerUnavailable, KBSkinBridgeErrorZipMissing, KBSkinBridgeErrorUnzipFailed, KBSkinBridgeErrorApplyFailed, }; @implementation KBSkinInstallBridge + (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)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:kKBSkinBridgeErrorDomain 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:kKBSkinBridgeErrorDomain code:KBSkinBridgeErrorInvalidPayload userInfo:nil]; } return NO; } NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup]; if (!containerURL) { if (error) { *error = [NSError errorWithDomain:kKBSkinBridgeErrorDomain code:KBSkinBridgeErrorContainerUnavailable userInfo:@{NSLocalizedDescriptionKey: @"App Group container unavailable"}]; } return NO; } 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"]; 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:kKBSkinBridgeErrorDomain 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:kKBSkinBridgeErrorDomain 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:kKBSkinBridgeErrorDomain code:KBSkinBridgeErrorApplyFailed userInfo:nil]; } return ok; #endif } @end