From f43f94b94dca0615d5b86c23abd9e990e66b0237 Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Tue, 4 Nov 2025 21:01:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=94=AE=E7=9B=98=E8=83=8C?= =?UTF-8?q?=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CustomKeyboard/KeyboardViewController.m | 49 ++++++ CustomKeyboard/View/KBFunctionView.h | 3 + CustomKeyboard/View/KBFunctionView.m | 14 +- CustomKeyboard/View/KBKeyBoardMainView.h | 3 + CustomKeyboard/View/KBKeyBoardMainView.m | 19 +- CustomKeyboard/View/KBKeyButton.m | 15 +- CustomKeyboard/View/KBKeyboardView.m | 3 +- Shared/KBSkinManager.h | 58 ++++++ Shared/KBSkinManager.m | 214 +++++++++++++++++++++++ keyBoard.xcodeproj/project.pbxproj | 14 ++ keyBoard/Class/Home/VC/HomeVC.m | 8 +- keyBoard/Class/Home/VC/KBSkinCenterVC.h | 11 ++ keyBoard/Class/Home/VC/KBSkinCenterVC.m | 132 ++++++++++++++ 13 files changed, 528 insertions(+), 15 deletions(-) create mode 100644 Shared/KBSkinManager.h create mode 100644 Shared/KBSkinManager.m create mode 100644 keyBoard/Class/Home/VC/KBSkinCenterVC.h create mode 100644 keyBoard/Class/Home/VC/KBSkinCenterVC.m diff --git a/CustomKeyboard/KeyboardViewController.m b/CustomKeyboard/KeyboardViewController.m index 320d2bf..aa3670d 100644 --- a/CustomKeyboard/KeyboardViewController.m +++ b/CustomKeyboard/KeyboardViewController.m @@ -14,6 +14,7 @@ #import "Masonry.h" #import "KBAuthManager.h" #import "KBFullAccessManager.h" +#import "KBSkinManager.h" static CGFloat KEYBOARDHEIGHT = 256 + 20; @@ -22,6 +23,7 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20; @property (nonatomic, strong) KBKeyBoardMainView *keyBoardMainView; // 功能面板视图(点击工具栏第0个时显示) @property (nonatomic, strong) KBFunctionView *functionView; // 功能面板视图(点击工具栏第0个时显示) @property (nonatomic, strong) KBSettingView *settingView; // 设置页 +@property (nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层) @end @implementation KeyboardViewController @@ -40,12 +42,23 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20; __unused id token = [[NSNotificationCenter defaultCenter] addObserverForName:KBFullAccessChangedNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) { // 如需,可在此刷新与完全访问相关的 UI }]; + + // 皮肤变化时,立即应用 + __unused id token2 = [[NSNotificationCenter defaultCenter] addObserverForName:KBSkinDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) { + [self kb_applyTheme]; + }]; + [self kb_applyTheme]; } - (void)setupUI { // 固定键盘整体高度 [self.view.heightAnchor constraintEqualToConstant:KEYBOARDHEIGHT].active = YES; + // 背景图铺底 + [self.view addSubview:self.bgImageView]; + [self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.view); + }]; // 预置功能面板(默认隐藏),与键盘区域共享相同布局 self.functionView.hidden = YES; [self.view addSubview:self.functionView]; @@ -217,4 +230,40 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20; __unused typeof(weakSelf) selfStrong = weakSelf; }]; } + +#pragma mark - Theme + +- (void)kb_applyTheme { + KBSkinTheme *t = [KBSkinManager shared].current; + UIImage *img = [[KBSkinManager shared] currentBackgroundImage]; + self.bgImageView.image = img; + BOOL hasImg = (img != nil); + self.view.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground; + self.keyBoardMainView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground; + // 触发键区按主题重绘 + if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) { + // method declared in KBKeyBoardMainView.h + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [self.keyBoardMainView performSelector:@selector(kb_applyTheme)]; + #pragma clang diagnostic pop + } + if ([self.functionView respondsToSelector:@selector(kb_applyTheme)]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [self.functionView performSelector:@selector(kb_applyTheme)]; +#pragma clang diagnostic pop + } +} + +#pragma mark - Lazy + +- (UIImageView *)bgImageView { + if (!_bgImageView) { + _bgImageView = [[UIImageView alloc] init]; + _bgImageView.contentMode = UIViewContentModeScaleAspectFill; + _bgImageView.clipsToBounds = YES; + } + return _bgImageView; +} @end diff --git a/CustomKeyboard/View/KBFunctionView.h b/CustomKeyboard/View/KBFunctionView.h index ebe7247..958461a 100644 --- a/CustomKeyboard/View/KBFunctionView.h +++ b/CustomKeyboard/View/KBFunctionView.h @@ -33,6 +33,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong, readonly) UIButton *clearButton; // 右侧-清空 @property (nonatomic, strong, readonly) UIButton *sendButton; // 右侧-发送 +/// 应用当前皮肤(更新背景/强调色) +- (void)kb_applyTheme; + @end NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/View/KBFunctionView.m b/CustomKeyboard/View/KBFunctionView.m index 520ba9c..547b9db 100644 --- a/CustomKeyboard/View/KBFunctionView.m +++ b/CustomKeyboard/View/KBFunctionView.m @@ -14,6 +14,7 @@ #import #import "KBFullAccessGuideView.h" #import "KBFullAccessManager.h" +#import "KBSkinManager.h" static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId"; @@ -40,8 +41,8 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId"; - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { - // 整体绿色背景(接近截图效果,项目可自行替换素材) - self.backgroundColor = [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0]; + // 背景使用当前主题强调色 + [self kb_applyTheme]; [self setupUI]; [self reloadDemoData]; @@ -52,6 +53,15 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId"; return self; } +#pragma mark - Theme + +- (void)kb_applyTheme { + KBSkinManager *mgr = [KBSkinManager shared]; + UIColor *accent = mgr.current.accentColor ?: [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0]; + BOOL hasImg = ([mgr currentBackgroundImage] != nil); + self.backgroundColor = hasImg ? [accent colorWithAlphaComponent:0.65] : accent; +} + - (void)dealloc { [self stopPasteboardMonitor]; } diff --git a/CustomKeyboard/View/KBKeyBoardMainView.h b/CustomKeyboard/View/KBKeyBoardMainView.h index 9f50800..3ff4f46 100644 --- a/CustomKeyboard/View/KBKeyBoardMainView.h +++ b/CustomKeyboard/View/KBKeyBoardMainView.h @@ -28,6 +28,9 @@ NS_ASSUME_NONNULL_BEGIN @interface KBKeyBoardMainView : UIView @property (nonatomic, weak) id delegate; +/// 应用当前皮肤(会触发键区重载以应用按键颜色) +- (void)kb_applyTheme; + @end NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/View/KBKeyBoardMainView.m b/CustomKeyboard/View/KBKeyBoardMainView.m index fb41669..40acc9c 100644 --- a/CustomKeyboard/View/KBKeyBoardMainView.m +++ b/CustomKeyboard/View/KBKeyBoardMainView.m @@ -11,6 +11,7 @@ #import "KBFunctionView.h" #import "KBKey.h" #import "Masonry.h" +#import "KBSkinManager.h" @interface KBKeyBoardMainView () @property (nonatomic, strong) KBToolBar *topBar; @@ -21,7 +22,7 @@ - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { - self.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0]; + self.backgroundColor = [KBSkinManager shared].current.keyboardBackground; // 顶部栏 self.topBar = [[KBToolBar alloc] init]; self.topBar.delegate = self; @@ -31,7 +32,7 @@ make.top.equalTo(self.mas_top).offset(6); make.height.mas_equalTo(40); }]; - + // 键盘区域 self.keyboardView = [[KBKeyboardView alloc] init]; self.keyboardView.delegate = self; @@ -41,9 +42,9 @@ make.top.equalTo(self.topBar.mas_bottom).offset(4); make.bottom.equalTo(self.mas_bottom).offset(-4); }]; - + // 功能面板切换交由外部控制器处理;此处不直接创建/管理 - + } return self; } @@ -114,5 +115,15 @@ // 切换功能面板交由外部控制器处理(此处不再实现) // 设置页展示改由 KeyboardViewController 统一处理 +#pragma mark - Theme + +- (void)kb_applyTheme { + KBSkinManager *mgr = [KBSkinManager shared]; + BOOL hasImg = ([mgr currentBackgroundImage] != nil); + UIColor *bg = mgr.current.keyboardBackground; + self.backgroundColor = hasImg ? [UIColor clearColor] : bg; + self.keyboardView.backgroundColor = hasImg ? [UIColor clearColor] : bg; + [self.keyboardView reloadKeys]; +} @end diff --git a/CustomKeyboard/View/KBKeyButton.m b/CustomKeyboard/View/KBKeyButton.m index 085d3eb..4de0114 100644 --- a/CustomKeyboard/View/KBKeyButton.m +++ b/CustomKeyboard/View/KBKeyButton.m @@ -5,6 +5,7 @@ #import "KBKeyButton.h" #import "KBKey.h" +#import "KBSkinManager.h" @implementation KBKeyButton @@ -16,10 +17,11 @@ } - (void)applyDefaultStyle { - self.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold]; // 字体样式 - [self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; - [self setTitleColor:[UIColor blackColor] forState:UIControlStateHighlighted]; - self.backgroundColor = [UIColor whiteColor]; + self.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold]; + KBSkinTheme *t = [KBSkinManager shared].current; + [self setTitleColor:t.keyTextColor forState:UIControlStateNormal]; + [self setTitleColor:t.keyTextColor forState:UIControlStateHighlighted]; + self.backgroundColor = t.keyBackground; self.layer.cornerRadius = 6.0; // 圆角 self.layer.masksToBounds = NO; self.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor; // 阴影效果 @@ -46,10 +48,11 @@ - (void)refreshStateAppearance { // 选中态用于 Shift/CapsLock 等特殊按键的高亮显示 + KBSkinTheme *t = [KBSkinManager shared].current; if (self.isSelected) { - self.backgroundColor = [UIColor colorWithWhite:0.85 alpha:1.0]; + self.backgroundColor = t.keyHighlightBackground ?: t.keyBackground; } else { - self.backgroundColor = [UIColor whiteColor]; + self.backgroundColor = t.keyBackground; } } diff --git a/CustomKeyboard/View/KBKeyboardView.m b/CustomKeyboard/View/KBKeyboardView.m index bc1c48f..7fd0593 100644 --- a/CustomKeyboard/View/KBKeyboardView.m +++ b/CustomKeyboard/View/KBKeyboardView.m @@ -7,6 +7,7 @@ #import "KBKeyButton.h" #import "KBKey.h" #import "KBResponderUtils.h" // 封装的响应链工具 +#import "KBSkinManager.h" @interface KBKeyboardView () @property (nonatomic, strong) UIView *row1; @@ -22,7 +23,7 @@ - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { - self.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0]; + self.backgroundColor = [KBSkinManager shared].current.keyboardBackground; _layoutStyle = KBKeyboardLayoutStyleLetters; // 默认小写:与需求一致,初始不开启 Shift _shiftOn = NO; diff --git a/Shared/KBSkinManager.h b/Shared/KBSkinManager.h new file mode 100644 index 0000000..b82350a --- /dev/null +++ b/Shared/KBSkinManager.h @@ -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 +#import + +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 +@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 diff --git a/Shared/KBSkinManager.m b/Shared/KBSkinManager.m new file mode 100644 index 0000000..6497ff4 --- /dev/null +++ b/Shared/KBSkinManager.m @@ -0,0 +1,214 @@ +// +// 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 diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 71ca812..3cd1f21 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -8,6 +8,9 @@ /* Begin PBXBuildFile section */ 043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EAE12EAF940F0089C901 /* KBPermissionViewController.m */; }; + 0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */; }; + 0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; }; + 0459D1B82EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; }; 04A9FE0F2EB481100020DB6D /* KBHUD.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC97082EB31B14007BD342 /* KBHUD.m */; }; 04A9FE132EB4D0D20020DB6D /* KBFullAccessManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */; }; 04A9FE162EB873C80020DB6D /* UIViewController+Extension.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE152EB873C80020DB6D /* UIViewController+Extension.m */; }; @@ -87,6 +90,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0459D1B22EBA284C00F2D189 /* KBSkinCenterVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinCenterVC.h; sourceTree = ""; }; + 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinCenterVC.m; sourceTree = ""; }; + 0459D1B52EBA287900F2D189 /* KBSkinManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinManager.h; sourceTree = ""; }; + 0459D1B62EBA287900F2D189 /* KBSkinManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinManager.m; sourceTree = ""; }; 04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBResponderUtils.h; sourceTree = ""; }; 04A9FE102EB4D0D20020DB6D /* KBFullAccessManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFullAccessManager.h; sourceTree = ""; }; 04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFullAccessManager.m; sourceTree = ""; }; @@ -329,6 +336,8 @@ 04FC95CE2EB1E7A1007BD342 /* HomeVC.m */, A1B2D7002EB8C00100000001 /* KBLangTestVC.h */, A1B2D7012EB8C00100000001 /* KBLangTestVC.m */, + 0459D1B22EBA284C00F2D189 /* KBSkinCenterVC.h */, + 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */, ); path = VC; sourceTree = ""; @@ -582,6 +591,8 @@ A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */, 04A9FE182EB892460020DB6D /* KBLocalizationManager.h */, 04A9FE192EB892460020DB6D /* KBLocalizationManager.m */, + 0459D1B52EBA287900F2D189 /* KBSkinManager.h */, + 0459D1B62EBA287900F2D189 /* KBSkinManager.m */, ); path = Shared; sourceTree = ""; @@ -817,6 +828,7 @@ 04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */, 04FC95732EB09570007BD342 /* KBFunctionBarView.m in Sources */, 04C6EAD82EAF870B0089C901 /* KeyboardViewController.m in Sources */, + 0459D1B82EBA287900F2D189 /* KBSkinManager.m in Sources */, 04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */, A1B2C3D42EB0A0A100000001 /* KBFunctionTagCell.m in Sources */, 04A9FE1A2EB892460020DB6D /* KBLocalizationManager.m in Sources */, @@ -860,10 +872,12 @@ A1B2D7022EB8C00100000001 /* KBLangTestVC.m in Sources */, 04C6EABF2EAF86530089C901 /* main.m in Sources */, 04FC95CC2EB1E780007BD342 /* BaseTabBarController.m in Sources */, + 0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */, 04FC95F42EB339C1007BD342 /* AppleSignInManager.m in Sources */, 04C6EAC12EAF86530089C901 /* ViewController.m in Sources */, A1B2C4002EB4A0A100000004 /* KBAuthManager.m in Sources */, A1B2C4212EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */, + 0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/keyBoard/Class/Home/VC/HomeVC.m b/keyBoard/Class/Home/VC/HomeVC.m index 346f4b4..b3b0812 100644 --- a/keyBoard/Class/Home/VC/HomeVC.m +++ b/keyBoard/Class/Home/VC/HomeVC.m @@ -38,7 +38,7 @@ self.tableView.tableHeaderView = header; [self.view addSubview:self.tableView]; - self.items = @[ KBLocalized(@"home_item_lang_test"), KBLocalized(@"home_item_keyboard_permission") ]; + self.items = @[ KBLocalized(@"home_item_lang_test"), KBLocalized(@"home_item_keyboard_permission"), @"皮肤中心" ]; dispatch_async(dispatch_get_main_queue(), ^{ [self.textView becomeFirstResponder]; }); @@ -50,7 +50,7 @@ - (void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; self.title = KBLocalized(@"home_title"); - self.items = @[ KBLocalized(@"home_item_lang_test"), KBLocalized(@"home_item_keyboard_permission") ]; + self.items = @[ KBLocalized(@"home_item_lang_test"), KBLocalized(@"home_item_keyboard_permission"), @"皮肤中心" ]; [self.tableView reloadData]; } @@ -85,6 +85,10 @@ // 键盘权限引导页 KBGuideVC *vc = [KBGuideVC new]; [self.navigationController pushViewController:vc animated:YES]; + } else if (indexPath.row == 2) { + // 皮肤中心 + UIViewController *vc = [NSClassFromString(@"KBSkinCenterVC") new]; + if (vc) { [self.navigationController pushViewController:vc animated:YES]; } } } diff --git a/keyBoard/Class/Home/VC/KBSkinCenterVC.h b/keyBoard/Class/Home/VC/KBSkinCenterVC.h new file mode 100644 index 0000000..6c8ecc2 --- /dev/null +++ b/keyBoard/Class/Home/VC/KBSkinCenterVC.h @@ -0,0 +1,11 @@ +// +// KBSkinCenterVC.h +// 简单的皮肤中心:展示两款示例皮肤,点击“下载并应用”后立即同步到键盘扩展。 +// + +#import + +@interface KBSkinCenterVC : UIViewController + +@end + diff --git a/keyBoard/Class/Home/VC/KBSkinCenterVC.m b/keyBoard/Class/Home/VC/KBSkinCenterVC.m new file mode 100644 index 0000000..5a097bd --- /dev/null +++ b/keyBoard/Class/Home/VC/KBSkinCenterVC.m @@ -0,0 +1,132 @@ +// +// KBSkinCenterVC.m +// + +#import "KBSkinCenterVC.h" +#import "Masonry.h" +#import "KBNetworkManager.h" +#import "KBSkinManager.h" +#import "KBHUD.h" + +@interface KBSkinCell : UITableViewCell +@property (nonatomic, strong) UIButton *applyBtn; +@end + +@implementation KBSkinCell +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { + if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { + self.selectionStyle = UITableViewCellSelectionStyleNone; + _applyBtn = [UIButton buttonWithType:UIButtonTypeSystem]; + [_applyBtn setTitle:@"下载并应用" forState:UIControlStateNormal]; + _applyBtn.layer.cornerRadius = 6; _applyBtn.layer.borderWidth = 1; + _applyBtn.layer.borderColor = [UIColor colorWithWhite:0.85 alpha:1].CGColor; + [self.contentView addSubview:_applyBtn]; + [_applyBtn mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.contentView).offset(-16); + make.centerY.equalTo(self.contentView); + make.width.mas_equalTo(110); + make.height.mas_equalTo(34); + }]; + } + return self; +} +@end + +@interface KBSkinCenterVC () +@property (nonatomic, strong) UITableView *tableView; +@property (nonatomic, copy) NSArray *skins; // id, name, img(url relative to KB_BASE_URL) +@end + +@implementation KBSkinCenterVC + +- (void)viewDidLoad { + [super viewDidLoad]; + self.title = @"皮肤中心"; + self.view.backgroundColor = [UIColor whiteColor]; + // 绝对 URL 的测试皮肤图片(无需 KB_BASE_URL)。 + // 说明:使用 picsum.photos 的固定 id,稳定可直接访问。 + self.skins = @[ + @{ @"id": @"aurora", @"name": @"极光", @"img": @"https://picsum.photos/id/1018/1600/900.jpg" }, + @{ @"id": @"alps", @"name": @"雪山", @"img": @"https://picsum.photos/id/1016/1600/900.jpg" }, + @{ @"id": @"lake", @"name": @"湖面", @"img": @"https://picsum.photos/id/1039/1600/900.jpg" }, + ]; + + self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleInsetGrouped]; + self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.tableView.delegate = self; self.tableView.dataSource = self; + [self.view addSubview:self.tableView]; +} + +#pragma mark - UITableView + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.skins.count; } + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *cid = @"skin.cell"; + KBSkinCell *cell = [tableView dequeueReusableCellWithIdentifier:cid]; + if (!cell) { cell = [[KBSkinCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cid]; } + NSDictionary *skin = self.skins[indexPath.row]; + cell.textLabel.text = skin[@"name"]; cell.detailTextLabel.text = skin[@"id"]; + [cell.applyBtn removeTarget:nil action:NULL forControlEvents:UIControlEventTouchUpInside]; + [cell.applyBtn addTarget:self action:@selector(onApplyBtn:) forControlEvents:UIControlEventTouchUpInside]; + cell.applyBtn.tag = indexPath.row; + return cell; +} + +- (void)onApplyBtn:(UIButton *)sender { + NSInteger idx = sender.tag; + if (idx < 0 || idx >= self.skins.count) return; + NSDictionary *skin = self.skins[idx]; + NSString *path = skin[@"img"] ?: @""; // 相对 KB_BASE_URL + + // 下载图片数据(非 JSON 将以 NSData 返回) + [[KBNetworkManager shared] GET:path parameters:nil headers:nil completion:^(id jsonOrData, NSURLResponse *response, NSError *error) { + NSData *data = ([jsonOrData isKindOfClass:NSData.class] ? (NSData *)jsonOrData : nil); + // 尝试压缩尺寸,避免 Keychain 过大:将宽度限制到 1500px + if (data && data.length > 0) { + UIImage *img = [UIImage imageWithData:data]; + if (img) { + CGFloat maxW = 1500.0; + if (img.size.width > maxW) { + CGFloat scale = maxW / img.size.width; + CGSize newSize = CGSizeMake(maxW, floor(img.size.height * scale)); + UIGraphicsBeginImageContextWithOptions(newSize, YES, 1.0); + [img drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; + UIImage *resized = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + img = resized ?: img; + } + data = UIImageJPEGRepresentation(img, 0.85) ?: data; // 压成 JPEG + } + } + dispatch_async(dispatch_get_main_queue(), ^{ + NSData *payload = data; + if (payload.length == 0) { + // 兜底:生成一张简单渐变图片 + CGSize size = CGSizeMake(1200, 600); + UIGraphicsBeginImageContextWithOptions(size, YES, 1.0); + CGContextRef ctx = UIGraphicsGetCurrentContext(); + UIColor *c1 = [UIColor colorWithRed:0.76 green:0.91 blue:0.86 alpha:1]; + UIColor *c2 = [UIColor colorWithRed:0.93 green:0.97 blue:0.91 alpha:1]; + if ([skin[@"id"] hasPrefix:@"dark"]) { + c1 = [UIColor colorWithRed:0.1 green:0.12 blue:0.16 alpha:1]; + c2 = [UIColor colorWithRed:0.22 green:0.24 blue:0.28 alpha:1]; + } + CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB(); + NSArray *colors = @[(__bridge id)c1.CGColor, (__bridge id)c2.CGColor]; + CGFloat locs[] = {0,1}; + CGGradientRef grad = CGGradientCreateWithColors(space, (__bridge CFArrayRef)colors, locs); + CGContextDrawLinearGradient(ctx, grad, CGPointZero, CGPointMake(size.width, size.height), 0); + CGGradientRelease(grad); CGColorSpaceRelease(space); + UIImage *img = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + payload = UIImageJPEGRepresentation(img, 0.9); + } + BOOL ok = (payload.length > 0) ? [[KBSkinManager shared] applyImageSkinWithData:payload skinId:skin[@"id"] name:skin[@"name"]] : NO; + [KBHUD showInfo:(ok ? @"已应用,切到键盘查看" : @"应用失败")]; + }); + }]; +} + +@end