From a49e16f5911f4b3fe77d6b0a95d83337c815fa74 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 13 Oct 2023 13:40:08 -0500 Subject: [PATCH] Supports both iPhone + iPad screenshots Prefers showing screenshots for current device, but falls back to all screenshots if there are no relevant ones. --- AltStore.xcodeproj/project.pbxproj | 4 - .../App Detail/AppContentViewController.swift | 2 +- .../AppScreenshotCollectionViewCell.swift | 19 +++ .../AppScreenshotsViewController.swift | 25 +++- .../PreviewAppScreenshotsViewController.swift | 25 +++- .../Browse/BrowseCollectionViewCell.swift | 2 +- AltStore/Operations/Errors/SourceError.swift | 16 +++ .../Operations/FetchSourceOperation.swift | 6 + AltStore/Types/ScreenshotProcessor.swift | 27 ----- .../AltStore 13.xcdatamodel/contents | 4 +- AltStoreCore/Model/AppScreenshot.swift | 113 +++++++++++++++++- AltStoreCore/Model/MergePolicy.swift | 30 ++--- AltStoreCore/Model/StoreApp.swift | 61 +++++++--- 13 files changed, 258 insertions(+), 76 deletions(-) delete mode 100644 AltStore/Types/ScreenshotProcessor.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 62075ca3..1cff0060 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -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 = ""; }; D5CD805E29CA755E00E591B0 /* SourceDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceDetailViewController.swift; sourceTree = ""; }; D5CF56812A0D83F9006D93E2 /* VerificationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationError.swift; sourceTree = ""; }; - D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotProcessor.swift; sourceTree = ""; }; D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePatronsOperation.swift; sourceTree = ""; }; D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTLocalizedError.swift; sourceTree = ""; }; D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateKnownSourcesOperation.swift; sourceTree = ""; }; @@ -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 */, diff --git a/AltStore/App Detail/AppContentViewController.swift b/AltStore/App Detail/AppContentViewController.swift index 5eff1864..2cc4a544 100644 --- a/AltStore/App Detail/AppContentViewController.swift +++ b/AltStore/App Detail/AppContentViewController.swift @@ -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: diff --git a/AltStore/App Detail/Screenshots/AppScreenshotCollectionViewCell.swift b/AltStore/App Detail/Screenshots/AppScreenshotCollectionViewCell.swift index 426595b7..803ee034 100644 --- a/AltStore/App Detail/Screenshots/AppScreenshotCollectionViewCell.swift +++ b/AltStore/App Detail/Screenshots/AppScreenshotCollectionViewCell.swift @@ -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() diff --git a/AltStore/App Detail/Screenshots/AppScreenshotsViewController.swift b/AltStore/App Detail/Screenshots/AppScreenshotsViewController.swift index 4a88e31a..46b89eb1 100644 --- a/AltStore/App Detail/Screenshots/AppScreenshotsViewController.swift +++ b/AltStore/App Detail/Screenshots/AppScreenshotsViewController.swift @@ -98,16 +98,29 @@ private extension AppScreenshotsViewController func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource { - let dataSource = RSTArrayCollectionViewPrefetchingDataSource(items: self.app.screenshots) - dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in + let screenshots = self.app.preferredScreenshots() + + let dataSource = RSTArrayCollectionViewPrefetchingDataSource(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 { diff --git a/AltStore/App Detail/Screenshots/PreviewAppScreenshotsViewController.swift b/AltStore/App Detail/Screenshots/PreviewAppScreenshotsViewController.swift index 9dd40797..8ffed408 100644 --- a/AltStore/App Detail/Screenshots/PreviewAppScreenshotsViewController.swift +++ b/AltStore/App Detail/Screenshots/PreviewAppScreenshotsViewController.swift @@ -106,16 +106,29 @@ private extension PreviewAppScreenshotsViewController func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource { - let dataSource = RSTArrayCollectionViewPrefetchingDataSource(items: self.app.screenshots) - dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in + let screenshots = self.app.preferredScreenshots() + + let dataSource = RSTArrayCollectionViewPrefetchingDataSource(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 { diff --git a/AltStore/Browse/BrowseCollectionViewCell.swift b/AltStore/Browse/BrowseCollectionViewCell.swift index 2fae77bd..bcbf8ff6 100644 --- a/AltStore/Browse/BrowseCollectionViewCell.swift +++ b/AltStore/Browse/BrowseCollectionViewCell.swift @@ -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() } diff --git a/AltStore/Operations/Errors/SourceError.swift b/AltStore/Operations/Errors/SourceError.swift index 6a5450bc..bbf0110d 100644 --- a/AltStore/Operations/Errors/SourceError.swift +++ b/AltStore/Operations/Errors/SourceError.swift @@ -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 } } diff --git a/AltStore/Operations/FetchSourceOperation.swift b/AltStore/Operations/FetchSourceOperation.swift index 914f3376..9ce9a4a3 100644 --- a/AltStore/Operations/FetchSourceOperation.swift +++ b/AltStore/Operations/FetchSourceOperation.swift @@ -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 diff --git a/AltStore/Types/ScreenshotProcessor.swift b/AltStore/Types/ScreenshotProcessor.swift deleted file mode 100644 index 2f3f6b45..00000000 --- a/AltStore/Types/ScreenshotProcessor.swift +++ /dev/null @@ -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() } -} diff --git a/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 13.xcdatamodel/contents b/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 13.xcdatamodel/contents index 2f921b6e..dc9d4873 100644 --- a/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 13.xcdatamodel/contents +++ b/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 13.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -44,6 +44,7 @@ + @@ -52,6 +53,7 @@ + diff --git a/AltStoreCore/Model/AppScreenshot.swift b/AltStoreCore/Model/AppScreenshot.swift index f2a2be0b..8a143a34 100644 --- a/AltStoreCore/Model/AppScreenshot.swift +++ b/AltStoreCore/Model/AppScreenshot.swift @@ -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(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 + + 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) + } + } + } + } +} diff --git a/AltStoreCore/Model/MergePolicy.swift b/AltStoreCore/Model/MergePolicy.swift index 134800dd..4bac336c 100644 --- a/AltStoreCore/Model/MergePolicy.swift +++ b/AltStoreCore/Model/MergePolicy.swift @@ -189,7 +189,7 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy var permissionsByGlobalAppID = [String: Set]() 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) } } } diff --git a/AltStoreCore/Model/StoreApp.swift b/AltStoreCore/Model/StoreApp.swift index 4741eb1a..1f97895d 100644 --- a/AltStoreCore/Model/StoreApp.swift +++ b/AltStoreCore/Model/StoreApp.swift @@ -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? {