@@ -6,11 +6,15 @@
# import "KBConfig.h"
# import "KBSkinManager.h"
# if __has _include ( "KBNetworkManager.h" )
# import "KBNetworkManager.h"
# endif
# if __has _include ( < SSZipArchive / SSZipArchive . h > )
# import < SSZipArchive / SSZipArchive . h >
# endif
NSString * const KBDarwinSkinInstallRequestNotification = @ "com.loveKey.nyx.skin.install.request" ;
NSErrorDomain const KBSkinBridgeErrorDomain = @ "com.loveKey.nyx.skin.bridge" ;
static NSString * const kKBSkinPendingRequestKey = @ "com.loveKey.nyx.skin.pending" ;
static NSString * const kKBSkinPendingSkinIdKey = @ "skinId" ;
@@ -20,16 +24,6 @@ static NSString * const kKBSkinPendingKindKey = @"kind";
static NSString * const kKBSkinPendingTimestampKey = @ "timestamp" ;
static NSString * const kKBSkinPendingIconShortKey = @ "iconShortNames" ;
static NSString * const kKBSkinBridgeErrorDomain = @ "com.loveKey.nyx.skin.bridge" ;
typedef NS_ENUM ( NSUInteger , KBSkinBridgeErrorCode ) {
KBSkinBridgeErrorInvalidPayload = 1 ,
KBSkinBridgeErrorContainerUnavailable ,
KBSkinBridgeErrorZipMissing ,
KBSkinBridgeErrorUnzipFailed ,
KBSkinBridgeErrorApplyFailed ,
} ;
@ implementation KBSkinInstallBridge
+ ( NSDictionary < NSString * , NSString * > * ) defaultIconShortNames {
@@ -54,6 +48,245 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
return [ [ NSUserDefaults alloc ] initWithSuiteName : AppGroup ] ;
}
+ ( void ) installRemoteSkinWithJSON : ( NSDictionary * ) skinJSON
completion : ( KBSkinInstallConsumeCompletion ) completion {
if ( ! [ skinJSON isKindOfClass : NSDictionary . class ] || skinJSON . count = = 0 ) {
if ( completion ) {
NSError * err = [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorInvalidPayload
userInfo : nil ] ;
dispatch_async ( dispatch_get _main _queue ( ) , ^ { completion ( NO , err ) ; } ) ;
}
return ;
}
NSString * skinId = skinJSON [ @ "id" ] ? : @ "remote" ;
NSString * name = skinJSON [ @ "name" ] ? : skinId ;
NSString * zipURL = skinJSON [ @ "zip_url" ] ? : @ "" ;
// key_icons 可 选 :
// - 若 后 端 提 供 key_icons , 则 优 先 使 用 服 务 端 映 射 ;
// - 若 未 提 供 , 则 回 退 到 本 地 默 认 映 射 , 这 样 后 端 只 需 返 回 id / name / zip_url 。
NSDictionary * iconShortNames = nil ;
if ( [ skinJSON [ @ "key_icons" ] isKindOfClass : NSDictionary . class ] ) {
iconShortNames = skinJSON [ @ "key_icons" ] ;
} else {
iconShortNames = [ self defaultIconShortNames ] ;
}
NSFileManager * fm = [ NSFileManager defaultManager ] ;
NSURL * containerURL = [ fm containerURLForSecurityApplicationGroupIdentifier : AppGroup ] ;
if ( ! containerURL ) {
if ( completion ) {
NSError * err = [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorContainerUnavailable
userInfo : @ { NSLocalizedDescriptionKey : @ "Shared container unavailable" } ] ;
dispatch_async ( dispatch_get _main _queue ( ) , ^ { completion ( NO , err ) ; } ) ;
}
return ;
}
NSString * skinsRoot = [ containerURL . path stringByAppendingPathComponent : @ "Skins" ] ;
NSString * skinRoot = [ skinsRoot stringByAppendingPathComponent : skinId ] ;
NSString * iconsDir = [ skinRoot stringByAppendingPathComponent : @ "icons" ] ;
[ 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 ;
BOOL hasCachedAssets = ( contents . count > 0 ) ;
NSString * bgPath = [ skinRoot stringByAppendingPathComponent : @ "background.png" ] ;
dispatch_group _t group = dispatch_group _create ( ) ;
__block BOOL zipOK = YES ;
__block NSError * innerError = nil ;
# 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 ;
if ( ! innerError ) {
innerError = [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorZipMissing
userInfo : @ { NSLocalizedDescriptionKey : @ "Zip data is empty" } ] ;
}
dispatch_group _leave ( group ) ;
return ;
}
// 将 Zip 写 入 临 时 路 径 再 解 压
[ fm createDirectoryAtPath : skinRoot
withIntermediateDirectories : YES
attributes : nil
error : NULL ] ;
NSString * zipPath = [ skinRoot stringByAppendingPathComponent : @ "skin.zip" ] ;
if ( ! [ data writeToFile : zipPath atomically : YES ] ) {
zipOK = NO ;
if ( ! innerError ) {
innerError = [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorUnzipFailed
userInfo : @ { NSLocalizedDescriptionKey : @ "Failed to write zip file" } ] ;
}
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 ;
if ( ! innerError ) {
innerError = unzipError ? : [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorUnzipFailed
userInfo : nil ] ;
}
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 * subName in subItems ) {
if ( [ subName isEqualToString : @ "icons" ] || [ subName isEqualToString : @ "__MACOSX" ] ) continue ;
NSString * nestedRoot = [ skinRoot stringByAppendingPathComponent : subName ] ;
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 ) ;
} ;
# if __has _include ( "KBNetworkManager.h" )
// 远 程 下 载 ( 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 ;
if ( ! innerError ) {
innerError = error ? : [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorZipMissing
userInfo : @ { NSLocalizedDescriptionKey : @ "Failed to download zip" } ] ;
}
dispatch_group _leave ( group ) ;
return ;
}
handleZipData ( data ) ;
} ] ;
# else
// 无 KBNetworkManager 时 , 退 回 到 简 单 的 dataWithContentsOfURL 下 载 ( 阻 塞 当 前 线 程 )
dispatch_async ( dispatch_get _global _queue ( QOS_CLASS _UTILITY , 0 ) , ^ {
NSURL * url = [ NSURL URLWithString : zipURL ] ;
NSData * data = url ? [ NSData dataWithContentsOfURL : url ] : nil ;
if ( ! data ) {
zipOK = NO ;
if ( ! innerError ) {
innerError = [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorZipMissing
userInfo : @ { NSLocalizedDescriptionKey : @ "Failed to download zip" } ] ;
}
dispatch_group _leave ( group ) ;
} else {
handleZipData ( data ) ;
}
} ) ;
# endif
}
# else
zipOK = NO ;
innerError = [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorUnzipFailed
userInfo : @ { NSLocalizedDescriptionKey : @ "SSZipArchive not available" } ] ;
# 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 = [ skinJSON mutableCopy ] ;
themeJSON [ @ "id" ] = skinId ;
if ( iconPathMap . count > 0 ) {
themeJSON [ @ "key_icons" ] = iconPathMap . copy ;
}
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 ( ! zipOK && ! hasCachedAssets ) {
ok = NO ;
}
NSError * finalError = nil ;
if ( ! ok ) {
finalError = innerError ? : [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorApplyFailed
userInfo : nil ] ;
}
if ( completion ) completion ( ok , finalError ) ;
} ) ;
}
+ ( void ) publishBundleSkinRequestWithId : ( NSString * ) skinId
name : ( NSString * ) name
zipName : ( NSString * ) zipName
@@ -115,7 +348,7 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
error : ( NSError * __autoreleasing * ) error {
# if ! __has _include ( < SSZipArchive / SSZipArchive . h > )
if ( error ) {
* error = [ NSError errorWithDomain : k KBSkinBridgeErrorDomain
* error = [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorUnzipFailed
userInfo : @ { NSLocalizedDescriptionKey : @ "SSZipArchive not available" } ] ;
}
@@ -126,7 +359,7 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
NSString * zipName = payload [ kKBSkinPendingZipKey ] ;
if ( skinId . length = = 0 || zipName . length = = 0 ) {
if ( error ) {
* error = [ NSError errorWithDomain : k KBSkinBridgeErrorDomain
* error = [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorInvalidPayload
userInfo : nil ] ;
}
@@ -192,9 +425,9 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
}
if ( zipPath . length = = 0 ) {
if ( error ) {
* error = [ NSError errorWithDomain : k KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorZipMissing
userInfo : @ { NSLocalizedDescriptionKey : @ "Zip resource not found" } ] ;
* error = [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorZipMissing
userInfo : @ { NSLocalizedDescriptionKey : @ "Zip resource not found" } ] ;
}
return NO ;
}
@@ -207,7 +440,7 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
error : & unzipError ] ;
if ( ! ok || unzipError ) {
if ( error ) {
* error = unzipError ? : [ NSError errorWithDomain : k KBSkinBridgeErrorDomain
* error = unzipError ? : [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorUnzipFailed
userInfo : nil ] ;
}
@@ -283,7 +516,7 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
}
if ( ! ok && error ) {
* error = [ NSError errorWithDomain : k KBSkinBridgeErrorDomain
* error = [ NSError errorWithDomain : KBSkinBridgeErrorDomain
code : KBSkinBridgeErrorApplyFailed
userInfo : nil ] ;
}