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:
Riley Testut
2023-10-13 13:40:08 -05:00
committed by Magesh K
parent 57059967c6
commit a49e16f591
13 changed files with 258 additions and 76 deletions

View File

@@ -426,7 +426,6 @@
D5CD805D29CA2C1E00E591B0 /* HeaderContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CD805C29CA2C1E00E591B0 /* HeaderContentViewController.swift */; };
D5CD805F29CA755E00E591B0 /* SourceDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CD805E29CA755E00E591B0 /* SourceDetailViewController.swift */; };
D5CF568C2A0D8EEC006D93E2 /* VerificationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CF56812A0D83F9006D93E2 /* VerificationError.swift */; };
D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */; };
D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */; };
D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
D5DB145B28F9DC5C00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
@@ -1093,7 +1092,6 @@
D5CD805C29CA2C1E00E591B0 /* HeaderContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderContentViewController.swift; sourceTree = "<group>"; };
D5CD805E29CA755E00E591B0 /* SourceDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceDetailViewController.swift; sourceTree = "<group>"; };
D5CF56812A0D83F9006D93E2 /* VerificationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationError.swift; sourceTree = "<group>"; };
D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotProcessor.swift; sourceTree = "<group>"; };
D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePatronsOperation.swift; sourceTree = "<group>"; };
D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTLocalizedError.swift; sourceTree = "<group>"; };
D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateKnownSourcesOperation.swift; sourceTree = "<group>"; };
@@ -1398,7 +1396,6 @@
isa = PBXGroup;
children = (
BF41B807233433C100C593A3 /* LoadingState.swift */,
D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */,
D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */,
);
path = Types;
@@ -3218,7 +3215,6 @@
BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */,
D5CF568C2A0D8EEC006D93E2 /* VerificationError.swift in Sources */,
D5B6F6AB2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift in Sources */,
D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */,
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
BFE00A202503097F00EB4D0C /* INInteraction+AltStore.swift in Sources */,

View File

@@ -186,7 +186,7 @@ extension AppContentViewController
switch Row.allCases[indexPath.row]
{
case .screenshots:
guard !self.app.screenshots.isEmpty else { return 0.0 }
guard !self.app.allScreenshots.isEmpty else { return 0.0 }
return UITableView.automaticDimension
case .permissions:

View File

@@ -93,6 +93,25 @@ class AppScreenshotCollectionViewCell: UICollectionViewCell
}
}
extension AppScreenshotCollectionViewCell
{
func setImage(_ image: UIImage?)
{
guard var image, let cgImage = image.cgImage else {
self.imageView.image = image
return
}
if image.size.width > image.size.height && self.aspectRatio.width < self.aspectRatio.height
{
// Image is landscape, but cell has portrait aspect ratio, so rotate image to match.
image = UIImage(cgImage: cgImage, scale: image.scale, orientation: .right)
}
self.imageView.image = image
}
}
private extension AppScreenshotCollectionViewCell
{
func updateAspectRatio()

View File

@@ -98,16 +98,29 @@ private extension AppScreenshotsViewController
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>
{
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>(items: self.app.screenshots)
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
let screenshots = self.app.preferredScreenshots()
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>(items: screenshots)
dataSource.cellConfigurationHandler = { [weak self] (cell, screenshot, indexPath) in
let cell = cell as! AppScreenshotCollectionViewCell
cell.imageView.image = nil
cell.imageView.isIndicatingActivity = true
cell.setImage(nil)
var aspectRatio = screenshot.size ?? AppScreenshot.defaultAspectRatio
if aspectRatio.width > aspectRatio.height
{
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
switch screenshot.deviceType
{
case .iphone:
// Always rotate landscape iPhone screenshots regardless of horizontal size class.
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
case .ipad where self?.traitCollection.horizontalSizeClass == .compact:
// Only rotate landscape iPad screenshots if we're in horizontally compact environment.
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
default: break
}
}
cell.aspectRatio = aspectRatio
@@ -115,7 +128,7 @@ private extension AppScreenshotsViewController
dataSource.prefetchHandler = { (screenshot, indexPath, completionHandler) in
let imageURL = screenshot.imageURL
return RSTAsyncBlockOperation() { (operation) in
let request = ImageRequest(url: imageURL, processors: [.screenshot])
let request = ImageRequest(url: imageURL)
ImagePipeline.shared.loadImage(with: request, progress: nil) { result in
guard !operation.isCancelled else { return operation.finish() }
@@ -130,7 +143,7 @@ private extension AppScreenshotsViewController
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! AppScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false
cell.imageView.image = image
cell.setImage(image)
if let error = error
{

View File

@@ -106,16 +106,29 @@ private extension PreviewAppScreenshotsViewController
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>
{
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>(items: self.app.screenshots)
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
let screenshots = self.app.preferredScreenshots()
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>(items: screenshots)
dataSource.cellConfigurationHandler = { [weak self] (cell, screenshot, indexPath) in
let cell = cell as! AppScreenshotCollectionViewCell
cell.imageView.image = nil
cell.imageView.isIndicatingActivity = true
cell.setImage(nil)
var aspectRatio = screenshot.size ?? AppScreenshot.defaultAspectRatio
if aspectRatio.width > aspectRatio.height
{
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
switch screenshot.deviceType
{
case .iphone:
// Always rotate landscape iPhone screenshots regardless of horizontal size class.
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
case .ipad where self?.traitCollection.horizontalSizeClass == .compact:
// Only rotate landscape iPad screenshots if we're in horizontally compact environment.
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
default: break
}
}
cell.aspectRatio = aspectRatio
@@ -123,7 +136,7 @@ private extension PreviewAppScreenshotsViewController
dataSource.prefetchHandler = { (screenshot, indexPath, completionHandler) in
let imageURL = screenshot.imageURL
return RSTAsyncBlockOperation() { (operation) in
let request = ImageRequest(url: imageURL, processors: [.screenshot])
let request = ImageRequest(url: imageURL)
ImagePipeline.shared.loadImage(with: request, progress: nil) { result in
guard !operation.isCancelled else { return operation.finish() }
@@ -138,7 +151,7 @@ private extension PreviewAppScreenshotsViewController
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! AppScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false
cell.imageView.image = image
cell.setImage(image)
if let error = error
{

View File

@@ -53,7 +53,7 @@ private extension BrowseCollectionViewCell
}
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
return RSTAsyncBlockOperation() { (operation) in
let request = ImageRequest(url: imageURL as URL, processors: [.screenshot])
let request = ImageRequest(url: imageURL as URL)
ImagePipeline.shared.loadImage(with: request, progress: nil) { result in
guard !operation.isCancelled else { return operation.finish() }

View File

@@ -23,6 +23,7 @@ extension SourceError
case duplicate
case missingPermissionUsageDescription
case missingScreenshotSize
}
static func unsupported(_ source: Source) -> SourceError { SourceError(code: .unsupported, source: source) }
@@ -36,6 +37,10 @@ extension SourceError
static func missingPermissionUsageDescription(for permission: any ALTAppPermission, app: StoreApp, source: Source) -> SourceError {
SourceError(code: .missingPermissionUsageDescription, source: source, app: app, permission: permission)
}
static func missingScreenshotSize(for screenshot: AppScreenshot, source: Source) -> SourceError {
SourceError(code: .missingScreenshotSize, source: source, app: screenshot.app, screenshotURL: screenshot.imageURL)
}
}
struct SourceError: ALTLocalizedError
@@ -59,6 +64,9 @@ struct SourceError: ALTLocalizedError
@UserInfoValue
var permission: (any ALTAppPermission)?
@UserInfoValue
var screenshotURL: URL?
var errorFailureReason: String {
switch self.code
{
@@ -112,6 +120,14 @@ struct SourceError: ALTLocalizedError
let permissionType = permission.type.localizedName ?? NSLocalizedString("Permission", comment: "")
let failureReason = String(format: NSLocalizedString("The %@ '%@' for %@ is missing a usage description.", comment: ""), permissionType.lowercased(), permission.rawValue, appName)
return failureReason
case .missingScreenshotSize:
let appName = self.$app.name ?? String(format: NSLocalizedString("an app in source “%@”", comment: ""), self.$source.name)
let baseMessage = String(format: NSLocalizedString("An iPad screenshot for %@ does not specify its size", comment: ""), appName)
guard let screenshotURL else { return baseMessage + "." }
let failureReason = baseMessage + ": \(screenshotURL.absoluteString)"
return failureReason
}
}

View File

@@ -209,6 +209,12 @@ private extension FetchSourceOperation
// Privacy permissions MUST have a usage description.
guard permission.usageDescription != nil else { throw SourceError.missingPermissionUsageDescription(for: permission.permission, app: app, source: source) }
}
for screenshot in app.screenshots(for: .ipad)
{
// All iPad screenshots MUST have an explicit size.
guard screenshot.size != nil else { throw SourceError.missingScreenshotSize(for: screenshot, source: source) }
}
}
if let previousSourceID = self.$source.identifier

View File

@@ -1,27 +0,0 @@
//
// ScreenshotProcessor.swift
// AltStore
//
// Created by Riley Testut on 4/11/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import Nuke
struct ScreenshotProcessor: ImageProcessing
{
var identifier: String { "io.altstore.ScreenshotProcessor" }
func process(_ image: PlatformImage) -> PlatformImage?
{
guard let cgImage = image.cgImage, image.size.width > image.size.height else { return image }
let rotatedImage = UIImage(cgImage: cgImage, scale: image.scale, orientation: .right)
return rotatedImage
}
}
extension ImageProcessing where Self == ScreenshotProcessor
{
static var screenshot: Self { Self() }
}

View File

@@ -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>

View File

@@ -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)
}
}
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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? {