2
This commit is contained in:
@@ -30,6 +30,7 @@ typedef void (^KBSkinInstallConsumeCompletion)(BOOL success, NSError * _Nullable
|
|||||||
@property (nonatomic, copy) NSString *name;
|
@property (nonatomic, copy) NSString *name;
|
||||||
@property (nonatomic, copy, nullable) NSString *previewImage;
|
@property (nonatomic, copy, nullable) NSString *previewImage;
|
||||||
@property (nonatomic, copy, nullable) NSString *zipURL;
|
@property (nonatomic, copy, nullable) NSString *zipURL;
|
||||||
|
@property (nonatomic, strong, nullable) NSDictionary *themeJSON;
|
||||||
@property (nonatomic, assign) NSTimeInterval installedAt;
|
@property (nonatomic, assign) NSTimeInterval installedAt;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -76,7 +77,12 @@ typedef void (^KBSkinInstallConsumeCompletion)(BOOL success, NSError * _Nullable
|
|||||||
+ (void)recordInstalledSkinWithId:(NSString *)skinId
|
+ (void)recordInstalledSkinWithId:(NSString *)skinId
|
||||||
name:(NSString *)name
|
name:(NSString *)name
|
||||||
preview:(nullable NSString *)preview
|
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
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ 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";
|
||||||
static NSString * const kKBSkinMetadataInstalledKey = @"installed_at";
|
static NSString * const kKBSkinMetadataInstalledKey = @"installed_at";
|
||||||
|
static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
||||||
|
|
||||||
@interface KBSkinDownloadRecord ()
|
@interface KBSkinDownloadRecord ()
|
||||||
- (instancetype)initWithSkinId:(NSString *)skinId metadata:(NSDictionary *)metadata;
|
- (instancetype)initWithSkinId:(NSString *)skinId metadata:(NSDictionary *)metadata;
|
||||||
@@ -53,6 +54,8 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at";
|
|||||||
installed = [[NSDate date] timeIntervalSince1970];
|
installed = [[NSDate date] timeIntervalSince1970];
|
||||||
}
|
}
|
||||||
_installedAt = installed;
|
_installedAt = installed;
|
||||||
|
NSDictionary *theme = [metadata[kKBSkinMetadataThemeKey] isKindOfClass:NSDictionary.class] ? metadata[kKBSkinMetadataThemeKey] : nil;
|
||||||
|
_themeJSON = theme;
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -78,10 +81,21 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at";
|
|||||||
return [skinRoot stringByAppendingPathComponent:kKBSkinMetadataFileName];
|
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
|
+ (void)kb_storeMetadataForSkinId:(NSString *)skinId
|
||||||
name:(NSString *)name
|
name:(NSString *)name
|
||||||
preview:(NSString *)preview
|
preview:(NSString *)preview
|
||||||
zipURL:(NSString *)zipURL {
|
zipURL:(NSString *)zipURL
|
||||||
|
themeJSON:(NSDictionary *)themeJSON {
|
||||||
if (skinId.length == 0) { return; }
|
if (skinId.length == 0) { return; }
|
||||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||||
NSString *metaPath = [self kb_metadataPathForSkinId:skinId];
|
NSString *metaPath = [self kb_metadataPathForSkinId:skinId];
|
||||||
@@ -101,6 +115,9 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at";
|
|||||||
if (zipURL.length > 0) {
|
if (zipURL.length > 0) {
|
||||||
dict[kKBSkinMetadataZipKey] = zipURL;
|
dict[kKBSkinMetadataZipKey] = zipURL;
|
||||||
}
|
}
|
||||||
|
if (themeJSON.count > 0) {
|
||||||
|
dict[kKBSkinMetadataThemeKey] = themeJSON;
|
||||||
|
}
|
||||||
dict[kKBSkinMetadataInstalledKey] = @(now);
|
dict[kKBSkinMetadataInstalledKey] = @(now);
|
||||||
[dict writeToFile:metaPath atomically:YES];
|
[dict writeToFile:metaPath atomically:YES];
|
||||||
});
|
});
|
||||||
@@ -146,7 +163,9 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at";
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
NSString *currentId = [KBSkinManager shared].current.skinId;
|
NSString *currentId = [KBSkinManager shared].current.skinId;
|
||||||
if ([currentId isKindOfClass:NSString.class] && [currentId isEqualToString:skinId]) {
|
if ([currentId isKindOfClass:NSString.class] && [currentId isEqualToString:skinId]) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[[KBSkinManager shared] resetToDefault];
|
[[KBSkinManager shared] resetToDefault];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
@@ -155,8 +174,13 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at";
|
|||||||
+ (void)recordInstalledSkinWithId:(NSString *)skinId
|
+ (void)recordInstalledSkinWithId:(NSString *)skinId
|
||||||
name:(NSString *)name
|
name:(NSString *)name
|
||||||
preview:(NSString *)preview
|
preview:(NSString *)preview
|
||||||
zipURL:(NSString *)zipURL {
|
zipURL:(NSString *)zipURL
|
||||||
[self kb_storeMetadataForSkinId:skinId name:name preview:preview zipURL: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<NSString *,NSString *> *)defaultIconShortNames {
|
+ (NSDictionary<NSString *,NSString *> *)defaultIconShortNames {
|
||||||
@@ -440,7 +464,8 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at";
|
|||||||
[self recordInstalledSkinWithId:skinId
|
[self recordInstalledSkinWithId:skinId
|
||||||
name:name ?: skinId
|
name:name ?: skinId
|
||||||
preview:preview
|
preview:preview
|
||||||
zipURL:zipURL];
|
zipURL:zipURL
|
||||||
|
themeJSON:themeJSON];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -682,10 +707,63 @@ static NSString * const kKBSkinMetadataInstalledKey = @"installed_at";
|
|||||||
[self recordInstalledSkinWithId:skinId
|
[self recordInstalledSkinWithId:skinId
|
||||||
name:name ?: skinId
|
name:name ?: skinId
|
||||||
preview:nil
|
preview:nil
|
||||||
zipURL:zipName];
|
zipURL:zipName
|
||||||
|
themeJSON:themeJSON];
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
#endif
|
#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
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// “我的” tab 默认为第 4 个(索引 3),未登录时先跳转登录页
|
// “我的” tab 默认为第 4 个(索引 3),未登录时先跳转登录页
|
||||||
if (index == 2 && ![KBUserSessionManager shared].isLoggedIn) {
|
if ((index == 1 || index == 2) && ![KBUserSessionManager shared].isLoggedIn) {
|
||||||
[[KBUserSessionManager shared] goLoginVC];
|
[[KBUserSessionManager shared] goLoginVC];
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ typedef NS_ENUM(NSUInteger, KBSkinSourceMode) {
|
|||||||
mode:(KBSkinSourceMode)mode
|
mode:(KBSkinSourceMode)mode
|
||||||
completion:(nullable KBSkinApplyCompletion)completion;
|
completion:(nullable KBSkinApplyCompletion)completion;
|
||||||
|
|
||||||
|
typedef void(^KBSkinDeleteCompletion)(BOOL success, NSError *_Nullable error);
|
||||||
|
|
||||||
|
/// 删除本地皮肤资源,并在必要时自动切换到其他已下载皮肤或默认皮肤。
|
||||||
|
- (void)deleteSkinsWithIds:(NSArray<NSString *> *)skinIds
|
||||||
|
completion:(nullable KBSkinDeleteCompletion)completion;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -279,10 +279,102 @@
|
|||||||
[KBSkinInstallBridge recordInstalledSkinWithId:skinId
|
[KBSkinInstallBridge recordInstalledSkinWithId:skinId
|
||||||
name:name
|
name:name
|
||||||
preview:preview
|
preview:preview
|
||||||
zipURL:zipName];
|
zipURL:zipName
|
||||||
|
themeJSON:themeJSON];
|
||||||
}
|
}
|
||||||
[KBHUD showInfo:(ok ? KBLocalized(@"已应用,切到键盘查看") : KBLocalized(@"应用皮肤失败"))];
|
[KBHUD showInfo:(ok ? KBLocalized(@"已应用,切到键盘查看") : KBLocalized(@"应用皮肤失败"))];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)deleteSkinsWithIds:(NSArray<NSString *> *)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<KBSkinDownloadRecord *> *beforeDesc = [KBSkinInstallBridge installedSkinRecords];
|
||||||
|
NSArray<KBSkinDownloadRecord *> *beforeAsc = beforeDesc.count > 0 ? [[[beforeDesc reverseObjectEnumerator] allObjects] copy] : @[];
|
||||||
|
NSString *currentId = [KBSkinManager shared].current.skinId ?: @"";
|
||||||
|
NSSet<NSString *> *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<KBSkinDownloadRecord *> *afterDesc = [KBSkinInstallBridge installedSkinRecords];
|
||||||
|
NSArray<KBSkinDownloadRecord *> *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<KBSkinDownloadRecord *> *)orderAsc
|
||||||
|
deletedSet:(NSSet<NSString *> *)deletedSet
|
||||||
|
remainingAsc:(NSArray<KBSkinDownloadRecord *> *)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
|
@end
|
||||||
|
|||||||
@@ -126,8 +126,7 @@
|
|||||||
- (void)configWithTitle:(NSString *)title imageURL:(NSString *)imageURL {
|
- (void)configWithTitle:(NSString *)title imageURL:(NSString *)imageURL {
|
||||||
self.titleLabel.text = title.length ? title : @"Dopamine";
|
self.titleLabel.text = title.length ? title : @"Dopamine";
|
||||||
self.coverView.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0];
|
self.coverView.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0];
|
||||||
UIImage *placeholder = [UIImage imageNamed:@"my_skin_placeholder"];
|
[self.coverView kb_setImageURL:imageURL placeholder:nil];
|
||||||
[self.coverView kb_setImageURL:imageURL placeholder:placeholder];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)setEditing:(BOOL)editing {
|
- (void)setEditing:(BOOL)editing {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#import "KBAPI.h"
|
#import "KBAPI.h"
|
||||||
//#import <MJExtension/MJExtension.h>
|
//#import <MJExtension/MJExtension.h>
|
||||||
#import "KBMyMainModel.h"
|
#import "KBMyMainModel.h"
|
||||||
|
#import "KBSkinService.h"
|
||||||
#import "KBSkinInstallBridge.h"
|
#import "KBSkinInstallBridge.h"
|
||||||
|
|
||||||
NSString * const KBUserCharacterDeletedNotification = @"KBUserCharacterDeletedNotification";
|
NSString * const KBUserCharacterDeletedNotification = @"KBUserCharacterDeletedNotification";
|
||||||
@@ -117,22 +118,12 @@ NSString * const KBUserCharacterDeletedNotification = @"KBUserCharacterDeletedNo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
[[KBSkinService shared] deleteSkinsWithIds:themeIds
|
||||||
NSError *lastError = nil;
|
completion:^(BOOL success, NSError * _Nullable error) {
|
||||||
for (NSString *skinId in themeIds) {
|
|
||||||
if (skinId.length == 0) { continue; }
|
|
||||||
BOOL ok = [KBSkinInstallBridge removeInstalledSkinWithId:skinId error:&lastError];
|
|
||||||
if (!ok) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BOOL success = (lastError == nil);
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
if (completion) {
|
if (completion) {
|
||||||
completion(success, lastError);
|
completion(success, error);
|
||||||
}
|
}
|
||||||
});
|
}];
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新用户人设排序
|
/// 更新用户人设排序
|
||||||
|
|||||||
Reference in New Issue
Block a user