This commit is contained in:
2025-12-11 19:43:55 +08:00
parent cccced6afa
commit 577b749198
12 changed files with 244 additions and 80 deletions

View File

@@ -49,7 +49,6 @@
#define API_THEME_LIST_ALL_STYLES @"/themes/listAllStyles" // 查询所有主题风格
#define API_THEME_LIST_BY_STYLE @"/themes/listByStyle" // 按风格查询主题列表
#define API_THEME_PURCHASED @"/themes/purchased" // 查询已购买主题
#define API_THEME_BATCH_DELETE @"/user-themes/batch-delete" // 批量删除用户主题
#define API_WALLET_BALANCE @"/wallet/balance" // 查询钱包余额
#define API_THEME_DETAIL @"/themes/detail" // 查询主题详情
#define API_THEME_PURCHASE @"/themes/purchase" // 购买主题

View File

@@ -24,6 +24,15 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
typedef void (^KBSkinInstallConsumeCompletion)(BOOL success, NSError * _Nullable error);
/// 已安装的皮肤记录,用于“我的皮肤”列表展示。
@interface KBSkinDownloadRecord : NSObject
@property (nonatomic, copy) NSString *skinId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy, nullable) NSString *previewImage;
@property (nonatomic, copy, nullable) NSString *zipURL;
@property (nonatomic, assign) NSTimeInterval installedAt;
@end
@interface KBSkinInstallBridge : NSObject
/// 默认图标短文件名映射(从 KBSkinIconMap.strings 读取)。
@@ -56,6 +65,19 @@ typedef void (^KBSkinInstallConsumeCompletion)(BOOL success, NSError * _Nullable
+ (void)consumePendingRequestFromBundle:(NSBundle *)bundle
completion:(nullable KBSkinInstallConsumeCompletion)completion;
/// 枚举当前 App Group 中所有已解压的皮肤信息。
+ (NSArray<KBSkinDownloadRecord *> *)installedSkinRecords;
/// 删除指定皮肤的本地资源目录(用于“我的皮肤 - 删除”)。
+ (BOOL)removeInstalledSkinWithId:(NSString *)skinId
error:(NSError * _Nullable __autoreleasing *)error;
/// 记录皮肤元数据(下载完成后调用,方便“我的皮肤”读取)。
+ (void)recordInstalledSkinWithId:(NSString *)skinId
name:(NSString *)name
preview:(nullable NSString *)preview
zipURL:(nullable NSString *)zipURL;
@end
NS_ASSUME_NONNULL_END

View File

@@ -23,9 +23,142 @@ static NSString * const kKBSkinPendingZipKey = @"zipName";
static NSString * const kKBSkinPendingKindKey = @"kind";
static NSString * const kKBSkinPendingTimestampKey = @"timestamp";
static NSString * const kKBSkinPendingIconShortKey = @"iconShortNames";
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
@implementation KBSkinInstallBridge
+ (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];
}
+ (NSDictionary<NSString *,NSString *> *)defaultIconShortNames {
static NSDictionary<NSString *, NSString *> *map;
static dispatch_once_t onceToken;
@@ -302,6 +435,13 @@ static NSString * const kKBSkinPendingIconShortKey = @"iconShortNames";
userInfo:nil];
}
if (completion) completion(ok, finalError);
if (ok) {
NSString *preview = [skinJSON[@"preview"] isKindOfClass:NSString.class] ? skinJSON[@"preview"] : nil;
[self recordInstalledSkinWithId:skinId
name:name ?: skinId
preview:preview
zipURL:zipURL];
}
});
}
@@ -538,6 +678,12 @@ static NSString * const kKBSkinPendingIconShortKey = @"iconShortNames";
code:KBSkinBridgeErrorApplyFailed
userInfo:nil];
}
if (ok) {
[self recordInstalledSkinWithId:skinId
name:name ?: skinId
preview:nil
zipURL:zipName];
}
return ok;
#endif
}