From 000d6032417b370df10490272948bc94965a2a15 Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Tue, 23 Dec 2025 15:26:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=8B=E8=BD=BD=E7=9A=AE?= =?UTF-8?q?=E8=82=A4=E2=9D=A4=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CustomKeyboard/KeyboardViewController.m | 1 + Shared/KBSkinInstallBridge.m | 113 ++++++++++++++++++++---- Shared/KBSkinManager.m | 1 + keyBoard/Class/Manager/KBSkinService.m | 7 ++ keyBoard/Class/Shop/VC/KBSkinDetailVC.m | 9 +- 5 files changed, 111 insertions(+), 20 deletions(-) diff --git a/CustomKeyboard/KeyboardViewController.m b/CustomKeyboard/KeyboardViewController.m index c3465ad..7e6d2c4 100644 --- a/CustomKeyboard/KeyboardViewController.m +++ b/CustomKeyboard/KeyboardViewController.m @@ -589,6 +589,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, - (void)kb_applyTheme { KBSkinTheme *t = [KBSkinManager shared].current; UIImage *img = [[KBSkinManager shared] currentBackgroundImage]; + NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil)); self.bgImageView.image = img; BOOL hasImg = (img != nil); self.view.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground; diff --git a/Shared/KBSkinInstallBridge.m b/Shared/KBSkinInstallBridge.m index 3c67915..62fe188 100644 --- a/Shared/KBSkinInstallBridge.m +++ b/Shared/KBSkinInstallBridge.m @@ -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() - // 若本地尚未缓存该皮肤资源且提供了 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//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); + NSArray *iconsContent = [fm contentsOfDirectoryAtPath:workingIconsDir error:NULL]; + BOOL iconsValid = ([fm fileExistsAtPath:workingIconsDir isDirectory:&isDir2] && isDir2 && iconsContent.count > 0); if (!iconsValid) { - NSArray *subItems = [fm contentsOfDirectoryAtPath:skinRoot error:NULL]; + NSArray *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 diff --git a/Shared/KBSkinManager.m b/Shared/KBSkinManager.m index 73293c3..fd192e2 100644 --- a/Shared/KBSkinManager.m +++ b/Shared/KBSkinManager.m @@ -157,6 +157,7 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer, - (BOOL)applyTheme:(KBSkinTheme *)theme { if (!theme) return NO; + NSLog(@"🎨[SkinManager] apply theme id=%@ name=%@", theme.skinId, theme.name); // 将主题写入 App Group 存储(失败也不影响本次进程内的使用) [self p_saveToStore:theme]; // 始终更新当前主题并广播通知,确保当前进程和扩展之间保持同步。 diff --git a/keyBoard/Class/Manager/KBSkinService.m b/keyBoard/Class/Manager/KBSkinService.m index bbcb8c8..969bcf8 100644 --- a/keyBoard/Class/Manager/KBSkinService.m +++ b/keyBoard/Class/Manager/KBSkinService.m @@ -42,6 +42,13 @@ if (completion) completion(NO); 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. 点击应用皮肤时,检查键盘启用 & 完全访问状态,并尽量给出友好提示。 // KBKeyboardPermissionManager *perm = [KBKeyboardPermissionManager shared]; diff --git a/keyBoard/Class/Shop/VC/KBSkinDetailVC.m b/keyBoard/Class/Shop/VC/KBSkinDetailVC.m index 46fa82f..d7c0d22 100644 --- a/keyBoard/Class/Shop/VC/KBSkinDetailVC.m +++ b/keyBoard/Class/Shop/VC/KBSkinDetailVC.m @@ -242,6 +242,7 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) { [KBHUD showInfo:KBLocalized(@"正在加载主题详情")]; return; } + NSLog(@"🧩[SkinDetail] action themeId=%@ purchased=%d", self.themeId, self.detailModel.isPurchased); if (self.detailModel.isPurchased) { [self requestDownload]; } else { @@ -287,11 +288,17 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) { if (self.detailModel.themePreviewImageUrl.length > 0) { skin[@"preview"] = self.detailModel.themePreviewImageUrl; } + skin[@"force_download"] = @(YES); + NSLog(@"⬇️[SkinDetail] download request id=%@ zip=%@ force=YES", + skin[@"id"], skin[@"zip_url"]); [KBHUD showWithStatus:@"正在下载..."]; [[KBSkinService shared] applySkinWithJSON:skin fromViewController:self mode:KBSkinSourceModeRemoteZip - completion:^(BOOL success) { + completion:^(BOOL success) { + NSLog(@"%@[SkinDetail] download result id=%@", + (success ? @"✅" : @"❌"), + self.detailModel.themeId); if (success) { NSString *themeId = self.detailModel.themeId; if (themeId.length > 0) {