[AltStoreCore] Supports additional source JSON values for detailed “About” page

This commit is contained in:
Riley Testut
2023-04-04 13:46:04 -05:00
committed by Magesh K
parent 404bd1450b
commit bdb1d68b6b
3 changed files with 104 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21512" systemVersion="21G83" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D68" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES"> <entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/> <attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/> <attribute name="firstName" attributeType="String"/>
@@ -150,10 +150,18 @@
</entity> </entity>
<entity name="Source" representedClassName="Source" syncable="YES"> <entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/> <attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="hasFeaturedApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="headerImageURL" optional="YES" attributeType="URI"/>
<attribute name="iconURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/> <attribute name="identifier" attributeType="String"/>
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
<attribute name="name" attributeType="String"/> <attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/> <attribute name="sourceURL" attributeType="URI"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="websiteURL" optional="YES" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/> <relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="featuredApps" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="featuringSource" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/> <relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints> <uniquenessConstraints>
<uniquenessConstraint> <uniquenessConstraint>
@@ -178,6 +186,7 @@
<attribute name="version" attributeType="String"/> <attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/> <attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/> <attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="featuringSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="featuredApps" inverseEntity="Source"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/> <relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/> <relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/>
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/> <relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>

View File

@@ -176,6 +176,27 @@ public struct SourceJSON: Codable {
} }
public extension Source
{
// Fallbacks for optional JSON values.
var effectiveIconURL: URL? {
return self.iconURL ?? self.apps.first?.iconURL
}
var effectiveHeaderImageURL: URL? {
return self.headerImageURL ?? self.effectiveIconURL
}
var effectiveTintColor: UIColor? {
return self.tintColor ?? self.apps.first?.tintColor
}
var effectiveFeaturedApps: [StoreApp] {
return self.featuredApps ?? self.apps
}
}
@objc(Source) @objc(Source)
public class Source: NSManagedObject, Fetchable, Decodable public class Source: NSManagedObject, Fetchable, Decodable
{ {
@@ -184,6 +205,17 @@ public class Source: NSManagedObject, Fetchable, Decodable
@NSManaged public var identifier: String @NSManaged public var identifier: String
@NSManaged public var sourceURL: URL @NSManaged public var sourceURL: URL
/* Source Detail */
@NSManaged public var subtitle: String?
@NSManaged public var websiteURL: URL?
@NSManaged public var localizedDescription: String?
// Optional properties with fallbacks.
// `private` to prevent accidentally using instead of `effective[PropertyName]`
@NSManaged private var iconURL: URL?
@NSManaged private var headerImageURL: URL?
@NSManaged private var tintColor: UIColor?
@NSManaged public var error: NSError? @NSManaged public var error: NSError?
/* Non-Core Data Properties */ /* Non-Core Data Properties */
@@ -193,6 +225,9 @@ public class Source: NSManagedObject, Fetchable, Decodable
@objc(apps) @NSManaged public private(set) var _apps: NSOrderedSet @objc(apps) @NSManaged public private(set) var _apps: NSOrderedSet
@objc(newsItems) @NSManaged public private(set) var _newsItems: NSOrderedSet @objc(newsItems) @NSManaged public private(set) var _newsItems: NSOrderedSet
@objc(featuredApps) @NSManaged public private(set) var _featuredApps: NSOrderedSet
@objc(hasFeaturedApps) @NSManaged private var _hasFeaturedApps: Bool
@nonobjc public var apps: [StoreApp] { @nonobjc public var apps: [StoreApp] {
get { get {
return self._apps.array as! [StoreApp] return self._apps.array as! [StoreApp]
@@ -211,14 +246,27 @@ public class Source: NSManagedObject, Fetchable, Decodable
} }
} }
// `internal` to prevent accidentally using instead of `effectiveFeaturedApps`
@nonobjc internal var featuredApps: [StoreApp]? {
return self._hasFeaturedApps ? self._featuredApps.array as? [StoreApp] : nil
}
private enum CodingKeys: String, CodingKey private enum CodingKeys: String, CodingKey
{ {
case name case name
case identifier case identifier
case sourceURL case sourceURL
case userInfo case subtitle
case localizedDescription = "description"
case iconURL
case headerImageURL = "headerURL"
case websiteURL = "website"
case tintColor
case apps case apps
case news case news
case featuredApps
case userInfo
} }
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
@@ -241,6 +289,22 @@ public class Source: NSManagedObject, Fetchable, Decodable
self.name = try container.decode(String.self, forKey: .name) self.name = try container.decode(String.self, forKey: .name)
self.identifier = try container.decode(String.self, forKey: .identifier) self.identifier = try container.decode(String.self, forKey: .identifier)
// Optional Values
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
self.websiteURL = try container.decodeIfPresent(URL.self, forKey: .websiteURL)
self.localizedDescription = try container.decodeIfPresent(String.self, forKey: .localizedDescription)
self.iconURL = try container.decodeIfPresent(URL.self, forKey: .iconURL)
self.headerImageURL = try container.decodeIfPresent(URL.self, forKey: .headerImageURL)
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor)
{
guard let tintColor = UIColor(hexString: tintColorHex) else {
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
}
self.tintColor = tintColor
}
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo) let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
self.userInfo = userInfo?.reduce(into: [:]) { $0[ALTSourceUserInfoKey($1.key)] = $1.value } self.userInfo = userInfo?.reduce(into: [:]) { $0[ALTSourceUserInfoKey($1.key)] = $1.value }
@@ -275,6 +339,10 @@ public class Source: NSManagedObject, Fetchable, Decodable
} }
} }
self._newsItems = NSMutableOrderedSet(array: newsItems) self._newsItems = NSMutableOrderedSet(array: newsItems)
let featuredAppBundleIDs = try container.decodeIfPresent([String].self, forKey: .featuredApps)
let featuredApps = featuredAppBundleIDs?.compactMap { appsByID[$0] }
self.setFeaturedApps(featuredApps)
} }
catch catch
{ {
@@ -288,6 +356,29 @@ public class Source: NSManagedObject, Fetchable, Decodable
} }
} }
internal extension Source
{
func setFeaturedApps(_ featuredApps: [StoreApp]?)
{
// Explicitly update relationships for all apps to ensure featuredApps merges correctly.
for case let storeApp as StoreApp in self._apps
{
if let featuredApps, featuredApps.contains(where: { $0.bundleIdentifier == storeApp.bundleIdentifier })
{
storeApp.featuringSource = self
}
else
{
storeApp.featuringSource = nil
}
}
self._featuredApps = NSOrderedSet(array: featuredApps ?? [])
self._hasFeaturedApps = (featuredApps != nil)
}
}
public extension Source public extension Source
{ {
@nonobjc class func fetchRequest() -> NSFetchRequest<Source> @nonobjc class func fetchRequest() -> NSFetchRequest<Source>

View File

@@ -144,6 +144,8 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
@NSManaged public var newsItems: Set<NewsItem> @NSManaged public var newsItems: Set<NewsItem>
@NSManaged @objc(source) public var _source: Source? @NSManaged @objc(source) public var _source: Source?
@NSManaged public internal(set) var featuringSource: Source?
@NSManaged @objc(permissions) public var _permissions: NSOrderedSet @NSManaged @objc(permissions) public var _permissions: NSOrderedSet
@NSManaged @objc(latestVersion) public private(set) var latestSupportedVersion: AppVersion? @NSManaged @objc(latestVersion) public private(set) var latestSupportedVersion: AppVersion?