Files
keyboard/CustomKeyboard/KeyboardViewControllerHelp/KeyboardViewController+Theme.m

370 lines
15 KiB
Mathematica
Raw Normal View History

2026-02-24 13:38:51 +08:00
//
// KeyboardViewController+Theme.m
// CustomKeyboard
//
// Created by Codex on 2026/02/22.
//
#import "KeyboardViewController+Private.h"
#import "KBFunctionView.h"
#import "KBKeyBoardMainView.h"
#import "KBSkinInstallBridge.h"
#import "KBSkinManager.h"
#import "UIImage+KBColor.h"
#import <QuartzCore/QuartzCore.h>
static NSString *const kKBDefaultSkinIdLight = @"normal_them";
static NSString *const kKBDefaultSkinZipNameLight = @"normal_them";
static NSString *const kKBDefaultSkinIdDark = @"normal_hei_them";
static NSString *const kKBDefaultSkinZipNameDark = @"normal_hei_them";
// 使 static kb_consumePendingShopSkin
@interface KeyboardViewController (KBSkinShopBridge)
- (void)kb_consumePendingShopSkin;
@end
static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
void *observer, CFStringRef name,
const void *object,
CFDictionaryRef userInfo) {
KeyboardViewController *strongSelf =
(__bridge KeyboardViewController *)observer;
if (!strongSelf) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
if ([strongSelf respondsToSelector:@selector(kb_consumePendingShopSkin)]) {
[strongSelf kb_consumePendingShopSkin];
}
});
}
@implementation KeyboardViewController (Theme)
- (void)kb_registerDarwinSkinInstallObserver {
CFNotificationCenterAddObserver(
CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self), KBSkinInstallNotificationCallback,
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification, NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
}
- (void)kb_unregisterDarwinSkinInstallObserver {
CFNotificationCenterRemoveObserver(
CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification, NULL);
}
- (void)kb_applyTheme {
@autoreleasepool {
KBSkinTheme *t = [KBSkinManager shared].current;
UIImage *img = nil;
BOOL isDefaultTheme = [self kb_isDefaultKeyboardTheme:t];
BOOL isDarkMode = [self kb_isDarkModeActive];
NSString *skinId = t.skinId ?: @"";
NSString *themeKey =
[NSString stringWithFormat:@"%@|default=%d|dark=%d", skinId,
isDefaultTheme, isDarkMode];
BOOL themeChanged =
(self.kb_lastAppliedThemeKey.length == 0 ||
![self.kb_lastAppliedThemeKey isEqualToString:themeKey]);
if (themeChanged) {
self.kb_lastAppliedThemeKey = themeKey;
}
CGSize size = self.bgImageView.bounds.size;
if (isDefaultTheme) {
if (isDarkMode) {
// 使使
//
img = nil;
self.bgImageView.image = nil;
[self.kb_defaultGradientLayer removeFromSuperlayer];
self.kb_defaultGradientLayer = nil;
// 使
if (@available(iOS 13.0, *)) {
// iOS 使 (RGB: 44, 44, 46 in sRGB, #2C2C2E)
// 使
UIColor *kbBgColor =
[UIColor colorWithDynamicProvider:^UIColor *_Nonnull(
UITraitCollection *_Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle ==
UIUserInterfaceStyleDark) {
//
return [UIColor colorWithRed:43.0 / 255.0
green:43.0 / 255.0
blue:43.0 / 255.0
alpha:1.0];
} else {
return [UIColor colorWithRed:209.0 / 255.0
green:211.0 / 255.0
blue:219.0 / 255.0
alpha:1.0];
}
}];
self.contentView.backgroundColor = kbBgColor;
self.bgImageView.backgroundColor = kbBgColor;
} else {
UIColor *darkColor = [UIColor colorWithRed:43.0 / 255.0
green:43.0 / 255.0
blue:43.0 / 255.0
alpha:1.0];
self.contentView.backgroundColor = darkColor;
self.bgImageView.backgroundColor = darkColor;
}
} else {
// 使
if (size.width <= 0 || size.height <= 0) {
[self.view layoutIfNeeded];
size = self.bgImageView.bounds.size;
}
if (size.width <= 0 || size.height <= 0) {
size = self.view.bounds.size;
}
if (size.width <= 0 || size.height <= 0) {
size = [UIScreen mainScreen].bounds.size;
}
UIColor *topColor = [UIColor colorWithHex:0xDEDFE4];
UIColor *bottomColor = [UIColor colorWithHex:0xD1D3DB];
UIColor *resolvedTopColor = topColor;
UIColor *resolvedBottomColor = bottomColor;
if (@available(iOS 13.0, *)) {
resolvedTopColor =
[topColor resolvedColorWithTraitCollection:self.traitCollection];
resolvedBottomColor =
[bottomColor resolvedColorWithTraitCollection:self.traitCollection];
}
CAGradientLayer *layer = self.kb_defaultGradientLayer;
if (!layer) {
layer = [CAGradientLayer layer];
layer.startPoint = CGPointMake(0.5, 0.0);
layer.endPoint = CGPointMake(0.5, 1.0);
[self.bgImageView.layer insertSublayer:layer atIndex:0];
self.kb_defaultGradientLayer = layer;
}
layer.colors =
@[ (id)resolvedTopColor.CGColor, (id)resolvedBottomColor.CGColor ];
layer.frame = (CGRect){CGPointZero, size};
img = nil;
self.bgImageView.image = nil;
self.contentView.backgroundColor = [UIColor clearColor];
self.bgImageView.backgroundColor = [UIColor clearColor];
}
NSLog(@"===");
} else {
// 使
self.contentView.backgroundColor = [UIColor clearColor];
self.bgImageView.backgroundColor = [UIColor clearColor];
[self.kb_defaultGradientLayer removeFromSuperlayer];
self.kb_defaultGradientLayer = nil;
img = [[KBSkinManager shared] currentBackgroundImage];
}
NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil));
[self kb_logSkinDiagnosticsWithTheme:t backgroundImage:img];
self.bgImageView.image = img;
2026-03-04 16:11:13 +08:00
// 使 skinId
if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
2026-02-24 13:38:51 +08:00
// 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
}
// 访 self.functionView
KBFunctionView *functionView = [self kb_functionViewIfCreated];
2026-03-04 16:11:13 +08:00
if (functionView &&
2026-02-24 13:38:51 +08:00
[functionView respondsToSelector:@selector(kb_applyTheme)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[functionView performSelector:@selector(kb_applyTheme)];
#pragma clang diagnostic pop
}
}
}
- (BOOL)kb_isDefaultKeyboardTheme:(KBSkinTheme *)theme {
NSString *skinId = theme.skinId ?: @"";
if (skinId.length == 0 || [skinId isEqualToString:@"default"]) {
return YES;
}
if ([skinId isEqualToString:kKBDefaultSkinIdLight]) {
return YES;
}
return [skinId isEqualToString:kKBDefaultSkinIdDark];
}
- (BOOL)kb_isDarkModeActive {
if (@available(iOS 13.0, *)) {
return self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark;
}
return NO;
}
- (NSString *)kb_defaultSkinIdForCurrentStyle {
return [self kb_isDarkModeActive] ? kKBDefaultSkinIdDark
: kKBDefaultSkinIdLight;
}
- (NSString *)kb_defaultSkinZipNameForCurrentStyle {
return [self kb_isDarkModeActive] ? kKBDefaultSkinZipNameDark
: kKBDefaultSkinZipNameLight;
}
- (UIImage *)kb_defaultGradientImageWithSize:(CGSize)size
topColor:(UIColor *)topColor
bottomColor:(UIColor *)bottomColor {
if (size.width <= 0 || size.height <= 0) {
return nil;
}
//
if (self.kb_cachedGradientImage &&
CGSizeEqualToSize(self.kb_cachedGradientSize, size)) {
return self.kb_cachedGradientImage;
}
UIColor *resolvedTopColor = topColor;
UIColor *resolvedBottomColor = bottomColor;
if (@available(iOS 13.0, *)) {
resolvedTopColor =
[topColor resolvedColorWithTraitCollection:self.traitCollection];
resolvedBottomColor =
[bottomColor resolvedColorWithTraitCollection:self.traitCollection];
}
CAGradientLayer *layer = [CAGradientLayer layer];
layer.frame = CGRectMake(0, 0, size.width, size.height);
layer.startPoint = CGPointMake(0.5, 0.0);
layer.endPoint = CGPointMake(0.5, 1.0);
layer.colors =
@[ (id)resolvedTopColor.CGColor, (id)resolvedBottomColor.CGColor ];
UIGraphicsBeginImageContextWithOptions(size, YES, 0);
[layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.kb_cachedGradientImage = image;
self.kb_cachedGradientSize = size;
return image;
}
- (void)kb_logSkinDiagnosticsWithTheme:(KBSkinTheme *)theme
backgroundImage:(UIImage *)image {
#if DEBUG
NSString *skinId = theme.skinId ?: @"";
NSString *name = theme.name ?: @"";
NSMutableArray<NSString *> *roots = [NSMutableArray array];
NSURL *containerURL = [[NSFileManager defaultManager]
containerURLForSecurityApplicationGroupIdentifier:AppGroup];
if (containerURL.path.length > 0) {
[roots addObject:containerURL.path];
}
NSString *cacheRoot = NSSearchPathForDirectoriesInDomains(
NSCachesDirectory, NSUserDomainMask, YES)
.firstObject;
if (cacheRoot.length > 0) {
[roots addObject:cacheRoot];
}
NSFileManager *fm = [NSFileManager defaultManager];
NSMutableArray<NSString *> *lines = [NSMutableArray array];
for (NSString *root in roots) {
NSString *iconsDir = [[root stringByAppendingPathComponent:@"Skins"]
stringByAppendingPathComponent:skinId];
iconsDir = [iconsDir stringByAppendingPathComponent:@"icons"];
BOOL isDir = NO;
BOOL exists = [fm fileExistsAtPath:iconsDir isDirectory:&isDir] && isDir;
NSArray *contents =
exists ? [fm contentsOfDirectoryAtPath:iconsDir error:nil] : nil;
NSUInteger count = contents.count;
BOOL hasQ =
exists &&
[fm fileExistsAtPath:[iconsDir
stringByAppendingPathComponent:@"key_q.png"]];
BOOL hasQUp =
exists && [fm fileExistsAtPath:[iconsDir stringByAppendingPathComponent:
@"key_q_up.png"]];
BOOL hasDel =
exists && [fm fileExistsAtPath:[iconsDir stringByAppendingPathComponent:
@"key_del.png"]];
BOOL hasShift =
exists &&
[fm fileExistsAtPath:[iconsDir
stringByAppendingPathComponent:@"key_up.png"]];
BOOL hasShiftUpper =
exists && [fm fileExistsAtPath:[iconsDir stringByAppendingPathComponent:
@"key_up_upper.png"]];
NSString *line = [NSString
stringWithFormat:@"root=%@ icons=%@ exist=%d count=%tu key_q=%d "
@"key_q_up=%d key_del=%d key_up=%d key_up_upper=%d",
root, iconsDir, exists, count, hasQ, hasQUp, hasDel,
hasShift, hasShiftUpper];
[lines addObject:line];
}
NSLog(@"[Keyboard] theme id=%@ name=%@ hasBg=%d\n%@", skinId, name,
(image != nil), [lines componentsJoinedByString:@"\n"]);
#endif
}
- (void)kb_consumePendingShopSkin {
KBWeakSelf [KBSkinInstallBridge
consumePendingRequestFromBundle:NSBundle.mainBundle
completion:^(BOOL success,
NSError *_Nullable error) {
if (!success) {
if (error) {
NSLog(@"[Keyboard] skin request failed: %@",
error);
[KBHUD
showInfo:
KBLocalized(
@"皮肤资源准备失败,请稍后再试")];
}
return;
}
[weakSelf kb_applyTheme];
[KBHUD showInfo:KBLocalized(
@"皮肤已更新,立即体验吧")];
}];
}
- (void)kb_applyDefaultSkinIfNeeded {
NSDictionary *pending = [KBSkinInstallBridge pendingRequestPayload];
if (pending.count > 0) {
return;
}
NSString *currentId = [KBSkinManager shared].current.skinId ?: @"";
BOOL isDefault =
(currentId.length == 0 || [currentId isEqualToString:@"default"]);
BOOL isLightDefault = [currentId isEqualToString:kKBDefaultSkinIdLight];
BOOL isDarkDefault = [currentId isEqualToString:kKBDefaultSkinIdDark];
if (!isDefault && !isLightDefault && !isDarkDefault) {
//
return;
}
NSString *targetId = [self kb_defaultSkinIdForCurrentStyle];
if (currentId.length > 0 && [currentId isEqualToString:targetId]) {
return;
}
NSError *applyError = nil;
if ([KBSkinInstallBridge applyInstalledSkinWithId:targetId error:&applyError]) {
return;
}
2026-03-02 09:19:06 +08:00
// zip App bundle
// App zip
if (applyError) {
NSLog(@"[Keyboard] default skin %@ not installed in AppGroup yet: %@",
targetId, applyError);
}
2026-02-24 13:38:51 +08:00
}
@end