优化下载皮肤❤️

This commit is contained in:
2025-12-23 15:26:32 +08:00
parent fbf9fe9f2a
commit 000d603241
5 changed files with 111 additions and 20 deletions

View File

@@ -589,6 +589,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
- (void)kb_applyTheme { - (void)kb_applyTheme {
KBSkinTheme *t = [KBSkinManager shared].current; KBSkinTheme *t = [KBSkinManager shared].current;
UIImage *img = [[KBSkinManager shared] currentBackgroundImage]; UIImage *img = [[KBSkinManager shared] currentBackgroundImage];
NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil));
self.bgImageView.image = img; self.bgImageView.image = img;
BOOL hasImg = (img != nil); BOOL hasImg = (img != nil);
self.view.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground; self.view.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;

View File

@@ -24,6 +24,7 @@ static NSString * const kKBSkinPendingKindKey = @"kind";
static NSString * const kKBSkinPendingTimestampKey = @"timestamp"; static NSString * const kKBSkinPendingTimestampKey = @"timestamp";
static NSString * const kKBSkinPendingIconShortKey = @"iconShortNames"; static NSString * const kKBSkinPendingIconShortKey = @"iconShortNames";
static NSString * const kKBSkinMetadataFileName = @"metadata.plist"; static NSString * const kKBSkinMetadataFileName = @"metadata.plist";
static NSString * const kKBSkinForceDownloadKey = @"force_download";
static NSString * const kKBSkinMetadataNameKey = @"name"; static NSString * const kKBSkinMetadataNameKey = @"name";
static NSString * const kKBSkinMetadataPreviewKey = @"preview"; static NSString * const kKBSkinMetadataPreviewKey = @"preview";
static NSString * const kKBSkinMetadataZipKey = @"zip_url"; static NSString * const kKBSkinMetadataZipKey = @"zip_url";
@@ -220,6 +221,11 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
NSString *skinId = skinJSON[@"id"] ?: @"remote"; NSString *skinId = skinJSON[@"id"] ?: @"remote";
NSString *name = skinJSON[@"name"] ?: skinId; NSString *name = skinJSON[@"name"] ?: skinId;
NSString *zipURL = skinJSON[@"zip_url"] ?: @""; NSString *zipURL = skinJSON[@"zip_url"] ?: @"";
BOOL forceDownload = NO;
id forceValue = skinJSON[kKBSkinForceDownloadKey];
if ([forceValue respondsToSelector:@selector(boolValue)]) {
forceDownload = [forceValue boolValue];
}
// key_icons // key_icons
// - key_icons使 // - key_icons使
@@ -258,6 +264,21 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
BOOL hasCachedAssets = (contents.count > 0); BOOL hasCachedAssets = (contents.count > 0);
NSString *bgPath = [skinRoot stringByAppendingPathComponent:@"background.png"]; 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(); dispatch_group_t group = dispatch_group_create();
__block BOOL zipOK = YES; __block BOOL zipOK = YES;
@@ -265,8 +286,8 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
__block NSError *innerError = nil; __block NSError *innerError = nil;
#if __has_include(<SSZipArchive/SSZipArchive.h>) #if __has_include(<SSZipArchive/SSZipArchive.h>)
// zip_url Zip // zip_url Zip
if (!hasCachedAssets && zipURL.length > 0) { if ((forceDownload || !hasCachedAssets) && zipURL.length > 0) {
dispatch_group_enter(group); dispatch_group_enter(group);
void (^handleZipData)(NSData *) = ^(NSData *data) { void (^handleZipData)(NSData *) = ^(NSData *data) {
@@ -277,15 +298,17 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
code:KBSkinBridgeErrorZipMissing code:KBSkinBridgeErrorZipMissing
userInfo:@{NSLocalizedDescriptionKey: @"Zip data is empty"}]; userInfo:@{NSLocalizedDescriptionKey: @"Zip data is empty"}];
} }
NSLog(@"❌[SkinBridge] zip data empty id=%@", skinId);
dispatch_group_leave(group); dispatch_group_leave(group);
return; return;
} }
NSLog(@"📦[SkinBridge] unzip start id=%@ temp=%d", skinId, useTempRoot);
// Zip // Zip
[fm createDirectoryAtPath:skinRoot [fm createDirectoryAtPath:workingRoot
withIntermediateDirectories:YES withIntermediateDirectories:YES
attributes:nil attributes:nil
error:NULL]; error:NULL];
NSString *zipPath = [skinRoot stringByAppendingPathComponent:@"skin.zip"]; NSString *zipPath = [workingRoot stringByAppendingPathComponent:@"skin.zip"];
if (![data writeToFile:zipPath atomically:YES]) { if (![data writeToFile:zipPath atomically:YES]) {
zipOK = NO; zipOK = NO;
if (!innerError) { if (!innerError) {
@@ -293,13 +316,14 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
code:KBSkinBridgeErrorUnzipFailed code:KBSkinBridgeErrorUnzipFailed
userInfo:@{NSLocalizedDescriptionKey: @"Failed to write zip file"}]; userInfo:@{NSLocalizedDescriptionKey: @"Failed to write zip file"}];
} }
NSLog(@"❌[SkinBridge] zip write failed id=%@", skinId);
dispatch_group_leave(group); dispatch_group_leave(group);
return; return;
} }
NSError *unzipError = nil; NSError *unzipError = nil;
BOOL ok = [SSZipArchive unzipFileAtPath:zipPath BOOL ok = [SSZipArchive unzipFileAtPath:zipPath
toDestination:skinRoot toDestination:workingRoot
overwrite:YES overwrite:YES
password:nil password:nil
error:&unzipError]; error:&unzipError];
@@ -311,24 +335,22 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
code:KBSkinBridgeErrorUnzipFailed code:KBSkinBridgeErrorUnzipFailed
userInfo:nil]; userInfo:nil];
} }
NSLog(@"❌[SkinBridge] unzip failed id=%@ error=%@", skinId, unzipError);
dispatch_group_leave(group); dispatch_group_leave(group);
return; return;
} }
// 使 icons
didUnzip = YES;
// //
// Skins/<skinId>/icons Skins/<skinId>/<>/icons // Skins/<skinId>/icons Skins/<skinId>/<>/icons
// icons background.png // icons background.png
BOOL isDir2 = NO; BOOL isDir2 = NO;
NSArray *iconsContent = [fm contentsOfDirectoryAtPath:iconsDir error:NULL]; NSArray *iconsContent = [fm contentsOfDirectoryAtPath:workingIconsDir error:NULL];
BOOL iconsValid = ([fm fileExistsAtPath:iconsDir isDirectory:&isDir2] && isDir2 && iconsContent.count > 0); BOOL iconsValid = ([fm fileExistsAtPath:workingIconsDir isDirectory:&isDir2] && isDir2 && iconsContent.count > 0);
if (!iconsValid) { if (!iconsValid) {
NSArray<NSString *> *subItems = [fm contentsOfDirectoryAtPath:skinRoot error:NULL]; NSArray<NSString *> *subItems = [fm contentsOfDirectoryAtPath:workingRoot error:NULL];
for (NSString *subName in subItems) { for (NSString *subName in subItems) {
if ([subName isEqualToString:@"icons"] || [subName isEqualToString:@"__MACOSX"]) continue; if ([subName isEqualToString:@"icons"] || [subName isEqualToString:@"__MACOSX"]) continue;
NSString *nestedRoot = [skinRoot stringByAppendingPathComponent:subName]; NSString *nestedRoot = [workingRoot stringByAppendingPathComponent:subName];
BOOL isDirNested = NO; BOOL isDirNested = NO;
if (![fm fileExistsAtPath:nestedRoot isDirectory:&isDirNested] || !isDirNested) continue; 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]; NSArray *nestedFiles = [fm contentsOfDirectoryAtPath:nestedIcons error:NULL];
if (nestedFiles.count > 0) { if (nestedFiles.count > 0) {
// icons // icons
[fm createDirectoryAtPath:iconsDir [fm createDirectoryAtPath:workingIconsDir
withIntermediateDirectories:YES withIntermediateDirectories:YES
attributes:nil attributes:nil
error:NULL]; error:NULL];
// icons // icons
for (NSString *fn in nestedFiles) { for (NSString *fn in nestedFiles) {
NSString *from = [nestedIcons stringByAppendingPathComponent:fn]; NSString *from = [nestedIcons stringByAppendingPathComponent:fn];
NSString *to = [iconsDir stringByAppendingPathComponent:fn]; NSString *to = [workingIconsDir stringByAppendingPathComponent:fn];
[fm removeItemAtPath:to error:nil]; [fm removeItemAtPath:to error:nil];
[fm moveItemAtPath:from toPath:to error:nil]; [fm moveItemAtPath:from toPath:to error:nil];
} }
@@ -355,20 +377,65 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
// background.png skinRoot // background.png skinRoot
NSString *nestedBg = [nestedRoot stringByAppendingPathComponent:@"background.png"]; NSString *nestedBg = [nestedRoot stringByAppendingPathComponent:@"background.png"];
if ([fm fileExistsAtPath:nestedBg]) { if ([fm fileExistsAtPath:nestedBg]) {
[fm removeItemAtPath:bgPath error:nil]; [fm removeItemAtPath:workingBgPath error:nil];
[fm moveItemAtPath:nestedBg toPath:bgPath 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); dispatch_group_leave(group);
}; };
#if __has_include("KBNetworkManager.h") #if __has_include("KBNetworkManager.h")
// http/https // http/https
NSLog(@"[SkinBridge] will GET zip: %@", zipURL); NSLog(@"🌐[SkinBridge] will GET zip: %@", zipURL);
[KBHUD showWithStatus:@"正在下载..."]; [KBHUD showWithStatus:@"正在下载..."];
[[KBNetworkManager shared] GETData:zipURL parameters:nil headers:nil completion:^(NSData *data, NSURLResponse *response, NSError *error) { [[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) { if (error || data.length == 0) {
zipOK = NO; zipOK = NO;
if (!innerError) { if (!innerError) {
@@ -399,6 +466,9 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
} }
}); });
#endif #endif
} else {
NSLog(@"[SkinBridge] skip download id=%@ force=%d cached=%d zip=%@",
skinId, forceDownload, hasCachedAssets, zipURL);
} }
#else #else
zipOK = NO; zipOK = NO;
@@ -411,11 +481,12 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// //
// B // B
BOOL hasAssets = (hasCachedAssets || didUnzip); BOOL hasAssets = (didUnzip || (!forceDownload && hasCachedAssets));
if (!hasAssets) { if (!hasAssets) {
NSError *finalError = innerError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain NSError *finalError = innerError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain
code:KBSkinBridgeErrorZipMissing code:KBSkinBridgeErrorZipMissing
userInfo:@{NSLocalizedDescriptionKey: @"Zip resource not available"}]; userInfo:@{NSLocalizedDescriptionKey: @"Zip resource not available"}];
NSLog(@"❌[SkinBridge] apply aborted id=%@ error=%@", skinId, finalError);
if (completion) completion(NO, finalError); if (completion) completion(NO, finalError);
return; return;
} }
@@ -459,6 +530,10 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
userInfo:nil]; userInfo:nil];
} }
if (completion) completion(ok, finalError); if (completion) completion(ok, finalError);
NSLog(@"%@ [SkinBridge] apply %@ id=%@",
(ok ? @"✅" : @"❌"),
(ok ? @"ok" : @"failed"),
skinId);
if (ok) { if (ok) {
NSString *preview = [skinJSON[@"preview"] isKindOfClass:NSString.class] ? skinJSON[@"preview"] : nil; NSString *preview = [skinJSON[@"preview"] isKindOfClass:NSString.class] ? skinJSON[@"preview"] : nil;
[self recordInstalledSkinWithId:skinId [self recordInstalledSkinWithId:skinId

View File

@@ -157,6 +157,7 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
- (BOOL)applyTheme:(KBSkinTheme *)theme { - (BOOL)applyTheme:(KBSkinTheme *)theme {
if (!theme) return NO; if (!theme) return NO;
NSLog(@"🎨[SkinManager] apply theme id=%@ name=%@", theme.skinId, theme.name);
// App Group 使 // App Group 使
[self p_saveToStore:theme]; [self p_saveToStore:theme];
// 广 // 广

View File

@@ -42,6 +42,13 @@
if (completion) completion(NO); if (completion) completion(NO);
return; return;
} }
BOOL forceDownload = NO;
id forceValue = skinJSON[@"force_download"];
if ([forceValue respondsToSelector:@selector(boolValue)]) {
forceDownload = [forceValue boolValue];
}
NSLog(@"🧩[SkinService] apply mode=%lu id=%@ force=%d",
(unsigned long)mode, skinJSON[@"id"], forceDownload);
// // 1. & 访 // // 1. & 访
// KBKeyboardPermissionManager *perm = [KBKeyboardPermissionManager shared]; // KBKeyboardPermissionManager *perm = [KBKeyboardPermissionManager shared];

View File

@@ -242,6 +242,7 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
[KBHUD showInfo:KBLocalized(@"正在加载主题详情")]; [KBHUD showInfo:KBLocalized(@"正在加载主题详情")];
return; return;
} }
NSLog(@"🧩[SkinDetail] action themeId=%@ purchased=%d", self.themeId, self.detailModel.isPurchased);
if (self.detailModel.isPurchased) { if (self.detailModel.isPurchased) {
[self requestDownload]; [self requestDownload];
} else { } else {
@@ -287,11 +288,17 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
if (self.detailModel.themePreviewImageUrl.length > 0) { if (self.detailModel.themePreviewImageUrl.length > 0) {
skin[@"preview"] = self.detailModel.themePreviewImageUrl; skin[@"preview"] = self.detailModel.themePreviewImageUrl;
} }
skin[@"force_download"] = @(YES);
NSLog(@"⬇️[SkinDetail] download request id=%@ zip=%@ force=YES",
skin[@"id"], skin[@"zip_url"]);
[KBHUD showWithStatus:@"正在下载..."]; [KBHUD showWithStatus:@"正在下载..."];
[[KBSkinService shared] applySkinWithJSON:skin [[KBSkinService shared] applySkinWithJSON:skin
fromViewController:self fromViewController:self
mode:KBSkinSourceModeRemoteZip mode:KBSkinSourceModeRemoteZip
completion:^(BOOL success) { completion:^(BOOL success) {
NSLog(@"%@[SkinDetail] download result id=%@",
(success ? @"✅" : @"❌"),
self.detailModel.themeId);
if (success) { if (success) {
NSString *themeId = self.detailModel.themeId; NSString *themeId = self.detailModel.themeId;
if (themeId.length > 0) { if (themeId.length > 0) {