From 35597f89cab82fa86fcd41c45be861c4bdc6e39b Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Thu, 11 Dec 2025 20:40:49 +0800 Subject: [PATCH] 2 --- Shared/KBSkinInstallBridge.h | 8 +- Shared/KBSkinInstallBridge.m | 90 ++++++++++++++++-- keyBoard/Class/Base/VC/BaseTabBarController.m | 2 +- keyBoard/Class/Manager/KBSkinService.h | 6 ++ keyBoard/Class/Manager/KBSkinService.m | 94 ++++++++++++++++++- keyBoard/Class/Me/V/MySkinCell.m | 3 +- keyBoard/Class/Me/VM/KBMyVM.m | 21 ++--- 7 files changed, 198 insertions(+), 26 deletions(-) diff --git a/Shared/KBSkinInstallBridge.h b/Shared/KBSkinInstallBridge.h index 4c6f7d1..08e7cb2 100644 --- a/Shared/KBSkinInstallBridge.h +++ b/Shared/KBSkinInstallBridge.h @@ -30,6 +30,7 @@ typedef void (^KBSkinInstallConsumeCompletion)(BOOL success, NSError * _Nullable @property (nonatomic, copy) NSString *name; @property (nonatomic, copy, nullable) NSString *previewImage; @property (nonatomic, copy, nullable) NSString *zipURL; +@property (nonatomic, strong, nullable) NSDictionary *themeJSON; @property (nonatomic, assign) NSTimeInterval installedAt; @end @@ -76,7 +77,12 @@ typedef void (^KBSkinInstallConsumeCompletion)(BOOL success, NSError * _Nullable + (void)recordInstalledSkinWithId:(NSString *)skinId name:(NSString *)name preview:(nullable NSString *)preview - zipURL:(nullable NSString *)zipURL; + zipURL:(nullable NSString *)zipURL + themeJSON:(nullable NSDictionary *)themeJSON; + +/// 重新应用现有皮肤(从 App Group 读取主题配置与背景图)。 ++ (BOOL)applyInstalledSkinWithId:(NSString *)skinId + error:(NSError * _Nullable __autoreleasing *)error; @end diff --git a/Shared/KBSkinInstallBridge.m b/Shared/KBSkinInstallBridge.m index bf75e33..ce01f7d 100644 --- a/Shared/KBSkinInstallBridge.m +++ b/Shared/KBSkinInstallBridge.m @@ -28,6 +28,7 @@ static NSString * const kKBSkinMetadataNameKey = @"name"; static NSString * const kKBSkinMetadataPreviewKey = @"preview"; static NSString * const kKBSkinMetadataZipKey = @"zip_url"; static NSString * const kKBSkinMetadataInstalledKey = @"installed_at"; +static NSString * const kKBSkinMetadataThemeKey = @"theme_json"; @interface KBSkinDownloadRecord () - (instancetype)initWithSkinId:(NSString *)skinId metadata:(NSDictionary *)metadata; @@ -53,6 +54,8 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at"; installed = [[NSDate date] timeIntervalSince1970]; } _installedAt = installed; + NSDictionary *theme = [metadata[kKBSkinMetadataThemeKey] isKindOfClass:NSDictionary.class] ? metadata[kKBSkinMetadataThemeKey] : nil; + _themeJSON = theme; } return self; } @@ -78,10 +81,21 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at"; return [skinRoot stringByAppendingPathComponent:kKBSkinMetadataFileName]; } ++ (NSDictionary *)kb_metadataForSkinId:(NSString *)skinId { + NSString *metaPath = [self kb_metadataPathForSkinId:skinId]; + if (metaPath.length == 0) { return nil; } + NSDictionary *meta = [NSDictionary dictionaryWithContentsOfFile:metaPath]; + if ([meta isKindOfClass:NSDictionary.class]) { + return meta; + } + return nil; +} + + (void)kb_storeMetadataForSkinId:(NSString *)skinId name:(NSString *)name preview:(NSString *)preview - zipURL:(NSString *)zipURL { + zipURL:(NSString *)zipURL + themeJSON:(NSDictionary *)themeJSON { if (skinId.length == 0) { return; } dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ NSString *metaPath = [self kb_metadataPathForSkinId:skinId]; @@ -101,6 +115,9 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at"; if (zipURL.length > 0) { dict[kKBSkinMetadataZipKey] = zipURL; } + if (themeJSON.count > 0) { + dict[kKBSkinMetadataThemeKey] = themeJSON; + } dict[kKBSkinMetadataInstalledKey] = @(now); [dict writeToFile:metaPath atomically:YES]; }); @@ -146,7 +163,9 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at"; if (ok) { NSString *currentId = [KBSkinManager shared].current.skinId; if ([currentId isKindOfClass:NSString.class] && [currentId isEqualToString:skinId]) { - [[KBSkinManager shared] resetToDefault]; + dispatch_async(dispatch_get_main_queue(), ^{ + [[KBSkinManager shared] resetToDefault]; + }); } } return ok; @@ -155,8 +174,13 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at"; + (void)recordInstalledSkinWithId:(NSString *)skinId name:(NSString *)name preview:(NSString *)preview - zipURL:(NSString *)zipURL { - [self kb_storeMetadataForSkinId:skinId name:name preview:preview zipURL:zipURL]; + zipURL:(NSString *)zipURL + themeJSON:(NSDictionary *)themeJSON { + NSMutableDictionary *jsonToSave = themeJSON ? [themeJSON mutableCopy] : nil; + if (jsonToSave.count > 0 && skinId.length > 0) { + jsonToSave[@"id"] = skinId; + } + [self kb_storeMetadataForSkinId:skinId name:name preview:preview zipURL:zipURL themeJSON:jsonToSave]; } + (NSDictionary *)defaultIconShortNames { @@ -440,7 +464,8 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at"; [self recordInstalledSkinWithId:skinId name:name ?: skinId preview:preview - zipURL:zipURL]; + zipURL:zipURL + themeJSON:themeJSON]; } }); } @@ -682,10 +707,63 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at"; [self recordInstalledSkinWithId:skinId name:name ?: skinId preview:nil - zipURL:zipName]; + zipURL:zipName + themeJSON:themeJSON]; } return ok; #endif } ++ (BOOL)applyInstalledSkinWithId:(NSString *)skinId + error:(NSError *__autoreleasing *)error { + if (skinId.length == 0) { + if (error) { + *error = [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorInvalidPayload + userInfo:@{NSLocalizedDescriptionKey: @"Invalid skin id"}]; + } + return NO; + } + NSDictionary *meta = [self kb_metadataForSkinId:skinId]; + NSDictionary *themeJSON = [meta[kKBSkinMetadataThemeKey] isKindOfClass:NSDictionary.class] ? meta[kKBSkinMetadataThemeKey] : nil; + if (themeJSON.count == 0) { + if (error) { + *error = [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorInvalidPayload + userInfo:@{NSLocalizedDescriptionKey: @"Theme data missing"}]; + } + return NO; + } + NSString *name = [meta[kKBSkinMetadataNameKey] isKindOfClass:NSString.class] ? meta[kKBSkinMetadataNameKey] : skinId; + NSString *bgPath = [[[self kb_skinsRootPath] stringByAppendingPathComponent:skinId] stringByAppendingPathComponent:@"background.png"]; + NSData *bgData = [NSData dataWithContentsOfFile:bgPath]; + __block NSError *applyError = nil; + __block BOOL applyOK = NO; + void (^applyBlock)(void) = ^{ + BOOL themeOK = [[KBSkinManager shared] applyThemeFromJSON:themeJSON]; + BOOL ok = themeOK; + if (bgData.length > 0) { + ok = [[KBSkinManager shared] applyImageSkinWithData:bgData skinId:skinId name:name ?: skinId]; + } + if (!ok) { + applyError = [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorApplyFailed + userInfo:@{NSLocalizedDescriptionKey: @"Failed to apply skin"}]; + } + applyOK = ok; + }; + if ([NSThread isMainThread]) { + applyBlock(); + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + applyBlock(); + }); + } + if (!applyOK && error) { + *error = applyError; + } + return applyOK; +} + @end + diff --git a/keyBoard/Class/Base/VC/BaseTabBarController.m b/keyBoard/Class/Base/VC/BaseTabBarController.m index c243681..f7a70d7 100644 --- a/keyBoard/Class/Base/VC/BaseTabBarController.m +++ b/keyBoard/Class/Base/VC/BaseTabBarController.m @@ -120,7 +120,7 @@ } // “我的” tab 默认为第 4 个(索引 3),未登录时先跳转登录页 - if (index == 2 && ![KBUserSessionManager shared].isLoggedIn) { + if ((index == 1 || index == 2) && ![KBUserSessionManager shared].isLoggedIn) { [[KBUserSessionManager shared] goLoginVC]; return NO; } diff --git a/keyBoard/Class/Manager/KBSkinService.h b/keyBoard/Class/Manager/KBSkinService.h index c452058..a57a496 100644 --- a/keyBoard/Class/Manager/KBSkinService.h +++ b/keyBoard/Class/Manager/KBSkinService.h @@ -40,6 +40,12 @@ typedef NS_ENUM(NSUInteger, KBSkinSourceMode) { mode:(KBSkinSourceMode)mode completion:(nullable KBSkinApplyCompletion)completion; +typedef void(^KBSkinDeleteCompletion)(BOOL success, NSError *_Nullable error); + +/// 删除本地皮肤资源,并在必要时自动切换到其他已下载皮肤或默认皮肤。 +- (void)deleteSkinsWithIds:(NSArray *)skinIds + completion:(nullable KBSkinDeleteCompletion)completion; + @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Manager/KBSkinService.m b/keyBoard/Class/Manager/KBSkinService.m index 24acdb2..bbcb8c8 100644 --- a/keyBoard/Class/Manager/KBSkinService.m +++ b/keyBoard/Class/Manager/KBSkinService.m @@ -279,10 +279,102 @@ [KBSkinInstallBridge recordInstalledSkinWithId:skinId name:name preview:preview - zipURL:zipName]; + zipURL:zipName + themeJSON:themeJSON]; } [KBHUD showInfo:(ok ? KBLocalized(@"已应用,切到键盘查看") : KBLocalized(@"应用皮肤失败"))]; }); } +- (void)deleteSkinsWithIds:(NSArray *)skinIds + completion:(KBSkinDeleteCompletion)completion { + if (skinIds.count == 0) { + if (completion) { + NSError *error = [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorInvalidPayload + userInfo:@{NSLocalizedDescriptionKey: @"Invalid skin ids"}]; + completion(NO, error); + } + return; + } + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + NSArray *beforeDesc = [KBSkinInstallBridge installedSkinRecords]; + NSArray *beforeAsc = beforeDesc.count > 0 ? [[[beforeDesc reverseObjectEnumerator] allObjects] copy] : @[]; + NSString *currentId = [KBSkinManager shared].current.skinId ?: @""; + NSSet *deleteSet = [NSSet setWithArray:skinIds]; + NSError *lastError = nil; + for (NSString *skinId in skinIds) { + if (skinId.length == 0) { continue; } + BOOL ok = [KBSkinInstallBridge removeInstalledSkinWithId:skinId error:&lastError]; + if (!ok) { break; } + } + if (!lastError && [deleteSet containsObject:currentId]) { + NSArray *afterDesc = [KBSkinInstallBridge installedSkinRecords]; + NSArray *afterAsc = afterDesc.count > 0 ? [[[afterDesc reverseObjectEnumerator] allObjects] copy] : @[]; + NSString *fallbackId = [self kb_fallbackSkinIdForCurrent:currentId + orderAsc:beforeAsc + deletedSet:deleteSet + remainingAsc:afterAsc]; + if (fallbackId.length > 0) { + BOOL ok = [KBSkinInstallBridge applyInstalledSkinWithId:fallbackId error:&lastError]; + if (!ok) { + if (lastError == nil) { + lastError = [NSError errorWithDomain:KBSkinBridgeErrorDomain + code:KBSkinBridgeErrorApplyFailed + userInfo:nil]; + } + dispatch_async(dispatch_get_main_queue(), ^{ + [[KBSkinManager shared] resetToDefault]; + }); + } + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + [[KBSkinManager shared] resetToDefault]; + }); + } + } + dispatch_async(dispatch_get_main_queue(), ^{ + if (completion) completion(lastError == nil, lastError); + if (lastError) { + NSLog(@"[KBSkinService] delete skins failed: %@", lastError); + } + }); + }); +} + +- (NSString *)kb_fallbackSkinIdForCurrent:(NSString *)currentId + orderAsc:(NSArray *)orderAsc + deletedSet:(NSSet *)deletedSet + remainingAsc:(NSArray *)remainingAsc { + if (currentId.length == 0) { return nil; } + NSInteger idx = NSNotFound; + for (NSInteger i = 0; i < (NSInteger)orderAsc.count; i++) { + KBSkinDownloadRecord *record = orderAsc[i]; + if ([record.skinId isEqualToString:currentId]) { + idx = i; + break; + } + } + if (idx != NSNotFound) { + for (NSInteger left = idx - 1; left >= 0; left--) { + KBSkinDownloadRecord *candidate = orderAsc[left]; + if (candidate.skinId.length > 0 && ![deletedSet containsObject:candidate.skinId]) { + return candidate.skinId; + } + } + for (NSInteger right = idx + 1; right < (NSInteger)orderAsc.count; right++) { + KBSkinDownloadRecord *candidate = orderAsc[right]; + if (candidate.skinId.length > 0 && ![deletedSet containsObject:candidate.skinId]) { + return candidate.skinId; + } + } + } + for (KBSkinDownloadRecord *record in remainingAsc) { + if (record.skinId.length > 0) { + return record.skinId; + } + } + return nil; +} + @end diff --git a/keyBoard/Class/Me/V/MySkinCell.m b/keyBoard/Class/Me/V/MySkinCell.m index 9499377..bfa8eeb 100644 --- a/keyBoard/Class/Me/V/MySkinCell.m +++ b/keyBoard/Class/Me/V/MySkinCell.m @@ -126,8 +126,7 @@ - (void)configWithTitle:(NSString *)title imageURL:(NSString *)imageURL { self.titleLabel.text = title.length ? title : @"Dopamine"; self.coverView.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0]; - UIImage *placeholder = [UIImage imageNamed:@"my_skin_placeholder"]; - [self.coverView kb_setImageURL:imageURL placeholder:placeholder]; + [self.coverView kb_setImageURL:imageURL placeholder:nil]; } - (void)setEditing:(BOOL)editing { diff --git a/keyBoard/Class/Me/VM/KBMyVM.m b/keyBoard/Class/Me/VM/KBMyVM.m index bf6ed9c..c166fc2 100644 --- a/keyBoard/Class/Me/VM/KBMyVM.m +++ b/keyBoard/Class/Me/VM/KBMyVM.m @@ -12,6 +12,7 @@ #import "KBAPI.h" //#import #import "KBMyMainModel.h" +#import "KBSkinService.h" #import "KBSkinInstallBridge.h" NSString * const KBUserCharacterDeletedNotification = @"KBUserCharacterDeletedNotification"; @@ -117,22 +118,12 @@ NSString * const KBUserCharacterDeletedNotification = @"KBUserCharacterDeletedNo return; } - dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ - NSError *lastError = nil; - for (NSString *skinId in themeIds) { - if (skinId.length == 0) { continue; } - BOOL ok = [KBSkinInstallBridge removeInstalledSkinWithId:skinId error:&lastError]; - if (!ok) { - break; - } + [[KBSkinService shared] deleteSkinsWithIds:themeIds + completion:^(BOOL success, NSError * _Nullable error) { + if (completion) { + completion(success, error); } - BOOL success = (lastError == nil); - dispatch_async(dispatch_get_main_queue(), ^{ - if (completion) { - completion(success, lastError); - } - }); - }); + }]; } /// 更新用户人设排序