This commit is contained in:
2025-11-18 20:53:47 +08:00
parent 254e65906a
commit 3dcc4932c3
109 changed files with 1432 additions and 84 deletions

View File

@@ -19,6 +19,13 @@
/// APP Groups
#define AppGroup @"group.com.loveKey.nyx"
/// 皮肤图标加载模式:
/// 0 = 使用本地 Assets 图片名key_icons 的 value 写成图片名,例如 "kb_q_melon"
/// 1 = 使用远程 URL 下载到 App Group 目录,再由键盘扩展从文件系统加载
#ifndef KB_SKIN_ICON_USE_REMOTE
#define KB_SKIN_ICON_USE_REMOTE 0
#endif
// 基础baseUrl
#ifndef KB_BASE_URL
@@ -84,4 +91,3 @@ static inline CGFloat KBFit(CGFloat designValue) {
#ifndef KBWeakSelf
#define KBWeakSelf __weak __typeof(self) weakSelf = self;
#endif

View File

@@ -26,6 +26,10 @@ extern NSString * const KBDarwinSkinChanged; // cross-process
@property (nonatomic, strong) UIColor *accentColor; // function view accents
/// 可选:键盘背景图片的 PNG/JPEG 数据(若存在,优先显示图片)
@property (nonatomic, strong, nullable) NSData *backgroundImageData;
/// 可选:需要隐藏文字的按键标识集合(例如 @"letter_q" @"space" 等)
@property (nonatomic, strong, nullable) NSArray<NSString *> *hiddenKeyTextIdentifiers;
/// 可选:按键图标映射(按键标识 -> 图标名或路径),具体加载策略由上层决定
@property (nonatomic, strong, nullable) NSDictionary<NSString *, NSString *> *keyIconMap;
@end
/// Shared skin manager (Keychain Sharing based)
@@ -50,6 +54,23 @@ extern NSString * const KBDarwinSkinChanged; // cross-process
/// 当前背景图片(若存在)
- (nullable UIImage *)currentBackgroundImage;
/// 当前主题下,指定按键标识的文字是否应被隐藏(例如图标里已包含字母)
- (BOOL)shouldHideKeyTextForIdentifier:(nullable NSString *)identifier;
/// 当前主题下,指定按键标识对应的图标名(可作为 imageName 或文件相对路径),若未配置则为 nil
- (nullable NSString *)iconNameForKeyIdentifier:(nullable NSString *)identifier;
/// 当前主题下,返回指定按键的图标图片。
/// 若 keyIconMap 中的 value 不包含 "/",则按本地 Assets 名称加载;
/// 若包含 "/",则视为相对 App Group 根目录的文件路径,从文件系统加载。
- (nullable UIImage *)iconImageForKeyIdentifier:(nullable NSString *)identifier;
/// 带大小写变体的按键图标获取接口。
/// @param identifier 逻辑按键标识(如 @"letter_q" @"space"
/// @param caseVariant 0=无变体/非字母1=小写2=大写(与 KBKeyCaseVariant 对应)
- (nullable UIImage *)iconImageForKeyIdentifier:(nullable NSString *)identifier
caseVariant:(NSInteger)caseVariant;
/// Parse a hex color string like "#RRGGBB"/"#RRGGBBAA"
+ (UIColor *)colorFromHexString:(NSString *)hex defaultColor:(UIColor *)fallback;

View File

@@ -27,6 +27,12 @@ static NSString * const kKBSkinAccount = @"current"; // Keychain ac
if (self.backgroundImageData) {
[coder encodeObject:self.backgroundImageData forKey:@"backgroundImageData"];
}
if (self.hiddenKeyTextIdentifiers.count > 0) {
[coder encodeObject:self.hiddenKeyTextIdentifiers forKey:@"hiddenKeyTextIdentifiers"];
}
if (self.keyIconMap.count > 0) {
[coder encodeObject:self.keyIconMap forKey:@"keyIconMap"];
}
}
- (instancetype)initWithCoder:(NSCoder *)coder {
@@ -39,6 +45,9 @@ static NSString * const kKBSkinAccount = @"current"; // Keychain ac
_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"];
//
_hiddenKeyTextIdentifiers = [coder decodeObjectOfClass:NSArray.class forKey:@"hiddenKeyTextIdentifiers"];
_keyIconMap = [coder decodeObjectOfClass:NSDictionary.class forKey:@"keyIconMap"];
}
return self;
}
@@ -91,6 +100,16 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
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];
// hidden_keys
id hidden = json[@"hidden_keys"];
if ([hidden isKindOfClass:NSArray.class]) {
t.hiddenKeyTextIdentifiers = hidden;
}
// key_icons -> /
id icons = json[@"key_icons"];
if ([icons isKindOfClass:NSDictionary.class]) {
t.keyIconMap = icons;
}
return [self applyTheme:t];
}
@@ -121,6 +140,9 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
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.hiddenKeyTextIdentifiers = base.hiddenKeyTextIdentifiers;
t.keyIconMap = base.keyIconMap;
t.backgroundImageData = imageData;
return [self applyTheme:t];
}
@@ -131,6 +153,103 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
return [UIImage imageWithData:d scale:[UIScreen mainScreen].scale] ?: nil;
}
- (BOOL)shouldHideKeyTextForIdentifier:(NSString *)identifier {
if (identifier.length == 0) return NO;
NSArray<NSString *> *list = self.current.hiddenKeyTextIdentifiers;
if (list.count == 0) return NO;
// 线 NSSet
for (NSString *s in list) {
if ([s isKindOfClass:NSString.class] && [s isEqualToString:identifier]) {
return YES;
}
}
return NO;
}
- (NSString *)iconNameForKeyIdentifier:(NSString *)identifier {
if (identifier.length == 0) return nil;
NSDictionary<NSString *, NSString *> *map = self.current.keyIconMap;
if (map.count == 0) return nil;
NSString *name = map[identifier];
if (![name isKindOfClass:NSString.class] || name.length == 0) return nil;
return name;
}
- (UIImage *)iconImageForKeyIdentifier:(NSString *)identifier {
return [self iconImageForKeyIdentifier:identifier caseVariant:0];
}
- (UIImage *)iconImageForKeyIdentifier:(NSString *)identifier caseVariant:(NSInteger)caseVariant {
NSDictionary<NSString *, NSString *> *map = self.current.keyIconMap;
NSString *value = nil;
if (identifier.length > 0 && map.count > 0) {
// 1) letter_q_upper / letter_q_lower
if (caseVariant == 2) { // upper
NSString *keyUpper = [identifier stringByAppendingString:@"_upper"];
NSString *candidate = map[keyUpper];
if ([candidate isKindOfClass:NSString.class] && candidate.length > 0) {
value = candidate;
}
} else if (caseVariant == 1) { // lower
NSString *keyLower = [identifier stringByAppendingString:@"_lower"];
NSString *candidate = map[keyLower];
if ([candidate isKindOfClass:NSString.class] && candidate.length > 0) {
value = candidate;
}
}
// 2) 退 idletter_q
if (value.length == 0) {
NSString *candidate = map[identifier];
if ([candidate isKindOfClass:NSString.class] && candidate.length > 0) {
value = candidate;
}
}
}
// keyIconMap value
if (value.length > 0) {
if ([value containsString:@"/"]) {
// App Group
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
if (!containerURL) return nil;
NSString *fullPath = [[containerURL.path stringByAppendingPathComponent:value] stringByStandardizingPath];
if (![[NSFileManager defaultManager] fileExistsAtPath:fullPath]) return nil;
return [UIImage imageWithContentsOfFile:fullPath];
}
// Assets
return [UIImage imageNamed:value];
}
// keyIconMap App Group
// Skins/<skinId>/icons/(identifier[_upper/_lower]).png
NSString *skinId = self.current.skinId;
if (skinId.length == 0 || identifier.length == 0) return nil;
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
if (!containerURL) return nil;
//
if (caseVariant == 2) {
NSString *relativeUpper = [NSString stringWithFormat:@"Skins/%@/icons/%@_upper.png", skinId, identifier];
NSString *fullUpper = [[containerURL.path stringByAppendingPathComponent:relativeUpper] stringByStandardizingPath];
if ([[NSFileManager defaultManager] fileExistsAtPath:fullUpper]) {
return [UIImage imageWithContentsOfFile:fullUpper];
}
} else if (caseVariant == 1) {
NSString *relativeLower = [NSString stringWithFormat:@"Skins/%@/icons/%@_lower.png", skinId, identifier];
NSString *fullLower = [[containerURL.path stringByAppendingPathComponent:relativeLower] stringByStandardizingPath];
if ([[NSFileManager defaultManager] fileExistsAtPath:fullLower]) {
return [UIImage imageWithContentsOfFile:fullLower];
}
}
// 退 idSkins/<skinId>/icons/<identifier>.png
NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@.png", skinId, identifier];
NSString *fullPath = [[containerURL.path stringByAppendingPathComponent:relative] stringByStandardizingPath];
if (![[NSFileManager defaultManager] fileExistsAtPath:fullPath]) return nil;
return [UIImage imageWithContentsOfFile:fullPath];
}
+ (UIColor *)colorFromHexString:(NSString *)hex defaultColor:(UIColor *)fallback {
if (![hex isKindOfClass:NSString.class] || hex.length == 0) return fallback;
NSString *s = [[hex stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString];