mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Supports both iPhone + iPad screenshots
Prefers showing screenshots for current device, but falls back to all screenshots if there are no relevant ones.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22D68" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="22G91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
@@ -44,6 +44,7 @@
|
||||
</entity>
|
||||
<entity name="AppScreenshot" representedClassName="AppScreenshot" syncable="YES">
|
||||
<attribute name="appBundleID" attributeType="String"/>
|
||||
<attribute name="deviceType" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="height" optional="YES" attributeType="Integer 16" usesScalarValueType="NO"/>
|
||||
<attribute name="imageURL" attributeType="URI"/>
|
||||
<attribute name="sourceID" attributeType="String"/>
|
||||
@@ -52,6 +53,7 @@
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="imageURL"/>
|
||||
<constraint value="deviceType"/>
|
||||
<constraint value="appBundleID"/>
|
||||
<constraint value="sourceID"/>
|
||||
</uniquenessConstraint>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
|
||||
public extension AppScreenshot
|
||||
{
|
||||
static let defaultAspectRatio = CGSize(width: 9, height: 19.5)
|
||||
@@ -40,6 +42,13 @@ public class AppScreenshot: NSManagedObject, Fetchable, Decodable
|
||||
@NSManaged private var width: NSNumber?
|
||||
@NSManaged private var height: NSNumber?
|
||||
|
||||
// Defaults to .iphone
|
||||
@nonobjc public var deviceType: ALTDeviceType {
|
||||
get { ALTDeviceType(rawValue: Int(_deviceType)) }
|
||||
set { _deviceType = Int16(newValue.rawValue) }
|
||||
}
|
||||
@NSManaged @objc(deviceType) private var _deviceType: Int16
|
||||
|
||||
@NSManaged public internal(set) var appBundleID: String
|
||||
@NSManaged public internal(set) var sourceID: String
|
||||
|
||||
@@ -58,12 +67,13 @@ public class AppScreenshot: NSManagedObject, Fetchable, Decodable
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
internal init(imageURL: URL, size: CGSize?, context: NSManagedObjectContext)
|
||||
internal init(imageURL: URL, size: CGSize?, deviceType: ALTDeviceType, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: AppScreenshot.entity(), insertInto: context)
|
||||
|
||||
self.imageURL = imageURL
|
||||
self.size = size
|
||||
self.deviceType = deviceType
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws
|
||||
@@ -79,6 +89,21 @@ public class AppScreenshot: NSManagedObject, Fetchable, Decodable
|
||||
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 override func awakeFromInsert()
|
||||
{
|
||||
super.awakeFromInsert()
|
||||
|
||||
self.deviceType = .iphone
|
||||
}
|
||||
}
|
||||
|
||||
extension AppScreenshot
|
||||
{
|
||||
var screenshotID: String {
|
||||
let screenshotID = "\(self.imageURL.absoluteString)|\(self.deviceType)"
|
||||
return screenshotID
|
||||
}
|
||||
}
|
||||
|
||||
public extension AppScreenshot
|
||||
@@ -88,3 +113,89 @@ public extension AppScreenshot
|
||||
return NSFetchRequest<AppScreenshot>(entityName: "AppScreenshot")
|
||||
}
|
||||
}
|
||||
|
||||
internal struct AppScreenshots: Decodable
|
||||
{
|
||||
var screenshots: [AppScreenshot] = []
|
||||
|
||||
enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case iphone
|
||||
case ipad
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws
|
||||
{
|
||||
let container: KeyedDecodingContainer<CodingKeys>
|
||||
|
||||
do
|
||||
{
|
||||
container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
}
|
||||
catch DecodingError.typeMismatch
|
||||
{
|
||||
// ONLY catch the container's DecodingError.typeMismatch, not the below decodeIfPresent()'s
|
||||
|
||||
// Fallback to single array.
|
||||
|
||||
var collection = try Collection(from: decoder)
|
||||
collection.deviceType = .iphone
|
||||
|
||||
self.screenshots = collection.screenshots
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if var collection = try container.decodeIfPresent(Collection.self, forKey: .iphone)
|
||||
{
|
||||
collection.deviceType = .iphone
|
||||
self.screenshots += collection.screenshots
|
||||
}
|
||||
|
||||
if var collection = try container.decodeIfPresent(Collection.self, forKey: .ipad)
|
||||
{
|
||||
collection.deviceType = .ipad
|
||||
self.screenshots += collection.screenshots
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppScreenshots
|
||||
{
|
||||
struct Collection: Decodable
|
||||
{
|
||||
var screenshots: [AppScreenshot] = []
|
||||
|
||||
var deviceType: ALTDeviceType = .iphone {
|
||||
didSet {
|
||||
self.screenshots.forEach { $0.deviceType = self.deviceType }
|
||||
}
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws
|
||||
{
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
|
||||
var container = try decoder.unkeyedContainer()
|
||||
|
||||
while !container.isAtEnd
|
||||
{
|
||||
do
|
||||
{
|
||||
// Attempt parsing as URL first.
|
||||
let imageURL = try container.decode(URL.self)
|
||||
|
||||
let screenshot = AppScreenshot(imageURL: imageURL, size: nil, deviceType: self.deviceType, context: context)
|
||||
self.screenshots.append(screenshot)
|
||||
}
|
||||
catch DecodingError.typeMismatch
|
||||
{
|
||||
// Fall back to parsing full AppScreenshot (preferred).
|
||||
|
||||
let screenshot = try container.decode(AppScreenshot.self)
|
||||
self.screenshots.append(screenshot)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
|
||||
var permissionsByGlobalAppID = [String: Set<AnyHashable>]()
|
||||
var sortedVersionIDsByGlobalAppID = [String: NSOrderedSet]()
|
||||
var sortedScreenshotURLsByGlobalAppID = [String: NSOrderedSet]()
|
||||
var sortedScreenshotIDsByGlobalAppID = [String: NSOrderedSet]()
|
||||
|
||||
var featuredAppIDsBySourceID = [String: [String]]()
|
||||
|
||||
@@ -220,10 +220,10 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
}
|
||||
|
||||
// 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)
|
||||
let contextScreenshotIDs = NSOrderedSet(array: contextApp._screenshots.lazy.compactMap { $0 as? AppScreenshot }.map { $0.screenshotID })
|
||||
for case let databaseScreenshot as AppScreenshot in databaseObject._screenshots where !contextScreenshotIDs.contains(databaseScreenshot.screenshotID)
|
||||
{
|
||||
// Screenshot's imageURL does NOT exist in context, so delete existing databaseScreenshot.
|
||||
// Screenshot ID does NOT exist in context, so delete existing databaseScreenshot.
|
||||
databaseScreenshot.managedObjectContext?.delete(databaseScreenshot)
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
{
|
||||
permissionsByGlobalAppID[globallyUniqueID] = contextPermissions
|
||||
sortedVersionIDsByGlobalAppID[globallyUniqueID] = contextVersionIDs
|
||||
sortedScreenshotURLsByGlobalAppID[globallyUniqueID] = contextScreenshotURLs
|
||||
sortedScreenshotIDsByGlobalAppID[globallyUniqueID] = contextScreenshotIDs
|
||||
}
|
||||
|
||||
case let databaseObject as Source:
|
||||
@@ -313,27 +313,27 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
}
|
||||
|
||||
// Screenshots
|
||||
if let sortedScreenshotURLs = sortedScreenshotURLsByGlobalAppID[globallyUniqueID],
|
||||
let sortedScreenshotURLsArray = sortedScreenshotURLs.array as? [URL],
|
||||
case let databaseScreenshotURLs = databaseObject.screenshots.map({ $0.imageURL }),
|
||||
databaseScreenshotURLs != sortedScreenshotURLsArray
|
||||
if let sortedScreenshotIDs = sortedScreenshotIDsByGlobalAppID[globallyUniqueID],
|
||||
let sortedScreenshotIDsArray = sortedScreenshotIDs.array as? [String],
|
||||
case let databaseScreenshotIDs = databaseObject.allScreenshots.map({ $0.screenshotID }),
|
||||
databaseScreenshotIDs != sortedScreenshotIDsArray
|
||||
{
|
||||
// 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)
|
||||
let fixedScreenshots = databaseObject.allScreenshots.sorted { (screenshotA, screenshotB) in
|
||||
let indexA = sortedScreenshotIDs.index(of: screenshotA.screenshotID)
|
||||
let indexB = sortedScreenshotIDs.index(of: screenshotB.screenshotID)
|
||||
return indexA < indexB
|
||||
}
|
||||
|
||||
let appScreenshotURLs = fixedScreenshots.map { $0.imageURL }
|
||||
if appScreenshotURLs == sortedScreenshotURLsArray
|
||||
let appScreenshotIDs = fixedScreenshots.map { $0.screenshotID }
|
||||
if appScreenshotIDs == sortedScreenshotIDsArray
|
||||
{
|
||||
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)
|
||||
print("Failed to re-sort screenshots into correct order. Expected:", sortedScreenshotIDsArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
permission.sourceID = self.sourceIdentifier ?? ""
|
||||
}
|
||||
|
||||
for screenshot in self.screenshots
|
||||
for screenshot in self.allScreenshots
|
||||
{
|
||||
screenshot.sourceID = self.sourceIdentifier ?? ""
|
||||
}
|
||||
@@ -304,32 +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)
|
||||
let appScreenshots: [AppScreenshot]
|
||||
|
||||
if let screenshots = try container.decodeIfPresent(AppScreenshots.self, forKey: .screenshots)
|
||||
{
|
||||
for screenshot in screenshots
|
||||
{
|
||||
screenshot.appBundleID = self.bundleIdentifier
|
||||
}
|
||||
|
||||
self.setScreenshots(screenshots)
|
||||
appScreenshots = screenshots.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
|
||||
appScreenshots = screenshotURLs.map { imageURL in
|
||||
let screenshot = AppScreenshot(imageURL: imageURL, size: legacyAspectRatio, deviceType: .iphone, context: context)
|
||||
return screenshot
|
||||
}
|
||||
|
||||
self.setScreenshots(screenshots)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.setScreenshots([])
|
||||
appScreenshots = []
|
||||
}
|
||||
|
||||
for screenshot in appScreenshots
|
||||
{
|
||||
screenshot.appBundleID = self.bundleIdentifier
|
||||
}
|
||||
|
||||
self.setScreenshots(appScreenshots)
|
||||
|
||||
if let appPermissions = try container.decodeIfPresent(AppPermissions.self, forKey: .permissions)
|
||||
{
|
||||
@@ -464,6 +465,38 @@ internal extension StoreApp
|
||||
}
|
||||
}
|
||||
|
||||
public extension StoreApp
|
||||
{
|
||||
func screenshots(for deviceType: ALTDeviceType) -> [AppScreenshot]
|
||||
{
|
||||
//TODO: Support multiple device types
|
||||
let filteredScreenshots = self.allScreenshots.filter { $0.deviceType == deviceType }
|
||||
return filteredScreenshots
|
||||
}
|
||||
|
||||
func preferredScreenshots() -> [AppScreenshot]
|
||||
{
|
||||
let deviceType: ALTDeviceType
|
||||
|
||||
if UIDevice.current.model.contains("iPad")
|
||||
{
|
||||
deviceType = .ipad
|
||||
}
|
||||
else
|
||||
{
|
||||
deviceType = .iphone
|
||||
}
|
||||
|
||||
let preferredScreenshots = self.screenshots(for: deviceType)
|
||||
guard !preferredScreenshots.isEmpty else {
|
||||
// There are no screenshots for deviceType, so return _all_ screenshots instead.
|
||||
return self.allScreenshots
|
||||
}
|
||||
|
||||
return preferredScreenshots
|
||||
}
|
||||
}
|
||||
|
||||
public extension StoreApp
|
||||
{
|
||||
var latestAvailableVersion: AppVersion? {
|
||||
|
||||
Reference in New Issue
Block a user