添加键盘背景
This commit is contained in:
58
Shared/KBSkinManager.h
Normal file
58
Shared/KBSkinManager.h
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// KBSkinManager.h
|
||||
// App & Keyboard Extension shared skin/theme manager.
|
||||
//
|
||||
// Stores a lightweight theme (colors, identifiers) to shared keychain so
|
||||
// both targets see the same current skin. Cross-process updates are delivered
|
||||
// via Darwin notification. Intended for immediate reflection in extension.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString * const KBSkinDidChangeNotification; // in-process
|
||||
extern NSString * const KBDarwinSkinChanged; // cross-process
|
||||
|
||||
/// Simple theme model (colors only; assets can be added later via App Group)
|
||||
@interface KBSkinTheme : NSObject <NSSecureCoding>
|
||||
@property (nonatomic, copy) NSString *skinId; // e.g. "mint"
|
||||
@property (nonatomic, copy) NSString *name; // display name
|
||||
@property (nonatomic, strong) UIColor *keyboardBackground;
|
||||
@property (nonatomic, strong) UIColor *keyBackground;
|
||||
@property (nonatomic, strong) UIColor *keyTextColor;
|
||||
@property (nonatomic, strong) UIColor *keyHighlightBackground; // selected/highlighted
|
||||
@property (nonatomic, strong) UIColor *accentColor; // function view accents
|
||||
/// 可选:键盘背景图片的 PNG/JPEG 数据(若存在,优先显示图片)
|
||||
@property (nonatomic, strong, nullable) NSData *backgroundImageData;
|
||||
@end
|
||||
|
||||
/// Shared skin manager (Keychain Sharing based)
|
||||
@interface KBSkinManager : NSObject
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
@property (atomic, strong, readonly) KBSkinTheme *current; // never nil (fallback to default)
|
||||
|
||||
/// Save theme from JSON dictionary (keys: id, name, background, key_bg, key_text, key_highlight, accent)
|
||||
- (BOOL)applyThemeFromJSON:(NSDictionary *)json;
|
||||
|
||||
/// Save explicit theme
|
||||
- (BOOL)applyTheme:(KBSkinTheme *)theme;
|
||||
|
||||
/// Reset to default theme
|
||||
- (void)resetToDefault;
|
||||
|
||||
/// 直接应用图片皮肤(使用 JPEG/PNG 数据)。建议大小 < 512KB。
|
||||
- (BOOL)applyImageSkinWithData:(NSData *)imageData skinId:(NSString *)skinId name:(NSString *)name;
|
||||
|
||||
/// 当前背景图片(若存在)
|
||||
- (nullable UIImage *)currentBackgroundImage;
|
||||
|
||||
/// Parse a hex color string like "#RRGGBB"/"#RRGGBBAA"
|
||||
+ (UIColor *)colorFromHexString:(NSString *)hex defaultColor:(UIColor *)fallback;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
214
Shared/KBSkinManager.m
Normal file
214
Shared/KBSkinManager.m
Normal file
@@ -0,0 +1,214 @@
|
||||
//
|
||||
// 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
|
||||
Reference in New Issue
Block a user