优化下载皮肤❤️
This commit is contained in:
@@ -24,6 +24,7 @@ static NSString * const kKBSkinPendingKindKey = @"kind";
|
||||
static NSString * const kKBSkinPendingTimestampKey = @"timestamp";
|
||||
static NSString * const kKBSkinPendingIconShortKey = @"iconShortNames";
|
||||
static NSString * const kKBSkinMetadataFileName = @"metadata.plist";
|
||||
static NSString * const kKBSkinForceDownloadKey = @"force_download";
|
||||
static NSString * const kKBSkinMetadataNameKey = @"name";
|
||||
static NSString * const kKBSkinMetadataPreviewKey = @"preview";
|
||||
static NSString * const kKBSkinMetadataZipKey = @"zip_url";
|
||||
@@ -220,6 +221,11 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
||||
NSString *skinId = skinJSON[@"id"] ?: @"remote";
|
||||
NSString *name = skinJSON[@"name"] ?: skinId;
|
||||
NSString *zipURL = skinJSON[@"zip_url"] ?: @"";
|
||||
BOOL forceDownload = NO;
|
||||
id forceValue = skinJSON[kKBSkinForceDownloadKey];
|
||||
if ([forceValue respondsToSelector:@selector(boolValue)]) {
|
||||
forceDownload = [forceValue boolValue];
|
||||
}
|
||||
|
||||
// key_icons 可选:
|
||||
// - 若后端提供 key_icons,则优先使用服务端映射;
|
||||
@@ -258,6 +264,21 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
||||
BOOL hasCachedAssets = (contents.count > 0);
|
||||
|
||||
NSString *bgPath = [skinRoot stringByAppendingPathComponent:@"background.png"];
|
||||
BOOL useTempRoot = forceDownload;
|
||||
NSString *tempToken = nil;
|
||||
NSString *workingRoot = skinRoot;
|
||||
NSString *workingIconsDir = iconsDir;
|
||||
NSString *workingBgPath = bgPath;
|
||||
if (useTempRoot) {
|
||||
tempToken = [NSString stringWithFormat:@"%lld", (long long)([[NSDate date] timeIntervalSince1970] * 1000)];
|
||||
NSString *tmpName = [NSString stringWithFormat:@"%@__tmp_%@", skinId, tempToken];
|
||||
workingRoot = [skinsRoot stringByAppendingPathComponent:tmpName];
|
||||
workingIconsDir = [workingRoot stringByAppendingPathComponent:@"icons"];
|
||||
workingBgPath = [workingRoot stringByAppendingPathComponent:@"background.png"];
|
||||
[fm removeItemAtPath:workingRoot error:nil];
|
||||
}
|
||||
NSLog(@"⬇️[SkinBridge] request id=%@ force=%d cached=%d zip=%@",
|
||||
skinId, forceDownload, hasCachedAssets, zipURL);
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
__block BOOL zipOK = YES;
|
||||
@@ -265,8 +286,8 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
||||
__block NSError *innerError = nil;
|
||||
|
||||
#if __has_include(<SSZipArchive/SSZipArchive.h>)
|
||||
// 若本地尚未缓存该皮肤资源且提供了 zip_url,则通过网络下载并解压 Zip 包。
|
||||
if (!hasCachedAssets && zipURL.length > 0) {
|
||||
// 若需要强制下载,或本地尚未缓存该皮肤资源且提供了 zip_url,则下载并解压 Zip 包。
|
||||
if ((forceDownload || !hasCachedAssets) && zipURL.length > 0) {
|
||||
dispatch_group_enter(group);
|
||||
|
||||
void (^handleZipData)(NSData *) = ^(NSData *data) {
|
||||
@@ -277,15 +298,17 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
||||
code:KBSkinBridgeErrorZipMissing
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Zip data is empty"}];
|
||||
}
|
||||
NSLog(@"❌[SkinBridge] zip data empty id=%@", skinId);
|
||||
dispatch_group_leave(group);
|
||||
return;
|
||||
}
|
||||
NSLog(@"📦[SkinBridge] unzip start id=%@ temp=%d", skinId, useTempRoot);
|
||||
// 将 Zip 写入临时路径再解压
|
||||
[fm createDirectoryAtPath:skinRoot
|
||||
[fm createDirectoryAtPath:workingRoot
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:NULL];
|
||||
NSString *zipPath = [skinRoot stringByAppendingPathComponent:@"skin.zip"];
|
||||
NSString *zipPath = [workingRoot stringByAppendingPathComponent:@"skin.zip"];
|
||||
if (![data writeToFile:zipPath atomically:YES]) {
|
||||
zipOK = NO;
|
||||
if (!innerError) {
|
||||
@@ -293,13 +316,14 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
||||
code:KBSkinBridgeErrorUnzipFailed
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to write zip file"}];
|
||||
}
|
||||
NSLog(@"❌[SkinBridge] zip write failed id=%@", skinId);
|
||||
dispatch_group_leave(group);
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *unzipError = nil;
|
||||
BOOL ok = [SSZipArchive unzipFileAtPath:zipPath
|
||||
toDestination:skinRoot
|
||||
toDestination:workingRoot
|
||||
overwrite:YES
|
||||
password:nil
|
||||
error:&unzipError];
|
||||
@@ -311,24 +335,22 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
||||
code:KBSkinBridgeErrorUnzipFailed
|
||||
userInfo:nil];
|
||||
}
|
||||
NSLog(@"❌[SkinBridge] unzip failed id=%@ error=%@", skinId, unzipError);
|
||||
dispatch_group_leave(group);
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记已成功解压一次(即使 icons 目录结构需要后续整理)。
|
||||
didUnzip = YES;
|
||||
|
||||
// 兼容“额外包一层目录”的压缩结构:
|
||||
// 若 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);
|
||||
NSArray *iconsContent = [fm contentsOfDirectoryAtPath:workingIconsDir error:NULL];
|
||||
BOOL iconsValid = ([fm fileExistsAtPath:workingIconsDir isDirectory:&isDir2] && isDir2 && iconsContent.count > 0);
|
||||
if (!iconsValid) {
|
||||
NSArray<NSString *> *subItems = [fm contentsOfDirectoryAtPath:skinRoot error:NULL];
|
||||
NSArray<NSString *> *subItems = [fm contentsOfDirectoryAtPath:workingRoot error:NULL];
|
||||
for (NSString *subName in subItems) {
|
||||
if ([subName isEqualToString:@"icons"] || [subName isEqualToString:@"__MACOSX"]) continue;
|
||||
NSString *nestedRoot = [skinRoot stringByAppendingPathComponent:subName];
|
||||
NSString *nestedRoot = [workingRoot stringByAppendingPathComponent:subName];
|
||||
BOOL isDirNested = NO;
|
||||
if (![fm fileExistsAtPath:nestedRoot isDirectory:&isDirNested] || !isDirNested) continue;
|
||||
|
||||
@@ -338,14 +360,14 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
||||
NSArray *nestedFiles = [fm contentsOfDirectoryAtPath:nestedIcons error:NULL];
|
||||
if (nestedFiles.count > 0) {
|
||||
// 确保目标 icons 目录存在
|
||||
[fm createDirectoryAtPath:iconsDir
|
||||
[fm createDirectoryAtPath:workingIconsDir
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:NULL];
|
||||
// 将 icons 下所有文件上移一层
|
||||
for (NSString *fn in nestedFiles) {
|
||||
NSString *from = [nestedIcons stringByAppendingPathComponent:fn];
|
||||
NSString *to = [iconsDir stringByAppendingPathComponent:fn];
|
||||
NSString *to = [workingIconsDir stringByAppendingPathComponent:fn];
|
||||
[fm removeItemAtPath:to error:nil];
|
||||
[fm moveItemAtPath:from toPath:to error:nil];
|
||||
}
|
||||
@@ -355,20 +377,65 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
||||
// 处理 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];
|
||||
[fm removeItemAtPath:workingBgPath error:nil];
|
||||
[fm moveItemAtPath:nestedBg toPath:workingBgPath error:nil];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (useTempRoot) {
|
||||
NSString *backupName = [NSString stringWithFormat:@"%@__bak_%@", skinId, (tempToken ?: @"0")];
|
||||
NSString *backupRoot = [skinsRoot stringByAppendingPathComponent:backupName];
|
||||
[fm removeItemAtPath:backupRoot error:nil];
|
||||
NSError *swapError = nil;
|
||||
BOOL movedOld = NO;
|
||||
if ([fm fileExistsAtPath:skinRoot]) {
|
||||
movedOld = [fm moveItemAtPath:skinRoot toPath:backupRoot error:&swapError];
|
||||
if (!movedOld && swapError) {
|
||||
zipOK = NO;
|
||||
if (!innerError) {
|
||||
innerError = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||
code:KBSkinBridgeErrorUnzipFailed
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to backup old skin"}];
|
||||
}
|
||||
NSLog(@"❌[SkinBridge] backup failed id=%@ error=%@", skinId, swapError);
|
||||
dispatch_group_leave(group);
|
||||
return;
|
||||
}
|
||||
}
|
||||
BOOL movedNew = [fm moveItemAtPath:workingRoot toPath:skinRoot error:&swapError];
|
||||
if (!movedNew || swapError) {
|
||||
zipOK = NO;
|
||||
if (!innerError) {
|
||||
innerError = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||
code:KBSkinBridgeErrorUnzipFailed
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to replace skin assets"}];
|
||||
}
|
||||
if (movedOld) {
|
||||
[fm moveItemAtPath:backupRoot toPath:skinRoot error:nil];
|
||||
}
|
||||
NSLog(@"❌[SkinBridge] replace failed id=%@ error=%@", skinId, swapError);
|
||||
dispatch_group_leave(group);
|
||||
return;
|
||||
}
|
||||
if (movedOld) {
|
||||
[fm removeItemAtPath:backupRoot error:nil];
|
||||
}
|
||||
NSLog(@"🧹[SkinBridge] replaced old skin id=%@", skinId);
|
||||
}
|
||||
|
||||
// 标记已成功解压一次(即使 icons 目录结构需要后续整理)。
|
||||
didUnzip = YES;
|
||||
NSLog(@"✅[SkinBridge] unzip done id=%@", skinId);
|
||||
dispatch_group_leave(group);
|
||||
};
|
||||
|
||||
#if __has_include("KBNetworkManager.h")
|
||||
// 远程下载(http/https)
|
||||
NSLog(@"[SkinBridge] will GET zip: %@", zipURL);
|
||||
NSLog(@"🌐[SkinBridge] will GET zip: %@", zipURL);
|
||||
[KBHUD showWithStatus:@"正在下载..."];
|
||||
[[KBNetworkManager shared] GETData:zipURL parameters:nil headers:nil completion:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
NSLog(@"[SkinBridge] GET finished, error = %@", error);
|
||||
NSLog(@"🌐[SkinBridge] GET finished id=%@ error=%@", skinId, error);
|
||||
if (error || data.length == 0) {
|
||||
zipOK = NO;
|
||||
if (!innerError) {
|
||||
@@ -399,6 +466,9 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
||||
}
|
||||
});
|
||||
#endif
|
||||
} else {
|
||||
NSLog(@"ℹ️[SkinBridge] skip download id=%@ force=%d cached=%d zip=%@",
|
||||
skinId, forceDownload, hasCachedAssets, zipURL);
|
||||
}
|
||||
#else
|
||||
zipOK = NO;
|
||||
@@ -411,11 +481,12 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
||||
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
||||
// 若既没有预先存在的缓存资源,也没有在本次流程中成功解压出资源,
|
||||
// 说明当前皮肤 B 的资源完全不可用,此时不应覆盖现有皮肤主题。
|
||||
BOOL hasAssets = (hasCachedAssets || didUnzip);
|
||||
BOOL hasAssets = (didUnzip || (!forceDownload && hasCachedAssets));
|
||||
if (!hasAssets) {
|
||||
NSError *finalError = innerError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||
code:KBSkinBridgeErrorZipMissing
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Zip resource not available"}];
|
||||
NSLog(@"❌[SkinBridge] apply aborted id=%@ error=%@", skinId, finalError);
|
||||
if (completion) completion(NO, finalError);
|
||||
return;
|
||||
}
|
||||
@@ -459,6 +530,10 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
||||
userInfo:nil];
|
||||
}
|
||||
if (completion) completion(ok, finalError);
|
||||
NSLog(@"%@ [SkinBridge] apply %@ id=%@",
|
||||
(ok ? @"✅" : @"❌"),
|
||||
(ok ? @"ok" : @"failed"),
|
||||
skinId);
|
||||
if (ok) {
|
||||
NSString *preview = [skinJSON[@"preview"] isKindOfClass:NSString.class] ? skinJSON[@"preview"] : nil;
|
||||
[self recordInstalledSkinWithId:skinId
|
||||
|
||||
Reference in New Issue
Block a user