diff --git a/CustomKeyboard/Resource/KBSkinIconMap.strings b/CustomKeyboard/Resource/KBSkinIconMap.strings index b750723..bffe465 100644 --- a/CustomKeyboard/Resource/KBSkinIconMap.strings +++ b/CustomKeyboard/Resource/KBSkinIconMap.strings @@ -1,132 +1,132 @@ /* 字母 q(小写) */ "letter_q_lower" = "key_q"; /* 字母 Q(大写) */ -"letter_q_upper" = "key_q"; +"letter_q_upper" = "key_q_up"; /* 字母 w(小写) */ "letter_w_lower" = "key_w"; /* 字母 W(大写) */ -"letter_w_upper" = "key_w"; +"letter_w_upper" = "key_w_up"; /* 字母 e(小写) */ "letter_e_lower" = "key_e"; /* 字母 E(大写) */ -"letter_e_upper" = "key_e"; +"letter_e_upper" = "key_e_up"; /* 字母 r(小写) */ "letter_r_lower" = "key_r"; /* 字母 R(大写) */ -"letter_r_upper" = "key_r"; +"letter_r_upper" = "key_r_up"; /* 字母 t(小写) */ "letter_t_lower" = "key_t"; /* 字母 T(大写) */ -"letter_t_upper" = "key_t"; +"letter_t_upper" = "key_t_up"; /* 字母 y(小写) */ "letter_y_lower" = "key_y"; /* 字母 Y(大写) */ -"letter_y_upper" = "key_y"; +"letter_y_upper" = "key_y_up"; /* 字母 u(小写) */ "letter_u_lower" = "key_u"; /* 字母 U(大写) */ -"letter_u_upper" = "key_u"; +"letter_u_upper" = "key_u_up"; /* 字母 i(小写) */ "letter_i_lower" = "key_i"; /* 字母 I(大写) */ -"letter_i_upper" = "key_i"; +"letter_i_upper" = "key_i_up"; /* 字母 o(小写) */ "letter_o_lower" = "key_o"; /* 字母 O(大写) */ -"letter_o_upper" = "key_o"; +"letter_o_upper" = "key_o_up"; /* 字母 p(小写) */ "letter_p_lower" = "key_p"; /* 字母 P(大写) */ -"letter_p_upper" = "key_p"; +"letter_p_upper" = "key_p_up"; /* 字母 a(小写) */ "letter_a_lower" = "key_a"; /* 字母 A(大写) */ -"letter_a_upper" = "key_a"; +"letter_a_upper" = "key_a_up"; /* 字母 s(小写) */ "letter_s_lower" = "key_s"; /* 字母 S(大写) */ -"letter_s_upper" = "key_s"; +"letter_s_upper" = "key_s_up"; /* 字母 d(小写) */ "letter_d_lower" = "key_d"; /* 字母 D(大写) */ -"letter_d_upper" = "key_d"; +"letter_d_upper" = "key_d_up"; /* 字母 f(小写) */ "letter_f_lower" = "key_f"; /* 字母 F(大写) */ -"letter_f_upper" = "key_f"; +"letter_f_upper" = "key_f_up"; /* 字母 g(小写) */ "letter_g_lower" = "key_g"; /* 字母 G(大写) */ -"letter_g_upper" = "key_g"; +"letter_g_upper" = "key_f_up"; /* 字母 h(小写) */ "letter_h_lower" = "key_h"; /* 字母 H(大写) */ -"letter_h_upper" = "key_h"; +"letter_h_upper" = "key_h_up"; /* 字母 j(小写) */ "letter_j_lower" = "key_j"; /* 字母 J(大写) */ -"letter_j_upper" = "key_j"; +"letter_j_upper" = "key_j_up"; /* 字母 k(小写) */ "letter_k_lower" = "key_k"; /* 字母 K(大写) */ -"letter_k_upper" = "key_k"; +"letter_k_upper" = "key_k_up"; /* 字母 l(小写) */ "letter_l_lower" = "key_l"; /* 字母 L(大写) */ -"letter_l_upper" = "key_l"; +"letter_l_upper" = "key_l_up"; /* 字母 z(小写) */ "letter_z_lower" = "key_z"; /* 字母 Z(大写) */ -"letter_z_upper" = "key_z"; +"letter_z_upper" = "key_z_up"; /* 字母 x(小写) */ "letter_x_lower" = "key_x"; /* 字母 X(大写) */ -"letter_x_upper" = "key_x"; +"letter_x_upper" = "key_x_up"; /* 字母 c(小写) */ "letter_c_lower" = "key_c"; /* 字母 C(大写) */ -"letter_c_upper" = "key_c"; +"letter_c_upper" = "key_c_up"; /* 字母 v(小写) */ "letter_v_lower" = "key_v"; /* 字母 V(大写) */ -"letter_v_upper" = "key_v"; +"letter_v_upper" = "key_v_up"; /* 字母 b(小写) */ "letter_b_lower" = "key_b"; /* 字母 B(大写) */ -"letter_b_upper" = "key_b"; +"letter_b_upper" = "key_b_up"; /* 字母 n(小写) */ "letter_n_lower" = "key_n"; /* 字母 N(大写) */ -"letter_n_upper" = "key_n"; +"letter_n_upper" = "key_n_up"; /* 字母 m(小写) */ "letter_m_lower" = "key_m"; /* 字母 M(大写) */ -"letter_m_upper" = "key_m"; +"letter_m_upper" = "key_m_up"; /* 数字 1 */ "digit_1" = "key_1"; diff --git a/CustomKeyboard/Resource/fense.zip b/CustomKeyboard/Resource/fense.zip new file mode 100644 index 0000000..f35019b Binary files /dev/null and b/CustomKeyboard/Resource/fense.zip differ diff --git a/CustomKeyboard/View/KBKeyButton.m b/CustomKeyboard/View/KBKeyButton.m index 116d98d..8a63db7 100644 --- a/CustomKeyboard/View/KBKeyButton.m +++ b/CustomKeyboard/View/KBKeyButton.m @@ -106,6 +106,9 @@ [self setTitle:@"" forState:UIControlStateHighlighted]; [self setTitle:@"" forState:UIControlStateSelected]; self.titleLabel.hidden = YES; + KBSkinTheme *t = [KBSkinManager shared].current; + t.keyBackground = [UIColor clearColor]; + } else { // 无图标:按键标题正常显示(使用 key.title),并根据 hidden_keys 决定要不要隐藏 [self setTitle:self.key.title forState:UIControlStateNormal]; diff --git a/Shared/KBSkinInstallBridge.m b/Shared/KBSkinInstallBridge.m index 54f3de3..fd900eb 100644 --- a/Shared/KBSkinInstallBridge.m +++ b/Shared/KBSkinInstallBridge.m @@ -133,25 +133,36 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) { return NO; } - NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup]; - if (!containerURL) { - if (error) { - *error = [NSError errorWithDomain:kKBSkinBridgeErrorDomain - code:KBSkinBridgeErrorContainerUnavailable - userInfo:@{NSLocalizedDescriptionKey: @"App Group container unavailable"}]; + // 皮肤根目录:优先 App Group,若不可写则退回当前进程的 Caches 目录。 + NSFileManager *fm = [NSFileManager defaultManager]; + NSString *baseRoot = nil; + NSURL *containerURL = [fm containerURLForSecurityApplicationGroupIdentifier:AppGroup]; + if (containerURL.path.length > 0) { + // 探测写权限:在 Skins/.kb_write_test 下创建临时目录 + NSString *testDir = [[containerURL.path stringByAppendingPathComponent:@"Skins"] + stringByAppendingPathComponent:@".kb_write_test"]; + NSError *probeError = nil; + BOOL canWrite = [fm createDirectoryAtPath:testDir + withIntermediateDirectories:YES + attributes:nil + error:&probeError]; + if (canWrite) { + baseRoot = containerURL.path; + [fm removeItemAtPath:testDir error:NULL]; } - return NO; + } + if (baseRoot.length == 0) { + NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + baseRoot = dirs.firstObject ?: NSTemporaryDirectory(); } - NSString *skinsRoot = [containerURL.path stringByAppendingPathComponent:@"Skins"]; + NSString *skinsRoot = [baseRoot 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]; + [fm createDirectoryAtPath:iconsDir + withIntermediateDirectories:YES + attributes:nil + error:NULL]; BOOL isDir = NO; BOOL hasIconsDir = [fm fileExistsAtPath:iconsDir isDirectory:&isDir] && isDir; NSArray *contents = hasIconsDir ? [fm contentsOfDirectoryAtPath:iconsDir error:NULL] : nil; diff --git a/Shared/KBSkinManager.m b/Shared/KBSkinManager.m index d024170..9d5adbe 100644 --- a/Shared/KBSkinManager.m +++ b/Shared/KBSkinManager.m @@ -59,18 +59,38 @@ static NSString * const kKBSkinThemeStoreKey = @"KBSkinThemeCurrent"; @implementation KBSkinManager -/// 判断指定 skinId 在 App Group 中是否存在资源目录(Skins//...)。 +/// 返回所有可能的皮肤根目录(优先 App Group,其次当前进程的 Caches)。 ++ (NSArray *)kb_candidateBaseRoots { + NSMutableArray *roots = [NSMutableArray array]; + NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup]; + if (containerURL.path.length > 0) { + [roots addObject:containerURL.path]; + } + NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + NSString *caches = dirs.firstObject; + if (caches.length > 0) { + [roots addObject:caches]; + } + return roots; +} + +/// 判断指定 skinId 是否有可用资源目录(任一根目录下存在 Skins//...)。 /// 默认皮肤(nil / @"default")始终视为存在。 + (BOOL)kb_hasAssetsForSkinId:(NSString *)skinId { if (skinId.length == 0 || [skinId isEqualToString:@"default"]) { return YES; } - NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup]; - if (!containerURL) return NO; - NSString *skinsRoot = [containerURL.path stringByAppendingPathComponent:@"Skins"]; - NSString *skinRoot = [skinsRoot stringByAppendingPathComponent:skinId]; - BOOL isDir = NO; - return [[NSFileManager defaultManager] fileExistsAtPath:skinRoot isDirectory:&isDir] && isDir; + NSArray *roots = [self kb_candidateBaseRoots]; + NSFileManager *fm = [NSFileManager defaultManager]; + for (NSString *base in roots) { + NSString *skinsRoot = [base stringByAppendingPathComponent:@"Skins"]; + NSString *skinRoot = [skinsRoot stringByAppendingPathComponent:skinId]; + BOOL isDir = NO; + if ([fm fileExistsAtPath:skinRoot isDirectory:&isDir] && isDir) { + return YES; + } + } + return NO; } + (instancetype)shared { @@ -176,27 +196,25 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer, } - (UIImage *)currentBackgroundImage { - // 新策略:始终从 App Group 容器按 skinId 读取背景图文件, - // 确保当 AppGroup 目录被系统清理时,背景图与按键图标一起消失, - // 不再依赖 Keychain 中可能残留的 backgroundImageData。 NSString *skinId = self.current.skinId; if (skinId.length == 0) return nil; - NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup]; - if (!containerURL) return nil; - - NSString *relative = [NSString stringWithFormat:@"Skins/%@/background.png", skinId]; - NSString *bgPath = [[containerURL.path stringByAppendingPathComponent:relative] stringByStandardizingPath]; - + NSArray *roots = [self.class kb_candidateBaseRoots]; NSFileManager *fm = [NSFileManager defaultManager]; - BOOL isDir = NO; - if (![fm fileExistsAtPath:bgPath isDirectory:&isDir] || isDir) { - return nil; - } + NSString *relative = [NSString stringWithFormat:@"Skins/%@/background.png", skinId]; - NSData *data = [NSData dataWithContentsOfFile:bgPath]; - if (data.length == 0) return nil; - return [UIImage imageWithData:data scale:[UIScreen mainScreen].scale] ?: nil; + for (NSString *base in roots) { + NSString *bgPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath]; + BOOL isDir = NO; + if (![fm fileExistsAtPath:bgPath isDirectory:&isDir] || isDir) { + continue; + } + NSData *data = [NSData dataWithContentsOfFile:bgPath]; + if (data.length == 0) continue; + UIImage *img = [UIImage imageWithData:data scale:[UIScreen mainScreen].scale]; + if (img) return img; + } + return nil; } - (BOOL)shouldHideKeyTextForIdentifier:(NSString *)identifier { @@ -257,12 +275,19 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer, // 若在 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]; + // 视为相对皮肤根目录(App Group / Caches)的文件路径。 + NSArray *roots = [self.class kb_candidateBaseRoots]; + NSFileManager *fm = [NSFileManager defaultManager]; + for (NSString *base in roots) { + NSString *fullPath = [[base stringByAppendingPathComponent:value] stringByStandardizingPath]; + BOOL isDir = NO; + if (![fm fileExistsAtPath:fullPath isDirectory:&isDir] || isDir) { + continue; + } + UIImage *img = [UIImage imageWithContentsOfFile:fullPath]; + if (img) return img; + } + return nil; } // 否则按本地 Assets 名称加载(兼容旧实现) return [UIImage imageNamed:value]; @@ -272,28 +297,34 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer, // Skins//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; + NSArray *roots = [self.class kb_candidateBaseRoots]; + NSFileManager *fm = [NSFileManager defaultManager]; + // 先尝试大小写后缀 - 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]; + if (caseVariant == 2 || caseVariant == 1) { + NSString *suffix = (caseVariant == 2) ? @"_upper" : @"_lower"; + NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@%@.png", skinId, identifier, suffix]; + for (NSString *base in roots) { + NSString *fullPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath]; + BOOL isDir = NO; + if ([fm fileExistsAtPath:fullPath isDirectory:&isDir] && !isDir) { + UIImage *img = [UIImage imageWithContentsOfFile:fullPath]; + if (img) return img; + } } } // 最后回退到基础 id:Skins//icons/.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]; + for (NSString *base in roots) { + NSString *fullPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath]; + BOOL isDir = NO; + if ([fm fileExistsAtPath:fullPath isDirectory:&isDir] && !isDir) { + UIImage *img = [UIImage imageWithContentsOfFile:fullPath]; + if (img) return img; + } + } + return nil; } + (UIColor *)colorFromHexString:(NSString *)hex defaultColor:(UIColor *)fallback { diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 860af10..2a12a66 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */; }; 0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; }; 0459D1B82EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; }; + 0461310F2ECF0FBC00A6FADF /* fense.zip in Resources */ = {isa = PBXBuildFile; fileRef = 0461310E2ECF0FBC00A6FADF /* fense.zip */; }; 0477BD952EBAFF4E0055D639 /* KBURLOpenBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BD932EBAFF4E0055D639 /* KBURLOpenBridge.m */; }; 0477BDF02EBB76E30055D639 /* HomeSheetVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDEF2EBB76E30055D639 /* HomeSheetVC.m */; }; 0477BDF32EBB7B850055D639 /* KBDirectionIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDF22EBB7B850055D639 /* KBDirectionIndicatorView.m */; }; @@ -228,6 +229,7 @@ 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinCenterVC.m; sourceTree = ""; }; 0459D1B52EBA287900F2D189 /* KBSkinManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinManager.h; sourceTree = ""; }; 0459D1B62EBA287900F2D189 /* KBSkinManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinManager.m; sourceTree = ""; }; + 0461310E2ECF0FBC00A6FADF /* fense.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = fense.zip; sourceTree = ""; }; 0477BD922EBAFF4E0055D639 /* KBURLOpenBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBURLOpenBridge.h; sourceTree = ""; }; 0477BD932EBAFF4E0055D639 /* KBURLOpenBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBURLOpenBridge.m; sourceTree = ""; }; 0477BDEE2EBB76E30055D639 /* HomeSheetVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeSheetVC.h; sourceTree = ""; }; @@ -482,6 +484,7 @@ children = ( 041007D12ECE012000D203BB /* KBSkinIconMap.strings */, 041007D32ECE012500D203BB /* 002.zip */, + 0461310E2ECF0FBC00A6FADF /* fense.zip */, ); path = Resource; sourceTree = ""; @@ -1422,6 +1425,7 @@ buildActionMask = 2147483647; files = ( 04A9FE202EB893F10020DB6D /* Localizable.strings in Resources */, + 0461310F2ECF0FBC00A6FADF /* fense.zip in Resources */, 041007D42ECE012500D203BB /* 002.zip in Resources */, 041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */, 04286A0B2ECD88B400CE730C /* KeyboardAssets.xcassets in Resources */, @@ -1474,10 +1478,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-keyBoard/Pods-keyBoard-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-keyBoard/Pods-keyBoard-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-keyBoard/Pods-keyBoard-frameworks.sh\"\n"; diff --git a/keyBoard/Class/Shop/VC/KBShopItemVC.m b/keyBoard/Class/Shop/VC/KBShopItemVC.m index 3109858..f75435d 100644 --- a/keyBoard/Class/Shop/VC/KBShopItemVC.m +++ b/keyBoard/Class/Shop/VC/KBShopItemVC.m @@ -122,10 +122,10 @@ - (void)kb_handleShopTapAtIndexPath:(NSIndexPath *)indexPath { NSString *title = (indexPath.item < self.dataSource.count) ? self.dataSource[indexPath.item] : KBLocalized(@"专属皮肤002"); // 将需求固定到 002.zip,本地写死皮肤 id,便于键盘扩展识别并解压。 - static NSString * const kKBBundleSkinId002 = @"bundle_skin_002"; + static NSString * const kKBBundleSkinId002 = @"bundle_skin_fense"; [KBSkinInstallBridge publishBundleSkinRequestWithId:kKBBundleSkinId002 name:title ?: kKBBundleSkinId002 - zipName:@"002.zip" + zipName:@"fense.zip" iconShortNames:nil]; [KBHUD showInfo:KBLocalized(@"已通知键盘解压,切换到自定义键盘即可生效")]; }