// // 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 *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 *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//icons/.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