// // KBSkinManager.m // #import "KBSkinManager.h" #import #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