215 lines
9.5 KiB
Objective-C
215 lines
9.5 KiB
Objective-C
//
|
|
// KBSkinManager.m
|
|
//
|
|
|
|
#import "KBSkinManager.h"
|
|
#import <Security/Security.h>
|
|
#import "KBConfig.h"
|
|
|
|
NSString * const KBSkinDidChangeNotification = @"KBSkinDidChangeNotification";
|
|
NSString * const KBDarwinSkinChanged = @"com.loveKey.nyx.skin.changed";
|
|
|
|
static NSString * const kKBSkinService = @"com.loveKey.nyx.skin"; // Keychain service
|
|
static NSString * const kKBSkinAccount = @"current"; // Keychain account
|
|
|
|
@implementation KBSkinTheme
|
|
|
|
+ (BOOL)supportsSecureCoding { return YES; }
|
|
|
|
- (void)encodeWithCoder:(NSCoder *)coder {
|
|
[coder encodeObject:self.skinId forKey:@"skinId"];
|
|
[coder encodeObject:self.name forKey:@"name"];
|
|
[coder encodeObject:self.keyboardBackground forKey:@"keyboardBackground"];
|
|
[coder encodeObject:self.keyBackground forKey:@"keyBackground"];
|
|
[coder encodeObject:self.keyTextColor forKey:@"keyTextColor"];
|
|
[coder encodeObject:self.keyHighlightBackground forKey:@"keyHighlightBackground"];
|
|
[coder encodeObject:self.accentColor forKey:@"accentColor"];
|
|
if (self.backgroundImageData) {
|
|
[coder encodeObject:self.backgroundImageData forKey:@"backgroundImageData"];
|
|
}
|
|
}
|
|
|
|
- (instancetype)initWithCoder:(NSCoder *)coder {
|
|
if (self = [super init]) {
|
|
_skinId = [coder decodeObjectOfClass:NSString.class forKey:@"skinId"] ?: @"default";
|
|
_name = [coder decodeObjectOfClass:NSString.class forKey:@"name"] ?: @"Default";
|
|
_keyboardBackground = [coder decodeObjectOfClass:UIColor.class forKey:@"keyboardBackground"] ?: [UIColor colorWithWhite:0.95 alpha:1.0];
|
|
_keyBackground = [coder decodeObjectOfClass:UIColor.class forKey:@"keyBackground"] ?: UIColor.whiteColor;
|
|
_keyTextColor = [coder decodeObjectOfClass:UIColor.class forKey:@"keyTextColor"] ?: UIColor.blackColor;
|
|
_keyHighlightBackground = [coder decodeObjectOfClass:UIColor.class forKey:@"keyHighlightBackground"] ?: [UIColor colorWithWhite:0.85 alpha:1.0];
|
|
_accentColor = [coder decodeObjectOfClass:UIColor.class forKey:@"accentColor"] ?: [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
|
|
_backgroundImageData = [coder decodeObjectOfClass:NSData.class forKey:@"backgroundImageData"];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface KBSkinManager ()
|
|
@property (atomic, strong, readwrite) KBSkinTheme *current;
|
|
@end
|
|
|
|
@implementation KBSkinManager
|
|
|
|
+ (instancetype)shared {
|
|
static KBSkinManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBSkinManager new]; });
|
|
return m;
|
|
}
|
|
|
|
- (instancetype)init {
|
|
if (self = [super init]) {
|
|
_current = [self p_loadFromKeychain] ?: [self.class defaultTheme];
|
|
// Observe Darwin notification for cross-process updates
|
|
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
|
|
(__bridge const void *)(self),
|
|
KBSkinDarwinCallback,
|
|
(__bridge CFStringRef)KBDarwinSkinChanged,
|
|
NULL,
|
|
CFNotificationSuspensionBehaviorDeliverImmediately);
|
|
}
|
|
return self;
|
|
}
|
|
|
|
static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
|
|
KBSkinManager *self = (__bridge KBSkinManager *)observer;
|
|
[self p_reloadFromKeychainAndBroadcast:YES];
|
|
}
|
|
|
|
- (void)dealloc {
|
|
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), (__bridge CFStringRef)KBDarwinSkinChanged, NULL);
|
|
}
|
|
|
|
#pragma mark - Public
|
|
|
|
- (BOOL)applyThemeFromJSON:(NSDictionary *)json {
|
|
if (json.count == 0) return NO;
|
|
KBSkinTheme *t = [KBSkinTheme new];
|
|
t.skinId = [json[@"id"] isKindOfClass:NSString.class] ? json[@"id"] : @"custom";
|
|
t.name = [json[@"name"] isKindOfClass:NSString.class] ? json[@"name"] : t.skinId;
|
|
t.keyboardBackground = [self.class colorFromHexString:json[@"background"] defaultColor:[self.class defaultTheme].keyboardBackground];
|
|
t.keyBackground = [self.class colorFromHexString:json[@"key_bg"] defaultColor:[self.class defaultTheme].keyBackground];
|
|
t.keyTextColor = [self.class colorFromHexString:json[@"key_text"] defaultColor:[self.class defaultTheme].keyTextColor];
|
|
t.keyHighlightBackground = [self.class colorFromHexString:json[@"key_highlight"] defaultColor:[self.class defaultTheme].keyHighlightBackground];
|
|
t.accentColor = [self.class colorFromHexString:json[@"accent"] defaultColor:[self.class defaultTheme].accentColor];
|
|
return [self applyTheme:t];
|
|
}
|
|
|
|
- (BOOL)applyTheme:(KBSkinTheme *)theme {
|
|
if (!theme) return NO;
|
|
if ([self p_saveToKeychain:theme]) {
|
|
self.current = theme;
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:KBSkinDidChangeNotification object:nil];
|
|
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)KBDarwinSkinChanged, NULL, NULL, true);
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (void)resetToDefault {
|
|
[self applyTheme:[self.class defaultTheme]];
|
|
}
|
|
|
|
- (BOOL)applyImageSkinWithData:(NSData *)imageData skinId:(NSString *)skinId name:(NSString *)name {
|
|
if (imageData.length == 0) return NO;
|
|
// 构造新主题,继承当前配色作为按键/强调色的默认值
|
|
KBSkinTheme *base = self.current ?: [self.class defaultTheme];
|
|
KBSkinTheme *t = [KBSkinTheme new];
|
|
t.skinId = skinId ?: @"image";
|
|
t.name = name ?: t.skinId;
|
|
t.keyboardBackground = base.keyboardBackground ?: [self.class defaultTheme].keyboardBackground;
|
|
t.keyBackground = base.keyBackground ?: [self.class defaultTheme].keyBackground;
|
|
t.keyTextColor = base.keyTextColor ?: [self.class defaultTheme].keyTextColor;
|
|
t.keyHighlightBackground = base.keyHighlightBackground ?: [self.class defaultTheme].keyHighlightBackground;
|
|
t.accentColor = base.accentColor ?: [self.class defaultTheme].accentColor;
|
|
t.backgroundImageData = imageData;
|
|
return [self applyTheme:t];
|
|
}
|
|
|
|
- (UIImage *)currentBackgroundImage {
|
|
NSData *d = self.current.backgroundImageData;
|
|
if (d.length == 0) return nil;
|
|
return [UIImage imageWithData:d scale:[UIScreen mainScreen].scale] ?: nil;
|
|
}
|
|
|
|
+ (UIColor *)colorFromHexString:(NSString *)hex defaultColor:(UIColor *)fallback {
|
|
if (![hex isKindOfClass:NSString.class] || hex.length == 0) return fallback;
|
|
NSString *s = [[hex stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString];
|
|
if ([s hasPrefix:@"#"]) s = [s substringFromIndex:1];
|
|
unsigned long long v = 0; NSScanner *scanner = [NSScanner scannerWithString:s];
|
|
if (![scanner scanHexLongLong:&v]) return fallback;
|
|
if (s.length == 6) { // RRGGBB
|
|
CGFloat r = ((v >> 16) & 0xFF) / 255.0;
|
|
CGFloat g = ((v >> 8) & 0xFF) / 255.0;
|
|
CGFloat b = (v & 0xFF) / 255.0;
|
|
return [UIColor colorWithRed:r green:g blue:b alpha:1.0];
|
|
} else if (s.length == 8) { // RRGGBBAA
|
|
CGFloat r = ((v >> 24) & 0xFF) / 255.0;
|
|
CGFloat g = ((v >> 16) & 0xFF) / 255.0;
|
|
CGFloat b = ((v >> 8) & 0xFF) / 255.0;
|
|
CGFloat a = (v & 0xFF) / 255.0;
|
|
return [UIColor colorWithRed:r green:g blue:b alpha:a];
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
#pragma mark - Defaults
|
|
|
|
+ (KBSkinTheme *)defaultTheme {
|
|
KBSkinTheme *t = [KBSkinTheme new];
|
|
t.skinId = @"default";
|
|
t.name = @"Default";
|
|
t.keyboardBackground = [UIColor colorWithWhite:0.95 alpha:1.0];
|
|
t.keyBackground = UIColor.whiteColor;
|
|
t.keyTextColor = UIColor.blackColor;
|
|
t.keyHighlightBackground = [UIColor colorWithWhite:0.85 alpha:1.0];
|
|
t.accentColor = [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
|
|
t.backgroundImageData = nil;
|
|
return t;
|
|
}
|
|
|
|
#pragma mark - Keychain
|
|
|
|
- (NSMutableDictionary *)baseKCQuery {
|
|
NSMutableDictionary *q = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
|
(__bridge id)kSecAttrService: kKBSkinService,
|
|
(__bridge id)kSecAttrAccount: kKBSkinAccount } mutableCopy];
|
|
q[(__bridge id)kSecAttrAccessGroup] = KB_KEYCHAIN_ACCESS_GROUP;
|
|
return q;
|
|
}
|
|
|
|
- (BOOL)p_saveToKeychain:(KBSkinTheme *)theme {
|
|
NSError *err = nil;
|
|
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:theme requiringSecureCoding:YES error:&err];
|
|
if (err || data.length == 0) return NO;
|
|
NSMutableDictionary *q = [self baseKCQuery];
|
|
SecItemDelete((__bridge CFDictionaryRef)q);
|
|
q[(__bridge id)kSecValueData] = data;
|
|
q[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
|
OSStatus st = SecItemAdd((__bridge CFDictionaryRef)q, NULL);
|
|
return (st == errSecSuccess);
|
|
}
|
|
|
|
- (KBSkinTheme *)p_loadFromKeychain {
|
|
NSMutableDictionary *q = [self baseKCQuery];
|
|
q[(__bridge id)kSecReturnData] = @YES;
|
|
q[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
|
CFTypeRef dataRef = NULL; OSStatus st = SecItemCopyMatching((__bridge CFDictionaryRef)q, &dataRef);
|
|
if (st != errSecSuccess || !dataRef) return nil;
|
|
NSData *data = (__bridge_transfer NSData *)dataRef;
|
|
if (data.length == 0) return nil;
|
|
@try {
|
|
KBSkinTheme *t = [NSKeyedUnarchiver unarchivedObjectOfClass:KBSkinTheme.class fromData:data error:NULL];
|
|
return t;
|
|
} @catch (__unused NSException *e) { return nil; }
|
|
}
|
|
|
|
- (void)p_reloadFromKeychainAndBroadcast:(BOOL)broadcast {
|
|
KBSkinTheme *t = [self p_loadFromKeychain] ?: [self.class defaultTheme];
|
|
self.current = t;
|
|
if (broadcast) {
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:KBSkinDidChangeNotification object:nil];
|
|
}
|
|
}
|
|
|
|
@end
|