mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
* Change error from Swift.Error to NSError
* Adds ResultOperation.localizedFailure
* Finish Riley's monster commit
3b38d725d7
May the Gods have mercy on my soul.
* Fix format strings I broke
* Include "Enable JIT" errors in Error Log
* Fix minimuxer status checking
* [skip ci] Update the no wifi message to include VPN
* Opens Error Log when tapping ToastView
* Fixes Error Log context menu covering cell content
* Fixes Error Log context menu appearing while scrolling
* Fixes incorrect Search FAQ URL
* Fix Error Log showing UIAlertController on iOS 14+
* Fix Error Log not showing UIAlertController on iOS <=13
* Fix wrong color in AuthenticationViewController
* Fix typo
* Fixes logging non-AltServerErrors as AltServerError.underlyingError
* Limits quitting other AltStore/SideStore processes to database migrations
* Skips logging cancelled errors
* Replaces StoreApp.latestVersion with latestSupportedVersion + latestAvailableVersion
We now store the latest supported version as a relationship on StoreApp, rather than the latest available version. This allows us to reference the latest supported version in predicates and sort descriptors.
However, we kept the underlying Core Data property name the same to avoid extra migration.
* Conforms OperatingSystemVersion to Comparable
* Parses AppVersion.minOSVersion/maxOSVersion from source JSON
* Supports non-NSManagedObjects for @Managed properties
This allows us to use @Managed with properties that may or may not be NSManagedObjects at runtime (e.g. protocols). If they are, Managed will keep strong reference to context like before.
* Supports optional @Managed properties
* Conforms AppVersion to AppProtocol
* Verifies min/max OS version before downloading app + asks user to download older app version if necessary
* Improves error message when file does not exist at AppVersion.downloadURL
* Removes unnecessary StoreApp convenience properties
* Removes unnecessary StoreApp convenience properties as well as fix other issues
* Remove Settings bundle, add SwiftUI view instead
Fix refresh all shortcut intent
* Update AuthenticationOperation.swift
Signed-off-by: June Park <rjp2030@outlook.com>
* Fix build issues given by develop
* Add availability check to fix CI build(?)
* If it's gonna be that way...
---------
Signed-off-by: June Park <rjp2030@outlook.com>
Co-authored-by: nythepegasus <nythepegasus84@gmail.com>
Co-authored-by: Riley Testut <riley@rileytestut.com>
Co-authored-by: ny <me@nythepegas.us>
364 lines
13 KiB
Swift
364 lines
13 KiB
Swift
//
|
|
// StoreApp.swift
|
|
// AltStore
|
|
//
|
|
// Created by Riley Testut on 5/20/19.
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import CoreData
|
|
|
|
import Roxas
|
|
import AltSign
|
|
|
|
public extension StoreApp
|
|
{
|
|
#if ALPHA
|
|
static let altstoreAppID = Bundle.Info.appbundleIdentifier
|
|
#elseif BETA
|
|
static let altstoreAppID = Bundle.Info.appbundleIdentifier
|
|
#else
|
|
static let altstoreAppID = Bundle.Info.appbundleIdentifier
|
|
#endif
|
|
|
|
static let dolphinAppID = "me.oatmealdome.dolphinios-njb"
|
|
}
|
|
|
|
@objc
|
|
public enum Platform: UInt, Codable {
|
|
case ios
|
|
case tvos
|
|
case macos
|
|
}
|
|
|
|
@objc
|
|
public final class PlatformURL: NSManagedObject, Decodable {
|
|
/* Properties */
|
|
@NSManaged public private(set) var platform: Platform
|
|
@NSManaged public private(set) var downloadURL: URL
|
|
|
|
|
|
private enum CodingKeys: String, CodingKey
|
|
{
|
|
case platform
|
|
case downloadURL
|
|
}
|
|
|
|
|
|
public init(from decoder: Decoder) throws
|
|
{
|
|
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
|
|
|
// Must initialize with context in order for child context saves to work correctly.
|
|
super.init(entity: PlatformURL.entity(), insertInto: context)
|
|
|
|
do
|
|
{
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
self.platform = try container.decode(Platform.self, forKey: .platform)
|
|
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
|
}
|
|
catch
|
|
{
|
|
if let context = self.managedObjectContext
|
|
{
|
|
context.delete(self)
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PlatformURL: Comparable {
|
|
public static func < (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
|
|
return lhs.platform.rawValue < rhs.platform.rawValue
|
|
}
|
|
|
|
public static func > (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
|
|
return lhs.platform.rawValue > rhs.platform.rawValue
|
|
}
|
|
|
|
public static func <= (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
|
|
return lhs.platform.rawValue <= rhs.platform.rawValue
|
|
}
|
|
|
|
public static func >= (lhs: PlatformURL, rhs: PlatformURL) -> Bool {
|
|
return lhs.platform.rawValue >= rhs.platform.rawValue
|
|
}
|
|
}
|
|
|
|
public typealias PlatformURLs = [PlatformURL]
|
|
|
|
@objc(StoreApp)
|
|
public class StoreApp: NSManagedObject, Decodable, Fetchable
|
|
{
|
|
/* Properties */
|
|
@NSManaged public private(set) var name: String
|
|
@NSManaged public private(set) var bundleIdentifier: String
|
|
@NSManaged public private(set) var subtitle: String?
|
|
|
|
@NSManaged public private(set) var developerName: String
|
|
@NSManaged public private(set) var localizedDescription: String
|
|
@NSManaged @objc(size) internal var _size: Int32
|
|
|
|
@NSManaged public private(set) var iconURL: URL
|
|
@NSManaged public private(set) var screenshotURLs: [URL]
|
|
|
|
@NSManaged @objc(version) internal var _version: String
|
|
@NSManaged @objc(versionDate) internal var _versionDate: Date
|
|
@NSManaged @objc(versionDescription) internal var _versionDescription: String?
|
|
|
|
@NSManaged @objc(downloadURL) internal var _downloadURL: URL
|
|
@NSManaged public private(set) var platformURLs: PlatformURLs?
|
|
|
|
@NSManaged public private(set) var tintColor: UIColor?
|
|
@NSManaged public private(set) var isBeta: Bool
|
|
|
|
@objc public internal(set) var sourceIdentifier: String? {
|
|
get {
|
|
self.willAccessValue(forKey: #keyPath(sourceIdentifier))
|
|
defer { self.didAccessValue(forKey: #keyPath(sourceIdentifier)) }
|
|
|
|
let sourceIdentifier = self.primitiveSourceIdentifier
|
|
return sourceIdentifier
|
|
}
|
|
set {
|
|
self.willChangeValue(forKey: #keyPath(sourceIdentifier))
|
|
self.primitiveSourceIdentifier = newValue
|
|
self.didChangeValue(forKey: #keyPath(sourceIdentifier))
|
|
|
|
for version in self.versions
|
|
{
|
|
version.sourceID = newValue
|
|
}
|
|
}
|
|
}
|
|
@NSManaged private var primitiveSourceIdentifier: String?
|
|
|
|
@NSManaged public var sortIndex: Int32
|
|
|
|
/* Relationships */
|
|
@NSManaged public var installedApp: InstalledApp?
|
|
@NSManaged public var newsItems: Set<NewsItem>
|
|
|
|
@NSManaged @objc(source) public var _source: Source?
|
|
@NSManaged @objc(permissions) public var _permissions: NSOrderedSet
|
|
|
|
@NSManaged @objc(latestVersion) public private(set) var latestSupportedVersion: AppVersion?
|
|
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
|
|
|
|
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
|
|
|
|
@nonobjc public var source: Source? {
|
|
set {
|
|
self._source = newValue
|
|
self.sourceIdentifier = newValue?.identifier
|
|
}
|
|
get {
|
|
return self._source
|
|
}
|
|
}
|
|
|
|
@nonobjc public var permissions: [AppPermission] {
|
|
return self._permissions.array as! [AppPermission]
|
|
}
|
|
|
|
@nonobjc public var versions: [AppVersion] {
|
|
return self._versions.array as! [AppVersion]
|
|
}
|
|
|
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
|
{
|
|
super.init(entity: entity, insertInto: context)
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey
|
|
{
|
|
case name
|
|
case bundleIdentifier
|
|
case developerName
|
|
case localizedDescription
|
|
case version
|
|
case versionDescription
|
|
case versionDate
|
|
case iconURL
|
|
case screenshotURLs
|
|
case downloadURL
|
|
case platformURLs
|
|
case tintColor
|
|
case subtitle
|
|
case permissions
|
|
case size
|
|
case isBeta = "beta"
|
|
case versions
|
|
}
|
|
|
|
public required init(from decoder: Decoder) throws
|
|
{
|
|
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
|
|
|
// Must initialize with context in order for child context saves to work correctly.
|
|
super.init(entity: StoreApp.entity(), insertInto: context)
|
|
|
|
do
|
|
{
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
self.name = try container.decode(String.self, forKey: .name)
|
|
self.bundleIdentifier = try container.decode(String.self, forKey: .bundleIdentifier)
|
|
self.developerName = try container.decode(String.self, forKey: .developerName)
|
|
self.localizedDescription = try container.decode(String.self, forKey: .localizedDescription)
|
|
|
|
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
|
|
|
|
self.iconURL = try container.decode(URL.self, forKey: .iconURL)
|
|
self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
|
|
|
|
let downloadURL = try container.decodeIfPresent(URL.self, forKey: .downloadURL)
|
|
let platformURLs = try container.decodeIfPresent(PlatformURLs.self.self, forKey: .platformURLs)
|
|
if let platformURLs = platformURLs {
|
|
self.platformURLs = platformURLs
|
|
// Backwards compatibility, use the fiirst (iOS will be first since sorted that way)
|
|
if let first = platformURLs.sorted().first {
|
|
self._downloadURL = first.downloadURL
|
|
} else {
|
|
throw DecodingError.dataCorruptedError(forKey: .platformURLs, in: container, debugDescription: "platformURLs has no entries")
|
|
|
|
}
|
|
|
|
} else if let downloadURL = downloadURL {
|
|
self._downloadURL = downloadURL
|
|
} else {
|
|
throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.")
|
|
}
|
|
|
|
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor)
|
|
{
|
|
guard let tintColor = UIColor(hexString: tintColorHex) else {
|
|
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
|
|
}
|
|
|
|
self.tintColor = tintColor
|
|
}
|
|
|
|
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
|
|
|
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
|
|
self._permissions = NSOrderedSet(array: permissions)
|
|
|
|
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions)
|
|
{
|
|
//TODO: Throw error if there isn't at least one version.
|
|
|
|
for version in versions
|
|
{
|
|
version.appBundleID = self.bundleIdentifier
|
|
}
|
|
|
|
self.setVersions(versions)
|
|
}
|
|
else
|
|
{
|
|
let version = try container.decode(String.self, forKey: .version)
|
|
let versionDate = try container.decode(Date.self, forKey: .versionDate)
|
|
let versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
|
|
|
|
let downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
|
let size = try container.decode(Int32.self, forKey: .size)
|
|
|
|
let appVersion = AppVersion.makeAppVersion(version: version,
|
|
date: versionDate,
|
|
localizedDescription: versionDescription,
|
|
downloadURL: downloadURL,
|
|
size: Int64(size),
|
|
appBundleID: self.bundleIdentifier,
|
|
in: context)
|
|
self.setVersions([appVersion])
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
if let context = self.managedObjectContext
|
|
{
|
|
context.delete(self)
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
internal extension StoreApp
|
|
{
|
|
func setVersions(_ versions: [AppVersion])
|
|
{
|
|
self._versions = NSOrderedSet(array: versions)
|
|
|
|
let latestSupportedVersion = versions.first(where: { $0.isSupported })
|
|
self.latestSupportedVersion = latestSupportedVersion
|
|
|
|
for case let version as AppVersion in self._versions
|
|
{
|
|
if version == latestSupportedVersion
|
|
{
|
|
version.latestSupportedVersionApp = self
|
|
}
|
|
else
|
|
{
|
|
// Ensure we replace any previous relationship when merging.
|
|
version.latestSupportedVersionApp = nil
|
|
}
|
|
}
|
|
|
|
// Preserve backwards compatibility by assigning legacy property values.
|
|
guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") }
|
|
self._version = latestVersion.version
|
|
self._versionDate = latestVersion.date
|
|
self._versionDescription = latestVersion.localizedDescription
|
|
self._downloadURL = latestVersion.downloadURL
|
|
self._size = Int32(latestVersion.size)
|
|
}
|
|
}
|
|
|
|
public extension StoreApp
|
|
{
|
|
var latestAvailableVersion: AppVersion? {
|
|
return self._versions.firstObject as? AppVersion
|
|
}
|
|
|
|
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
|
|
{
|
|
return NSFetchRequest<StoreApp>(entityName: "StoreApp")
|
|
}
|
|
|
|
class func makeAltStoreApp(in context: NSManagedObjectContext) -> StoreApp
|
|
{
|
|
let app = StoreApp(context: context)
|
|
app.name = "SideStore"
|
|
app.bundleIdentifier = StoreApp.altstoreAppID
|
|
app.developerName = "Side Team"
|
|
app.localizedDescription = "SideStore is an alternative App Store."
|
|
app.iconURL = URL(string: "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png")!
|
|
app.screenshotURLs = []
|
|
app.sourceIdentifier = Source.altStoreIdentifier
|
|
|
|
let appVersion = AppVersion.makeAppVersion(version: "0.3.0",
|
|
date: Date(),
|
|
downloadURL: URL(string: "http://rileytestut.com")!,
|
|
size: 0,
|
|
appBundleID: app.bundleIdentifier,
|
|
sourceID: Source.altStoreIdentifier,
|
|
in: context)
|
|
app.setVersions([appVersion])
|
|
|
|
print("makeAltStoreApp StoreApp: \(String(describing: app))")
|
|
|
|
#if BETA
|
|
app.isBeta = true
|
|
#endif
|
|
|
|
return app
|
|
}
|
|
}
|