mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
[AltStoreCore] Adds AppScreenshot to support dynamically-sized screenshots
Preserves StoreApp.imageURL field in database model for backwards compatibility.
This commit is contained in:
@@ -435,6 +435,7 @@
|
||||
D5F48B4C29CD0C48002B52A4 /* AsyncManaged.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F48B4929CD0B67002B52A4 /* AsyncManaged.swift */; };
|
||||
D5F5AF2E28FDD2EC00C938F5 /* TestErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F5AF2D28FDD2EC00C938F5 /* TestErrors.swift */; };
|
||||
D5F5AF7D28ECEA990067C736 /* ErrorDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */; };
|
||||
D5F9821D2AB900060045751F /* AppScreenshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F9821C2AB900060045751F /* AppScreenshot.swift */; };
|
||||
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */; };
|
||||
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */; };
|
||||
D5FB28EC2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */; };
|
||||
@@ -1096,6 +1097,7 @@
|
||||
D5F48B4929CD0B67002B52A4 /* AsyncManaged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncManaged.swift; sourceTree = "<group>"; };
|
||||
D5F5AF2D28FDD2EC00C938F5 /* TestErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestErrors.swift; sourceTree = "<group>"; };
|
||||
D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsViewController.swift; sourceTree = "<group>"; };
|
||||
D5F9821C2AB900060045751F /* AppScreenshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppScreenshot.swift; sourceTree = "<group>"; };
|
||||
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore10ToAltStore11.xcmappingmodel; sourceTree = "<group>"; };
|
||||
D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp10ToStoreApp11Policy.swift; sourceTree = "<group>"; };
|
||||
D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFontDescriptor+Bold.swift"; sourceTree = "<group>"; };
|
||||
@@ -1666,6 +1668,7 @@
|
||||
BF66EEC92501AECA007EE018 /* Account.swift */,
|
||||
BF66EEC72501AECA007EE018 /* AppID.swift */,
|
||||
BF66EEC62501AECA007EE018 /* AppPermission.swift */,
|
||||
D5F9821C2AB900060045751F /* AppScreenshot.swift */,
|
||||
D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */,
|
||||
BF66EECA2501AECA007EE018 /* DatabaseManager.swift */,
|
||||
D5FD4ECA2A9532960097BEE8 /* DatabaseManager+Async.swift */,
|
||||
@@ -3056,6 +3059,7 @@
|
||||
BF66EEE82501AED0007EE018 /* UserDefaults+AltStore.swift in Sources */,
|
||||
BF340E9A250AD39500A192CB /* ViewApp.intentdefinition in Sources */,
|
||||
BFAECC522501B0A400528F27 /* CodableError.swift in Sources */,
|
||||
D5F9821D2AB900060045751F /* AppScreenshot.swift in Sources */,
|
||||
BF66EE9E2501AEC1007EE018 /* Fetchable.swift in Sources */,
|
||||
BF66EEDF2501AECA007EE018 /* PatreonAccount.swift in Sources */,
|
||||
BFAECC532501B0A400528F27 /* ServerProtocol.swift in Sources */,
|
||||
|
||||
@@ -42,6 +42,21 @@
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppScreenshot" representedClassName="AppScreenshot" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="height" optional="YES" attributeType="Integer 16" usesScalarValueType="NO"/>
|
||||
<attribute name="imageURL" attributeType="URI"/>
|
||||
<attribute name="sourceID" attributeType="String"/>
|
||||
<attribute name="width" optional="YES" attributeType="Integer 16" usesScalarValueType="NO"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="screenshots" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="imageURL"/>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="buildVersion" attributeType="String"/>
|
||||
@@ -207,6 +222,7 @@
|
||||
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="screenshots" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppScreenshot" inverseName="app" inverseEntity="AppScreenshot"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
|
||||
<uniquenessConstraints>
|
||||
|
||||
85
AltStoreCore/Model/AppScreenshot.swift
Normal file
85
AltStoreCore/Model/AppScreenshot.swift
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// AppScreenshot.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 9/18/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
@objc(AppScreenshot)
|
||||
public class AppScreenshot: NSManagedObject, Fetchable, Decodable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public private(set) var imageURL: URL
|
||||
|
||||
public private(set) var size: CGSize? {
|
||||
get {
|
||||
guard let width = self.width?.doubleValue, let height = self.height?.doubleValue else { return nil }
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
set {
|
||||
if let newValue
|
||||
{
|
||||
self.width = NSNumber(value: newValue.width)
|
||||
self.height = NSNumber(value: newValue.height)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.width = nil
|
||||
self.height = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@NSManaged private var width: NSNumber?
|
||||
@NSManaged private var height: NSNumber?
|
||||
|
||||
@NSManaged public internal(set) var appBundleID: String
|
||||
@NSManaged public internal(set) var sourceID: String
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public internal(set) var app: StoreApp?
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case imageURL
|
||||
case width
|
||||
case height
|
||||
}
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
internal init(imageURL: URL, size: CGSize?, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: AppScreenshot.entity(), insertInto: context)
|
||||
|
||||
self.imageURL = imageURL
|
||||
self.size = size
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws
|
||||
{
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
|
||||
super.init(entity: AppScreenshot.entity(), insertInto: context)
|
||||
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.imageURL = try container.decode(URL.self, forKey: .imageURL)
|
||||
|
||||
self.width = try container.decodeIfPresent(Int16.self, forKey: .width).map { NSNumber(value: $0) }
|
||||
self.height = try container.decodeIfPresent(Int16.self, forKey: .height).map { NSNumber(value: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
public extension AppScreenshot
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<AppScreenshot>
|
||||
{
|
||||
return NSFetchRequest<AppScreenshot>(entityName: "AppScreenshot")
|
||||
}
|
||||
}
|
||||
@@ -156,6 +156,12 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
{
|
||||
appVersion.managedObjectContext?.delete(appVersion)
|
||||
}
|
||||
|
||||
// Delete previous screenshots (different than below).
|
||||
for case let appScreenshot as AppScreenshot in previousApp._screenshots where appScreenshot.app == nil
|
||||
{
|
||||
appScreenshot.managedObjectContext?.delete(appScreenshot)
|
||||
}
|
||||
}
|
||||
|
||||
case is AppVersion where conflict.conflictingObjects.count == 2:
|
||||
@@ -181,8 +187,9 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
return
|
||||
}
|
||||
|
||||
var sortedVersionIDsByGlobalAppID = [String: NSOrderedSet]()
|
||||
var permissionsByGlobalAppID = [String: Set<AnyHashable>]()
|
||||
var sortedVersionIDsByGlobalAppID = [String: NSOrderedSet]()
|
||||
var sortedScreenshotURLsByGlobalAppID = [String: NSOrderedSet]()
|
||||
|
||||
var featuredAppIDsBySourceID = [String: [String]]()
|
||||
|
||||
@@ -212,10 +219,19 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
databaseVersion.managedObjectContext?.delete(databaseVersion)
|
||||
}
|
||||
|
||||
// Screenshots
|
||||
let contextScreenshotURLs = NSOrderedSet(array: contextApp._screenshots.lazy.compactMap { $0 as? AppScreenshot }.map { $0.imageURL })
|
||||
for case let databaseScreenshot as AppScreenshot in databaseObject._screenshots where !contextScreenshotURLs.contains(databaseScreenshot.imageURL)
|
||||
{
|
||||
// Screenshot's imageURL does NOT exist in context, so delete existing databaseScreenshot.
|
||||
databaseScreenshot.managedObjectContext?.delete(databaseScreenshot)
|
||||
}
|
||||
|
||||
if let globallyUniqueID = contextApp.globallyUniqueID
|
||||
{
|
||||
permissionsByGlobalAppID[globallyUniqueID] = contextPermissions
|
||||
sortedVersionIDsByGlobalAppID[globallyUniqueID] = contextVersionIDs
|
||||
sortedScreenshotURLsByGlobalAppID[globallyUniqueID] = contextScreenshotURLs
|
||||
}
|
||||
|
||||
case let databaseObject as Source:
|
||||
@@ -295,6 +311,31 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
|
||||
appVersions = fixedAppVersions
|
||||
}
|
||||
|
||||
// Screenshots
|
||||
if let sortedScreenshotURLs = sortedScreenshotURLsByGlobalAppID[globallyUniqueID],
|
||||
let sortedScreenshotURLsArray = sortedScreenshotURLs.array as? [URL],
|
||||
case let databaseScreenshotURLs = databaseObject.screenshots.map({ $0.imageURL }),
|
||||
databaseScreenshotURLs != sortedScreenshotURLsArray
|
||||
{
|
||||
// Screenshot order is incorrect, so attempt to fix by re-sorting.
|
||||
let fixedScreenshots = databaseObject.screenshots.sorted { (screenshotA, screenshotB) in
|
||||
let indexA = sortedScreenshotURLs.index(of: screenshotA.imageURL)
|
||||
let indexB = sortedScreenshotURLs.index(of: screenshotB.imageURL)
|
||||
return indexA < indexB
|
||||
}
|
||||
|
||||
let appScreenshotURLs = fixedScreenshots.map { $0.imageURL }
|
||||
if appScreenshotURLs == sortedScreenshotURLsArray
|
||||
{
|
||||
databaseObject.setScreenshots(fixedScreenshots)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Screenshots are still not in correct order, but not worth throwing error so ignore.
|
||||
print("Failed to re-sort screenshots into correct order. Expected:", sortedScreenshotURLsArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always update versions post-merging to make sure latestSupportedVersion is correct.
|
||||
|
||||
@@ -136,6 +136,11 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
{
|
||||
permission.sourceID = self.sourceIdentifier ?? ""
|
||||
}
|
||||
|
||||
for screenshot in self.screenshots
|
||||
{
|
||||
screenshot.sourceID = self.sourceIdentifier ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@NSManaged private var primitiveSourceIdentifier: String?
|
||||
@@ -200,6 +205,10 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
guard let version = self.latestSupportedVersion else { return nil }
|
||||
return version.downloadURL
|
||||
}
|
||||
@nonobjc public var screenshots: [AppScreenshot] {
|
||||
return self._screenshots.array as! [AppScreenshot]
|
||||
}
|
||||
@NSManaged @objc(screenshots) private(set) var _screenshots: NSOrderedSet
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
@@ -212,19 +221,24 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
case bundleIdentifier
|
||||
case developerName
|
||||
case localizedDescription
|
||||
case version
|
||||
case versionDescription
|
||||
case versionDate
|
||||
case iconURL
|
||||
case screenshotURLs
|
||||
case downloadURL
|
||||
case platformURLs
|
||||
case screenshots
|
||||
case tintColor
|
||||
case subtitle
|
||||
case permissions = "appPermissions"
|
||||
case size
|
||||
case isBeta = "beta"
|
||||
case versions
|
||||
|
||||
// Legacy
|
||||
case version
|
||||
case versionDescription
|
||||
case versionDate
|
||||
case downloadURL
|
||||
case screenshotURLs
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws
|
||||
@@ -245,7 +259,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
|
||||
|
||||
self.iconURL = try container.decode(URL.self, forKey: .iconURL)
|
||||
self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
|
||||
|
||||
var downloadURL = try container.decodeIfPresent(URL.self, forKey: .downloadURL)
|
||||
let platformURLs = try container.decodeIfPresent(PlatformURLs.self.self, forKey: .platformURLs)
|
||||
@@ -291,6 +304,33 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
|
||||
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
||||
|
||||
if let screenshots = try container.decodeIfPresent([AppScreenshot].self, forKey: .screenshots)
|
||||
{
|
||||
for screenshot in screenshots
|
||||
{
|
||||
screenshot.appBundleID = self.bundleIdentifier
|
||||
}
|
||||
|
||||
self.setScreenshots(screenshots)
|
||||
}
|
||||
else if let screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs)
|
||||
{
|
||||
// Assume 9:16 iPhone 8 screen dimensions for legacy screenshotURLs.
|
||||
let legacyAspectRatio = CGSize(width: 750, height: 1334)
|
||||
|
||||
let screenshots = screenshotURLs.map { imageURL in
|
||||
let screenshot = AppScreenshot(imageURL: imageURL, size: legacyAspectRatio, context: context)
|
||||
screenshot.appBundleID = self.bundleIdentifier
|
||||
return screenshot
|
||||
}
|
||||
|
||||
self.setScreenshots(screenshots)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.setScreenshots([])
|
||||
}
|
||||
|
||||
if let appPermissions = try container.decodeIfPresent(AppPermissions.self, forKey: .permissions)
|
||||
{
|
||||
let allPermissions = appPermissions.entitlements + appPermissions.privacy
|
||||
@@ -402,6 +442,26 @@ internal extension StoreApp
|
||||
|
||||
self._permissions = permissions as NSSet
|
||||
}
|
||||
|
||||
func setScreenshots(_ screenshots: [AppScreenshot])
|
||||
{
|
||||
for case let screenshot as AppScreenshot in self._screenshots
|
||||
{
|
||||
if screenshots.contains(screenshot)
|
||||
{
|
||||
screenshot.app = self
|
||||
}
|
||||
else
|
||||
{
|
||||
screenshot.app = nil
|
||||
}
|
||||
}
|
||||
|
||||
self._screenshots = NSOrderedSet(array: screenshots)
|
||||
|
||||
// Backwards compatibility
|
||||
self.screenshotURLs = screenshots.map { $0.imageURL }
|
||||
}
|
||||
}
|
||||
|
||||
public extension StoreApp
|
||||
|
||||
Reference in New Issue
Block a user