238 lines
12 KiB
Objective-C
238 lines
12 KiB
Objective-C
//
|
||
// KBSkinService.m
|
||
// keyBoard
|
||
//
|
||
|
||
#import "KBSkinService.h"
|
||
|
||
#import "KBSkinManager.h"
|
||
#import "KBConfig.h"
|
||
#import "KBKeyboardPermissionManager.h"
|
||
#import "KBNetworkManager.h"
|
||
#import "KBHUD.h"
|
||
|
||
@implementation KBSkinService
|
||
|
||
+ (instancetype)shared {
|
||
static KBSkinService *s; static dispatch_once_t onceToken;
|
||
dispatch_once(&onceToken, ^{ s = [KBSkinService new]; });
|
||
return s;
|
||
}
|
||
|
||
- (void)applySkinWithJSON:(NSDictionary *)skinJSON
|
||
fromViewController:(UIViewController *)presenting
|
||
completion:(KBSkinApplyCompletion)completion {
|
||
if (skinJSON.count == 0) {
|
||
if (completion) completion(NO);
|
||
return;
|
||
}
|
||
|
||
// 1. 点击应用皮肤时,检查键盘启用 & 完全访问状态,并尽量给出友好提示。
|
||
KBKeyboardPermissionManager *perm = [KBKeyboardPermissionManager shared];
|
||
BOOL enabled = [perm isKeyboardEnabled];
|
||
KBFARecord fa = [perm lastKnownFullAccess];
|
||
BOOL hasFullAccess = (fa == KBFARecordGranted);
|
||
|
||
if (!enabled || !hasFullAccess) {
|
||
// 引导页(内部有自己的展示策略,避免过度打扰)
|
||
[perm presentPermissionIfNeededFrom:presenting];
|
||
|
||
// 简单提示:皮肤可以应用,但未开启完全访问时扩展无法读取 App Group 中的图片。
|
||
[KBHUD showInfo:KBLocalized(@"皮肤已应用,键盘需开启“允许完全访问”后才能显示图片")];
|
||
}
|
||
|
||
#if KB_SKIN_ICON_USE_REMOTE
|
||
[self kb_applySkinUsingRemoteIcons:skinJSON completion:completion];
|
||
#else
|
||
[self kb_applySkinUsingLocalIcons:skinJSON completion:completion];
|
||
#endif
|
||
}
|
||
|
||
#pragma mark - Internal helpers
|
||
|
||
/// 本地模式:key_icons 的 value 为 Assets 名称(主 App bundle 内),导出到 App Group 后再写入主题。
|
||
- (void)kb_applySkinUsingLocalIcons:(NSDictionary *)skin completion:(KBSkinApplyCompletion)completion {
|
||
NSString *skinId = skin[@"id"] ?: @"local";
|
||
NSString *name = skin[@"name"] ?: skinId;
|
||
|
||
NSDictionary *iconNames = [skin[@"key_icons"] isKindOfClass:NSDictionary.class] ? skin[@"key_icons"] : @{};
|
||
NSMutableDictionary<NSString *, NSString *> *iconPathMap = [NSMutableDictionary dictionary];
|
||
|
||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||
if (containerURL && iconNames.count > 0) {
|
||
NSString *skinsRoot = [containerURL.path stringByAppendingPathComponent:@"Skins"];
|
||
NSString *skinRoot = [skinsRoot stringByAppendingPathComponent:skinId];
|
||
NSString *iconsDir = [skinRoot stringByAppendingPathComponent:@"icons"];
|
||
[[NSFileManager defaultManager] createDirectoryAtPath:iconsDir
|
||
withIntermediateDirectories:YES
|
||
attributes:nil
|
||
error:NULL];
|
||
|
||
[iconNames enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, NSString *imageName, BOOL *stop) {
|
||
if (![imageName isKindOfClass:NSString.class] || imageName.length == 0) return;
|
||
UIImage *img = [UIImage imageNamed:imageName];
|
||
if (!img) return;
|
||
NSData *data = UIImagePNGRepresentation(img);
|
||
if (data.length == 0) return;
|
||
NSString *fileName = [NSString stringWithFormat:@"%@.png", identifier];
|
||
NSString *fullPath = [iconsDir stringByAppendingPathComponent:fileName];
|
||
if ([data writeToFile:fullPath atomically:YES]) {
|
||
NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@", skinId, fileName];
|
||
iconPathMap[identifier] = relative;
|
||
}
|
||
}];
|
||
}
|
||
|
||
NSMutableDictionary *themeJSON = [skin mutableCopy];
|
||
if (iconPathMap.count > 0) {
|
||
themeJSON[@"key_icons"] = iconPathMap.copy; // value 改为 App Group 相对路径
|
||
}
|
||
|
||
// 先应用颜色 / hidden_keys / key_icons(此时为相对路径,扩展从 App Group 读取)
|
||
BOOL themeOK = [[KBSkinManager shared] applyThemeFromJSON:themeJSON];
|
||
|
||
// 再处理背景图片(background_image),与原逻辑一致
|
||
NSString *bgURL = skin[@"background_image"] ?: @"";
|
||
if (bgURL.length == 0) {
|
||
if (completion) completion(themeOK);
|
||
if (themeOK) {
|
||
[KBHUD showInfo:KBLocalized(@"已应用皮肤(无背景图)")];
|
||
} else {
|
||
[KBHUD showInfo:KBLocalized(@"应用皮肤失败")];
|
||
}
|
||
return;
|
||
}
|
||
|
||
[[KBNetworkManager shared] GET:bgURL parameters:nil headers:nil completion:^(id jsonOrData, NSURLResponse *response, NSError *error) {
|
||
NSData *data = ([jsonOrData isKindOfClass:NSData.class] ? (NSData *)jsonOrData : nil);
|
||
// 压缩尺寸,避免 Keychain 过大
|
||
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;
|
||
}
|
||
}
|
||
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:skinId name:name]
|
||
: themeOK;
|
||
if (completion) completion(ok);
|
||
[KBHUD showInfo:(ok ? KBLocalized(@"已应用,切到键盘查看") : KBLocalized(@"应用皮肤失败"))];
|
||
});
|
||
}];
|
||
}
|
||
|
||
/// 远程模式:key_icons 的 value 为 URL,需要下载到 App Group,写入主题后再应用。
|
||
- (void)kb_applySkinUsingRemoteIcons:(NSDictionary *)skin completion:(KBSkinApplyCompletion)completion {
|
||
NSString *skinId = skin[@"id"] ?: @"remote";
|
||
NSString *name = skin[@"name"] ?: skinId;
|
||
NSString *bgURL = skin[@"background_image"] ?: @"";
|
||
NSDictionary *iconURLs = [skin[@"key_icons"] isKindOfClass:NSDictionary.class] ? skin[@"key_icons"] : @{};
|
||
|
||
dispatch_group_t group = dispatch_group_create();
|
||
__block NSData *bgData = nil;
|
||
__block NSMutableDictionary<NSString *, NSString *> *iconPathMap = [NSMutableDictionary dictionary];
|
||
|
||
// 背景图
|
||
if (bgURL.length > 0) {
|
||
dispatch_group_enter(group);
|
||
[[KBNetworkManager shared] GET:bgURL parameters:nil headers:nil completion:^(id jsonOrData, NSURLResponse *response, NSError *error) {
|
||
NSData *data = ([jsonOrData isKindOfClass:NSData.class] ? (NSData *)jsonOrData : nil);
|
||
if (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;
|
||
}
|
||
}
|
||
bgData = data;
|
||
dispatch_group_leave(group);
|
||
}];
|
||
}
|
||
|
||
// 图标下载到 App Group:Skins/<skinId>/icons/<identifier>.png
|
||
[iconURLs enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, NSString *urlString, BOOL *stop) {
|
||
if (![urlString isKindOfClass:NSString.class] || urlString.length == 0) return;
|
||
dispatch_group_enter(group);
|
||
[[KBNetworkManager shared] GET:urlString parameters:nil headers:nil completion:^(id jsonOrData, NSURLResponse *response, NSError *error) {
|
||
NSData *data = ([jsonOrData isKindOfClass:NSData.class] ? (NSData *)jsonOrData : nil);
|
||
if (data.length > 0) {
|
||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||
if (containerURL) {
|
||
NSString *skinsRoot = [containerURL.path stringByAppendingPathComponent:@"Skins"];
|
||
NSString *skinRoot = [skinsRoot stringByAppendingPathComponent:skinId];
|
||
NSString *iconsDir = [skinRoot stringByAppendingPathComponent:@"icons"];
|
||
[[NSFileManager defaultManager] createDirectoryAtPath:iconsDir
|
||
withIntermediateDirectories:YES
|
||
attributes:nil
|
||
error:NULL];
|
||
NSString *fileName = [NSString stringWithFormat:@"%@.png", identifier];
|
||
NSString *fullPath = [iconsDir stringByAppendingPathComponent:fileName];
|
||
if ([data writeToFile:fullPath atomically:YES]) {
|
||
NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@", skinId, fileName];
|
||
iconPathMap[identifier] = relative;
|
||
}
|
||
}
|
||
}
|
||
dispatch_group_leave(group);
|
||
}];
|
||
}];
|
||
|
||
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
||
NSMutableDictionary *themeJSON = [skin mutableCopy];
|
||
themeJSON[@"key_icons"] = iconPathMap.copy; // value 改为 App Group 相对路径
|
||
|
||
BOOL themeOK = [[KBSkinManager shared] applyThemeFromJSON:themeJSON];
|
||
BOOL ok = themeOK;
|
||
if (bgData.length > 0) {
|
||
ok = [[KBSkinManager shared] applyImageSkinWithData:bgData skinId:skinId name:name];
|
||
}
|
||
if (completion) completion(ok);
|
||
[KBHUD showInfo:(ok ? KBLocalized(@"已应用,切到键盘查看") : KBLocalized(@"应用皮肤失败"))];
|
||
});
|
||
}
|
||
|
||
@end
|
||
|