Files
keyboard/Shared/KBSkinInstallBridge.m

692 lines
31 KiB
Mathematica
Raw Normal View History

2025-11-20 14:27:57 +08:00
//
// KBSkinInstallBridge.m
//
#import "KBSkinInstallBridge.h"
#import "KBConfig.h"
#import "KBSkinManager.h"
2025-11-25 18:54:53 +08:00
#if __has_include("KBNetworkManager.h")
#import "KBNetworkManager.h"
#endif
2025-11-20 14:27:57 +08:00
#if __has_include(<SSZipArchive/SSZipArchive.h>)
#import <SSZipArchive/SSZipArchive.h>
#endif
NSString * const KBDarwinSkinInstallRequestNotification = @"com.loveKey.nyx.skin.install.request";
2025-11-25 18:54:53 +08:00
NSErrorDomain const KBSkinBridgeErrorDomain = @"com.loveKey.nyx.skin.bridge";
2025-11-20 14:27:57 +08:00
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";
2025-12-11 19:43:55 +08:00
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
2025-11-20 14:27:57 +08:00
@implementation KBSkinInstallBridge
2025-12-11 19:43:55 +08:00
+ (NSString *)kb_skinsRootPath {
NSFileManager *fm = [NSFileManager defaultManager];
NSURL *containerURL = [fm containerURLForSecurityApplicationGroupIdentifier:AppGroup];
NSString *root = containerURL.path;
if (root.length == 0) {
NSArray<NSString *> *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<KBSkinDownloadRecord *> *)installedSkinRecords {
NSString *root = [self kb_skinsRootPath];
NSFileManager *fm = [NSFileManager defaultManager];
BOOL isDir = NO;
if (![fm fileExistsAtPath:root isDirectory:&isDir] || !isDir) {
return @[];
}
NSArray<NSString *> *entries = [fm contentsOfDirectoryAtPath:root error:NULL] ?: @[];
NSMutableArray<KBSkinDownloadRecord *> *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];
}
2025-11-20 14:27:57 +08:00
+ (NSDictionary<NSString *,NSString *> *)defaultIconShortNames {
static NSDictionary<NSString *, NSString *> *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];
}
2025-11-25 18:54:53 +08:00
+ (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;
//
2025-11-25 18:54:53 +08:00
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
2025-11-25 18:54:53 +08:00
__block NSError *innerError = nil;
#if __has_include(<SSZipArchive/SSZipArchive.h>)
// 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;
2025-11-25 18:54:53 +08:00
//
// 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 *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);
2025-11-25 20:35:08 +08:00
[KBHUD show];
2025-12-03 13:54:57 +08:00
[[KBNetworkManager shared] GETData:zipURL parameters:nil headers:nil completion:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(@"[SkinBridge] GET finished, error = %@", error);
2025-11-25 18:54:53 +08:00
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;
}
2025-11-25 18:54:53 +08:00
// 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 = [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);
2025-12-11 19:43:55 +08:00
if (ok) {
NSString *preview = [skinJSON[@"preview"] isKindOfClass:NSString.class] ? skinJSON[@"preview"] : nil;
[self recordInstalledSkinWithId:skinId
name:name ?: skinId
preview:preview
zipURL:zipURL];
}
2025-11-25 18:54:53 +08:00
});
}
2025-11-20 14:27:57 +08:00
+ (void)publishBundleSkinRequestWithId:(NSString *)skinId
name:(NSString *)name
zipName:(NSString *)zipName
iconShortNames:(NSDictionary<NSString *,NSString *> *)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(<SSZipArchive/SSZipArchive.h>)
if (error) {
2025-11-25 18:54:53 +08:00
*error = [NSError errorWithDomain:KBSkinBridgeErrorDomain
2025-11-20 14:27:57 +08:00
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) {
2025-11-25 18:54:53 +08:00
*error = [NSError errorWithDomain:KBSkinBridgeErrorDomain
2025-11-20 14:27:57 +08:00
code:KBSkinBridgeErrorInvalidPayload
userInfo:nil];
}
return NO;
}
2025-11-20 18:23:56 +08:00
// 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];
2025-11-20 14:27:57 +08:00
}
2025-11-20 18:23:56 +08:00
}
if (baseRoot.length == 0) {
NSArray<NSString *> *dirs = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
baseRoot = dirs.firstObject ?: NSTemporaryDirectory();
2025-11-20 14:27:57 +08:00
}
2025-11-20 18:23:56 +08:00
NSString *skinsRoot = [baseRoot stringByAppendingPathComponent:@"Skins"];
2025-11-20 14:27:57 +08:00
NSString *skinRoot = [skinsRoot stringByAppendingPathComponent:skinId];
NSString *iconsDir = [skinRoot stringByAppendingPathComponent:@"icons"];
2025-11-20 18:23:56 +08:00
[fm createDirectoryAtPath:iconsDir
withIntermediateDirectories:YES
attributes:nil
error:NULL];
2025-11-20 14:27:57 +08:00
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) {
2025-11-25 18:54:53 +08:00
*error = [NSError errorWithDomain:KBSkinBridgeErrorDomain
code:KBSkinBridgeErrorZipMissing
userInfo:@{NSLocalizedDescriptionKey: @"Zip resource not found"}];
2025-11-20 14:27:57 +08:00
}
return NO;
}
NSError *unzipError = nil;
BOOL ok = [SSZipArchive unzipFileAtPath:zipPath
toDestination:skinRoot
overwrite:YES
password:nil
error:&unzipError];
if (!ok || unzipError) {
if (error) {
2025-11-25 18:54:53 +08:00
*error = unzipError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain
2025-11-20 14:27:57 +08:00
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<NSString *> *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<NSString *, NSString *> *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) {
2025-11-25 18:54:53 +08:00
*error = [NSError errorWithDomain:KBSkinBridgeErrorDomain
2025-11-20 14:27:57 +08:00
code:KBSkinBridgeErrorApplyFailed
userInfo:nil];
}
2025-12-11 19:43:55 +08:00
if (ok) {
[self recordInstalledSkinWithId:skinId
name:name ?: skinId
preview:nil
zipURL:zipName];
}
2025-11-20 14:27:57 +08:00
return ok;
#endif
}
@end