3
This commit is contained in:
@@ -77,178 +77,25 @@
|
||||
/// - skinJSON.key_icons 的 value 填写 Zip 内的图标“短文件名”(不含路径,可不含扩展名),例如 "key_a"
|
||||
/// 应用时会被转换为相对 App Group 根目录的路径:Skins/<skinId>/icons/<shortName>.png
|
||||
- (void)kb_applySkinUsingRemoteIcons:(NSDictionary *)skin completion:(KBSkinApplyCompletion)completion {
|
||||
NSString *skinId = skin[@"id"] ?: @"remote";
|
||||
NSString *name = skin[@"name"] ?: skinId;
|
||||
NSString *zipURL = skin[@"zip_url"] ?: @""; // 新协议:远程 Zip 包地址
|
||||
|
||||
// key_icons 可选:
|
||||
// - 若后端提供 key_icons,则优先使用服务端映射;
|
||||
// - 若未提供,则回退到本地默认映射(KBSkinInstallBridge.defaultIconShortNames),这样后端只需返回 id/name/zip_url。
|
||||
NSDictionary *iconShortNames = nil;
|
||||
if ([skin[@"key_icons"] isKindOfClass:NSDictionary.class]) {
|
||||
iconShortNames = skin[@"key_icons"];
|
||||
} else {
|
||||
iconShortNames = [KBSkinInstallBridge defaultIconShortNames];
|
||||
}
|
||||
|
||||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||||
if (!containerURL) {
|
||||
if (completion) completion(NO);
|
||||
[KBHUD showInfo:KBLocalized(@"无法访问共享容器,应用皮肤失败")];
|
||||
return;
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
BOOL isDir = NO;
|
||||
BOOL hasIconsDir = [fm fileExistsAtPath:iconsDir isDirectory:&isDir] && isDir;
|
||||
NSArray *contents = hasIconsDir ? [fm contentsOfDirectoryAtPath:iconsDir error:NULL] : nil;
|
||||
BOOL hasCachedAssets = (contents.count > 0);
|
||||
|
||||
NSString *bgPath = [skinRoot stringByAppendingPathComponent:@"background.png"];
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
__block BOOL zipOK = YES;
|
||||
|
||||
#if __has_include(<SSZipArchive/SSZipArchive.h>)
|
||||
// 若本地尚未缓存该皮肤资源且提供了 zip_url,则通过网络下载并解压 Zip 包。
|
||||
if (!hasCachedAssets && zipURL.length > 0) {
|
||||
dispatch_group_enter(group);
|
||||
|
||||
void (^handleZipData)(NSData *) = ^(NSData *data) {
|
||||
if (data.length == 0) {
|
||||
zipOK = NO;
|
||||
dispatch_group_leave(group);
|
||||
return;
|
||||
}
|
||||
// 将 Zip 写入临时路径再解压
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:skinRoot
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:NULL];
|
||||
NSString *zipPath = [skinRoot stringByAppendingPathComponent:@"skin.zip"];
|
||||
if (![data writeToFile:zipPath atomically:YES]) {
|
||||
zipOK = NO;
|
||||
dispatch_group_leave(group);
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *unzipError = nil;
|
||||
BOOL ok = [SSZipArchive unzipFileAtPath:zipPath
|
||||
toDestination:skinRoot
|
||||
overwrite:YES
|
||||
password:nil
|
||||
error:&unzipError];
|
||||
[fm removeItemAtPath:zipPath error:nil];
|
||||
if (!ok || unzipError) {
|
||||
zipOK = NO;
|
||||
dispatch_group_leave(group);
|
||||
return;
|
||||
}
|
||||
|
||||
// 兼容“额外包一层目录”的压缩结构:
|
||||
// 若 Skins/<skinId>/icons 为空,但存在 Skins/<skinId>/<子目录>/icons,
|
||||
// 则将实际 icons 与 background.png 上移到预期位置。
|
||||
BOOL isDir2 = NO;
|
||||
NSArray *iconsContent = [fm contentsOfDirectoryAtPath:iconsDir error:NULL];
|
||||
BOOL iconsValid = ([fm fileExistsAtPath:iconsDir isDirectory:&isDir2] && isDir2 && iconsContent.count > 0);
|
||||
if (!iconsValid) {
|
||||
NSArray<NSString *> *subItems = [fm contentsOfDirectoryAtPath:skinRoot error:NULL];
|
||||
for (NSString *name in subItems) {
|
||||
if ([name isEqualToString:@"icons"] || [name isEqualToString:@"__MACOSX"]) continue;
|
||||
NSString *nestedRoot = [skinRoot stringByAppendingPathComponent:name];
|
||||
BOOL isDirNested = NO;
|
||||
if (![fm fileExistsAtPath:nestedRoot isDirectory:&isDirNested] || !isDirNested) continue;
|
||||
|
||||
NSString *nestedIcons = [nestedRoot stringByAppendingPathComponent:@"icons"];
|
||||
BOOL isDirNestedIcons = NO;
|
||||
if ([fm fileExistsAtPath:nestedIcons isDirectory:&isDirNestedIcons] && isDirNestedIcons) {
|
||||
NSArray *nestedFiles = [fm contentsOfDirectoryAtPath:nestedIcons error:NULL];
|
||||
if (nestedFiles.count > 0) {
|
||||
// 确保目标 icons 目录存在
|
||||
[fm createDirectoryAtPath:iconsDir
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:NULL];
|
||||
// 将 icons 下所有文件上移一层
|
||||
for (NSString *fn in nestedFiles) {
|
||||
NSString *from = [nestedIcons stringByAppendingPathComponent:fn];
|
||||
NSString *to = [iconsDir stringByAppendingPathComponent:fn];
|
||||
[fm removeItemAtPath:to error:nil];
|
||||
[fm moveItemAtPath:from toPath:to error:nil];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 background.png:若在子目录下存在,则上移到 skinRoot
|
||||
NSString *nestedBg = [nestedRoot stringByAppendingPathComponent:@"background.png"];
|
||||
if ([fm fileExistsAtPath:nestedBg]) {
|
||||
[fm removeItemAtPath:bgPath error:nil];
|
||||
[fm moveItemAtPath:nestedBg toPath:bgPath error:nil];
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
};
|
||||
|
||||
// 远程下载(http/https)
|
||||
[[KBNetworkManager shared] GET:zipURL parameters:nil headers:nil completion:^(id jsonOrData, NSURLResponse *response, NSError *error) {
|
||||
NSData *data = ([jsonOrData isKindOfClass:NSData.class] ? (NSData *)jsonOrData : nil);
|
||||
if (error || data.length == 0) {
|
||||
zipOK = NO;
|
||||
dispatch_group_leave(group);
|
||||
return;
|
||||
}
|
||||
handleZipData(data);
|
||||
}];
|
||||
}
|
||||
#else
|
||||
zipOK = NO;
|
||||
#endif
|
||||
|
||||
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
||||
// 构造 key_icons -> App Group 相对路径 映射
|
||||
NSMutableDictionary<NSString *, NSString *> *iconPathMap = [NSMutableDictionary dictionary];
|
||||
[iconShortNames enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, NSString *shortName, BOOL *stop) {
|
||||
if (![shortName isKindOfClass:NSString.class] || shortName.length == 0) return;
|
||||
NSString *fileName = shortName;
|
||||
// 若未带扩展名,默认按 .png 处理
|
||||
if (fileName.pathExtension.length == 0) {
|
||||
fileName = [fileName stringByAppendingPathExtension:@"png"];
|
||||
}
|
||||
NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@", skinId, fileName];
|
||||
iconPathMap[identifier] = relative;
|
||||
}];
|
||||
|
||||
NSMutableDictionary *themeJSON = [skin mutableCopy];
|
||||
themeJSON[@"id"] = skinId;
|
||||
if (iconPathMap.count > 0) {
|
||||
themeJSON[@"key_icons"] = iconPathMap.copy;
|
||||
[KBSkinInstallBridge installRemoteSkinWithJSON:skin
|
||||
completion:^(BOOL success, NSError * _Nullable error) {
|
||||
if (completion) {
|
||||
completion(success);
|
||||
}
|
||||
|
||||
BOOL themeOK = [[KBSkinManager shared] applyThemeFromJSON:themeJSON];
|
||||
|
||||
// 背景图优先从 Zip 解压出的 background.png 读取
|
||||
NSData *bgData = [NSData dataWithContentsOfFile:bgPath];
|
||||
BOOL ok = themeOK;
|
||||
if (bgData.length > 0) {
|
||||
ok = [[KBSkinManager shared] applyImageSkinWithData:bgData skinId:skinId name:name];
|
||||
if (!success && error) {
|
||||
NSLog(@"[KBSkinService] remote skin install failed: %@", error);
|
||||
}
|
||||
|
||||
if (!zipOK && !hasCachedAssets) {
|
||||
ok = NO;
|
||||
NSString *message = nil;
|
||||
if (success) {
|
||||
message = KBLocalized(@"已应用,切到键盘查看");
|
||||
} else if ([error.domain isEqualToString:KBSkinBridgeErrorDomain] &&
|
||||
error.code == KBSkinBridgeErrorContainerUnavailable) {
|
||||
message = KBLocalized(@"无法访问共享容器,应用皮肤失败");
|
||||
} else {
|
||||
message = KBLocalized(@"应用皮肤失败");
|
||||
}
|
||||
|
||||
if (completion) completion(ok);
|
||||
[KBHUD showInfo:(ok ? KBLocalized(@"已应用,切到键盘查看") : KBLocalized(@"应用皮肤失败"))];
|
||||
});
|
||||
[KBHUD showInfo:message];
|
||||
}];
|
||||
}
|
||||
|
||||
/// 本地 bundle 模式:不走网络,skin[@"zip_url"] 直接为 bundle 内 zip 文件名(可带/不带扩展名)。
|
||||
|
||||
@@ -43,15 +43,15 @@
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.kb_titleLabel.text = @"Settings"; // 导航标题
|
||||
self.kb_titleLabel.text = KBLocalized(@"Settings"); // 导航标题
|
||||
self.kb_navView.backgroundColor = [UIColor clearColor];
|
||||
self.view.backgroundColor = [UIColor colorWithHex:0xF8F8F8];
|
||||
|
||||
// 构造数据
|
||||
self.items = @[
|
||||
@{ @"title": @"Nickname", @"value": @"Nickname", @"arrow": @YES, @"copy": @NO },
|
||||
@{ @"title": @"Gender", @"value": @"Choose", @"arrow": @YES, @"copy": @NO },
|
||||
@{ @"title": @"User ID", @"value": @"8888888", @"arrow": @NO, @"copy": @YES },
|
||||
@{ @"title": KBLocalized(@"Nickname"), @"value": @"Nickname", @"arrow": @YES, @"copy": @NO },
|
||||
@{ @"title": KBLocalized(@"Gender"), @"value": @"Choose", @"arrow": @YES, @"copy": @NO },
|
||||
@{ @"title": KBLocalized(@"User ID"), @"value": @"8888888", @"arrow": @NO, @"copy": @YES },
|
||||
];
|
||||
|
||||
[self.view addSubview:self.tableView];
|
||||
|
||||
Reference in New Issue
Block a user