mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-18 19:23:43 +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:
@@ -426,7 +426,6 @@
|
|||||||
D5CD805D29CA2C1E00E591B0 /* HeaderContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CD805C29CA2C1E00E591B0 /* HeaderContentViewController.swift */; };
|
D5CD805D29CA2C1E00E591B0 /* HeaderContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CD805C29CA2C1E00E591B0 /* HeaderContentViewController.swift */; };
|
||||||
D5CD805F29CA755E00E591B0 /* SourceDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CD805E29CA755E00E591B0 /* SourceDetailViewController.swift */; };
|
D5CD805F29CA755E00E591B0 /* SourceDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CD805E29CA755E00E591B0 /* SourceDetailViewController.swift */; };
|
||||||
D5CF568C2A0D8EEC006D93E2 /* VerificationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CF56812A0D83F9006D93E2 /* VerificationError.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 */; };
|
D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */; };
|
||||||
D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
|
D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
|
||||||
D5DB145B28F9DC5C00A8F606 /* 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateKnownSourcesOperation.swift; sourceTree = "<group>"; };
|
||||||
@@ -1398,7 +1396,6 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
BF41B807233433C100C593A3 /* LoadingState.swift */,
|
BF41B807233433C100C593A3 /* LoadingState.swift */,
|
||||||
D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */,
|
|
||||||
D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */,
|
D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */,
|
||||||
);
|
);
|
||||||
path = Types;
|
path = Types;
|
||||||
@@ -3218,7 +3215,6 @@
|
|||||||
BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */,
|
BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */,
|
||||||
D5CF568C2A0D8EEC006D93E2 /* VerificationError.swift in Sources */,
|
D5CF568C2A0D8EEC006D93E2 /* VerificationError.swift in Sources */,
|
||||||
D5B6F6AB2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift in Sources */,
|
D5B6F6AB2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift in Sources */,
|
||||||
D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */,
|
|
||||||
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
|
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
|
||||||
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
|
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
|
||||||
BFE00A202503097F00EB4D0C /* INInteraction+AltStore.swift in Sources */,
|
BFE00A202503097F00EB4D0C /* INInteraction+AltStore.swift in Sources */,
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ extension AppContentViewController
|
|||||||
switch Row.allCases[indexPath.row]
|
switch Row.allCases[indexPath.row]
|
||||||
{
|
{
|
||||||
case .screenshots:
|
case .screenshots:
|
||||||
guard !self.app.screenshots.isEmpty else { return 0.0 }
|
guard !self.app.allScreenshots.isEmpty else { return 0.0 }
|
||||||
return UITableView.automaticDimension
|
return UITableView.automaticDimension
|
||||||
|
|
||||||
case .permissions:
|
case .permissions:
|
||||||
|
|||||||
@@ -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
|
private extension AppScreenshotCollectionViewCell
|
||||||
{
|
{
|
||||||
func updateAspectRatio()
|
func updateAspectRatio()
|
||||||
|
|||||||
@@ -98,16 +98,29 @@ private extension AppScreenshotsViewController
|
|||||||
|
|
||||||
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>
|
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>
|
||||||
{
|
{
|
||||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>(items: self.app.screenshots)
|
let screenshots = self.app.preferredScreenshots()
|
||||||
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
|
||||||
|
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>(items: screenshots)
|
||||||
|
dataSource.cellConfigurationHandler = { [weak self] (cell, screenshot, indexPath) in
|
||||||
let cell = cell as! AppScreenshotCollectionViewCell
|
let cell = cell as! AppScreenshotCollectionViewCell
|
||||||
cell.imageView.image = nil
|
|
||||||
cell.imageView.isIndicatingActivity = true
|
cell.imageView.isIndicatingActivity = true
|
||||||
|
cell.setImage(nil)
|
||||||
|
|
||||||
var aspectRatio = screenshot.size ?? AppScreenshot.defaultAspectRatio
|
var aspectRatio = screenshot.size ?? AppScreenshot.defaultAspectRatio
|
||||||
if aspectRatio.width > aspectRatio.height
|
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
|
cell.aspectRatio = aspectRatio
|
||||||
@@ -115,7 +128,7 @@ private extension AppScreenshotsViewController
|
|||||||
dataSource.prefetchHandler = { (screenshot, indexPath, completionHandler) in
|
dataSource.prefetchHandler = { (screenshot, indexPath, completionHandler) in
|
||||||
let imageURL = screenshot.imageURL
|
let imageURL = screenshot.imageURL
|
||||||
return RSTAsyncBlockOperation() { (operation) in
|
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
|
ImagePipeline.shared.loadImage(with: request, progress: nil) { result in
|
||||||
guard !operation.isCancelled else { return operation.finish() }
|
guard !operation.isCancelled else { return operation.finish() }
|
||||||
|
|
||||||
@@ -130,7 +143,7 @@ private extension AppScreenshotsViewController
|
|||||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
let cell = cell as! AppScreenshotCollectionViewCell
|
let cell = cell as! AppScreenshotCollectionViewCell
|
||||||
cell.imageView.isIndicatingActivity = false
|
cell.imageView.isIndicatingActivity = false
|
||||||
cell.imageView.image = image
|
cell.setImage(image)
|
||||||
|
|
||||||
if let error = error
|
if let error = error
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -106,16 +106,29 @@ private extension PreviewAppScreenshotsViewController
|
|||||||
|
|
||||||
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>
|
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>
|
||||||
{
|
{
|
||||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>(items: self.app.screenshots)
|
let screenshots = self.app.preferredScreenshots()
|
||||||
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
|
||||||
|
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>(items: screenshots)
|
||||||
|
dataSource.cellConfigurationHandler = { [weak self] (cell, screenshot, indexPath) in
|
||||||
let cell = cell as! AppScreenshotCollectionViewCell
|
let cell = cell as! AppScreenshotCollectionViewCell
|
||||||
cell.imageView.image = nil
|
|
||||||
cell.imageView.isIndicatingActivity = true
|
cell.imageView.isIndicatingActivity = true
|
||||||
|
cell.setImage(nil)
|
||||||
|
|
||||||
var aspectRatio = screenshot.size ?? AppScreenshot.defaultAspectRatio
|
var aspectRatio = screenshot.size ?? AppScreenshot.defaultAspectRatio
|
||||||
if aspectRatio.width > aspectRatio.height
|
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
|
cell.aspectRatio = aspectRatio
|
||||||
@@ -123,7 +136,7 @@ private extension PreviewAppScreenshotsViewController
|
|||||||
dataSource.prefetchHandler = { (screenshot, indexPath, completionHandler) in
|
dataSource.prefetchHandler = { (screenshot, indexPath, completionHandler) in
|
||||||
let imageURL = screenshot.imageURL
|
let imageURL = screenshot.imageURL
|
||||||
return RSTAsyncBlockOperation() { (operation) in
|
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
|
ImagePipeline.shared.loadImage(with: request, progress: nil) { result in
|
||||||
guard !operation.isCancelled else { return operation.finish() }
|
guard !operation.isCancelled else { return operation.finish() }
|
||||||
|
|
||||||
@@ -138,7 +151,7 @@ private extension PreviewAppScreenshotsViewController
|
|||||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
let cell = cell as! AppScreenshotCollectionViewCell
|
let cell = cell as! AppScreenshotCollectionViewCell
|
||||||
cell.imageView.isIndicatingActivity = false
|
cell.imageView.isIndicatingActivity = false
|
||||||
cell.imageView.image = image
|
cell.setImage(image)
|
||||||
|
|
||||||
if let error = error
|
if let error = error
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ private extension BrowseCollectionViewCell
|
|||||||
}
|
}
|
||||||
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
|
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
|
||||||
return RSTAsyncBlockOperation() { (operation) 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
|
ImagePipeline.shared.loadImage(with: request, progress: nil) { result in
|
||||||
guard !operation.isCancelled else { return operation.finish() }
|
guard !operation.isCancelled else { return operation.finish() }
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ extension SourceError
|
|||||||
case duplicate
|
case duplicate
|
||||||
|
|
||||||
case missingPermissionUsageDescription
|
case missingPermissionUsageDescription
|
||||||
|
case missingScreenshotSize
|
||||||
}
|
}
|
||||||
|
|
||||||
static func unsupported(_ source: Source) -> SourceError { SourceError(code: .unsupported, source: source) }
|
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 {
|
static func missingPermissionUsageDescription(for permission: any ALTAppPermission, app: StoreApp, source: Source) -> SourceError {
|
||||||
SourceError(code: .missingPermissionUsageDescription, source: source, app: app, permission: permission)
|
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
|
struct SourceError: ALTLocalizedError
|
||||||
@@ -59,6 +64,9 @@ struct SourceError: ALTLocalizedError
|
|||||||
@UserInfoValue
|
@UserInfoValue
|
||||||
var permission: (any ALTAppPermission)?
|
var permission: (any ALTAppPermission)?
|
||||||
|
|
||||||
|
@UserInfoValue
|
||||||
|
var screenshotURL: URL?
|
||||||
|
|
||||||
var errorFailureReason: String {
|
var errorFailureReason: String {
|
||||||
switch self.code
|
switch self.code
|
||||||
{
|
{
|
||||||
@@ -112,6 +120,14 @@ struct SourceError: ALTLocalizedError
|
|||||||
let permissionType = permission.type.localizedName ?? NSLocalizedString("Permission", comment: "")
|
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)
|
let failureReason = String(format: NSLocalizedString("The %@ '%@' for %@ is missing a usage description.", comment: ""), permissionType.lowercased(), permission.rawValue, appName)
|
||||||
return failureReason
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,12 @@ private extension FetchSourceOperation
|
|||||||
// Privacy permissions MUST have a usage description.
|
// Privacy permissions MUST have a usage description.
|
||||||
guard permission.usageDescription != nil else { throw SourceError.missingPermissionUsageDescription(for: permission.permission, app: app, source: source) }
|
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
|
if let previousSourceID = self.$source.identifier
|
||||||
|
|||||||
@@ -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() }
|
|
||||||
}
|
|
||||||
@@ -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="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">
|
<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"/>
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
</entity>
|
</entity>
|
||||||
<entity name="AppScreenshot" representedClassName="AppScreenshot" syncable="YES">
|
<entity name="AppScreenshot" representedClassName="AppScreenshot" syncable="YES">
|
||||||
<attribute name="appBundleID" attributeType="String"/>
|
<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="height" optional="YES" attributeType="Integer 16" usesScalarValueType="NO"/>
|
||||||
<attribute name="imageURL" attributeType="URI"/>
|
<attribute name="imageURL" attributeType="URI"/>
|
||||||
<attribute name="sourceID" attributeType="String"/>
|
<attribute name="sourceID" attributeType="String"/>
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
<uniquenessConstraints>
|
<uniquenessConstraints>
|
||||||
<uniquenessConstraint>
|
<uniquenessConstraint>
|
||||||
<constraint value="imageURL"/>
|
<constraint value="imageURL"/>
|
||||||
|
<constraint value="deviceType"/>
|
||||||
<constraint value="appBundleID"/>
|
<constraint value="appBundleID"/>
|
||||||
<constraint value="sourceID"/>
|
<constraint value="sourceID"/>
|
||||||
</uniquenessConstraint>
|
</uniquenessConstraint>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
public extension AppScreenshot
|
public extension AppScreenshot
|
||||||
{
|
{
|
||||||
static let defaultAspectRatio = CGSize(width: 9, height: 19.5)
|
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 width: NSNumber?
|
||||||
@NSManaged private var height: 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 appBundleID: String
|
||||||
@NSManaged public internal(set) var sourceID: String
|
@NSManaged public internal(set) var sourceID: String
|
||||||
|
|
||||||
@@ -58,12 +67,13 @@ public class AppScreenshot: NSManagedObject, Fetchable, Decodable
|
|||||||
super.init(entity: entity, insertInto: context)
|
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)
|
super.init(entity: AppScreenshot.entity(), insertInto: context)
|
||||||
|
|
||||||
self.imageURL = imageURL
|
self.imageURL = imageURL
|
||||||
self.size = size
|
self.size = size
|
||||||
|
self.deviceType = deviceType
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws
|
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.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) }
|
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
|
public extension AppScreenshot
|
||||||
@@ -88,3 +113,89 @@ public extension AppScreenshot
|
|||||||
return NSFetchRequest<AppScreenshot>(entityName: "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 permissionsByGlobalAppID = [String: Set<AnyHashable>]()
|
||||||
var sortedVersionIDsByGlobalAppID = [String: NSOrderedSet]()
|
var sortedVersionIDsByGlobalAppID = [String: NSOrderedSet]()
|
||||||
var sortedScreenshotURLsByGlobalAppID = [String: NSOrderedSet]()
|
var sortedScreenshotIDsByGlobalAppID = [String: NSOrderedSet]()
|
||||||
|
|
||||||
var featuredAppIDsBySourceID = [String: [String]]()
|
var featuredAppIDsBySourceID = [String: [String]]()
|
||||||
|
|
||||||
@@ -220,10 +220,10 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Screenshots
|
// Screenshots
|
||||||
let contextScreenshotURLs = NSOrderedSet(array: contextApp._screenshots.lazy.compactMap { $0 as? AppScreenshot }.map { $0.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 !contextScreenshotURLs.contains(databaseScreenshot.imageURL)
|
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)
|
databaseScreenshot.managedObjectContext?.delete(databaseScreenshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|||||||
{
|
{
|
||||||
permissionsByGlobalAppID[globallyUniqueID] = contextPermissions
|
permissionsByGlobalAppID[globallyUniqueID] = contextPermissions
|
||||||
sortedVersionIDsByGlobalAppID[globallyUniqueID] = contextVersionIDs
|
sortedVersionIDsByGlobalAppID[globallyUniqueID] = contextVersionIDs
|
||||||
sortedScreenshotURLsByGlobalAppID[globallyUniqueID] = contextScreenshotURLs
|
sortedScreenshotIDsByGlobalAppID[globallyUniqueID] = contextScreenshotIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
case let databaseObject as Source:
|
case let databaseObject as Source:
|
||||||
@@ -313,27 +313,27 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Screenshots
|
// Screenshots
|
||||||
if let sortedScreenshotURLs = sortedScreenshotURLsByGlobalAppID[globallyUniqueID],
|
if let sortedScreenshotIDs = sortedScreenshotIDsByGlobalAppID[globallyUniqueID],
|
||||||
let sortedScreenshotURLsArray = sortedScreenshotURLs.array as? [URL],
|
let sortedScreenshotIDsArray = sortedScreenshotIDs.array as? [String],
|
||||||
case let databaseScreenshotURLs = databaseObject.screenshots.map({ $0.imageURL }),
|
case let databaseScreenshotIDs = databaseObject.allScreenshots.map({ $0.screenshotID }),
|
||||||
databaseScreenshotURLs != sortedScreenshotURLsArray
|
databaseScreenshotIDs != sortedScreenshotIDsArray
|
||||||
{
|
{
|
||||||
// Screenshot order is incorrect, so attempt to fix by re-sorting.
|
// Screenshot order is incorrect, so attempt to fix by re-sorting.
|
||||||
let fixedScreenshots = databaseObject.screenshots.sorted { (screenshotA, screenshotB) in
|
let fixedScreenshots = databaseObject.allScreenshots.sorted { (screenshotA, screenshotB) in
|
||||||
let indexA = sortedScreenshotURLs.index(of: screenshotA.imageURL)
|
let indexA = sortedScreenshotIDs.index(of: screenshotA.screenshotID)
|
||||||
let indexB = sortedScreenshotURLs.index(of: screenshotB.imageURL)
|
let indexB = sortedScreenshotIDs.index(of: screenshotB.screenshotID)
|
||||||
return indexA < indexB
|
return indexA < indexB
|
||||||
}
|
}
|
||||||
|
|
||||||
let appScreenshotURLs = fixedScreenshots.map { $0.imageURL }
|
let appScreenshotIDs = fixedScreenshots.map { $0.screenshotID }
|
||||||
if appScreenshotURLs == sortedScreenshotURLsArray
|
if appScreenshotIDs == sortedScreenshotIDsArray
|
||||||
{
|
{
|
||||||
databaseObject.setScreenshots(fixedScreenshots)
|
databaseObject.setScreenshots(fixedScreenshots)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Screenshots are still not in correct order, but not worth throwing error so ignore.
|
// 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 ?? ""
|
permission.sourceID = self.sourceIdentifier ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
for screenshot in self.screenshots
|
for screenshot in self.allScreenshots
|
||||||
{
|
{
|
||||||
screenshot.sourceID = self.sourceIdentifier ?? ""
|
screenshot.sourceID = self.sourceIdentifier ?? ""
|
||||||
}
|
}
|
||||||
@@ -304,32 +304,33 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
|||||||
|
|
||||||
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
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
|
appScreenshots = screenshots.screenshots
|
||||||
{
|
|
||||||
screenshot.appBundleID = self.bundleIdentifier
|
|
||||||
}
|
|
||||||
|
|
||||||
self.setScreenshots(screenshots)
|
|
||||||
}
|
}
|
||||||
else if let screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs)
|
else if let screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs)
|
||||||
{
|
{
|
||||||
// Assume 9:16 iPhone 8 screen dimensions for legacy screenshotURLs.
|
// Assume 9:16 iPhone 8 screen dimensions for legacy screenshotURLs.
|
||||||
let legacyAspectRatio = CGSize(width: 750, height: 1334)
|
let legacyAspectRatio = CGSize(width: 750, height: 1334)
|
||||||
|
|
||||||
let screenshots = screenshotURLs.map { imageURL in
|
appScreenshots = screenshotURLs.map { imageURL in
|
||||||
let screenshot = AppScreenshot(imageURL: imageURL, size: legacyAspectRatio, context: context)
|
let screenshot = AppScreenshot(imageURL: imageURL, size: legacyAspectRatio, deviceType: .iphone, context: context)
|
||||||
screenshot.appBundleID = self.bundleIdentifier
|
|
||||||
return screenshot
|
return screenshot
|
||||||
}
|
}
|
||||||
|
|
||||||
self.setScreenshots(screenshots)
|
|
||||||
}
|
}
|
||||||
else
|
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)
|
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
|
public extension StoreApp
|
||||||
{
|
{
|
||||||
var latestAvailableVersion: AppVersion? {
|
var latestAvailableVersion: AppVersion? {
|
||||||
|
|||||||
Reference in New Issue
Block a user