source-code
This commit is contained in:
36
source-code/ALTs/AltDaemon/AltDaemon-Bridging-Header.h
Normal file
36
source-code/ALTs/AltDaemon/AltDaemon-Bridging-Header.h
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface AKDevice : NSObject
|
||||||
|
|
||||||
|
@property (class, readonly) AKDevice *currentDevice;
|
||||||
|
|
||||||
|
@property (strong, readonly) NSString *serialNumber;
|
||||||
|
@property (strong, readonly) NSString *uniqueDeviceIdentifier;
|
||||||
|
@property (strong, readonly) NSString *serverFriendlyDescription;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface AKAppleIDSession : NSObject
|
||||||
|
|
||||||
|
- (instancetype)initWithIdentifier:(NSString *)identifier;
|
||||||
|
|
||||||
|
- (NSDictionary<NSString *, NSString *> *)appleIDHeadersForRequest:(NSURLRequest *)request;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface LSApplicationWorkspace : NSObject
|
||||||
|
|
||||||
|
@property (class, readonly) LSApplicationWorkspace *defaultWorkspace;
|
||||||
|
|
||||||
|
- (BOOL)installApplication:(NSURL *)fileURL withOptions:(nullable NSDictionary<NSString *, id> *)options error:(NSError *_Nullable *)error;
|
||||||
|
- (BOOL)uninstallApplication:(NSString *)bundleIdentifier withOptions:(nullable NSDictionary *)options;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
22
source-code/ALTs/AltDaemon/AltDaemon.entitlements
Normal file
22
source-code/ALTs/AltDaemon/AltDaemon.entitlements
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>application-identifier</key>
|
||||||
|
<string>6XVY5G3U44.com.rileytestut.AltDaemon</string>
|
||||||
|
<key>get-task-allow</key>
|
||||||
|
<true/>
|
||||||
|
<key>platform-application</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.authkit.client.private</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.private.mobileinstall.allowedSPI</key>
|
||||||
|
<array>
|
||||||
|
<string>Install</string>
|
||||||
|
<string>Uninstall</string>
|
||||||
|
<string>InstallForLaunchServices</string>
|
||||||
|
<string>UninstallForLaunchServices</string>
|
||||||
|
<string>InstallLocalProvisioned</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
65
source-code/ALTs/AltDaemon/AnisetteDataManager.swift
Normal file
65
source-code/ALTs/AltDaemon/AnisetteDataManager.swift
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
//
|
||||||
|
// AnisetteDataManager.swift
|
||||||
|
// AltDaemon
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 6/1/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
private extension UserDefaults
|
||||||
|
{
|
||||||
|
@objc var localUserID: String? {
|
||||||
|
get { return self.string(forKey: #keyPath(UserDefaults.localUserID)) }
|
||||||
|
set { self.set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AnisetteDataManager
|
||||||
|
{
|
||||||
|
static let shared = AnisetteDataManager()
|
||||||
|
|
||||||
|
private let dateFormatter = ISO8601DateFormatter()
|
||||||
|
|
||||||
|
private init()
|
||||||
|
{
|
||||||
|
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestAnisetteData() throws -> ALTAnisetteData
|
||||||
|
{
|
||||||
|
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
|
||||||
|
let akAppleIDSession = unsafeBitCast(NSClassFromString("AKAppleIDSession")!, to: AKAppleIDSession.Type.self)
|
||||||
|
let akDevice = unsafeBitCast(NSClassFromString("AKDevice")!, to: AKDevice.Type.self)
|
||||||
|
|
||||||
|
let session = akAppleIDSession.init(identifier: "com.apple.gs.xcode.auth")
|
||||||
|
let headers = session.appleIDHeaders(for: request)
|
||||||
|
|
||||||
|
let device = akDevice.current
|
||||||
|
let date = self.dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
|
||||||
|
|
||||||
|
var localUserID = UserDefaults.standard.localUserID
|
||||||
|
if localUserID == nil
|
||||||
|
{
|
||||||
|
localUserID = UUID().uuidString
|
||||||
|
UserDefaults.standard.localUserID = localUserID
|
||||||
|
}
|
||||||
|
|
||||||
|
let anisetteData = ALTAnisetteData(machineID: headers["X-Apple-I-MD-M"] ?? "",
|
||||||
|
oneTimePassword: headers["X-Apple-I-MD"] ?? "",
|
||||||
|
localUserID: headers["X-Apple-I-MD-LU"] ?? localUserID ?? "",
|
||||||
|
routingInfo: UInt64(headers["X-Apple-I-MD-RINFO"] ?? "") ?? 0,
|
||||||
|
deviceUniqueIdentifier: device.uniqueDeviceIdentifier,
|
||||||
|
deviceSerialNumber: device.serialNumber,
|
||||||
|
deviceDescription: "<MacBookPro15,1> <Mac OS X;10.15.2;19C57> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>",
|
||||||
|
date: date,
|
||||||
|
locale: .current,
|
||||||
|
timeZone: .current)
|
||||||
|
return anisetteData
|
||||||
|
}
|
||||||
|
}
|
||||||
126
source-code/ALTs/AltDaemon/AppManager.swift
Normal file
126
source-code/ALTs/AltDaemon/AppManager.swift
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
//
|
||||||
|
// AppManager.swift
|
||||||
|
// AltDaemon
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 6/1/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
private extension URL
|
||||||
|
{
|
||||||
|
static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppManager
|
||||||
|
{
|
||||||
|
static let shared = AppManager()
|
||||||
|
|
||||||
|
private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated)
|
||||||
|
private let profilesQueue = OperationQueue()
|
||||||
|
|
||||||
|
private let fileCoordinator = NSFileCoordinator()
|
||||||
|
|
||||||
|
private init()
|
||||||
|
{
|
||||||
|
self.profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue"
|
||||||
|
self.profilesQueue.qualityOfService = .userInitiated
|
||||||
|
}
|
||||||
|
|
||||||
|
func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
|
self.appQueue.async {
|
||||||
|
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||||
|
|
||||||
|
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any]
|
||||||
|
let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) }
|
||||||
|
|
||||||
|
completionHandler(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
|
self.appQueue.async {
|
||||||
|
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||||
|
lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil)
|
||||||
|
|
||||||
|
completionHandler(.success(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||||
|
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if let error = error
|
||||||
|
{
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
let installingBundleIDs = Set(profiles.map(\.bundleIdentifier))
|
||||||
|
|
||||||
|
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||||
|
|
||||||
|
// Remove all inactive profiles (if active profiles are provided), and the previous profiles.
|
||||||
|
for fileURL in profileURLs
|
||||||
|
{
|
||||||
|
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
|
||||||
|
|
||||||
|
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile)
|
||||||
|
{
|
||||||
|
try FileManager.default.removeItem(at: fileURL)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
print("Ignoring:", profile.bundleIdentifier, profile.uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for profile in profiles
|
||||||
|
{
|
||||||
|
let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased())
|
||||||
|
try profile.data.write(to: destinationURL, options: .atomic)
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler(.success(()))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||||
|
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||||
|
|
||||||
|
for fileURL in profileURLs
|
||||||
|
{
|
||||||
|
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
|
||||||
|
|
||||||
|
if bundleIdentifiers.contains(profile.bundleIdentifier)
|
||||||
|
{
|
||||||
|
try FileManager.default.removeItem(at: fileURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler(.success(()))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
source-code/ALTs/AltDaemon/LocalConnectionHandler.swift
Normal file
110
source-code/ALTs/AltDaemon/LocalConnectionHandler.swift
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
//
|
||||||
|
// LocalConnectionHandler.swift
|
||||||
|
// AltDaemon
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 6/2/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Network
|
||||||
|
|
||||||
|
import AltKit
|
||||||
|
|
||||||
|
private let ReceivedLocalServerConnectionRequest: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
|
||||||
|
{ (center, observer, name, object, userInfo) in
|
||||||
|
guard let name = name, let observer = observer else { return }
|
||||||
|
|
||||||
|
let connection = unsafeBitCast(observer, to: LocalConnectionHandler.self)
|
||||||
|
connection.handle(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalConnectionHandler: ConnectionHandler
|
||||||
|
{
|
||||||
|
var connectionHandler: ((Connection) -> Void)?
|
||||||
|
var disconnectionHandler: ((Connection) -> Void)?
|
||||||
|
|
||||||
|
private let dispatchQueue = DispatchQueue(label: "io.altstore.LocalConnectionListener", qos: .utility)
|
||||||
|
|
||||||
|
deinit
|
||||||
|
{
|
||||||
|
self.stopListening()
|
||||||
|
}
|
||||||
|
|
||||||
|
func startListening()
|
||||||
|
{
|
||||||
|
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
||||||
|
let observer = Unmanaged.passUnretained(self).toOpaque()
|
||||||
|
|
||||||
|
CFNotificationCenterAddObserver(notificationCenter, observer, ReceivedLocalServerConnectionRequest, CFNotificationName.localServerConnectionAvailableRequest.rawValue, nil, .deliverImmediately)
|
||||||
|
CFNotificationCenterAddObserver(notificationCenter, observer, ReceivedLocalServerConnectionRequest, CFNotificationName.localServerConnectionStartRequest.rawValue, nil, .deliverImmediately)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopListening()
|
||||||
|
{
|
||||||
|
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
||||||
|
let observer = Unmanaged.passUnretained(self).toOpaque()
|
||||||
|
|
||||||
|
CFNotificationCenterRemoveObserver(notificationCenter, observer, .localServerConnectionAvailableRequest, nil)
|
||||||
|
CFNotificationCenterRemoveObserver(notificationCenter, observer, .localServerConnectionStartRequest, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func handle(_ notification: CFNotificationName)
|
||||||
|
{
|
||||||
|
switch notification
|
||||||
|
{
|
||||||
|
case .localServerConnectionAvailableRequest:
|
||||||
|
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
||||||
|
CFNotificationCenterPostNotification(notificationCenter, .localServerConnectionAvailableResponse, nil, nil, true)
|
||||||
|
|
||||||
|
case .localServerConnectionStartRequest:
|
||||||
|
let connection = NWConnection(host: "localhost", port: NWEndpoint.Port(rawValue: ALTDeviceListeningSocket)!, using: .tcp)
|
||||||
|
self.start(connection)
|
||||||
|
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension LocalConnectionHandler
|
||||||
|
{
|
||||||
|
func start(_ nwConnection: NWConnection)
|
||||||
|
{
|
||||||
|
print("Starting connection to:", nwConnection)
|
||||||
|
|
||||||
|
// Use same instance for all callbacks.
|
||||||
|
let connection = NetworkConnection(nwConnection)
|
||||||
|
|
||||||
|
nwConnection.stateUpdateHandler = { [weak self] (state) in
|
||||||
|
switch state
|
||||||
|
{
|
||||||
|
case .setup, .preparing: break
|
||||||
|
|
||||||
|
case .ready:
|
||||||
|
print("Connected to client:", nwConnection.endpoint)
|
||||||
|
self?.connectionHandler?(connection)
|
||||||
|
|
||||||
|
case .waiting:
|
||||||
|
print("Waiting for connection...")
|
||||||
|
|
||||||
|
case .failed(let error):
|
||||||
|
print("Failed to connect to service \(nwConnection.endpoint).", error)
|
||||||
|
self?.disconnect(connection)
|
||||||
|
|
||||||
|
case .cancelled:
|
||||||
|
self?.disconnect(connection)
|
||||||
|
|
||||||
|
@unknown default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nwConnection.start(queue: self.dispatchQueue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect(_ connection: Connection)
|
||||||
|
{
|
||||||
|
connection.disconnect()
|
||||||
|
|
||||||
|
self.disconnectionHandler?(connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
124
source-code/ALTs/AltDaemon/RequestHandler.swift
Normal file
124
source-code/ALTs/AltDaemon/RequestHandler.swift
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
//
|
||||||
|
// ConnectionManager.swift
|
||||||
|
// AltServer
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 6/1/20.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AltKit
|
||||||
|
|
||||||
|
typealias ConnectionManager = AltKit.ConnectionManager<RequestHandler>
|
||||||
|
|
||||||
|
private let connectionManager = ConnectionManager(requestHandler: RequestHandler(),
|
||||||
|
connectionHandlers: [LocalConnectionHandler()])
|
||||||
|
|
||||||
|
extension ConnectionManager
|
||||||
|
{
|
||||||
|
static var shared: ConnectionManager {
|
||||||
|
return connectionManager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RequestHandler: AltKit.RequestHandler
|
||||||
|
{
|
||||||
|
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
|
||||||
|
|
||||||
|
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
||||||
|
completionHandler(.success(response))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
|
||||||
|
{
|
||||||
|
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
|
||||||
|
|
||||||
|
print("Awaiting begin installation request...")
|
||||||
|
|
||||||
|
connection.receiveRequest() { (result) in
|
||||||
|
print("Received begin installation request with result:", result)
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
guard case .beginInstallation(let request) = try result.get() else { throw ALTServerError(.unknownRequest) }
|
||||||
|
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
|
||||||
|
|
||||||
|
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { (result) in
|
||||||
|
let result = result.map { InstallationProgressResponse(progress: 1.0) }
|
||||||
|
print("Installed app with result:", result)
|
||||||
|
|
||||||
|
completionHandler(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
|
||||||
|
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
|
||||||
|
{
|
||||||
|
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error):
|
||||||
|
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
|
||||||
|
case .success:
|
||||||
|
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
|
||||||
|
|
||||||
|
let response = InstallProvisioningProfilesResponse()
|
||||||
|
completionHandler(.success(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
|
||||||
|
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
|
||||||
|
{
|
||||||
|
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error):
|
||||||
|
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
|
||||||
|
case .success:
|
||||||
|
print("Removed profiles:", request.bundleIdentifiers)
|
||||||
|
|
||||||
|
let response = RemoveProvisioningProfilesResponse()
|
||||||
|
completionHandler(.success(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
|
||||||
|
{
|
||||||
|
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error):
|
||||||
|
print("Failed to remove app \(request.bundleIdentifier):", error)
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
|
||||||
|
case .success:
|
||||||
|
print("Removed app:", request.bundleIdentifier)
|
||||||
|
|
||||||
|
let response = RemoveAppResponse()
|
||||||
|
completionHandler(.success(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
source-code/ALTs/AltDaemon/main.swift
Normal file
14
source-code/ALTs/AltDaemon/main.swift
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// main.swift
|
||||||
|
// AltDaemon
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 6/2/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
autoreleasepool {
|
||||||
|
ConnectionManager.shared.start()
|
||||||
|
RunLoop.current.run()
|
||||||
|
}
|
||||||
10
source-code/ALTs/AltDaemon/package/DEBIAN/control
Normal file
10
source-code/ALTs/AltDaemon/package/DEBIAN/control
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Package: com.rileytestut.altdaemon
|
||||||
|
Name: AltDaemon
|
||||||
|
Depends:
|
||||||
|
Version: 0.1
|
||||||
|
Architecture: iphoneos-arm
|
||||||
|
Description: AltDaemon allows AltStore to install and refresh apps without a computer.
|
||||||
|
Maintainer: Riley Testut
|
||||||
|
Author: Riley Testut
|
||||||
|
Homepage: https://altstore.io
|
||||||
|
Section: System
|
||||||
2
source-code/ALTs/AltDaemon/package/DEBIAN/postinst
Normal file
2
source-code/ALTs/AltDaemon/package/DEBIAN/postinst
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
launchctl load /Library/LaunchDaemons/com.rileytestut.altdaemon.plist
|
||||||
2
source-code/ALTs/AltDaemon/package/DEBIAN/preinst
Normal file
2
source-code/ALTs/AltDaemon/package/DEBIAN/preinst
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist >> /dev/null 2>&1
|
||||||
2
source-code/ALTs/AltDaemon/package/DEBIAN/prerm
Normal file
2
source-code/ALTs/AltDaemon/package/DEBIAN/prerm
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.rileytestut.altdaemon</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/usr/bin/AltDaemon</string>
|
||||||
|
</array>
|
||||||
|
<key>UserName</key>
|
||||||
|
<string>mobile</string>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
source-code/ALTs/AltDaemon/package/usr/bin/AltDaemon
Normal file
BIN
source-code/ALTs/AltDaemon/package/usr/bin/AltDaemon
Normal file
Binary file not shown.
11
source-code/ALTs/AltStore/AltStore-Bridging-Header.h
Normal file
11
source-code/ALTs/AltStore/AltStore-Bridging-Header.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//
|
||||||
|
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "AltKit.h"
|
||||||
|
|
||||||
|
#import "ALTAppPermission.h"
|
||||||
|
#import "ALTPatreonBenefitType.h"
|
||||||
|
#import "ALTSourceUserInfoKey.h"
|
||||||
|
|
||||||
|
#import "NSAttributedString+Markdown.h"
|
||||||
12
source-code/ALTs/AltStore/AltStore.entitlements
Normal file
12
source-code/ALTs/AltStore/AltStore.entitlements
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.com.rileytestut.AltStore</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
105
source-code/ALTs/AltStore/Analytics/AnalyticsManager.swift
Normal file
105
source-code/ALTs/AltStore/Analytics/AnalyticsManager.swift
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
//
|
||||||
|
// AnalyticsManager.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/31/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import AppCenter
|
||||||
|
import AppCenterAnalytics
|
||||||
|
import AppCenterCrashes
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
private let appCenterAppSecret = "bb08e9bb-c126-408d-bf3f-324c8473fd40"
|
||||||
|
#elseif RELEASE
|
||||||
|
private let appCenterAppSecret = "b6718932-294a-432b-81f2-be1e17ff85c5"
|
||||||
|
#else
|
||||||
|
private let appCenterAppSecret = "e873f6ca-75eb-4685-818f-801e0e375d60"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extension AnalyticsManager
|
||||||
|
{
|
||||||
|
enum EventProperty: String
|
||||||
|
{
|
||||||
|
case name
|
||||||
|
case bundleIdentifier
|
||||||
|
case developerName
|
||||||
|
case version
|
||||||
|
case size
|
||||||
|
case tintColor
|
||||||
|
case sourceIdentifier
|
||||||
|
case sourceURL
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Event
|
||||||
|
{
|
||||||
|
case installedApp(InstalledApp)
|
||||||
|
case updatedApp(InstalledApp)
|
||||||
|
case refreshedApp(InstalledApp)
|
||||||
|
|
||||||
|
var name: String {
|
||||||
|
switch self
|
||||||
|
{
|
||||||
|
case .installedApp: return "installed_app"
|
||||||
|
case .updatedApp: return "updated_app"
|
||||||
|
case .refreshedApp: return "refreshed_app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var properties: [EventProperty: String] {
|
||||||
|
let properties: [EventProperty: String?]
|
||||||
|
|
||||||
|
switch self
|
||||||
|
{
|
||||||
|
case .installedApp(let app), .updatedApp(let app), .refreshedApp(let app):
|
||||||
|
let appBundleURL = InstalledApp.fileURL(for: app)
|
||||||
|
let appBundleSize = FileManager.default.directorySize(at: appBundleURL)
|
||||||
|
|
||||||
|
properties = [
|
||||||
|
.name: app.name,
|
||||||
|
.bundleIdentifier: app.bundleIdentifier,
|
||||||
|
.developerName: app.storeApp?.developerName,
|
||||||
|
.version: app.version,
|
||||||
|
.size: appBundleSize?.description,
|
||||||
|
.tintColor: app.storeApp?.tintColor?.hexString,
|
||||||
|
.sourceIdentifier: app.storeApp?.sourceIdentifier,
|
||||||
|
.sourceURL: app.storeApp?.source?.sourceURL.absoluteString
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties.compactMapValues { $0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnalyticsManager
|
||||||
|
{
|
||||||
|
static let shared = AnalyticsManager()
|
||||||
|
|
||||||
|
private init()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnalyticsManager
|
||||||
|
{
|
||||||
|
func start()
|
||||||
|
{
|
||||||
|
MSAppCenter.start(appCenterAppSecret, withServices:[
|
||||||
|
MSAnalytics.self,
|
||||||
|
MSCrashes.self
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func trackEvent(_ event: Event)
|
||||||
|
{
|
||||||
|
let properties = event.properties.reduce(into: [:]) { (properties, item) in
|
||||||
|
properties[item.key.rawValue] = item.value
|
||||||
|
}
|
||||||
|
|
||||||
|
MSAnalytics.trackEvent(event.name, withProperties: properties)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
//
|
||||||
|
// AppContentViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/22/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
import Nuke
|
||||||
|
|
||||||
|
extension AppContentViewController
|
||||||
|
{
|
||||||
|
private enum Row: Int, CaseIterable
|
||||||
|
{
|
||||||
|
case subtitle
|
||||||
|
case screenshots
|
||||||
|
case description
|
||||||
|
case versionDescription
|
||||||
|
case permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppContentViewController: UITableViewController
|
||||||
|
{
|
||||||
|
var app: StoreApp!
|
||||||
|
|
||||||
|
private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
|
||||||
|
private lazy var permissionsDataSource = self.makePermissionsDataSource()
|
||||||
|
|
||||||
|
private lazy var dateFormatter: DateFormatter = {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateStyle = .medium
|
||||||
|
dateFormatter.timeStyle = .none
|
||||||
|
return dateFormatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var byteCountFormatter: ByteCountFormatter = {
|
||||||
|
let formatter = ByteCountFormatter()
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
@IBOutlet private var subtitleLabel: UILabel!
|
||||||
|
@IBOutlet private var descriptionTextView: CollapsingTextView!
|
||||||
|
@IBOutlet private var versionDescriptionTextView: CollapsingTextView!
|
||||||
|
@IBOutlet private var versionLabel: UILabel!
|
||||||
|
@IBOutlet private var versionDateLabel: UILabel!
|
||||||
|
@IBOutlet private var sizeLabel: UILabel!
|
||||||
|
|
||||||
|
@IBOutlet private var screenshotsCollectionView: UICollectionView!
|
||||||
|
@IBOutlet private var permissionsCollectionView: UICollectionView!
|
||||||
|
|
||||||
|
var preferredScreenshotSize: CGSize? {
|
||||||
|
let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||||
|
|
||||||
|
let aspectRatio: CGFloat = 16.0 / 9.0 // Hardcoded for now.
|
||||||
|
|
||||||
|
let width = self.screenshotsCollectionView.bounds.width - (layout.minimumInteritemSpacing * 2)
|
||||||
|
|
||||||
|
let itemWidth = width / 1.5
|
||||||
|
let itemHeight = itemWidth * aspectRatio
|
||||||
|
|
||||||
|
return CGSize(width: itemWidth, height: itemHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.tableView.contentInset.bottom = 20
|
||||||
|
|
||||||
|
self.screenshotsCollectionView.dataSource = self.screenshotsDataSource
|
||||||
|
self.screenshotsCollectionView.prefetchDataSource = self.screenshotsDataSource
|
||||||
|
|
||||||
|
self.permissionsCollectionView.dataSource = self.permissionsDataSource
|
||||||
|
|
||||||
|
self.subtitleLabel.text = self.app.subtitle
|
||||||
|
self.descriptionTextView.text = self.app.localizedDescription
|
||||||
|
self.versionDescriptionTextView.text = self.app.versionDescription
|
||||||
|
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), self.app.version)
|
||||||
|
self.versionDateLabel.text = Date().relativeDateString(since: self.app.versionDate, dateFormatter: self.dateFormatter)
|
||||||
|
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: Int64(self.app.size))
|
||||||
|
|
||||||
|
self.descriptionTextView.maximumNumberOfLines = 5
|
||||||
|
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||||
|
|
||||||
|
self.versionDescriptionTextView.maximumNumberOfLines = 3
|
||||||
|
self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews()
|
||||||
|
{
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
guard var size = self.preferredScreenshotSize else { return }
|
||||||
|
size.height = min(size.height, self.screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning.
|
||||||
|
|
||||||
|
let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||||
|
layout.itemSize = size
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||||
|
{
|
||||||
|
guard segue.identifier == "showPermission" else { return }
|
||||||
|
|
||||||
|
guard let cell = sender as? UICollectionViewCell, let indexPath = self.permissionsCollectionView.indexPath(for: cell) else { return }
|
||||||
|
|
||||||
|
let permission = self.permissionsDataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
let maximumWidth = self.view.bounds.width - 20
|
||||||
|
|
||||||
|
let permissionPopoverViewController = segue.destination as! PermissionPopoverViewController
|
||||||
|
permissionPopoverViewController.permission = permission
|
||||||
|
permissionPopoverViewController.view.widthAnchor.constraint(lessThanOrEqualToConstant: maximumWidth).isActive = true
|
||||||
|
|
||||||
|
let size = permissionPopoverViewController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||||
|
permissionPopoverViewController.preferredContentSize = size
|
||||||
|
|
||||||
|
permissionPopoverViewController.popoverPresentationController?.delegate = self
|
||||||
|
permissionPopoverViewController.popoverPresentationController?.sourceRect = cell.frame
|
||||||
|
permissionPopoverViewController.popoverPresentationController?.sourceView = self.permissionsCollectionView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppContentViewController
|
||||||
|
{
|
||||||
|
func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
|
||||||
|
{
|
||||||
|
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: self.app.screenshotURLs as [NSURL])
|
||||||
|
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
||||||
|
let cell = cell as! ScreenshotCollectionViewCell
|
||||||
|
cell.imageView.image = nil
|
||||||
|
cell.imageView.isIndicatingActivity = true
|
||||||
|
}
|
||||||
|
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
|
||||||
|
return RSTAsyncBlockOperation() { (operation) in
|
||||||
|
ImagePipeline.shared.loadImage(with: imageURL as URL, progress: nil, completion: { (response, error) in
|
||||||
|
guard !operation.isCancelled else { return operation.finish() }
|
||||||
|
|
||||||
|
if let image = response?.image
|
||||||
|
{
|
||||||
|
completionHandler(image, nil)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completionHandler(nil, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
|
let cell = cell as! ScreenshotCollectionViewCell
|
||||||
|
cell.imageView.isIndicatingActivity = false
|
||||||
|
cell.imageView.image = image
|
||||||
|
|
||||||
|
if let error = error
|
||||||
|
{
|
||||||
|
print("Error loading image:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
|
||||||
|
{
|
||||||
|
let dataSource = RSTArrayCollectionViewDataSource(items: self.app.permissions)
|
||||||
|
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
|
||||||
|
let cell = cell as! PermissionCollectionViewCell
|
||||||
|
cell.button.setImage(permission.type.icon, for: .normal)
|
||||||
|
cell.textLabel.text = permission.type.localizedShortName
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppContentViewController
|
||||||
|
{
|
||||||
|
@objc func toggleCollapsingSection(_ sender: UIButton)
|
||||||
|
{
|
||||||
|
let indexPath: IndexPath
|
||||||
|
|
||||||
|
switch sender
|
||||||
|
{
|
||||||
|
case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
||||||
|
case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
||||||
|
default: return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable animations to prevent some potentially strange ones.
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.reloadRows(at: [indexPath], with: .none)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppContentViewController
|
||||||
|
{
|
||||||
|
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
|
||||||
|
{
|
||||||
|
cell.tintColor = self.app.tintColor
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
|
||||||
|
{
|
||||||
|
guard indexPath.row == Row.screenshots.rawValue else { return super.tableView(tableView, heightForRowAt: indexPath) }
|
||||||
|
|
||||||
|
guard let size = self.preferredScreenshotSize else { return 0.0 }
|
||||||
|
return size.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppContentViewController: UIPopoverPresentationControllerDelegate
|
||||||
|
{
|
||||||
|
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
|
||||||
|
{
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// AppContentViewControllerCells.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/24/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class PermissionCollectionViewCell: UICollectionViewCell
|
||||||
|
{
|
||||||
|
@IBOutlet var button: UIButton!
|
||||||
|
@IBOutlet var textLabel: UILabel!
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
self.button.layer.cornerRadius = self.button.bounds.midY
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tintColorDidChange()
|
||||||
|
{
|
||||||
|
super.tintColorDidChange()
|
||||||
|
|
||||||
|
self.button.backgroundColor = self.tintColor.withAlphaComponent(0.15)
|
||||||
|
self.textLabel.textColor = self.tintColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppContentTableViewCell: UITableViewCell
|
||||||
|
{
|
||||||
|
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
||||||
|
{
|
||||||
|
// Ensure cell is laid out so it will report correct size.
|
||||||
|
self.layoutIfNeeded()
|
||||||
|
|
||||||
|
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
562
source-code/ALTs/AltStore/App Detail/AppViewController.swift
Normal file
562
source-code/ALTs/AltStore/App Detail/AppViewController.swift
Normal file
@@ -0,0 +1,562 @@
|
|||||||
|
//
|
||||||
|
// AppViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/22/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
import Nuke
|
||||||
|
|
||||||
|
class AppViewController: UIViewController
|
||||||
|
{
|
||||||
|
var app: StoreApp!
|
||||||
|
|
||||||
|
private var contentViewController: AppContentViewController!
|
||||||
|
private var contentViewControllerShadowView: UIView!
|
||||||
|
|
||||||
|
private var blurAnimator: UIViewPropertyAnimator?
|
||||||
|
private var navigationBarAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
|
private var contentSizeObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
|
@IBOutlet private var scrollView: UIScrollView!
|
||||||
|
@IBOutlet private var contentView: UIView!
|
||||||
|
|
||||||
|
@IBOutlet private var bannerView: AppBannerView!
|
||||||
|
|
||||||
|
@IBOutlet private var backButton: UIButton!
|
||||||
|
@IBOutlet private var backButtonContainerView: UIVisualEffectView!
|
||||||
|
|
||||||
|
@IBOutlet private var backgroundAppIconImageView: UIImageView!
|
||||||
|
@IBOutlet private var backgroundBlurView: UIVisualEffectView!
|
||||||
|
|
||||||
|
@IBOutlet private var navigationBarTitleView: UIView!
|
||||||
|
@IBOutlet private var navigationBarDownloadButton: PillButton!
|
||||||
|
@IBOutlet private var navigationBarAppIconImageView: UIImageView!
|
||||||
|
@IBOutlet private var navigationBarAppNameLabel: UILabel!
|
||||||
|
|
||||||
|
private var _shouldResetLayout = false
|
||||||
|
private var _backgroundBlurEffect: UIBlurEffect?
|
||||||
|
private var _backgroundBlurTintColor: UIColor?
|
||||||
|
|
||||||
|
private var _preferredStatusBarStyle: UIStatusBarStyle = .default
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return _preferredStatusBarStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.navigationBarTitleView.sizeToFit()
|
||||||
|
self.navigationItem.titleView = self.navigationBarTitleView
|
||||||
|
|
||||||
|
self.contentViewControllerShadowView = UIView()
|
||||||
|
self.contentViewControllerShadowView.backgroundColor = .white
|
||||||
|
self.contentViewControllerShadowView.layer.cornerRadius = 38
|
||||||
|
self.contentViewControllerShadowView.layer.shadowColor = UIColor.black.cgColor
|
||||||
|
self.contentViewControllerShadowView.layer.shadowOffset = CGSize(width: 0, height: -1)
|
||||||
|
self.contentViewControllerShadowView.layer.shadowRadius = 10
|
||||||
|
self.contentViewControllerShadowView.layer.shadowOpacity = 0.3
|
||||||
|
self.contentViewController.view.superview?.insertSubview(self.contentViewControllerShadowView, at: 0)
|
||||||
|
|
||||||
|
self.contentView.addGestureRecognizer(self.scrollView.panGestureRecognizer)
|
||||||
|
|
||||||
|
self.contentViewController.view.layer.cornerRadius = 38
|
||||||
|
self.contentViewController.view.layer.masksToBounds = true
|
||||||
|
|
||||||
|
self.contentViewController.tableView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer)
|
||||||
|
self.contentViewController.tableView.showsVerticalScrollIndicator = false
|
||||||
|
|
||||||
|
// Bring to front so the scroll indicators are visible.
|
||||||
|
self.view.bringSubviewToFront(self.scrollView)
|
||||||
|
self.scrollView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.bannerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
|
||||||
|
self.bannerView.backgroundEffectView.effect = UIBlurEffect(style: .regular)
|
||||||
|
self.bannerView.backgroundEffectView.backgroundColor = .clear
|
||||||
|
self.bannerView.titleLabel.text = self.app.name
|
||||||
|
self.bannerView.subtitleLabel.text = self.app.developerName
|
||||||
|
self.bannerView.iconImageView.image = nil
|
||||||
|
self.bannerView.iconImageView.tintColor = self.app.tintColor
|
||||||
|
self.bannerView.button.tintColor = self.app.tintColor
|
||||||
|
self.bannerView.betaBadgeView.isHidden = !self.app.isBeta
|
||||||
|
self.bannerView.tintColor = self.app.tintColor
|
||||||
|
|
||||||
|
self.bannerView.button.addTarget(self, action: #selector(AppViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||||
|
|
||||||
|
self.backButtonContainerView.tintColor = self.app.tintColor
|
||||||
|
|
||||||
|
self.navigationController?.navigationBar.tintColor = self.app.tintColor
|
||||||
|
self.navigationBarDownloadButton.tintColor = self.app.tintColor
|
||||||
|
self.navigationBarAppNameLabel.text = self.app.name
|
||||||
|
self.navigationBarAppIconImageView.tintColor = self.app.tintColor
|
||||||
|
|
||||||
|
self.contentSizeObservation = self.contentViewController.tableView.observe(\.contentSize) { [weak self] (tableView, change) in
|
||||||
|
self?.view.setNeedsLayout()
|
||||||
|
self?.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didChangeApp(_:)), name: .NSManagedObjectContextObjectsDidChange, object: DatabaseManager.shared.viewContext)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||||
|
|
||||||
|
self._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect
|
||||||
|
self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor
|
||||||
|
|
||||||
|
// Load Images
|
||||||
|
for imageView in [self.bannerView.iconImageView!, self.backgroundAppIconImageView!, self.navigationBarAppIconImageView!]
|
||||||
|
{
|
||||||
|
imageView.isIndicatingActivity = true
|
||||||
|
|
||||||
|
Nuke.loadImage(with: self.app.iconURL, options: .shared, into: imageView, progress: nil) { [weak imageView] (response, error) in
|
||||||
|
if response?.image != nil
|
||||||
|
{
|
||||||
|
imageView?.isIndicatingActivity = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
self.prepareBlur()
|
||||||
|
|
||||||
|
// Update blur immediately.
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
|
||||||
|
self.transitionCoordinator?.animate(alongsideTransition: { (context) in
|
||||||
|
self.hideNavigationBar()
|
||||||
|
}, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
self._shouldResetLayout = true
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
|
||||||
|
// Guard against "dismissing" when presenting via 3D Touch pop.
|
||||||
|
guard self.navigationController != nil else { return }
|
||||||
|
|
||||||
|
// Store reference since self.navigationController will be nil after disappearing.
|
||||||
|
let navigationController = self.navigationController
|
||||||
|
navigationController?.navigationBar.barStyle = .default // Don't animate, or else status bar might appear messed-up.
|
||||||
|
|
||||||
|
self.transitionCoordinator?.animate(alongsideTransition: { (context) in
|
||||||
|
self.showNavigationBar(for: navigationController)
|
||||||
|
}, completion: { (context) in
|
||||||
|
if !context.isCancelled
|
||||||
|
{
|
||||||
|
self.showNavigationBar(for: navigationController)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
|
if self.navigationController == nil
|
||||||
|
{
|
||||||
|
self.resetNavigationBarAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||||
|
{
|
||||||
|
guard segue.identifier == "embedAppContentViewController" else { return }
|
||||||
|
|
||||||
|
self.contentViewController = segue.destination as? AppContentViewController
|
||||||
|
self.contentViewController.app = self.app
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews()
|
||||||
|
{
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
if self._shouldResetLayout
|
||||||
|
{
|
||||||
|
// Various events can cause UI to mess up, so reset affected components now.
|
||||||
|
|
||||||
|
if self.navigationController?.topViewController == self
|
||||||
|
{
|
||||||
|
self.hideNavigationBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prepareBlur()
|
||||||
|
|
||||||
|
// Reset navigation bar animation, and create a new one later in this method if necessary.
|
||||||
|
self.resetNavigationBarAnimation()
|
||||||
|
|
||||||
|
self._shouldResetLayout = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusBarHeight = UIApplication.shared.statusBarFrame.height
|
||||||
|
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
||||||
|
|
||||||
|
let inset = 12 as CGFloat
|
||||||
|
let padding = 20 as CGFloat
|
||||||
|
|
||||||
|
let backButtonSize = self.backButton.sizeThatFits(CGSize(width: 1000, height: 1000))
|
||||||
|
var backButtonFrame = CGRect(x: inset, y: statusBarHeight,
|
||||||
|
width: backButtonSize.width + 20, height: backButtonSize.height + 20)
|
||||||
|
|
||||||
|
var headerFrame = CGRect(x: inset, y: 0, width: self.view.bounds.width - inset * 2, height: self.bannerView.bounds.height)
|
||||||
|
var contentFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
|
||||||
|
var backgroundIconFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.width)
|
||||||
|
|
||||||
|
let minimumHeaderY = backButtonFrame.maxY + 8
|
||||||
|
|
||||||
|
let minimumContentY = minimumHeaderY + headerFrame.height + padding
|
||||||
|
let maximumContentY = self.view.bounds.width * 0.667
|
||||||
|
|
||||||
|
// A full blur is too much, so we reduce the visible blur by 0.3, resulting in 70% blur.
|
||||||
|
let minimumBlurFraction = 0.3 as CGFloat
|
||||||
|
|
||||||
|
contentFrame.origin.y = maximumContentY - self.scrollView.contentOffset.y
|
||||||
|
headerFrame.origin.y = contentFrame.origin.y - padding - headerFrame.height
|
||||||
|
|
||||||
|
// Stretch the app icon image to fill additional vertical space if necessary.
|
||||||
|
let height = max(contentFrame.origin.y + cornerRadius * 2, backgroundIconFrame.height)
|
||||||
|
backgroundIconFrame.size.height = height
|
||||||
|
|
||||||
|
let blurThreshold = 0 as CGFloat
|
||||||
|
if self.scrollView.contentOffset.y < blurThreshold
|
||||||
|
{
|
||||||
|
// Determine how much to lessen blur by.
|
||||||
|
|
||||||
|
let range = 75 as CGFloat
|
||||||
|
let difference = -self.scrollView.contentOffset.y
|
||||||
|
|
||||||
|
let fraction = min(difference, range) / range
|
||||||
|
|
||||||
|
let fractionComplete = (fraction * (1.0 - minimumBlurFraction)) + minimumBlurFraction
|
||||||
|
self.blurAnimator?.fractionComplete = fractionComplete
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Set blur to default.
|
||||||
|
|
||||||
|
self.blurAnimator?.fractionComplete = minimumBlurFraction
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animate navigation bar.
|
||||||
|
let showNavigationBarThreshold = (maximumContentY - minimumContentY) + backButtonFrame.origin.y
|
||||||
|
if self.scrollView.contentOffset.y > showNavigationBarThreshold
|
||||||
|
{
|
||||||
|
if self.navigationBarAnimator == nil
|
||||||
|
{
|
||||||
|
self.prepareNavigationBarAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
let difference = self.scrollView.contentOffset.y - showNavigationBarThreshold
|
||||||
|
let range = (headerFrame.height + padding) - (self.navigationController?.navigationBar.bounds.height ?? self.view.safeAreaInsets.top)
|
||||||
|
|
||||||
|
let fractionComplete = min(difference, range) / range
|
||||||
|
self.navigationBarAnimator?.fractionComplete = fractionComplete
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.resetNavigationBarAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
let beginMovingBackButtonThreshold = (maximumContentY - minimumContentY)
|
||||||
|
if self.scrollView.contentOffset.y > beginMovingBackButtonThreshold
|
||||||
|
{
|
||||||
|
let difference = self.scrollView.contentOffset.y - beginMovingBackButtonThreshold
|
||||||
|
backButtonFrame.origin.y -= difference
|
||||||
|
}
|
||||||
|
|
||||||
|
let pinContentToTopThreshold = maximumContentY
|
||||||
|
if self.scrollView.contentOffset.y > pinContentToTopThreshold
|
||||||
|
{
|
||||||
|
contentFrame.origin.y = 0
|
||||||
|
backgroundIconFrame.origin.y = 0
|
||||||
|
|
||||||
|
let difference = self.scrollView.contentOffset.y - pinContentToTopThreshold
|
||||||
|
self.contentViewController.tableView.contentOffset.y = difference
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Keep content table view's content offset at the top.
|
||||||
|
self.contentViewController.tableView.contentOffset.y = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep background app icon centered in gap between top of content and top of screen.
|
||||||
|
backgroundIconFrame.origin.y = (contentFrame.origin.y / 2) - backgroundIconFrame.height / 2
|
||||||
|
|
||||||
|
// Set frames.
|
||||||
|
self.contentViewController.view.superview?.frame = contentFrame
|
||||||
|
self.bannerView.frame = headerFrame
|
||||||
|
self.backgroundAppIconImageView.frame = backgroundIconFrame
|
||||||
|
self.backgroundBlurView.frame = backgroundIconFrame
|
||||||
|
self.backButtonContainerView.frame = backButtonFrame
|
||||||
|
|
||||||
|
self.contentViewControllerShadowView.frame = self.contentViewController.view.frame
|
||||||
|
|
||||||
|
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
|
||||||
|
|
||||||
|
self.scrollView.scrollIndicatorInsets.top = statusBarHeight
|
||||||
|
|
||||||
|
// Adjust content offset + size.
|
||||||
|
let contentOffset = self.scrollView.contentOffset
|
||||||
|
|
||||||
|
var contentSize = self.contentViewController.tableView.contentSize
|
||||||
|
contentSize.height += maximumContentY
|
||||||
|
|
||||||
|
self.scrollView.contentSize = contentSize
|
||||||
|
self.scrollView.contentOffset = contentOffset
|
||||||
|
|
||||||
|
self.bannerView.backgroundEffectView.backgroundColor = .clear
|
||||||
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
|
||||||
|
{
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
self._shouldResetLayout = true
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit
|
||||||
|
{
|
||||||
|
self.blurAnimator?.stopAnimation(true)
|
||||||
|
self.navigationBarAnimator?.stopAnimation(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppViewController
|
||||||
|
{
|
||||||
|
class func makeAppViewController(app: StoreApp) -> AppViewController
|
||||||
|
{
|
||||||
|
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
||||||
|
|
||||||
|
let appViewController = storyboard.instantiateViewController(withIdentifier: "appViewController") as! AppViewController
|
||||||
|
appViewController.app = app
|
||||||
|
return appViewController
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppViewController
|
||||||
|
{
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
for button in [self.bannerView.button!, self.navigationBarDownloadButton!]
|
||||||
|
{
|
||||||
|
button.tintColor = self.app.tintColor
|
||||||
|
button.isIndicatingActivity = false
|
||||||
|
|
||||||
|
if self.app.installedApp == nil
|
||||||
|
{
|
||||||
|
button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress = AppManager.shared.installationProgress(for: self.app)
|
||||||
|
button.progress = progress
|
||||||
|
}
|
||||||
|
|
||||||
|
if Date() < self.app.versionDate
|
||||||
|
{
|
||||||
|
self.bannerView.button.countdownDate = self.app.versionDate
|
||||||
|
self.navigationBarDownloadButton.countdownDate = self.app.versionDate
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.bannerView.button.countdownDate = nil
|
||||||
|
self.navigationBarDownloadButton.countdownDate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let barButtonItem = self.navigationItem.rightBarButtonItem
|
||||||
|
self.navigationItem.rightBarButtonItem = nil
|
||||||
|
self.navigationItem.rightBarButtonItem = barButtonItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func showNavigationBar(for navigationController: UINavigationController? = nil)
|
||||||
|
{
|
||||||
|
let navigationController = navigationController ?? self.navigationController
|
||||||
|
navigationController?.navigationBar.alpha = 1.0
|
||||||
|
navigationController?.navigationBar.tintColor = .altPrimary
|
||||||
|
navigationController?.navigationBar.setNeedsLayout()
|
||||||
|
|
||||||
|
if self.traitCollection.userInterfaceStyle == .dark
|
||||||
|
{
|
||||||
|
self._preferredStatusBarStyle = .lightContent
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self._preferredStatusBarStyle = .default
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideNavigationBar(for navigationController: UINavigationController? = nil)
|
||||||
|
{
|
||||||
|
let navigationController = navigationController ?? self.navigationController
|
||||||
|
navigationController?.navigationBar.alpha = 0.0
|
||||||
|
|
||||||
|
self._preferredStatusBarStyle = .lightContent
|
||||||
|
navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareBlur()
|
||||||
|
{
|
||||||
|
if let animator = self.blurAnimator
|
||||||
|
{
|
||||||
|
animator.stopAnimation(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backgroundBlurView.effect = self._backgroundBlurEffect
|
||||||
|
self.backgroundBlurView.contentView.backgroundColor = self._backgroundBlurTintColor
|
||||||
|
|
||||||
|
self.blurAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
|
||||||
|
self?.backgroundBlurView.effect = nil
|
||||||
|
self?.backgroundBlurView.contentView.backgroundColor = .clear
|
||||||
|
}
|
||||||
|
|
||||||
|
self.blurAnimator?.startAnimation()
|
||||||
|
self.blurAnimator?.pauseAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareNavigationBarAnimation()
|
||||||
|
{
|
||||||
|
self.resetNavigationBarAnimation()
|
||||||
|
|
||||||
|
self.navigationBarAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
|
||||||
|
self?.showNavigationBar()
|
||||||
|
self?.navigationController?.navigationBar.tintColor = self?.app.tintColor
|
||||||
|
self?.navigationController?.navigationBar.barTintColor = nil
|
||||||
|
self?.contentViewController.view.layer.cornerRadius = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
self.navigationBarAnimator?.startAnimation()
|
||||||
|
self.navigationBarAnimator?.pauseAnimation()
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetNavigationBarAnimation()
|
||||||
|
{
|
||||||
|
self.navigationBarAnimator?.stopAnimation(true)
|
||||||
|
self.navigationBarAnimator = nil
|
||||||
|
|
||||||
|
self.hideNavigationBar()
|
||||||
|
|
||||||
|
self.contentViewController.view.layer.cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppViewController
|
||||||
|
{
|
||||||
|
@IBAction func popViewController(_ sender: UIButton)
|
||||||
|
{
|
||||||
|
self.navigationController?.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func performAppAction(_ sender: PillButton)
|
||||||
|
{
|
||||||
|
if let installedApp = self.app.installedApp
|
||||||
|
{
|
||||||
|
self.open(installedApp)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.downloadApp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadApp()
|
||||||
|
{
|
||||||
|
guard self.app.installedApp == nil else { return }
|
||||||
|
|
||||||
|
let progress = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
_ = try result.get()
|
||||||
|
}
|
||||||
|
catch OperationError.cancelled
|
||||||
|
{
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.show(in: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.bannerView.button.progress = nil
|
||||||
|
self.navigationBarDownloadButton.progress = nil
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bannerView.button.progress = progress
|
||||||
|
self.navigationBarDownloadButton.progress = progress
|
||||||
|
}
|
||||||
|
|
||||||
|
func open(_ installedApp: InstalledApp)
|
||||||
|
{
|
||||||
|
UIApplication.shared.open(installedApp.openAppURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppViewController
|
||||||
|
{
|
||||||
|
@objc func didChangeApp(_ notification: Notification)
|
||||||
|
{
|
||||||
|
// Async so that AppManager.installationProgress(for:) is nil when we update.
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func willEnterForeground(_ notification: Notification)
|
||||||
|
{
|
||||||
|
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
|
||||||
|
|
||||||
|
self._shouldResetLayout = true
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func didBecomeActive(_ notification: Notification)
|
||||||
|
{
|
||||||
|
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
|
||||||
|
|
||||||
|
// Fixes Navigation Bar appearing after app becomes inactive -> active again.
|
||||||
|
self._shouldResetLayout = true
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppViewController: UIScrollViewDelegate
|
||||||
|
{
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView)
|
||||||
|
{
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// PermissionPopoverViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/23/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class PermissionPopoverViewController: UIViewController
|
||||||
|
{
|
||||||
|
var permission: AppPermission!
|
||||||
|
|
||||||
|
@IBOutlet private var nameLabel: UILabel!
|
||||||
|
@IBOutlet private var descriptionLabel: UILabel!
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.nameLabel.text = self.permission.type.localizedName
|
||||||
|
self.descriptionLabel.text = self.permission.usageDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
230
source-code/ALTs/AltStore/App IDs/AppIDsViewController.swift
Normal file
230
source-code/ALTs/AltStore/App IDs/AppIDsViewController.swift
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
//
|
||||||
|
// AppIDsViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 1/27/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
class AppIDsViewController: UICollectionViewController
|
||||||
|
{
|
||||||
|
private lazy var dataSource = self.makeDataSource()
|
||||||
|
|
||||||
|
private var didInitialFetch = false
|
||||||
|
private var isLoading = false {
|
||||||
|
didSet {
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBOutlet var activityIndicatorBarButtonItem: UIBarButtonItem!
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.collectionView.dataSource = self.dataSource
|
||||||
|
|
||||||
|
self.activityIndicatorBarButtonItem.isIndicatingActivity = true
|
||||||
|
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
|
refreshControl.addTarget(self, action: #selector(AppIDsViewController.fetchAppIDs), for: .primaryActionTriggered)
|
||||||
|
self.collectionView.refreshControl = refreshControl
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
if !self.didInitialFetch
|
||||||
|
{
|
||||||
|
self.fetchAppIDs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppIDsViewController
|
||||||
|
{
|
||||||
|
func makeDataSource() -> RSTFetchedResultsCollectionViewDataSource<AppID>
|
||||||
|
{
|
||||||
|
let fetchRequest = AppID.fetchRequest() as NSFetchRequest<AppID>
|
||||||
|
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \AppID.name, ascending: true),
|
||||||
|
NSSortDescriptor(keyPath: \AppID.bundleIdentifier, ascending: true),
|
||||||
|
NSSortDescriptor(keyPath: \AppID.expirationDate, ascending: true)]
|
||||||
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
|
||||||
|
if let team = DatabaseManager.shared.activeTeam()
|
||||||
|
{
|
||||||
|
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(AppID.team), team)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fetchRequest.predicate = NSPredicate(value: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataSource = RSTFetchedResultsCollectionViewDataSource<AppID>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||||
|
dataSource.proxy = self
|
||||||
|
dataSource.cellConfigurationHandler = { (cell, appID, indexPath) in
|
||||||
|
let tintColor = UIColor.altPrimary
|
||||||
|
|
||||||
|
let cell = cell as! BannerCollectionViewCell
|
||||||
|
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||||
|
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
cell.tintColor = tintColor
|
||||||
|
|
||||||
|
cell.bannerView.iconImageView.isHidden = true
|
||||||
|
cell.bannerView.button.isIndicatingActivity = false
|
||||||
|
cell.bannerView.betaBadgeView.isHidden = true
|
||||||
|
|
||||||
|
cell.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
|
||||||
|
|
||||||
|
if let expirationDate = appID.expirationDate
|
||||||
|
{
|
||||||
|
cell.bannerView.button.isHidden = false
|
||||||
|
cell.bannerView.button.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
cell.bannerView.buttonLabel.isHidden = false
|
||||||
|
|
||||||
|
let currentDate = Date()
|
||||||
|
|
||||||
|
let numberOfDays = expirationDate.numberOfCalendarDays(since: currentDate)
|
||||||
|
|
||||||
|
if numberOfDays == 1
|
||||||
|
{
|
||||||
|
cell.bannerView.button.setTitle(NSLocalizedString("1 DAY", comment: ""), for: .normal)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cell.bannerView.button.setTitle(String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays)), for: .normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cell.bannerView.button.isHidden = true
|
||||||
|
cell.bannerView.buttonLabel.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.bannerView.titleLabel.text = appID.name
|
||||||
|
cell.bannerView.subtitleLabel.text = appID.bundleIdentifier
|
||||||
|
cell.bannerView.subtitleLabel.numberOfLines = 2
|
||||||
|
|
||||||
|
// Make sure refresh button is correct size.
|
||||||
|
cell.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func fetchAppIDs()
|
||||||
|
{
|
||||||
|
guard !self.isLoading else { return }
|
||||||
|
self.isLoading = true
|
||||||
|
|
||||||
|
AppManager.shared.fetchAppIDs { (result) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let (_, context) = try result.get()
|
||||||
|
try context.save()
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.show(in: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
if !self.isLoading
|
||||||
|
{
|
||||||
|
self.collectionView.refreshControl?.endRefreshing()
|
||||||
|
self.activityIndicatorBarButtonItem.isIndicatingActivity = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppIDsViewController: UICollectionViewDelegateFlowLayout
|
||||||
|
{
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||||
|
{
|
||||||
|
return CGSize(width: collectionView.bounds.width, height: 80)
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize
|
||||||
|
{
|
||||||
|
let indexPath = IndexPath(row: 0, section: section)
|
||||||
|
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath)
|
||||||
|
|
||||||
|
// Use this view to calculate the optimal size based on the collection view's width
|
||||||
|
let size = headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
|
||||||
|
withHorizontalFittingPriority: .required, // Width is fixed
|
||||||
|
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
|
||||||
|
{
|
||||||
|
return CGSize(width: collectionView.bounds.width, height: 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||||
|
{
|
||||||
|
switch kind
|
||||||
|
{
|
||||||
|
case UICollectionView.elementKindSectionHeader:
|
||||||
|
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! TextCollectionReusableView
|
||||||
|
headerView.layoutMargins.left = self.view.layoutMargins.left
|
||||||
|
headerView.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
|
if let activeTeam = DatabaseManager.shared.activeTeam(), activeTeam.type == .free
|
||||||
|
{
|
||||||
|
let text = NSLocalizedString("""
|
||||||
|
Each app and app extension installed with AltStore must register an App ID with Apple. Apple limits free developer accounts to 10 App IDs at a time.
|
||||||
|
|
||||||
|
**App IDs can't be deleted**, but they do expire after one week. AltStore will automatically renew App IDs for all active apps once they've expired.
|
||||||
|
""", comment: "")
|
||||||
|
|
||||||
|
let attributedText = NSAttributedString(markdownRepresentation: text, attributes: [.font: headerView.textLabel.font as Any])
|
||||||
|
headerView.textLabel.attributedText = attributedText
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
headerView.textLabel.text = NSLocalizedString("""
|
||||||
|
Each app and app extension installed with AltStore must register an App ID with Apple.
|
||||||
|
|
||||||
|
App IDs for paid developer accounts never expire, and there is no limit to how many you can create.
|
||||||
|
""", comment: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerView
|
||||||
|
|
||||||
|
case UICollectionView.elementKindSectionFooter:
|
||||||
|
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath) as! TextCollectionReusableView
|
||||||
|
|
||||||
|
let count = self.dataSource.itemCount
|
||||||
|
if count == 1
|
||||||
|
{
|
||||||
|
footerView.textLabel.text = NSLocalizedString("1 App ID", comment: "")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs", comment: ""), NSNumber(value: count))
|
||||||
|
}
|
||||||
|
|
||||||
|
return footerView
|
||||||
|
|
||||||
|
default: fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
593
source-code/ALTs/AltStore/AppDelegate.swift
Normal file
593
source-code/ALTs/AltStore/AppDelegate.swift
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
//
|
||||||
|
// AppDelegate.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 5/9/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import UserNotifications
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
import AltKit
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
private enum RefreshError: LocalizedError
|
||||||
|
{
|
||||||
|
case noInstalledApps
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self
|
||||||
|
{
|
||||||
|
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension CFNotificationName
|
||||||
|
{
|
||||||
|
static let requestAppState = CFNotificationName("com.altstore.RequestAppState" as CFString)
|
||||||
|
static let appIsRunning = CFNotificationName("com.altstore.AppState.Running" as CFString)
|
||||||
|
|
||||||
|
static func requestAppState(for appID: String) -> CFNotificationName
|
||||||
|
{
|
||||||
|
let name = String(CFNotificationName.requestAppState.rawValue) + "." + appID
|
||||||
|
return CFNotificationName(name as CFString)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func appIsRunning(for appID: String) -> CFNotificationName
|
||||||
|
{
|
||||||
|
let name = String(CFNotificationName.appIsRunning.rawValue) + "." + appID
|
||||||
|
return CFNotificationName(name as CFString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
|
||||||
|
{ (center, observer, name, object, userInfo) in
|
||||||
|
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let name = name else { return }
|
||||||
|
appDelegate.receivedApplicationState(notification: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppDelegate
|
||||||
|
{
|
||||||
|
static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
|
||||||
|
static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification")
|
||||||
|
|
||||||
|
static let appBackupDidFinish = Notification.Name("com.rileytestut.AltStore.AppBackupDidFinish")
|
||||||
|
|
||||||
|
static let importAppDeepLinkURLKey = "fileURL"
|
||||||
|
static let appBackupResultKey = "result"
|
||||||
|
}
|
||||||
|
|
||||||
|
@UIApplicationMain
|
||||||
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
|
var window: UIWindow?
|
||||||
|
|
||||||
|
private var runningApplications: Set<String>?
|
||||||
|
private var backgroundRefreshContext: NSManagedObjectContext? // Keep context alive until finished refreshing.
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||||
|
{
|
||||||
|
AnalyticsManager.shared.start()
|
||||||
|
|
||||||
|
self.setTintColor()
|
||||||
|
|
||||||
|
ServerManager.shared.startDiscovering()
|
||||||
|
|
||||||
|
UserDefaults.standard.registerDefaults()
|
||||||
|
|
||||||
|
if UserDefaults.standard.firstLaunch == nil
|
||||||
|
{
|
||||||
|
Keychain.shared.reset()
|
||||||
|
UserDefaults.standard.firstLaunch = Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
|
||||||
|
|
||||||
|
#if DEBUG || BETA
|
||||||
|
UserDefaults.standard.isDebugModeEnabled = true
|
||||||
|
#endif
|
||||||
|
|
||||||
|
self.prepareForBackgroundFetch()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationDidEnterBackground(_ application: UIApplication)
|
||||||
|
{
|
||||||
|
ServerManager.shared.stopDiscovering()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationWillEnterForeground(_ application: UIApplication)
|
||||||
|
{
|
||||||
|
AppManager.shared.update()
|
||||||
|
ServerManager.shared.startDiscovering()
|
||||||
|
|
||||||
|
PatreonAPI.shared.refreshPatreonAccount()
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
||||||
|
{
|
||||||
|
return self.open(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppDelegate
|
||||||
|
{
|
||||||
|
func setTintColor()
|
||||||
|
{
|
||||||
|
self.window?.tintColor = .altPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
func open(_ url: URL) -> Bool
|
||||||
|
{
|
||||||
|
if url.isFileURL
|
||||||
|
{
|
||||||
|
guard url.pathExtension.lowercased() == "ipa" else { return false }
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url])
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
||||||
|
guard let host = components.host?.lowercased() else { return false }
|
||||||
|
|
||||||
|
switch host
|
||||||
|
{
|
||||||
|
case "patreon":
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "appbackupresponse":
|
||||||
|
let result: Result<Void, Error>
|
||||||
|
|
||||||
|
switch url.path.lowercased()
|
||||||
|
{
|
||||||
|
case "/success": result = .success(())
|
||||||
|
case "/failure":
|
||||||
|
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:]
|
||||||
|
guard
|
||||||
|
let errorDomain = queryItems["errorDomain"],
|
||||||
|
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
|
||||||
|
let errorDescription = queryItems["errorDescription"]
|
||||||
|
else { return false }
|
||||||
|
|
||||||
|
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
|
||||||
|
result = .failure(error)
|
||||||
|
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "install":
|
||||||
|
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
||||||
|
guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false }
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppDelegate
|
||||||
|
{
|
||||||
|
private func prepareForBackgroundFetch()
|
||||||
|
{
|
||||||
|
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
|
||||||
|
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
|
||||||
|
|
||||||
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
UIApplication.shared.registerForRemoteNotifications()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||||
|
{
|
||||||
|
let tokenParts = deviceToken.map { data -> String in
|
||||||
|
return String(format: "%02.2hhx", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = tokenParts.joined()
|
||||||
|
print("Push Token:", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
||||||
|
{
|
||||||
|
self.application(application, performFetchWithCompletionHandler: completionHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
||||||
|
{
|
||||||
|
if UserDefaults.standard.isBackgroundRefreshEnabled
|
||||||
|
{
|
||||||
|
ServerManager.shared.startDiscovering()
|
||||||
|
|
||||||
|
if !UserDefaults.standard.presentedLaunchReminderNotification
|
||||||
|
{
|
||||||
|
let threeHours: TimeInterval = 3 * 60 * 60
|
||||||
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
|
||||||
|
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = NSLocalizedString("App Refresh Tip", comment: "")
|
||||||
|
content.body = NSLocalizedString("The more you open AltStore, the more chances it's given to refresh apps in the background.", comment: "")
|
||||||
|
|
||||||
|
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
|
||||||
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
|
||||||
|
UserDefaults.standard.presentedLaunchReminderNotification = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let refreshIdentifier = UUID().uuidString
|
||||||
|
|
||||||
|
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
|
||||||
|
|
||||||
|
func finish(_ result: Result<[String: Result<InstalledApp, Error>], Error>)
|
||||||
|
{
|
||||||
|
// If finish is actually called, that means an error occured during installation.
|
||||||
|
|
||||||
|
if UserDefaults.standard.isBackgroundRefreshEnabled
|
||||||
|
{
|
||||||
|
ServerManager.shared.stopDiscovering()
|
||||||
|
self.scheduleFinishedRefreshingNotification(for: result, identifier: refreshIdentifier, delay: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
taskCompletionHandler()
|
||||||
|
|
||||||
|
self.backgroundRefreshContext = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error = taskResult.error
|
||||||
|
{
|
||||||
|
print("Error starting extended background task. Aborting.", error)
|
||||||
|
backgroundFetchCompletionHandler(.failed)
|
||||||
|
finish(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !DatabaseManager.shared.isStarted
|
||||||
|
{
|
||||||
|
DatabaseManager.shared.start() { (error) in
|
||||||
|
if let error = error
|
||||||
|
{
|
||||||
|
backgroundFetchCompletionHandler(.failed)
|
||||||
|
finish(.failure(error))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.refreshApps(identifier: refreshIdentifier, backgroundFetchCompletionHandler: backgroundFetchCompletionHandler, completionHandler: finish(_:))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.refreshApps(identifier: refreshIdentifier, backgroundFetchCompletionHandler: backgroundFetchCompletionHandler, completionHandler: finish(_:))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppDelegate
|
||||||
|
{
|
||||||
|
func refreshApps(identifier: String,
|
||||||
|
backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
||||||
|
completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
|
||||||
|
{
|
||||||
|
var fetchSourcesResult: Result<Set<Source>, Error>?
|
||||||
|
var serversResult: Result<Void, Error>?
|
||||||
|
|
||||||
|
let dispatchGroup = DispatchGroup()
|
||||||
|
dispatchGroup.enter()
|
||||||
|
|
||||||
|
AppManager.shared.fetchSources() { (result) in
|
||||||
|
fetchSourcesResult = result
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let sources = try result.get()
|
||||||
|
|
||||||
|
guard let context = sources.first?.managedObjectContext else { return }
|
||||||
|
|
||||||
|
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
||||||
|
previousUpdatesFetchRequest.includesPendingChanges = false
|
||||||
|
previousUpdatesFetchRequest.resultType = .dictionaryResultType
|
||||||
|
previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)]
|
||||||
|
|
||||||
|
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
|
||||||
|
previousNewsItemsFetchRequest.includesPendingChanges = false
|
||||||
|
previousNewsItemsFetchRequest.resultType = .dictionaryResultType
|
||||||
|
previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)]
|
||||||
|
|
||||||
|
let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]]
|
||||||
|
let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]]
|
||||||
|
|
||||||
|
try context.save()
|
||||||
|
|
||||||
|
let updatesFetchRequest = InstalledApp.updatesFetchRequest()
|
||||||
|
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
||||||
|
|
||||||
|
let updates = try context.fetch(updatesFetchRequest)
|
||||||
|
let newsItems = try context.fetch(newsItemsFetchRequest)
|
||||||
|
|
||||||
|
for update in updates
|
||||||
|
{
|
||||||
|
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
|
||||||
|
guard let storeApp = update.storeApp else { continue }
|
||||||
|
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = NSLocalizedString("New Update Available", comment: "")
|
||||||
|
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, storeApp.version)
|
||||||
|
content.sound = .default
|
||||||
|
|
||||||
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||||
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
for newsItem in newsItems
|
||||||
|
{
|
||||||
|
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
|
||||||
|
guard !newsItem.isSilent else { continue }
|
||||||
|
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
|
if let app = newsItem.storeApp
|
||||||
|
{
|
||||||
|
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
content.title = NSLocalizedString("AltStore News", comment: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
content.body = newsItem.title
|
||||||
|
content.sound = .default
|
||||||
|
|
||||||
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||||
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
UIApplication.shared.applicationIconBadgeNumber = updates.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Error fetching apps:", error)
|
||||||
|
|
||||||
|
fetchSourcesResult = .failure(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchGroup.leave()
|
||||||
|
}
|
||||||
|
|
||||||
|
if UserDefaults.standard.isBackgroundRefreshEnabled
|
||||||
|
{
|
||||||
|
dispatchGroup.enter()
|
||||||
|
|
||||||
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
|
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
|
||||||
|
guard !installedApps.isEmpty else {
|
||||||
|
serversResult = .success(())
|
||||||
|
dispatchGroup.leave()
|
||||||
|
|
||||||
|
completionHandler(.failure(RefreshError.noInstalledApps))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.runningApplications = []
|
||||||
|
self.backgroundRefreshContext = context
|
||||||
|
|
||||||
|
let identifiers = installedApps.compactMap { $0.bundleIdentifier }
|
||||||
|
print("Apps to refresh:", identifiers)
|
||||||
|
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
||||||
|
|
||||||
|
for identifier in identifiers
|
||||||
|
{
|
||||||
|
let appIsRunningNotification = CFNotificationName.appIsRunning(for: identifier)
|
||||||
|
CFNotificationCenterAddObserver(notificationCenter, nil, ReceivedApplicationState, appIsRunningNotification.rawValue, nil, .deliverImmediately)
|
||||||
|
|
||||||
|
let requestAppStateNotification = CFNotificationName.requestAppState(for: identifier)
|
||||||
|
CFNotificationCenterPostNotification(notificationCenter, requestAppStateNotification, nil, nil, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for three seconds to:
|
||||||
|
// a) give us time to discover AltServers
|
||||||
|
// b) give other processes a chance to respond to requestAppState notification
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||||
|
context.perform {
|
||||||
|
if ServerManager.shared.discoveredServers.isEmpty
|
||||||
|
{
|
||||||
|
serversResult = .failure(ConnectionError.serverNotFound)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serversResult = .success(())
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchGroup.leave()
|
||||||
|
|
||||||
|
let filteredApps = installedApps.filter { !(self.runningApplications?.contains($0.bundleIdentifier) ?? false) }
|
||||||
|
print("Filtered Apps to Refresh:", filteredApps.map { $0.bundleIdentifier })
|
||||||
|
|
||||||
|
let group = AppManager.shared.refresh(filteredApps, presentingViewController: nil)
|
||||||
|
group.beginInstallationHandler = { (installedApp) in
|
||||||
|
guard installedApp.bundleIdentifier == StoreApp.altstoreAppID else { return }
|
||||||
|
|
||||||
|
// We're starting to install AltStore, which means the app is about to quit.
|
||||||
|
// So, we schedule a "refresh successful" local notification to be displayed after a delay,
|
||||||
|
// but if the app is still running, we cancel the notification.
|
||||||
|
// Then, we schedule another notification and repeat the process.
|
||||||
|
|
||||||
|
// Also since AltServer has already received the app, it can finish installing even if we're no longer running in background.
|
||||||
|
|
||||||
|
if let error = group.context.error
|
||||||
|
{
|
||||||
|
self.scheduleFinishedRefreshingNotification(for: .failure(error), identifier: identifier)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var results = group.results
|
||||||
|
results[installedApp.bundleIdentifier] = .success(installedApp)
|
||||||
|
|
||||||
|
self.scheduleFinishedRefreshingNotification(for: .success(results), identifier: identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group.completionHandler = { (results) in
|
||||||
|
completionHandler(.success(results))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchGroup.notify(queue: .main) {
|
||||||
|
if !UserDefaults.standard.isBackgroundRefreshEnabled
|
||||||
|
{
|
||||||
|
guard let fetchSourcesResult = fetchSourcesResult else {
|
||||||
|
backgroundFetchCompletionHandler(.failed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fetchSourcesResult
|
||||||
|
{
|
||||||
|
case .failure: backgroundFetchCompletionHandler(.failed)
|
||||||
|
case .success: backgroundFetchCompletionHandler(.newData)
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler(.success([:]))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
guard let fetchSourcesResult = fetchSourcesResult, let serversResult = serversResult else {
|
||||||
|
backgroundFetchCompletionHandler(.failed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call completionHandler early to improve chances of refreshing in the background again.
|
||||||
|
switch (fetchSourcesResult, serversResult)
|
||||||
|
{
|
||||||
|
case (.success, .success): backgroundFetchCompletionHandler(.newData)
|
||||||
|
case (.success, .failure(ConnectionError.serverNotFound)): backgroundFetchCompletionHandler(.newData)
|
||||||
|
case (.failure, _), (_, .failure): backgroundFetchCompletionHandler(.failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func receivedApplicationState(notification: CFNotificationName)
|
||||||
|
{
|
||||||
|
let baseName = String(CFNotificationName.appIsRunning.rawValue)
|
||||||
|
|
||||||
|
let appID = String(notification.rawValue).replacingOccurrences(of: baseName + ".", with: "")
|
||||||
|
self.runningApplications?.insert(appID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scheduleFinishedRefreshingNotification(for result: Result<[String: Result<InstalledApp, Error>], Error>, identifier: String, delay: TimeInterval = 5)
|
||||||
|
{
|
||||||
|
func scheduleFinishedRefreshingNotification()
|
||||||
|
{
|
||||||
|
self.cancelFinishedRefreshingNotification(identifier: identifier)
|
||||||
|
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
|
var shouldPresentAlert = true
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let results = try result.get()
|
||||||
|
shouldPresentAlert = !results.isEmpty
|
||||||
|
|
||||||
|
for (_, result) in results
|
||||||
|
{
|
||||||
|
guard case let .failure(error) = result else { continue }
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
content.title = NSLocalizedString("Refreshed Apps", comment: "")
|
||||||
|
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
|
||||||
|
}
|
||||||
|
catch ConnectionError.serverNotFound
|
||||||
|
{
|
||||||
|
shouldPresentAlert = false
|
||||||
|
}
|
||||||
|
catch RefreshError.noInstalledApps
|
||||||
|
{
|
||||||
|
shouldPresentAlert = false
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to refresh apps in background.", error)
|
||||||
|
|
||||||
|
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
|
||||||
|
content.body = error.localizedDescription
|
||||||
|
|
||||||
|
shouldPresentAlert = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldPresentAlert
|
||||||
|
{
|
||||||
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)
|
||||||
|
|
||||||
|
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
||||||
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
|
||||||
|
if delay > 0
|
||||||
|
{
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
|
||||||
|
UNUserNotificationCenter.current().getPendingNotificationRequests() { (requests) in
|
||||||
|
// If app is still running at this point, we schedule another notification with same identifier.
|
||||||
|
// This prevents the currently scheduled notification from displaying, and starts another countdown timer.
|
||||||
|
// First though, make sure there _is_ still a pending request, otherwise it's been cancelled
|
||||||
|
// and we should stop polling.
|
||||||
|
guard requests.contains(where: { $0.identifier == identifier }) else { return }
|
||||||
|
|
||||||
|
scheduleFinishedRefreshingNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleFinishedRefreshingNotification()
|
||||||
|
|
||||||
|
// Perform synchronously to ensure app doesn't quit before we've finishing saving to disk.
|
||||||
|
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||||
|
context.performAndWait {
|
||||||
|
_ = RefreshAttempt(identifier: identifier, result: result, context: context)
|
||||||
|
|
||||||
|
do { try context.save() }
|
||||||
|
catch { print("Failed to save refresh attempt.", error) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelFinishedRefreshingNotification(identifier: String)
|
||||||
|
{
|
||||||
|
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,508 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15703"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Navigation Controller-->
|
||||||
|
<scene sceneID="lNR-II-WoW">
|
||||||
|
<objects>
|
||||||
|
<navigationController storyboardIdentifier="navigationController" id="ZTo-53-dSL" sceneMemberID="viewController">
|
||||||
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<color key="barTintColor" name="SettingsBackground"/>
|
||||||
|
<textAttributes key="titleTextAttributes">
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</textAttributes>
|
||||||
|
<textAttributes key="largeTitleTextAttributes">
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</textAttributes>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="boolean" keyPath="automaticallyAdjustsItemPositions" value="NO"/>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</navigationBar>
|
||||||
|
</navigationController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="9J6-jc-46k" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="-164" y="735"/>
|
||||||
|
</scene>
|
||||||
|
<!--Authentication View Controller-->
|
||||||
|
<scene sceneID="OCd-xc-Ms7">
|
||||||
|
<objects>
|
||||||
|
<viewController storyboardIdentifier="authenticationViewController" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="mjy-4S-hyH">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View">
|
||||||
|
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
|
||||||
|
</view>
|
||||||
|
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<subviews>
|
||||||
|
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
|
||||||
|
<rect key="frame" x="16" y="6" width="343" height="359.5"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to AltStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="333.5" height="41"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au">
|
||||||
|
<rect key="frame" x="0.0" y="47" width="308.5" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Aqh-MD-HFf">
|
||||||
|
<rect key="frame" x="0.0" y="117.5" width="343" height="242"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Oy6-xr-cZ7">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="343" height="159"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="H95-7V-Kk8" userLabel="Apple ID">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="343" height="72"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="KN1-Kp-M1q">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="APPLE ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="59N-O1-6bM">
|
||||||
|
<rect key="frame" x="14" y="0.0" width="329" height="17"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
|
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
|
||||||
|
</stackView>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gNe-dC-oI1">
|
||||||
|
<rect key="frame" x="0.0" y="21" width="343" height="51"/>
|
||||||
|
<subviews>
|
||||||
|
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="name@email.com" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="DBu-vt-hlo">
|
||||||
|
<rect key="frame" x="14" y="0.0" width="315" height="51"/>
|
||||||
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
|
||||||
|
<textInputTraits key="textInputTraits" returnKeyType="next" textContentType="email"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="yO1-iT-7NP" id="G13-jV-DLX"/>
|
||||||
|
</connections>
|
||||||
|
</textField>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" white="1" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="DBu-vt-hlo" secondAttribute="trailing" id="0Lf-vH-juh"/>
|
||||||
|
<constraint firstItem="DBu-vt-hlo" firstAttribute="centerY" secondItem="gNe-dC-oI1" secondAttribute="centerY" id="kgs-hg-ECM"/>
|
||||||
|
<constraint firstItem="DBu-vt-hlo" firstAttribute="height" secondItem="gNe-dC-oI1" secondAttribute="height" id="n7y-Xg-8MP"/>
|
||||||
|
<constraint firstItem="DBu-vt-hlo" firstAttribute="leading" secondItem="gNe-dC-oI1" secondAttribute="leadingMargin" id="sat-rb-OIu"/>
|
||||||
|
<constraint firstAttribute="height" constant="51" id="tuP-Uo-6qp"/>
|
||||||
|
</constraints>
|
||||||
|
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="14"/>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hd5-yc-rcq" userLabel="Password">
|
||||||
|
<rect key="frame" x="0.0" y="87" width="343" height="72"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lvX-im-C95">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="PASSWORD" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ava-XY-7vs">
|
||||||
|
<rect key="frame" x="14" y="0.0" width="329" height="17"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
|
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
|
||||||
|
</stackView>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cLc-iA-yq5">
|
||||||
|
<rect key="frame" x="0.0" y="21" width="343" height="51"/>
|
||||||
|
<subviews>
|
||||||
|
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="••••••••" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="R77-TQ-lVT">
|
||||||
|
<rect key="frame" x="14" y="0.0" width="315" height="51"/>
|
||||||
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
|
||||||
|
<textInputTraits key="textInputTraits" returnKeyType="go" enablesReturnKeyAutomatically="YES" secureTextEntry="YES" textContentType="password"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="yO1-iT-7NP" id="Wpg-DV-BNL"/>
|
||||||
|
</connections>
|
||||||
|
</textField>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" white="1" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="R77-TQ-lVT" firstAttribute="leading" secondItem="cLc-iA-yq5" secondAttribute="leadingMargin" id="130-RD-MwU"/>
|
||||||
|
<constraint firstAttribute="height" constant="51" id="9Jw-2V-fgf"/>
|
||||||
|
<constraint firstItem="R77-TQ-lVT" firstAttribute="height" secondItem="cLc-iA-yq5" secondAttribute="height" id="FFf-Bp-LPT"/>
|
||||||
|
<constraint firstItem="R77-TQ-lVT" firstAttribute="centerY" secondItem="cLc-iA-yq5" secondAttribute="centerY" id="agB-KM-ba3"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="R77-TQ-lVT" secondAttribute="trailing" id="jB5-Ye-cJB"/>
|
||||||
|
</constraints>
|
||||||
|
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="14"/>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
|
||||||
|
<rect key="frame" x="0.0" y="191" width="343" height="51"/>
|
||||||
|
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/>
|
||||||
|
</constraints>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
|
||||||
|
<state key="normal" title="Sign in">
|
||||||
|
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</state>
|
||||||
|
<connections>
|
||||||
|
<action selector="authenticate" destination="yO1-iT-7NP" eventType="primaryActionTriggered" id="LER-a2-CbC"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8">
|
||||||
|
<rect key="frame" x="16" y="518.5" width="343" height="96.5"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="249" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="on2-62-waY">
|
||||||
|
<rect key="frame" x="0.0" y="24.5" width="343" height="72"/>
|
||||||
|
<string key="text">Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.</string>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||||
|
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="DBk-rT-ZE8" firstAttribute="top" relation="greaterThanOrEqual" secondItem="YmX-7v-pxh" secondAttribute="bottom" constant="8" symbolic="YES" id="zTU-eY-DWd"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="2wp-qG-f0Z" firstAttribute="leading" secondItem="WXx-hX-AXv" secondAttribute="leading" id="13j-ii-X7W"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="2wp-qG-f0Z" secondAttribute="bottom" id="Ggl-es-C4C"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="2wp-qG-f0Z" secondAttribute="trailing" id="nl1-88-5mM"/>
|
||||||
|
<constraint firstItem="2wp-qG-f0Z" firstAttribute="top" secondItem="WXx-hX-AXv" secondAttribute="top" id="wiH-lv-L9P"/>
|
||||||
|
</constraints>
|
||||||
|
</scrollView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" name="SettingsBackground"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="WXx-hX-AXv" secondAttribute="bottom" id="0jL-Ky-ju6"/>
|
||||||
|
<constraint firstAttribute="leadingMargin" secondItem="YmX-7v-pxh" secondAttribute="leading" id="2PO-lG-dmB"/>
|
||||||
|
<constraint firstItem="DBk-rT-ZE8" firstAttribute="leading" secondItem="2wp-qG-f0Z" secondAttribute="leadingMargin" id="5AT-nV-ZP9"/>
|
||||||
|
<constraint firstItem="oyW-Fd-ojD" firstAttribute="top" secondItem="zMn-DV-fpy" secondAttribute="top" id="730-db-ukB"/>
|
||||||
|
<constraint firstItem="2wp-qG-f0Z" firstAttribute="bottomMargin" secondItem="DBk-rT-ZE8" secondAttribute="bottom" id="HgY-oY-8KM"/>
|
||||||
|
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="oyW-Fd-ojD" secondAttribute="trailing" id="KGE-CN-SWf"/>
|
||||||
|
<constraint firstItem="WXx-hX-AXv" firstAttribute="top" secondItem="mjy-4S-hyH" secondAttribute="top" id="LPQ-bF-ic0"/>
|
||||||
|
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="WXx-hX-AXv" secondAttribute="trailing" id="MG7-A6-pKp"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="YmX-7v-pxh" secondAttribute="trailing" id="O4T-nu-o3e"/>
|
||||||
|
<constraint firstItem="zMn-DV-fpy" firstAttribute="bottom" secondItem="oyW-Fd-ojD" secondAttribute="bottom" id="PuX-ab-cEq"/>
|
||||||
|
<constraint firstItem="oyW-Fd-ojD" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="SzC-gC-Nvi"/>
|
||||||
|
<constraint firstItem="2wp-qG-f0Z" firstAttribute="trailingMargin" secondItem="DBk-rT-ZE8" secondAttribute="trailing" id="VCf-bW-2K4"/>
|
||||||
|
<constraint firstItem="WXx-hX-AXv" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="d08-zF-5X6"/>
|
||||||
|
<constraint firstItem="2wp-qG-f0Z" firstAttribute="height" secondItem="oyW-Fd-ojD" secondAttribute="height" id="dFN-pw-TWt"/>
|
||||||
|
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
|
||||||
|
<constraint firstItem="2wp-qG-f0Z" firstAttribute="width" secondItem="oyW-Fd-ojD" secondAttribute="width" id="rYO-GN-0Lk"/>
|
||||||
|
</constraints>
|
||||||
|
<viewLayoutGuide key="safeArea" id="zMn-DV-fpy"/>
|
||||||
|
</view>
|
||||||
|
<toolbarItems/>
|
||||||
|
<navigationItem key="navigationItem" largeTitleDisplayMode="never" id="jCf-N4-xVD">
|
||||||
|
<barButtonItem key="leftBarButtonItem" title="Close" id="nDc-Zs-wnK">
|
||||||
|
<connections>
|
||||||
|
<action selector="cancel:" destination="yO1-iT-7NP" id="xls-in-Pre"/>
|
||||||
|
</connections>
|
||||||
|
</barButtonItem>
|
||||||
|
</navigationItem>
|
||||||
|
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||||
|
<nil key="simulatedBottomBarMetrics"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="appleIDBackgroundView" destination="gNe-dC-oI1" id="lab-WG-pyJ"/>
|
||||||
|
<outlet property="appleIDTextField" destination="DBu-vt-hlo" id="ZMK-9K-phY"/>
|
||||||
|
<outlet property="contentStackView" destination="YmX-7v-pxh" id="ZX5-Af-cEB"/>
|
||||||
|
<outlet property="passwordBackgroundView" destination="cLc-iA-yq5" id="2JD-nS-Gf7"/>
|
||||||
|
<outlet property="passwordTextField" destination="R77-TQ-lVT" id="cLQ-Wn-MsE"/>
|
||||||
|
<outlet property="scrollView" destination="WXx-hX-AXv" id="hOb-gl-0OP"/>
|
||||||
|
<outlet property="signInButton" destination="2N5-zd-fUj" id="ul1-bh-4l4"/>
|
||||||
|
</connections>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="U7A-Cx-Bo9" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="605.60000000000002" y="736.28185907046486"/>
|
||||||
|
</scene>
|
||||||
|
<!--How it works-->
|
||||||
|
<scene sceneID="dMt-EA-SGy">
|
||||||
|
<objects>
|
||||||
|
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="Otz-hn-WGS">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2">
|
||||||
|
<rect key="frame" x="0.0" y="44" width="375" height="564"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K">
|
||||||
|
<rect key="frame" x="16" y="35" width="343" height="95.5"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="1" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i9V-3h-B8f">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="59" id="ILg-0e-PW8"/>
|
||||||
|
</constraints>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="black" pointSize="80"/>
|
||||||
|
<color key="textColor" white="1" alpha="0.5" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Q20-ml-9D0">
|
||||||
|
<rect key="frame" x="79" y="16" width="264" height="64"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Launch AltServer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="XKD-XH-eB0">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Leave AltServer running in the background on your computer." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="6HP-Xh-sAH">
|
||||||
|
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||||
|
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
|
||||||
|
<rect key="frame" x="16" y="168" width="343" height="95.5"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="59" id="HzE-AA-eE5"/>
|
||||||
|
</constraints>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="black" pointSize="80"/>
|
||||||
|
<color key="textColor" white="1" alpha="0.5" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
|
||||||
|
<rect key="frame" x="79" y="15.5" width="264" height="64"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to WiFi" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable iTunes WiFi Sync and connect to the same WiFi as AltServer." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
|
||||||
|
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||||
|
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
|
||||||
|
<rect key="frame" x="16" y="300.5" width="343" height="95.5"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="59" id="fRj-b4-VTe"/>
|
||||||
|
</constraints>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="black" pointSize="80"/>
|
||||||
|
<color key="textColor" white="1" alpha="0.5" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL">
|
||||||
|
<rect key="frame" x="79" y="16" width="264" height="64"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Browse and download apps directly from AltStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="M7T-9j-uyt">
|
||||||
|
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||||
|
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
|
||||||
|
<rect key="frame" x="16" y="433.5" width="343" height="95.5"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="59" id="4Qg-s9-p7s"/>
|
||||||
|
</constraints>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="black" pointSize="80"/>
|
||||||
|
<color key="textColor" white="1" alpha="0.5" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz">
|
||||||
|
<rect key="frame" x="79" y="16" width="264" height="64"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps Refresh Automatically" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="nvb-Aq-sYa">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background when on same WiFi as AltServer." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d">
|
||||||
|
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||||
|
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<edgeInsets key="layoutMargins" top="35" left="0.0" bottom="35" right="0.0"/>
|
||||||
|
</stackView>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK">
|
||||||
|
<rect key="frame" x="16" y="608" width="343" height="51"/>
|
||||||
|
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/>
|
||||||
|
</constraints>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
|
||||||
|
<state key="normal" title="Got it">
|
||||||
|
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</state>
|
||||||
|
<connections>
|
||||||
|
<action selector="dismiss" destination="aFi-fb-W0B" eventType="primaryActionTriggered" id="sBq-zj-Mln"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" name="SettingsBackground"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="qZ9-AR-2zK" firstAttribute="top" secondItem="bp6-55-IG2" secondAttribute="bottom" id="3yt-cr-swd"/>
|
||||||
|
<constraint firstItem="bp6-55-IG2" firstAttribute="top" secondItem="Zek-aC-HOO" secondAttribute="top" id="42S-q2-YZn"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="qZ9-AR-2zK" secondAttribute="trailing" id="8b4-iU-U7R"/>
|
||||||
|
<constraint firstItem="bp6-55-IG2" firstAttribute="leading" secondItem="Zek-aC-HOO" secondAttribute="leading" id="K1R-1r-FP3"/>
|
||||||
|
<constraint firstItem="Zek-aC-HOO" firstAttribute="trailing" secondItem="bp6-55-IG2" secondAttribute="trailing" id="aKV-sS-alh"/>
|
||||||
|
<constraint firstAttribute="bottomMargin" secondItem="qZ9-AR-2zK" secondAttribute="bottom" id="e8e-9l-Mkt"/>
|
||||||
|
<constraint firstItem="qZ9-AR-2zK" firstAttribute="leading" secondItem="Otz-hn-WGS" secondAttribute="leadingMargin" id="t2b-3e-6ld"/>
|
||||||
|
</constraints>
|
||||||
|
<viewLayoutGuide key="safeArea" id="Zek-aC-HOO"/>
|
||||||
|
</view>
|
||||||
|
<navigationItem key="navigationItem" title="How it works" largeTitleDisplayMode="always" id="bCq-Jq-gf1"/>
|
||||||
|
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="contentStackView" destination="bp6-55-IG2" id="k0Q-yS-Dxp"/>
|
||||||
|
<outlet property="dismissButton" destination="qZ9-AR-2zK" id="w5c-v6-TcC"/>
|
||||||
|
</connections>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="3Q4-ya-qhc" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="1353" y="736"/>
|
||||||
|
</scene>
|
||||||
|
<!--Refresh AltStore-->
|
||||||
|
<scene sceneID="9Vh-dM-OqX">
|
||||||
|
<objects>
|
||||||
|
<viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fpO-Bf-gFY" customClass="RSTPlaceholderView">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
</view>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg">
|
||||||
|
<rect key="frame" x="16" y="570" width="343" height="89"/>
|
||||||
|
<subviews>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
|
||||||
|
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="51" id="SJA-N9-Z6u"/>
|
||||||
|
</constraints>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
|
||||||
|
<color key="tintColor" name="SettingsHighlighted"/>
|
||||||
|
<state key="normal" title="Refresh Now">
|
||||||
|
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</state>
|
||||||
|
<connections>
|
||||||
|
<action selector="refreshAltStore:" destination="aoK-yE-UVT" eventType="primaryActionTriggered" id="WQu-9b-Zgg"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ">
|
||||||
|
<rect key="frame" x="0.0" y="59" width="343" height="30"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||||
|
<state key="normal" title="Refresh Later">
|
||||||
|
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</state>
|
||||||
|
<connections>
|
||||||
|
<action selector="cancel:" destination="aoK-yE-UVT" eventType="primaryActionTriggered" id="ffO-0a-LdE"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" name="SettingsBackground"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="fpO-Bf-gFY" firstAttribute="leading" secondItem="iwE-xE-ziz" secondAttribute="leading" id="A77-nX-Wg2"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="tDQ-ao-1Jg" secondAttribute="trailing" id="KPg-sO-Rnc"/>
|
||||||
|
<constraint firstItem="fpO-Bf-gFY" firstAttribute="trailing" secondItem="iwE-xE-ziz" secondAttribute="trailing" id="SGI-1D-Eaw"/>
|
||||||
|
<constraint firstItem="fpO-Bf-gFY" firstAttribute="bottom" secondItem="R83-kV-365" secondAttribute="bottom" id="cHl-7X-dW1"/>
|
||||||
|
<constraint firstAttribute="bottomMargin" secondItem="tDQ-ao-1Jg" secondAttribute="bottom" id="kLN-e7-BJE"/>
|
||||||
|
<constraint firstItem="fpO-Bf-gFY" firstAttribute="top" secondItem="R83-kV-365" secondAttribute="top" id="oKo-10-7kD"/>
|
||||||
|
<constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/>
|
||||||
|
</constraints>
|
||||||
|
<viewLayoutGuide key="safeArea" id="iwE-xE-ziz"/>
|
||||||
|
</view>
|
||||||
|
<navigationItem key="navigationItem" title="Refresh AltStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
|
||||||
|
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/>
|
||||||
|
</connections>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="Chr-7g-qEw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="2101.5999999999999" y="733.5832083958021"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<namedColor name="SettingsBackground">
|
||||||
|
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
<namedColor name="SettingsHighlighted">
|
||||||
|
<color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
<color key="tintColor" name="Primary"/>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
//
|
||||||
|
// AuthenticationViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 9/5/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
class AuthenticationViewController: UIViewController
|
||||||
|
{
|
||||||
|
var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)?
|
||||||
|
var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)?
|
||||||
|
|
||||||
|
private weak var toastView: ToastView?
|
||||||
|
|
||||||
|
@IBOutlet private var appleIDTextField: UITextField!
|
||||||
|
@IBOutlet private var passwordTextField: UITextField!
|
||||||
|
@IBOutlet private var signInButton: UIButton!
|
||||||
|
|
||||||
|
@IBOutlet private var appleIDBackgroundView: UIView!
|
||||||
|
@IBOutlet private var passwordBackgroundView: UIView!
|
||||||
|
|
||||||
|
@IBOutlet private var scrollView: UIScrollView!
|
||||||
|
@IBOutlet private var contentStackView: UIStackView!
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.signInButton.activityIndicatorView.style = .white
|
||||||
|
|
||||||
|
for view in [self.appleIDBackgroundView!, self.passwordBackgroundView!, self.signInButton!]
|
||||||
|
{
|
||||||
|
view.clipsToBounds = true
|
||||||
|
view.layer.cornerRadius = 16
|
||||||
|
}
|
||||||
|
|
||||||
|
if UIScreen.main.isExtraCompactHeight
|
||||||
|
{
|
||||||
|
self.contentStackView.spacing = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationViewController.textFieldDidChangeText(_:)), name: UITextField.textDidChangeNotification, object: self.appleIDTextField)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationViewController.textFieldDidChangeText(_:)), name: UITextField.textDidChangeNotification, object: self.passwordTextField)
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
|
self.signInButton.isIndicatingActivity = false
|
||||||
|
self.toastView?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AuthenticationViewController
|
||||||
|
{
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
if let _ = self.validate()
|
||||||
|
{
|
||||||
|
self.signInButton.isEnabled = true
|
||||||
|
self.signInButton.alpha = 1.0
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.signInButton.isEnabled = false
|
||||||
|
self.signInButton.alpha = 0.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate() -> (String, String)?
|
||||||
|
{
|
||||||
|
guard
|
||||||
|
let emailAddress = self.appleIDTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !emailAddress.isEmpty,
|
||||||
|
let password = self.passwordTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !password.isEmpty
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
return (emailAddress, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AuthenticationViewController
|
||||||
|
{
|
||||||
|
@IBAction func authenticate()
|
||||||
|
{
|
||||||
|
guard let (emailAddress, password) = self.validate() else { return }
|
||||||
|
|
||||||
|
self.appleIDTextField.resignFirstResponder()
|
||||||
|
self.passwordTextField.resignFirstResponder()
|
||||||
|
|
||||||
|
self.signInButton.isIndicatingActivity = true
|
||||||
|
|
||||||
|
self.authenticationHandler?(emailAddress, password) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
|
||||||
|
// Ignore
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.signInButton.isIndicatingActivity = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error as NSError):
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Log In", comment: ""))
|
||||||
|
|
||||||
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.textLabel.textColor = .altPink
|
||||||
|
toastView.detailTextLabel.textColor = .altPink
|
||||||
|
toastView.show(in: self)
|
||||||
|
self.toastView = toastView
|
||||||
|
|
||||||
|
self.signInButton.isIndicatingActivity = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case .success((let account, let session)):
|
||||||
|
self.completionHandler?((account, session, password))
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.scrollView.setContentOffset(CGPoint(x: 0, y: -self.view.safeAreaInsets.top), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func cancel(_ sender: UIBarButtonItem)
|
||||||
|
{
|
||||||
|
self.completionHandler?(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AuthenticationViewController: UITextFieldDelegate
|
||||||
|
{
|
||||||
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool
|
||||||
|
{
|
||||||
|
switch textField
|
||||||
|
{
|
||||||
|
case self.appleIDTextField: self.passwordTextField.becomeFirstResponder()
|
||||||
|
case self.passwordTextField: self.authenticate()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func textFieldDidBeginEditing(_ textField: UITextField)
|
||||||
|
{
|
||||||
|
guard UIScreen.main.isExtraCompactHeight else { return }
|
||||||
|
|
||||||
|
// Position all the controls within visible frame.
|
||||||
|
var contentOffset = self.scrollView.contentOffset
|
||||||
|
contentOffset.y = 44
|
||||||
|
self.scrollView.setContentOffset(contentOffset, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AuthenticationViewController
|
||||||
|
{
|
||||||
|
@objc func textFieldDidChangeText(_ notification: Notification)
|
||||||
|
{
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// InstructionsViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 9/6/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class InstructionsViewController: UIViewController
|
||||||
|
{
|
||||||
|
var completionHandler: (() -> Void)?
|
||||||
|
|
||||||
|
var showsBottomButton: Bool = false
|
||||||
|
|
||||||
|
@IBOutlet private var contentStackView: UIStackView!
|
||||||
|
@IBOutlet private var dismissButton: UIButton!
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return .lightContent
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
if UIScreen.main.isExtraCompactHeight
|
||||||
|
{
|
||||||
|
self.contentStackView.layoutMargins.top = 0
|
||||||
|
self.contentStackView.layoutMargins.bottom = self.contentStackView.layoutMargins.left
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dismissButton.clipsToBounds = true
|
||||||
|
self.dismissButton.layer.cornerRadius = 16
|
||||||
|
|
||||||
|
if self.showsBottomButton
|
||||||
|
{
|
||||||
|
self.navigationItem.hidesBackButton = true
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.dismissButton.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension InstructionsViewController
|
||||||
|
{
|
||||||
|
@IBAction func dismiss()
|
||||||
|
{
|
||||||
|
self.completionHandler?()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
//
|
||||||
|
// RefreshAltStoreViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 10/26/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
class RefreshAltStoreViewController: UIViewController
|
||||||
|
{
|
||||||
|
var context: AuthenticatedOperationContext!
|
||||||
|
|
||||||
|
var completionHandler: ((Result<Void, Error>) -> Void)?
|
||||||
|
|
||||||
|
@IBOutlet private var placeholderView: RSTPlaceholderView!
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.placeholderView.textLabel.isHidden = true
|
||||||
|
|
||||||
|
self.placeholderView.detailTextLabel.textAlignment = .left
|
||||||
|
self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
|
||||||
|
self.placeholderView.detailTextLabel.text = NSLocalizedString("AltStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including AltStore.\n\nTo prevent AltStore from expiring early, please refresh the app now. AltStore will quit once refreshing is complete.", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension RefreshAltStoreViewController
|
||||||
|
{
|
||||||
|
@IBAction func refreshAltStore(_ sender: PillButton)
|
||||||
|
{
|
||||||
|
guard let altStore = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext) else { return }
|
||||||
|
|
||||||
|
func refresh()
|
||||||
|
{
|
||||||
|
sender.isIndicatingActivity = true
|
||||||
|
|
||||||
|
if let progress = AppManager.shared.installationProgress(for: altStore)
|
||||||
|
{
|
||||||
|
// Cancel pending AltStore installation so we can start a new one.
|
||||||
|
progress.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install, _not_ refresh, to ensure we are installing with a non-revoked certificate.
|
||||||
|
let progress = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .success: self.completionHandler?(.success(()))
|
||||||
|
case .failure(let error as NSError):
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
sender.progress = nil
|
||||||
|
sender.isIndicatingActivity = false
|
||||||
|
|
||||||
|
let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh AltStore", comment: ""), message: error.localizedFailureReason ?? error.localizedDescription, preferredStyle: .alert)
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { (action) in
|
||||||
|
refresh()
|
||||||
|
}))
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh Later", comment: ""), style: .cancel, handler: { (action) in
|
||||||
|
self.completionHandler?(.failure(error))
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.present(alertController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.progress = progress
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func cancel(_ sender: UIButton)
|
||||||
|
{
|
||||||
|
self.completionHandler?(.failure(OperationError.cancelled))
|
||||||
|
}
|
||||||
|
}
|
||||||
51
source-code/ALTs/AltStore/Base.lproj/LaunchScreen.storyboard
Normal file
51
source-code/ALTs/AltStore/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="6NO-wl-tj1">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" name="Background"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||||
|
</view>
|
||||||
|
<tabBarItem key="tabBarItem" title="" id="RiK-sx-Kgv"/>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="962.31884057971024" y="375"/>
|
||||||
|
</scene>
|
||||||
|
<!--Tab Bar Controller-->
|
||||||
|
<scene sceneID="9Yy-ze-Trt">
|
||||||
|
<objects>
|
||||||
|
<tabBarController automaticallyAdjustsScrollViewInsets="NO" id="6NO-wl-tj1" sceneMemberID="viewController">
|
||||||
|
<toolbarItems/>
|
||||||
|
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="4lc-l2-vDf">
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</tabBar>
|
||||||
|
<connections>
|
||||||
|
<segue destination="01J-lp-oVM" kind="relationship" relationship="viewControllers" id="2qH-aa-n0z"/>
|
||||||
|
</connections>
|
||||||
|
</tabBarController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="pxX-hL-ovw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="52.173913043478265" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<namedColor name="Background">
|
||||||
|
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
1056
source-code/ALTs/AltStore/Base.lproj/Main.storyboard
Normal file
1056
source-code/ALTs/AltStore/Base.lproj/Main.storyboard
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,98 @@
|
|||||||
|
//
|
||||||
|
// BrowseCollectionViewCell.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/15/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
import Nuke
|
||||||
|
|
||||||
|
@objc class BrowseCollectionViewCell: UICollectionViewCell
|
||||||
|
{
|
||||||
|
var imageURLs: [URL] = [] {
|
||||||
|
didSet {
|
||||||
|
self.dataSource.items = self.imageURLs as [NSURL]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private lazy var dataSource = self.makeDataSource()
|
||||||
|
|
||||||
|
@IBOutlet var bannerView: AppBannerView!
|
||||||
|
@IBOutlet var subtitleLabel: UILabel!
|
||||||
|
|
||||||
|
@IBOutlet private(set) var screenshotsCollectionView: UICollectionView!
|
||||||
|
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.contentView.preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
|
// Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷♂️.
|
||||||
|
self.screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||||
|
|
||||||
|
self.screenshotsCollectionView.delegate = self
|
||||||
|
self.screenshotsCollectionView.dataSource = self.dataSource
|
||||||
|
self.screenshotsCollectionView.prefetchDataSource = self.dataSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension BrowseCollectionViewCell
|
||||||
|
{
|
||||||
|
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
|
||||||
|
{
|
||||||
|
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: [])
|
||||||
|
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
||||||
|
let cell = cell as! ScreenshotCollectionViewCell
|
||||||
|
cell.imageView.image = nil
|
||||||
|
cell.imageView.isIndicatingActivity = true
|
||||||
|
}
|
||||||
|
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
|
||||||
|
return RSTAsyncBlockOperation() { (operation) in
|
||||||
|
ImagePipeline.shared.loadImage(with: imageURL as URL, progress: nil, completion: { (response, error) in
|
||||||
|
guard !operation.isCancelled else { return operation.finish() }
|
||||||
|
|
||||||
|
if let image = response?.image
|
||||||
|
{
|
||||||
|
completionHandler(image, nil)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completionHandler(nil, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
|
let cell = cell as! ScreenshotCollectionViewCell
|
||||||
|
cell.imageView.isIndicatingActivity = false
|
||||||
|
cell.imageView.image = image
|
||||||
|
|
||||||
|
if let error = error
|
||||||
|
{
|
||||||
|
print("Error loading image:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout
|
||||||
|
{
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||||
|
{
|
||||||
|
// Assuming 9.0 / 16.0 ratio for now.
|
||||||
|
let aspectRatio: CGFloat = 9.0 / 16.0
|
||||||
|
|
||||||
|
let itemHeight = collectionView.bounds.height
|
||||||
|
let itemWidth = itemHeight * aspectRatio
|
||||||
|
|
||||||
|
let size = CGSize(width: itemWidth.rounded(.down), height: itemHeight.rounded(.down))
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy">
|
||||||
|
<rect key="frame" x="16" y="0.0" width="343" height="369"/>
|
||||||
|
<subviews>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="88" id="z3D-cI-jhp"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Classic Nintendo games in your pocket." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Imx-Le-bcy">
|
||||||
|
<rect key="frame" x="0.0" y="103" width="343" height="17"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" scrollEnabled="NO" contentInsetAdjustmentBehavior="never" dataMode="none" translatesAutoresizingMaskIntoConstraints="NO" id="RFs-qp-Ca4">
|
||||||
|
<rect key="frame" x="0.0" y="135" width="343" height="234"/>
|
||||||
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="15" id="jH9-Jo-IHA">
|
||||||
|
<size key="itemSize" width="120" height="213"/>
|
||||||
|
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||||
|
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||||
|
<inset key="sectionInset" minX="8" minY="0.0" maxX="8" maxY="0.0"/>
|
||||||
|
</collectionViewFlowLayout>
|
||||||
|
<cells/>
|
||||||
|
</collectionView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="5gU-g3-Fsy" firstAttribute="top" secondItem="ln4-pC-7KY" secondAttribute="top" id="DnT-vq-BOc"/>
|
||||||
|
<constraint firstItem="5gU-g3-Fsy" firstAttribute="leading" secondItem="ln4-pC-7KY" secondAttribute="leadingMargin" id="YPy-xL-iUn"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="5gU-g3-Fsy" secondAttribute="bottom" id="gRu-Hz-CNL"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="5gU-g3-Fsy" secondAttribute="trailing" id="vf4-ql-4Vq"/>
|
||||||
|
</constraints>
|
||||||
|
<connections>
|
||||||
|
<outlet property="bannerView" destination="ziA-mP-AY2" id="yxo-ar-Cha"/>
|
||||||
|
<outlet property="screenshotsCollectionView" destination="RFs-qp-Ca4" id="xfi-AN-l17"/>
|
||||||
|
<outlet property="subtitleLabel" destination="Imx-Le-bcy" id="JVW-ZZ-51O"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="136.95652173913044" y="152.67857142857142"/>
|
||||||
|
</collectionViewCell>
|
||||||
|
</objects>
|
||||||
|
</document>
|
||||||
358
source-code/ALTs/AltStore/Browse/BrowseViewController.swift
Normal file
358
source-code/ALTs/AltStore/Browse/BrowseViewController.swift
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
//
|
||||||
|
// BrowseViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/15/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
import Nuke
|
||||||
|
|
||||||
|
class BrowseViewController: UICollectionViewController
|
||||||
|
{
|
||||||
|
private lazy var dataSource = self.makeDataSource()
|
||||||
|
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
|
||||||
|
|
||||||
|
private let prototypeCell = BrowseCollectionViewCell.instantiate(with: BrowseCollectionViewCell.nib!)!
|
||||||
|
|
||||||
|
private var loadingState: LoadingState = .loading {
|
||||||
|
didSet {
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cachedItemSizes = [String: CGSize]()
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
#if BETA
|
||||||
|
self.dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)]
|
||||||
|
self.navigationItem.searchController = self.dataSource.searchController
|
||||||
|
#else
|
||||||
|
// Hide Sources button for public version while in beta.
|
||||||
|
self.navigationItem.rightBarButtonItem = nil
|
||||||
|
#endif
|
||||||
|
|
||||||
|
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
self.collectionView.register(BrowseCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||||
|
|
||||||
|
self.collectionView.dataSource = self.dataSource
|
||||||
|
self.collectionView.prefetchDataSource = self.dataSource
|
||||||
|
|
||||||
|
self.registerForPreviewing(with: self, sourceView: self.collectionView)
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
self.fetchSource()
|
||||||
|
self.updateDataSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction private func unwindToBrowseViewController(_ segue: UIStoryboardSegue)
|
||||||
|
{
|
||||||
|
self.fetchSource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension BrowseViewController
|
||||||
|
{
|
||||||
|
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
|
||||||
|
{
|
||||||
|
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
|
||||||
|
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
|
||||||
|
NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true),
|
||||||
|
NSSortDescriptor(keyPath: \StoreApp.name, ascending: true),
|
||||||
|
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)]
|
||||||
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||||
|
|
||||||
|
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||||
|
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
|
||||||
|
let cell = cell as! BrowseCollectionViewCell
|
||||||
|
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||||
|
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
|
cell.subtitleLabel.text = app.subtitle
|
||||||
|
cell.imageURLs = Array(app.screenshotURLs.prefix(2))
|
||||||
|
cell.bannerView.titleLabel.text = app.name
|
||||||
|
cell.bannerView.subtitleLabel.text = app.developerName
|
||||||
|
cell.bannerView.betaBadgeView.isHidden = !app.isBeta
|
||||||
|
|
||||||
|
cell.bannerView.iconImageView.image = nil
|
||||||
|
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||||
|
|
||||||
|
cell.bannerView.button.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||||
|
cell.bannerView.button.activityIndicatorView.style = .white
|
||||||
|
|
||||||
|
// Explicitly set to false to ensure we're starting from a non-activity indicating state.
|
||||||
|
// Otherwise, cell reuse can mess up some cached values.
|
||||||
|
cell.bannerView.button.isIndicatingActivity = false
|
||||||
|
|
||||||
|
let tintColor = app.tintColor ?? .altPrimary
|
||||||
|
cell.tintColor = tintColor
|
||||||
|
|
||||||
|
if app.installedApp == nil
|
||||||
|
{
|
||||||
|
cell.bannerView.button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
|
||||||
|
|
||||||
|
let progress = AppManager.shared.installationProgress(for: app)
|
||||||
|
cell.bannerView.button.progress = progress
|
||||||
|
|
||||||
|
if Date() < app.versionDate
|
||||||
|
{
|
||||||
|
cell.bannerView.button.countdownDate = app.versionDate
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cell.bannerView.button.countdownDate = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cell.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||||
|
cell.bannerView.button.progress = nil
|
||||||
|
cell.bannerView.button.countdownDate = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.prefetchHandler = { (storeApp, indexPath, completionHandler) -> Foundation.Operation? in
|
||||||
|
let iconURL = storeApp.iconURL
|
||||||
|
|
||||||
|
return RSTAsyncBlockOperation() { (operation) in
|
||||||
|
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { (response, error) in
|
||||||
|
guard !operation.isCancelled else { return operation.finish() }
|
||||||
|
|
||||||
|
if let image = response?.image
|
||||||
|
{
|
||||||
|
completionHandler(image, nil)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completionHandler(nil, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
|
let cell = cell as! BrowseCollectionViewCell
|
||||||
|
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||||
|
cell.bannerView.iconImageView.image = image
|
||||||
|
|
||||||
|
if let error = error
|
||||||
|
{
|
||||||
|
print("Error loading image:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSource.placeholderView = self.placeholderView
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDataSource()
|
||||||
|
{
|
||||||
|
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||||
|
{
|
||||||
|
self.dataSource.predicate = nil
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.dataSource.predicate = NSPredicate(format: "%K == NO", #keyPath(StoreApp.isBeta))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchSource()
|
||||||
|
{
|
||||||
|
self.loadingState = .loading
|
||||||
|
|
||||||
|
AppManager.shared.fetchSources() { (result) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let sources = try result.get()
|
||||||
|
try sources.first?.managedObjectContext?.save()
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.loadingState = .finished(.success(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch let error as NSError
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if self.dataSource.itemCount > 0
|
||||||
|
{
|
||||||
|
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Fetch Sources", comment: ""))
|
||||||
|
|
||||||
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.show(in: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.loadingState = .finished(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
switch self.loadingState
|
||||||
|
{
|
||||||
|
case .loading:
|
||||||
|
self.placeholderView.textLabel.isHidden = true
|
||||||
|
self.placeholderView.detailTextLabel.isHidden = false
|
||||||
|
|
||||||
|
self.placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "")
|
||||||
|
|
||||||
|
self.placeholderView.activityIndicatorView.startAnimating()
|
||||||
|
|
||||||
|
case .finished(.failure(let error)):
|
||||||
|
self.placeholderView.textLabel.isHidden = false
|
||||||
|
self.placeholderView.detailTextLabel.isHidden = false
|
||||||
|
|
||||||
|
self.placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch Apps", comment: "")
|
||||||
|
self.placeholderView.detailTextLabel.text = error.localizedDescription
|
||||||
|
|
||||||
|
self.placeholderView.activityIndicatorView.stopAnimating()
|
||||||
|
|
||||||
|
case .finished(.success):
|
||||||
|
self.placeholderView.textLabel.isHidden = true
|
||||||
|
self.placeholderView.detailTextLabel.isHidden = true
|
||||||
|
|
||||||
|
self.placeholderView.activityIndicatorView.stopAnimating()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension BrowseViewController
|
||||||
|
{
|
||||||
|
@IBAction func performAppAction(_ sender: PillButton)
|
||||||
|
{
|
||||||
|
let point = self.collectionView.convert(sender.center, from: sender.superview)
|
||||||
|
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
|
||||||
|
|
||||||
|
let app = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
if let installedApp = app.installedApp
|
||||||
|
{
|
||||||
|
self.open(installedApp)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.install(app, at: indexPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func install(_ app: StoreApp, at indexPath: IndexPath)
|
||||||
|
{
|
||||||
|
let previousProgress = AppManager.shared.installationProgress(for: app)
|
||||||
|
guard previousProgress == nil else {
|
||||||
|
previousProgress?.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = AppManager.shared.install(app, presentingViewController: self) { (result) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(OperationError.cancelled): break // Ignore
|
||||||
|
case .failure(let error):
|
||||||
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.show(in: self)
|
||||||
|
|
||||||
|
case .success: print("Installed app:", app.bundleIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.collectionView.reloadItems(at: [indexPath])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.collectionView.reloadItems(at: [indexPath])
|
||||||
|
}
|
||||||
|
|
||||||
|
func open(_ installedApp: InstalledApp)
|
||||||
|
{
|
||||||
|
UIApplication.shared.open(installedApp.openAppURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BrowseViewController: UICollectionViewDelegateFlowLayout
|
||||||
|
{
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||||
|
{
|
||||||
|
let item = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
if let previousSize = self.cachedItemSizes[item.bundleIdentifier]
|
||||||
|
{
|
||||||
|
return previousSize
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxVisibleScreenshots = 2 as CGFloat
|
||||||
|
let aspectRatio: CGFloat = 16.0 / 9.0
|
||||||
|
|
||||||
|
let layout = self.prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||||
|
let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1)) + layout.sectionInset.left + layout.sectionInset.right
|
||||||
|
|
||||||
|
self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
|
||||||
|
|
||||||
|
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
|
||||||
|
widthConstraint.isActive = true
|
||||||
|
defer { widthConstraint.isActive = false }
|
||||||
|
|
||||||
|
// Manually update cell width & layout so we can accurately calculate screenshot sizes.
|
||||||
|
self.prototypeCell.frame.size.width = widthConstraint.constant
|
||||||
|
self.prototypeCell.layoutIfNeeded()
|
||||||
|
|
||||||
|
let collectionViewWidth = self.prototypeCell.screenshotsCollectionView.bounds.width
|
||||||
|
let screenshotWidth = ((collectionViewWidth - padding) / maxVisibleScreenshots).rounded(.down)
|
||||||
|
let screenshotHeight = screenshotWidth * aspectRatio
|
||||||
|
|
||||||
|
let heightConstraint = self.prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight)
|
||||||
|
heightConstraint.priority = .defaultHigh // Prevent temporary unsatisfiable constraints error.
|
||||||
|
heightConstraint.isActive = true
|
||||||
|
defer { heightConstraint.isActive = false }
|
||||||
|
|
||||||
|
let itemSize = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||||
|
self.cachedItemSizes[item.bundleIdentifier] = itemSize
|
||||||
|
return itemSize
|
||||||
|
}
|
||||||
|
|
||||||
|
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||||
|
{
|
||||||
|
let app = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
let appViewController = AppViewController.makeAppViewController(app: app)
|
||||||
|
self.navigationController?.pushViewController(appViewController, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BrowseViewController: UIViewControllerPreviewingDelegate
|
||||||
|
{
|
||||||
|
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
|
||||||
|
{
|
||||||
|
guard
|
||||||
|
let indexPath = self.collectionView.indexPathForItem(at: location),
|
||||||
|
let cell = self.collectionView.cellForItem(at: indexPath)
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
previewingContext.sourceRect = cell.frame
|
||||||
|
|
||||||
|
let app = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
let appViewController = AppViewController.makeAppViewController(app: app)
|
||||||
|
return appViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
|
||||||
|
{
|
||||||
|
self.navigationController?.pushViewController(viewControllerToCommit, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// ScreenshotCollectionViewCell.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/15/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
@objc(ScreenshotCollectionViewCell)
|
||||||
|
class ScreenshotCollectionViewCell: UICollectionViewCell
|
||||||
|
{
|
||||||
|
let imageView = UIImageView(image: nil)
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder)
|
||||||
|
{
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initialize()
|
||||||
|
{
|
||||||
|
self.imageView.layer.masksToBounds = true
|
||||||
|
self.addSubview(self.imageView, pinningEdgesWith: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
self.imageView.layer.cornerRadius = 4
|
||||||
|
}
|
||||||
|
}
|
||||||
49
source-code/ALTs/AltStore/Components/AppBannerView.swift
Normal file
49
source-code/ALTs/AltStore/Components/AppBannerView.swift
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// AppBannerView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 8/29/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
class AppBannerView: RSTNibView
|
||||||
|
{
|
||||||
|
private var originalTintColor: UIColor?
|
||||||
|
|
||||||
|
@IBOutlet var titleLabel: UILabel!
|
||||||
|
@IBOutlet var subtitleLabel: UILabel!
|
||||||
|
@IBOutlet var iconImageView: AppIconImageView!
|
||||||
|
@IBOutlet var button: PillButton!
|
||||||
|
@IBOutlet var buttonLabel: UILabel!
|
||||||
|
@IBOutlet var betaBadgeView: UIView!
|
||||||
|
|
||||||
|
@IBOutlet var backgroundEffectView: UIVisualEffectView!
|
||||||
|
@IBOutlet private var vibrancyView: UIVisualEffectView!
|
||||||
|
|
||||||
|
override func tintColorDidChange()
|
||||||
|
{
|
||||||
|
super.tintColorDidChange()
|
||||||
|
|
||||||
|
if self.tintAdjustmentMode != .dimmed
|
||||||
|
{
|
||||||
|
self.originalTintColor = self.tintColor
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppBannerView
|
||||||
|
{
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
self.clipsToBounds = true
|
||||||
|
self.layer.cornerRadius = 22
|
||||||
|
|
||||||
|
self.subtitleLabel.textColor = self.originalTintColor ?? self.tintColor
|
||||||
|
self.backgroundEffectView.backgroundColor = self.originalTintColor ?? self.tintColor
|
||||||
|
}
|
||||||
|
}
|
||||||
141
source-code/ALTs/AltStore/Components/AppBannerView.xib
Normal file
141
source-code/ALTs/AltStore/Components/AppBannerView.xib
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<connections>
|
||||||
|
<outlet property="backgroundEffectView" destination="rZk-be-tiI" id="fzU-VT-JeW"/>
|
||||||
|
<outlet property="betaBadgeView" destination="qQl-Ez-zC5" id="6O1-Cx-7qz"/>
|
||||||
|
<outlet property="button" destination="tVx-3G-dcu" id="joa-AH-syX"/>
|
||||||
|
<outlet property="buttonLabel" destination="Yd9-jw-faD" id="o7g-Gb-CIt"/>
|
||||||
|
<outlet property="iconImageView" destination="avS-dx-4iy" id="TQs-Ej-gin"/>
|
||||||
|
<outlet property="subtitleLabel" destination="oN5-vu-Dnw" id="gA4-iJ-Tix"/>
|
||||||
|
<outlet property="titleLabel" destination="mFe-zJ-eva" id="2OH-f8-cid"/>
|
||||||
|
<outlet property="vibrancyView" destination="fC4-1V-iMn" id="PXE-2B-A7w"/>
|
||||||
|
</connections>
|
||||||
|
</placeholder>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="FxI-Fh-ll5">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rZk-be-tiI">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="b8k-up-HtI">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" name="BlurTint"/>
|
||||||
|
</view>
|
||||||
|
<blurEffect style="systemChromeMaterial"/>
|
||||||
|
</visualEffectView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="avS-dx-4iy" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="14" y="14" width="60" height="60"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="60" id="6lU-H8-nEw"/>
|
||||||
|
<constraint firstAttribute="width" secondItem="avS-dx-4iy" secondAttribute="height" multiplier="1:1" id="AYT-3g-wcV"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="caL-vN-Svn">
|
||||||
|
<rect key="frame" x="85" y="18" width="190" height="52"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="500" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="1Es-pv-zwd">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="167" height="34"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="mFe-zJ-eva">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="79" height="34"/>
|
||||||
|
<accessibility key="accessibilityConfiguration" identifier="NameLabel"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="qQl-Ez-zC5">
|
||||||
|
<rect key="frame" x="85" y="0.0" width="82" height="34"/>
|
||||||
|
<accessibility key="accessibilityConfiguration" identifier="Beta Badge">
|
||||||
|
<accessibilityTraits key="traits" image="YES" notEnabled="YES"/>
|
||||||
|
<bool key="isElement" value="YES"/>
|
||||||
|
</accessibility>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fC4-1V-iMn">
|
||||||
|
<rect key="frame" x="0.0" y="36" width="190" height="16"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="LQh-pN-ePC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="190" height="16"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="750" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="oN5-vu-Dnw">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="190" height="16"/>
|
||||||
|
<accessibility key="accessibilityConfiguration" label="Developer"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="oN5-vu-Dnw" firstAttribute="top" secondItem="LQh-pN-ePC" secondAttribute="top" id="7RH-WP-LzL"/>
|
||||||
|
<constraint firstItem="oN5-vu-Dnw" firstAttribute="leading" secondItem="LQh-pN-ePC" secondAttribute="leading" id="By8-cR-kTu"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="oN5-vu-Dnw" secondAttribute="trailing" id="Hiv-6y-XrH"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="oN5-vu-Dnw" secondAttribute="bottom" id="yc2-Dr-Qnv"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<vibrancyEffect style="secondaryLabel">
|
||||||
|
<blurEffect style="systemChromeMaterial"/>
|
||||||
|
</vibrancyEffect>
|
||||||
|
</visualEffectView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="3ox-Q2-Rnd" userLabel="Button Stack View">
|
||||||
|
<rect key="frame" x="286" y="28.5" width="77" height="31"/>
|
||||||
|
<subviews>
|
||||||
|
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Yd9-jw-faD">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="77" height="0.0"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="10"/>
|
||||||
|
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="77" height="31"/>
|
||||||
|
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="31" id="Zwh-yQ-GTu"/>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="77" id="eGc-Dk-QbL"/>
|
||||||
|
</constraints>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
|
||||||
|
<state key="normal" title="FREE"/>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<edgeInsets key="layoutMargins" top="14" left="14" bottom="14" right="12"/>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="d1T-UD-gWG" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="5tv-QN-ZWU"/>
|
||||||
|
<constraint firstItem="rZk-be-tiI" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="N6R-B2-Rie"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="d1T-UD-gWG" secondAttribute="trailing" id="mlG-w3-Ly6"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="rZk-be-tiI" secondAttribute="trailing" id="nEy-pm-Fcs"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="d1T-UD-gWG" secondAttribute="bottom" id="nJo-To-LmX"/>
|
||||||
|
<constraint firstItem="rZk-be-tiI" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="pGD-Tl-U4c"/>
|
||||||
|
<constraint firstItem="d1T-UD-gWG" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="q2p-0S-Nv5"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="rZk-be-tiI" secondAttribute="bottom" id="yk0-pw-joP"/>
|
||||||
|
</constraints>
|
||||||
|
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||||
|
<point key="canvasLocation" x="139.85507246376812" y="152.67857142857142"/>
|
||||||
|
</view>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<image name="BetaBadge" width="41" height="17"/>
|
||||||
|
<namedColor name="BlurTint">
|
||||||
|
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
43
source-code/ALTs/AltStore/Components/AppIconImageView.swift
Normal file
43
source-code/ALTs/AltStore/Components/AppIconImageView.swift
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// AppIconImageView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 5/9/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class AppIconImageView: UIImageView
|
||||||
|
{
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.contentMode = .scaleAspectFill
|
||||||
|
self.clipsToBounds = true
|
||||||
|
|
||||||
|
self.backgroundColor = .white
|
||||||
|
|
||||||
|
if #available(iOS 13, *)
|
||||||
|
{
|
||||||
|
self.layer.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if self.layer.responds(to: Selector(("continuousCorners")))
|
||||||
|
{
|
||||||
|
self.layer.setValue(true, forKey: "continuousCorners")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
// Based off of 60pt icon having 12pt radius.
|
||||||
|
let radius = self.bounds.height / 5
|
||||||
|
self.layer.cornerRadius = radius
|
||||||
|
}
|
||||||
|
}
|
||||||
102
source-code/ALTs/AltStore/Components/BackgroundTaskManager.swift
Normal file
102
source-code/ALTs/AltStore/Components/BackgroundTaskManager.swift
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
//
|
||||||
|
// BackgroundTaskManager.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 6/19/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
class BackgroundTaskManager
|
||||||
|
{
|
||||||
|
static let shared = BackgroundTaskManager()
|
||||||
|
|
||||||
|
private var isPlaying = false
|
||||||
|
|
||||||
|
private let audioEngine: AVAudioEngine
|
||||||
|
private let player: AVAudioPlayerNode
|
||||||
|
private let audioFile: AVAudioFile
|
||||||
|
|
||||||
|
private let audioEngineQueue: DispatchQueue
|
||||||
|
|
||||||
|
private init()
|
||||||
|
{
|
||||||
|
self.audioEngine = AVAudioEngine()
|
||||||
|
self.audioEngine.mainMixerNode.outputVolume = 0.0
|
||||||
|
|
||||||
|
self.player = AVAudioPlayerNode()
|
||||||
|
self.audioEngine.attach(self.player)
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let audioFileURL = Bundle.main.url(forResource: "Silence", withExtension: "m4a")!
|
||||||
|
|
||||||
|
self.audioFile = try AVAudioFile(forReading: audioFileURL)
|
||||||
|
self.audioEngine.connect(self.player, to: self.audioEngine.mainMixerNode, format: self.audioFile.processingFormat)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
fatalError("Error. \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.audioEngineQueue = DispatchQueue(label: "com.altstore.BackgroundTaskManager")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BackgroundTaskManager
|
||||||
|
{
|
||||||
|
func performExtendedBackgroundTask(taskHandler: @escaping ((Result<Void, Error>, @escaping () -> Void) -> Void))
|
||||||
|
{
|
||||||
|
func finish()
|
||||||
|
{
|
||||||
|
self.player.stop()
|
||||||
|
self.audioEngine.stop()
|
||||||
|
|
||||||
|
self.isPlaying = false
|
||||||
|
}
|
||||||
|
|
||||||
|
self.audioEngineQueue.sync {
|
||||||
|
do
|
||||||
|
{
|
||||||
|
try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
|
||||||
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
|
||||||
|
// Schedule audio file buffers.
|
||||||
|
self.scheduleAudioFile()
|
||||||
|
self.scheduleAudioFile()
|
||||||
|
|
||||||
|
let outputFormat = self.audioEngine.outputNode.outputFormat(forBus: 0)
|
||||||
|
self.audioEngine.connect(self.audioEngine.mainMixerNode, to: self.audioEngine.outputNode, format: outputFormat)
|
||||||
|
|
||||||
|
try self.audioEngine.start()
|
||||||
|
self.player.play()
|
||||||
|
|
||||||
|
self.isPlaying = true
|
||||||
|
|
||||||
|
taskHandler(.success(())) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
taskHandler(.failure(error)) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension BackgroundTaskManager
|
||||||
|
{
|
||||||
|
func scheduleAudioFile()
|
||||||
|
{
|
||||||
|
self.player.scheduleFile(self.audioFile, at: nil) {
|
||||||
|
self.audioEngineQueue.async {
|
||||||
|
guard self.isPlaying else { return }
|
||||||
|
self.scheduleAudioFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// BannerCollectionViewCell.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/23/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class BannerCollectionViewCell: UICollectionViewCell
|
||||||
|
{
|
||||||
|
@IBOutlet var bannerView: AppBannerView!
|
||||||
|
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
self.contentView.preservesSuperviewLayoutMargins = true
|
||||||
|
}
|
||||||
|
}
|
||||||
65
source-code/ALTs/AltStore/Components/Button.swift
Normal file
65
source-code/ALTs/AltStore/Components/Button.swift
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
//
|
||||||
|
// Button.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 5/9/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class Button: UIButton
|
||||||
|
{
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
var size = super.intrinsicContentSize
|
||||||
|
size.width += 20
|
||||||
|
size.height += 10
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.setTitleColor(.white, for: .normal)
|
||||||
|
|
||||||
|
self.layer.masksToBounds = true
|
||||||
|
self.layer.cornerRadius = 8
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tintColorDidChange()
|
||||||
|
{
|
||||||
|
super.tintColorDidChange()
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var isHighlighted: Bool {
|
||||||
|
didSet {
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var isEnabled: Bool {
|
||||||
|
didSet {
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Button
|
||||||
|
{
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
if self.isEnabled
|
||||||
|
{
|
||||||
|
self.backgroundColor = self.tintColor
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.backgroundColor = .lightGray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
source-code/ALTs/AltStore/Components/CollapsingTextView.swift
Normal file
117
source-code/ALTs/AltStore/Components/CollapsingTextView.swift
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
//
|
||||||
|
// CollapsingTextView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/23/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class CollapsingTextView: UITextView
|
||||||
|
{
|
||||||
|
var isCollapsed = true {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var maximumNumberOfLines = 2 {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineSpacing: CGFloat = 2 {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let moreButton = UIButton(type: .system)
|
||||||
|
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.layoutManager.delegate = self
|
||||||
|
|
||||||
|
self.textContainerInset = .zero
|
||||||
|
self.textContainer.lineFragmentPadding = 0
|
||||||
|
self.textContainer.lineBreakMode = .byTruncatingTail
|
||||||
|
self.textContainer.heightTracksTextView = true
|
||||||
|
self.textContainer.widthTracksTextView = true
|
||||||
|
|
||||||
|
self.moreButton.setTitle(NSLocalizedString("More", comment: ""), for: .normal)
|
||||||
|
self.moreButton.addTarget(self, action: #selector(CollapsingTextView.toggleCollapsed(_:)), for: .primaryActionTriggered)
|
||||||
|
self.addSubview(self.moreButton)
|
||||||
|
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
guard let font = self.font else { return }
|
||||||
|
|
||||||
|
let buttonFont = UIFont.systemFont(ofSize: font.pointSize, weight: .medium)
|
||||||
|
self.moreButton.titleLabel?.font = buttonFont
|
||||||
|
|
||||||
|
let buttonY = (font.lineHeight + self.lineSpacing) * CGFloat(self.maximumNumberOfLines - 1)
|
||||||
|
let size = self.moreButton.sizeThatFits(CGSize(width: 1000, height: 1000))
|
||||||
|
|
||||||
|
let moreButtonFrame = CGRect(x: self.bounds.width - self.moreButton.bounds.width,
|
||||||
|
y: buttonY,
|
||||||
|
width: size.width,
|
||||||
|
height: font.lineHeight)
|
||||||
|
self.moreButton.frame = moreButtonFrame
|
||||||
|
|
||||||
|
if self.isCollapsed
|
||||||
|
{
|
||||||
|
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
|
||||||
|
|
||||||
|
let maximumCollapsedHeight = font.lineHeight * CGFloat(self.maximumNumberOfLines)
|
||||||
|
if self.intrinsicContentSize.height > maximumCollapsedHeight
|
||||||
|
{
|
||||||
|
var exclusionFrame = moreButtonFrame
|
||||||
|
exclusionFrame.origin.y += self.moreButton.bounds.midY
|
||||||
|
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
|
||||||
|
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
|
||||||
|
|
||||||
|
self.moreButton.isHidden = false
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.textContainer.exclusionPaths = []
|
||||||
|
|
||||||
|
self.moreButton.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.textContainer.maximumNumberOfLines = 0
|
||||||
|
self.textContainer.exclusionPaths = []
|
||||||
|
|
||||||
|
self.moreButton.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
self.invalidateIntrinsicContentSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension CollapsingTextView
|
||||||
|
{
|
||||||
|
@objc func toggleCollapsed(_ sender: UIButton)
|
||||||
|
{
|
||||||
|
self.isCollapsed.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CollapsingTextView: NSLayoutManagerDelegate
|
||||||
|
{
|
||||||
|
func layoutManager(_ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat
|
||||||
|
{
|
||||||
|
return self.lineSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// ForwardingNavigationController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 10/24/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ForwardingNavigationController: UINavigationController
|
||||||
|
{
|
||||||
|
override var childForStatusBarStyle: UIViewController? {
|
||||||
|
return self.topViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
override var childForStatusBarHidden: UIViewController? {
|
||||||
|
return self.topViewController
|
||||||
|
}
|
||||||
|
}
|
||||||
88
source-code/ALTs/AltStore/Components/Keychain.swift
Normal file
88
source-code/ALTs/AltStore/Components/Keychain.swift
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
//
|
||||||
|
// Keychain.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 6/4/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import KeychainAccess
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
@propertyWrapper
|
||||||
|
struct KeychainItem<Value>
|
||||||
|
{
|
||||||
|
let key: String
|
||||||
|
|
||||||
|
var wrappedValue: Value? {
|
||||||
|
get {
|
||||||
|
switch Value.self
|
||||||
|
{
|
||||||
|
case is Data.Type: return try? Keychain.shared.keychain.getData(self.key) as? Value
|
||||||
|
case is String.Type: return try? Keychain.shared.keychain.getString(self.key) as? Value
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
switch Value.self
|
||||||
|
{
|
||||||
|
case is Data.Type: Keychain.shared.keychain[data: self.key] = newValue as? Data
|
||||||
|
case is String.Type: Keychain.shared.keychain[self.key] = newValue as? String
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(key: String)
|
||||||
|
{
|
||||||
|
self.key = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Keychain
|
||||||
|
{
|
||||||
|
static let shared = Keychain()
|
||||||
|
|
||||||
|
fileprivate let keychain = KeychainAccess.Keychain(service: "com.rileytestut.AltStore").accessibility(.afterFirstUnlock).synchronizable(true)
|
||||||
|
|
||||||
|
@KeychainItem(key: "appleIDEmailAddress")
|
||||||
|
var appleIDEmailAddress: String?
|
||||||
|
|
||||||
|
@KeychainItem(key: "appleIDPassword")
|
||||||
|
var appleIDPassword: String?
|
||||||
|
|
||||||
|
@KeychainItem(key: "signingCertificatePrivateKey")
|
||||||
|
var signingCertificatePrivateKey: Data?
|
||||||
|
|
||||||
|
@KeychainItem(key: "signingCertificateSerialNumber")
|
||||||
|
var signingCertificateSerialNumber: String?
|
||||||
|
|
||||||
|
@KeychainItem(key: "signingCertificate")
|
||||||
|
var signingCertificate: Data?
|
||||||
|
|
||||||
|
@KeychainItem(key: "signingCertificatePassword")
|
||||||
|
var signingCertificatePassword: String?
|
||||||
|
|
||||||
|
@KeychainItem(key: "patreonAccessToken")
|
||||||
|
var patreonAccessToken: String?
|
||||||
|
|
||||||
|
@KeychainItem(key: "patreonRefreshToken")
|
||||||
|
var patreonRefreshToken: String?
|
||||||
|
|
||||||
|
@KeychainItem(key: "patreonCreatorAccessToken")
|
||||||
|
var patreonCreatorAccessToken: String?
|
||||||
|
|
||||||
|
private init()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
func reset()
|
||||||
|
{
|
||||||
|
self.appleIDEmailAddress = nil
|
||||||
|
self.appleIDPassword = nil
|
||||||
|
self.signingCertificatePrivateKey = nil
|
||||||
|
self.signingCertificateSerialNumber = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
103
source-code/ALTs/AltStore/Components/NavigationBar.swift
Normal file
103
source-code/ALTs/AltStore/Components/NavigationBar.swift
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
//
|
||||||
|
// NavigationBar.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/15/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
class NavigationBar: UINavigationBar
|
||||||
|
{
|
||||||
|
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true
|
||||||
|
|
||||||
|
private let backgroundColorView = UIView()
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder)
|
||||||
|
{
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
|
self.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initialize()
|
||||||
|
{
|
||||||
|
if #available(iOS 13, *)
|
||||||
|
{
|
||||||
|
let standardAppearance = UINavigationBarAppearance()
|
||||||
|
standardAppearance.configureWithDefaultBackground()
|
||||||
|
standardAppearance.shadowColor = nil
|
||||||
|
|
||||||
|
let edgeAppearance = UINavigationBarAppearance()
|
||||||
|
edgeAppearance.configureWithOpaqueBackground()
|
||||||
|
edgeAppearance.backgroundColor = self.barTintColor
|
||||||
|
edgeAppearance.shadowColor = nil
|
||||||
|
|
||||||
|
if let tintColor = self.barTintColor
|
||||||
|
{
|
||||||
|
let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
|
||||||
|
|
||||||
|
standardAppearance.backgroundColor = tintColor
|
||||||
|
standardAppearance.titleTextAttributes = textAttributes
|
||||||
|
standardAppearance.largeTitleTextAttributes = textAttributes
|
||||||
|
|
||||||
|
edgeAppearance.titleTextAttributes = textAttributes
|
||||||
|
edgeAppearance.largeTitleTextAttributes = textAttributes
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
standardAppearance.backgroundColor = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scrollEdgeAppearance = edgeAppearance
|
||||||
|
self.standardAppearance = standardAppearance
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.shadowImage = UIImage()
|
||||||
|
|
||||||
|
if let tintColor = self.barTintColor
|
||||||
|
{
|
||||||
|
self.backgroundColorView.backgroundColor = tintColor
|
||||||
|
|
||||||
|
// Top = -50 to cover status bar area above navigation bar on any device.
|
||||||
|
// Bottom = -1 to prevent a flickering gray line from appearing.
|
||||||
|
self.addSubview(self.backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.barTintColor = .white
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
if self.backgroundColorView.superview != nil
|
||||||
|
{
|
||||||
|
self.insertSubview(self.backgroundColorView, at: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.automaticallyAdjustsItemPositions
|
||||||
|
{
|
||||||
|
// We can't easily shift just the back button up, so we shift the entire content view slightly.
|
||||||
|
for contentView in self.subviews
|
||||||
|
{
|
||||||
|
guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue }
|
||||||
|
contentView.center.y -= 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
183
source-code/ALTs/AltStore/Components/PillButton.swift
Normal file
183
source-code/ALTs/AltStore/Components/PillButton.swift
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
//
|
||||||
|
// PillButton.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/15/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class PillButton: UIButton
|
||||||
|
{
|
||||||
|
var progress: Progress? {
|
||||||
|
didSet {
|
||||||
|
self.progressView.progress = Float(self.progress?.fractionCompleted ?? 0)
|
||||||
|
self.progressView.observedProgress = self.progress
|
||||||
|
|
||||||
|
let isUserInteractionEnabled = self.isUserInteractionEnabled
|
||||||
|
self.isIndicatingActivity = (self.progress != nil)
|
||||||
|
self.isUserInteractionEnabled = isUserInteractionEnabled
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressTintColor: UIColor? {
|
||||||
|
get {
|
||||||
|
return self.progressView.progressTintColor
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
self.progressView.progressTintColor = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var countdownDate: Date? {
|
||||||
|
didSet {
|
||||||
|
self.isEnabled = (self.countdownDate == nil)
|
||||||
|
self.displayLink.isPaused = (self.countdownDate == nil)
|
||||||
|
|
||||||
|
if self.countdownDate == nil
|
||||||
|
{
|
||||||
|
self.setTitle(nil, for: .disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let progressView = UIProgressView(progressViewStyle: .default)
|
||||||
|
|
||||||
|
private lazy var displayLink: CADisplayLink = {
|
||||||
|
let displayLink = CADisplayLink(target: self, selector: #selector(PillButton.updateCountdown))
|
||||||
|
displayLink.preferredFramesPerSecond = 15
|
||||||
|
displayLink.isPaused = true
|
||||||
|
displayLink.add(to: .main, forMode: .common)
|
||||||
|
return displayLink
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let dateComponentsFormatter: DateComponentsFormatter = {
|
||||||
|
let dateComponentsFormatter = DateComponentsFormatter()
|
||||||
|
dateComponentsFormatter.zeroFormattingBehavior = [.pad]
|
||||||
|
dateComponentsFormatter.collapsesLargestUnit = false
|
||||||
|
return dateComponentsFormatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
var size = super.intrinsicContentSize
|
||||||
|
size.width += 26
|
||||||
|
size.height += 3
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit
|
||||||
|
{
|
||||||
|
self.displayLink.remove(from: .main, forMode: RunLoop.Mode.default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.layer.masksToBounds = true
|
||||||
|
|
||||||
|
self.activityIndicatorView.style = .white
|
||||||
|
self.activityIndicatorView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.progressView.progress = 0
|
||||||
|
self.progressView.trackImage = UIImage()
|
||||||
|
self.progressView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(self.progressView)
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
self.progressView.bounds.size.width = self.bounds.width
|
||||||
|
|
||||||
|
let scale = self.bounds.height / self.progressView.bounds.height
|
||||||
|
|
||||||
|
self.progressView.transform = CGAffineTransform.identity.scaledBy(x: 1, y: scale)
|
||||||
|
self.progressView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
|
||||||
|
|
||||||
|
self.layer.cornerRadius = self.bounds.midY
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tintColorDidChange()
|
||||||
|
{
|
||||||
|
super.tintColorDidChange()
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension PillButton
|
||||||
|
{
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
if self.progress == nil
|
||||||
|
{
|
||||||
|
self.setTitleColor(.white, for: .normal)
|
||||||
|
self.backgroundColor = self.tintColor
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.setTitleColor(self.tintColor, for: .normal)
|
||||||
|
self.backgroundColor = self.tintColor.withAlphaComponent(0.15)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.progressView.progressTintColor = self.tintColor
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func updateCountdown()
|
||||||
|
{
|
||||||
|
guard let endDate = self.countdownDate else { return }
|
||||||
|
|
||||||
|
let startDate = Date()
|
||||||
|
|
||||||
|
let interval = endDate.timeIntervalSince(startDate)
|
||||||
|
guard interval > 0 else {
|
||||||
|
self.isEnabled = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let text: String?
|
||||||
|
|
||||||
|
if interval < (1 * 60 * 60)
|
||||||
|
{
|
||||||
|
self.dateComponentsFormatter.unitsStyle = .positional
|
||||||
|
self.dateComponentsFormatter.allowedUnits = [.minute, .second]
|
||||||
|
|
||||||
|
text = self.dateComponentsFormatter.string(from: startDate, to: endDate)
|
||||||
|
}
|
||||||
|
else if interval < (2 * 24 * 60 * 60)
|
||||||
|
{
|
||||||
|
self.dateComponentsFormatter.unitsStyle = .positional
|
||||||
|
self.dateComponentsFormatter.allowedUnits = [.hour, .minute, .second]
|
||||||
|
|
||||||
|
text = self.dateComponentsFormatter.string(from: startDate, to: endDate)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.dateComponentsFormatter.unitsStyle = .full
|
||||||
|
self.dateComponentsFormatter.allowedUnits = [.day]
|
||||||
|
|
||||||
|
let numberOfDays = endDate.numberOfCalendarDays(since: startDate)
|
||||||
|
text = String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let text = text
|
||||||
|
{
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.isEnabled = false
|
||||||
|
self.setTitle(text, for: .disabled)
|
||||||
|
self.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// TextCollectionReusableView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/23/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class TextCollectionReusableView: UICollectionReusableView
|
||||||
|
{
|
||||||
|
@IBOutlet var textLabel: UILabel!
|
||||||
|
}
|
||||||
113
source-code/ALTs/AltStore/Components/ToastView.swift
Normal file
113
source-code/ALTs/AltStore/Components/ToastView.swift
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
//
|
||||||
|
// ToastView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/19/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
extension TimeInterval
|
||||||
|
{
|
||||||
|
static let shortToastViewDuration = 4.0
|
||||||
|
static let longToastViewDuration = 8.0
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToastView: RSTToastView
|
||||||
|
{
|
||||||
|
var preferredDuration: TimeInterval
|
||||||
|
|
||||||
|
override init(text: String, detailText detailedText: String?)
|
||||||
|
{
|
||||||
|
if detailedText == nil
|
||||||
|
{
|
||||||
|
self.preferredDuration = .shortToastViewDuration
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.preferredDuration = .longToastViewDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
super.init(text: text, detailText: detailedText)
|
||||||
|
|
||||||
|
self.layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 10, right: 16)
|
||||||
|
self.setNeedsLayout()
|
||||||
|
|
||||||
|
if let stackView = self.textLabel.superview as? UIStackView
|
||||||
|
{
|
||||||
|
// RSTToastView does not expose stack view containing labels,
|
||||||
|
// so we access it indirectly as the labels' superview.
|
||||||
|
stackView.spacing = (detailedText != nil) ? 4.0 : 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(error: Error)
|
||||||
|
{
|
||||||
|
var error = error as NSError
|
||||||
|
var underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError
|
||||||
|
|
||||||
|
var preferredDuration: TimeInterval?
|
||||||
|
|
||||||
|
if
|
||||||
|
let unwrappedUnderlyingError = underlyingError,
|
||||||
|
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
|
||||||
|
{
|
||||||
|
// Treat underlyingError as the primary error.
|
||||||
|
|
||||||
|
error = unwrappedUnderlyingError
|
||||||
|
underlyingError = nil
|
||||||
|
|
||||||
|
preferredDuration = .longToastViewDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
let detailText: String?
|
||||||
|
|
||||||
|
if let failure = error.localizedFailure
|
||||||
|
{
|
||||||
|
text = failure
|
||||||
|
detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription
|
||||||
|
}
|
||||||
|
else if let reason = error.localizedFailureReason
|
||||||
|
{
|
||||||
|
text = reason
|
||||||
|
detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text = error.localizedDescription
|
||||||
|
detailText = underlyingError?.localizedDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(text: text, detailText: detailText)
|
||||||
|
|
||||||
|
if let preferredDuration = preferredDuration
|
||||||
|
{
|
||||||
|
self.preferredDuration = preferredDuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews()
|
||||||
|
{
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
// Rough calculation to determine height of ToastView with one-line textLabel.
|
||||||
|
let minimumHeight = self.textLabel.font.lineHeight.rounded() + 18
|
||||||
|
self.layer.cornerRadius = minimumHeight/2
|
||||||
|
}
|
||||||
|
|
||||||
|
func show(in viewController: UIViewController)
|
||||||
|
{
|
||||||
|
self.show(in: viewController.navigationController?.view ?? viewController.view, duration: self.preferredDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func show(in view: UIView)
|
||||||
|
{
|
||||||
|
self.show(in: view, duration: self.preferredDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
34
source-code/ALTs/AltStore/Extensions/Date+RelativeDate.swift
Normal file
34
source-code/ALTs/AltStore/Extensions/Date+RelativeDate.swift
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
//
|
||||||
|
// Date+RelativeDate.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/28/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Date
|
||||||
|
{
|
||||||
|
func numberOfCalendarDays(since date: Date) -> Int
|
||||||
|
{
|
||||||
|
let today = Calendar.current.startOfDay(for: self)
|
||||||
|
let previousDay = Calendar.current.startOfDay(for: date)
|
||||||
|
|
||||||
|
let components = Calendar.current.dateComponents([.day], from: previousDay, to: today)
|
||||||
|
return components.day!
|
||||||
|
}
|
||||||
|
|
||||||
|
func relativeDateString(since date: Date, dateFormatter: DateFormatter) -> String
|
||||||
|
{
|
||||||
|
let numberOfDays = self.numberOfCalendarDays(since: date)
|
||||||
|
|
||||||
|
switch numberOfDays
|
||||||
|
{
|
||||||
|
case 0: return NSLocalizedString("Today", comment: "")
|
||||||
|
case 1: return NSLocalizedString("Yesterday", comment: "")
|
||||||
|
case 2...7: return String(format: NSLocalizedString("%@ days ago", comment: ""), NSNumber(value: numberOfDays))
|
||||||
|
default: return dateFormatter.string(from: date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// FileManager+DirectorySize.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/31/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension FileManager
|
||||||
|
{
|
||||||
|
func directorySize(at directoryURL: URL) -> Int?
|
||||||
|
{
|
||||||
|
guard let enumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey]) else { return nil }
|
||||||
|
|
||||||
|
var total: Int = 0
|
||||||
|
|
||||||
|
for case let fileURL as URL in enumerator
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
|
||||||
|
guard let fileSize = resourceValues.fileSize else { continue }
|
||||||
|
|
||||||
|
total += fileSize
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to read file size for item: \(fileURL).", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// FileManager+SharedDirectories.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 5/14/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import AltKit
|
||||||
|
|
||||||
|
extension FileManager
|
||||||
|
{
|
||||||
|
var altstoreSharedDirectory: URL? {
|
||||||
|
guard let appGroup = Bundle.main.appGroups.first else { return nil }
|
||||||
|
|
||||||
|
let sharedDirectoryURL = self.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
|
||||||
|
return sharedDirectoryURL
|
||||||
|
}
|
||||||
|
|
||||||
|
var appBackupsDirectory: URL? {
|
||||||
|
let appBackupsDirectory = self.altstoreSharedDirectory?.appendingPathComponent("Backups", isDirectory: true)
|
||||||
|
return appBackupsDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
func backupDirectoryURL(for app: InstalledApp) -> URL?
|
||||||
|
{
|
||||||
|
let backupDirectoryURL = self.appBackupsDirectory?.appendingPathComponent(app.bundleIdentifier, isDirectory: true)
|
||||||
|
return backupDirectoryURL
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
//
|
||||||
|
// JSONDecoder+Properties.swift
|
||||||
|
// Harmony
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 10/3/18.
|
||||||
|
// Copyright © 2018 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
extension CodingUserInfoKey
|
||||||
|
{
|
||||||
|
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
|
||||||
|
static let sourceURL = CodingUserInfoKey(rawValue: "sourceURL")!
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class JSONDecoder: Foundation.JSONDecoder
|
||||||
|
{
|
||||||
|
@DecoderItem(key: .managedObjectContext)
|
||||||
|
var managedObjectContext: NSManagedObjectContext?
|
||||||
|
|
||||||
|
@DecoderItem(key: .sourceURL)
|
||||||
|
var sourceURL: URL?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Decoder
|
||||||
|
{
|
||||||
|
var managedObjectContext: NSManagedObjectContext? { self.userInfo[.managedObjectContext] as? NSManagedObjectContext }
|
||||||
|
var sourceURL: URL? { self.userInfo[.sourceURL] as? URL }
|
||||||
|
}
|
||||||
|
|
||||||
|
@propertyWrapper
|
||||||
|
struct DecoderItem<Value>
|
||||||
|
{
|
||||||
|
let key: CodingUserInfoKey
|
||||||
|
|
||||||
|
var wrappedValue: Value? {
|
||||||
|
get { fatalError("only works on instance properties of classes") }
|
||||||
|
set { fatalError("only works on instance properties of classes") }
|
||||||
|
}
|
||||||
|
|
||||||
|
init(key: CodingUserInfoKey)
|
||||||
|
{
|
||||||
|
self.key = key
|
||||||
|
}
|
||||||
|
|
||||||
|
public static subscript<OuterSelf: JSONDecoder>(
|
||||||
|
_enclosingInstance decoder: OuterSelf,
|
||||||
|
wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Value?>,
|
||||||
|
storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self>
|
||||||
|
) -> Value? {
|
||||||
|
get {
|
||||||
|
let wrapper = decoder[keyPath: storageKeyPath]
|
||||||
|
|
||||||
|
let value = decoder.userInfo[wrapper.key] as? Value
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let wrapper = decoder[keyPath: storageKeyPath]
|
||||||
|
decoder.userInfo[wrapper.key] = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// NSError+LocalizedFailure.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/11/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension NSError
|
||||||
|
{
|
||||||
|
@objc(alt_localizedFailure)
|
||||||
|
var localizedFailure: String? {
|
||||||
|
let localizedFailure = (self.userInfo[NSLocalizedFailureErrorKey] as? String) ?? (NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSLocalizedFailureErrorKey) as? String)
|
||||||
|
return localizedFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
func withLocalizedFailure(_ failure: String) -> NSError
|
||||||
|
{
|
||||||
|
var userInfo = self.userInfo
|
||||||
|
userInfo[NSLocalizedFailureErrorKey] = failure
|
||||||
|
userInfo[NSLocalizedDescriptionKey] = self.localizedDescription
|
||||||
|
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason
|
||||||
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion
|
||||||
|
|
||||||
|
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol ALTLocalizedError: LocalizedError, CustomNSError
|
||||||
|
{
|
||||||
|
var errorFailure: String? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ALTLocalizedError
|
||||||
|
{
|
||||||
|
var errorUserInfo: [String : Any] {
|
||||||
|
let userInfo = [NSLocalizedDescriptionKey: self.errorDescription,
|
||||||
|
NSLocalizedFailureReasonErrorKey: self.failureReason,
|
||||||
|
NSLocalizedFailureErrorKey: self.errorFailure].compactMapValues { $0 }
|
||||||
|
return userInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
21
source-code/ALTs/AltStore/Extensions/UIColor+AltStore.swift
Normal file
21
source-code/ALTs/AltStore/Extensions/UIColor+AltStore.swift
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// UIColor+AltStore.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 5/9/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIColor
|
||||||
|
{
|
||||||
|
static let altPrimary = UIColor(named: "Primary")!
|
||||||
|
|
||||||
|
static let altPink = UIColor(named: "Pink")!
|
||||||
|
|
||||||
|
static let refreshRed = UIColor(named: "RefreshRed")!
|
||||||
|
static let refreshOrange = UIColor(named: "RefreshOrange")!
|
||||||
|
static let refreshYellow = UIColor(named: "RefreshYellow")!
|
||||||
|
static let refreshGreen = UIColor(named: "RefreshGreen")!
|
||||||
|
}
|
||||||
43
source-code/ALTs/AltStore/Extensions/UIColor+Hex.swift
Normal file
43
source-code/ALTs/AltStore/Extensions/UIColor+Hex.swift
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// UIColor+Hex.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/15/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIColor
|
||||||
|
{
|
||||||
|
// Borrowed from https://stackoverflow.com/a/26341062
|
||||||
|
var hexString: String {
|
||||||
|
let components = self.cgColor.components
|
||||||
|
let r: CGFloat = components?[0] ?? 0.0
|
||||||
|
let g: CGFloat = components?[1] ?? 0.0
|
||||||
|
let b: CGFloat = components?[2] ?? 0.0
|
||||||
|
|
||||||
|
let hexString = String.init(format: "%02lX%02lX%02lX", lroundf(Float(r * 255)), lroundf(Float(g * 255)), lroundf(Float(b * 255)))
|
||||||
|
return hexString
|
||||||
|
}
|
||||||
|
|
||||||
|
// Borrowed from https://stackoverflow.com/a/33397427
|
||||||
|
convenience init?(hexString: String)
|
||||||
|
{
|
||||||
|
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||||
|
var int = UInt32()
|
||||||
|
Scanner(string: hex).scanHexInt32(&int)
|
||||||
|
let a, r, g, b: UInt32
|
||||||
|
switch hex.count {
|
||||||
|
case 3: // RGB (12-bit)
|
||||||
|
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||||
|
case 6: // RGB (24-bit)
|
||||||
|
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||||
|
case 8: // ARGB (32-bit)
|
||||||
|
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// UIDevice+Jailbreak.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 6/5/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIDevice
|
||||||
|
{
|
||||||
|
var isJailbroken: Bool {
|
||||||
|
if
|
||||||
|
FileManager.default.fileExists(atPath: "/Applications/Cydia.app") ||
|
||||||
|
FileManager.default.fileExists(atPath: "/Library/MobileSubstrate/MobileSubstrate.dylib") ||
|
||||||
|
FileManager.default.fileExists(atPath: "/bin/bash") ||
|
||||||
|
FileManager.default.fileExists(atPath: "/usr/sbin/sshd") ||
|
||||||
|
FileManager.default.fileExists(atPath: "/etc/apt") ||
|
||||||
|
FileManager.default.fileExists(atPath: "/private/var/lib/apt/") ||
|
||||||
|
UIApplication.shared.canOpenURL(URL(string:"cydia://")!)
|
||||||
|
{
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// UIScreen+CompactHeight.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 9/6/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIScreen
|
||||||
|
{
|
||||||
|
var isExtraCompactHeight: Bool {
|
||||||
|
return self.fixedCoordinateSpace.bounds.height < 600
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
//
|
||||||
|
// UserDefaults+AltStore.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 6/4/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
extension UserDefaults
|
||||||
|
{
|
||||||
|
@NSManaged var firstLaunch: Date?
|
||||||
|
|
||||||
|
@NSManaged var preferredServerID: String?
|
||||||
|
|
||||||
|
@NSManaged var isBackgroundRefreshEnabled: Bool
|
||||||
|
@NSManaged var isDebugModeEnabled: Bool
|
||||||
|
@NSManaged var presentedLaunchReminderNotification: Bool
|
||||||
|
|
||||||
|
@NSManaged var legacySideloadedApps: [String]?
|
||||||
|
|
||||||
|
@NSManaged var isLegacyDeactivationSupported: Bool
|
||||||
|
@NSManaged var activeAppLimitIncludesExtensions: Bool
|
||||||
|
|
||||||
|
var activeAppsLimit: Int? {
|
||||||
|
get {
|
||||||
|
return self._activeAppsLimit?.intValue
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if let value = newValue
|
||||||
|
{
|
||||||
|
self._activeAppsLimit = NSNumber(value: value)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self._activeAppsLimit = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@NSManaged @objc(activeAppsLimit) private var _activeAppsLimit: NSNumber?
|
||||||
|
|
||||||
|
func registerDefaults()
|
||||||
|
{
|
||||||
|
let ios13_5 = OperatingSystemVersion(majorVersion: 13, minorVersion: 5, patchVersion: 0)
|
||||||
|
let isLegacyDeactivationSupported = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios13_5)
|
||||||
|
let activeAppLimitIncludesExtensions = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios13_5)
|
||||||
|
|
||||||
|
self.register(defaults: [
|
||||||
|
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
|
||||||
|
#keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported,
|
||||||
|
#keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
138
source-code/ALTs/AltStore/Info.plist
Normal file
138
source-code/ALTs/AltStore/Info.plist
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>ALTAppGroups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.com.rileytestut.AltStore</string>
|
||||||
|
</array>
|
||||||
|
<key>ALTDeviceID</key>
|
||||||
|
<string>00008030-001948590202802E</string>
|
||||||
|
<key>ALTServerID</key>
|
||||||
|
<string>1AAAB6FD-E8CE-4B70-8F26-4073215C03B0</string>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeIconFiles</key>
|
||||||
|
<array/>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>iOS App</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>com.apple.itunes.ipa</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>AltStore General</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>altstore</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>AltStore Backup</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>altstore-com.rileytestut.AltStore</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>altstore-com.rileytestut.AltStore</string>
|
||||||
|
<string>altstore-com.rileytestut.AltStore.Beta</string>
|
||||||
|
<string>altstore-com.rileytestut.Delta</string>
|
||||||
|
<string>altstore-com.rileytestut.Delta.Beta</string>
|
||||||
|
<string>altstore-com.rileytestut.Delta.Lite</string>
|
||||||
|
<string>altstore-com.rileytestut.Delta.Lite.Beta</string>
|
||||||
|
<string>altstore-com.rileytestut.Clip</string>
|
||||||
|
<string>altstore-com.rileytestut.Clip.Beta</string>
|
||||||
|
</array>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>audio</string>
|
||||||
|
<string>fetch</string>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>armv7</string>
|
||||||
|
</array>
|
||||||
|
<key>UIStatusBarTintParameters</key>
|
||||||
|
<dict>
|
||||||
|
<key>UINavigationBar</key>
|
||||||
|
<dict>
|
||||||
|
<key>Style</key>
|
||||||
|
<string>UIBarStyleDefault</string>
|
||||||
|
<key>Translucent</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UTImportedTypeDeclarations</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UTTypeConformsTo</key>
|
||||||
|
<array>
|
||||||
|
<string>public.data</string>
|
||||||
|
</array>
|
||||||
|
<key>UTTypeDescription</key>
|
||||||
|
<string>iOS App</string>
|
||||||
|
<key>UTTypeIconFiles</key>
|
||||||
|
<array/>
|
||||||
|
<key>UTTypeIdentifier</key>
|
||||||
|
<string>com.apple.itunes.ipa</string>
|
||||||
|
<key>UTTypeTagSpecification</key>
|
||||||
|
<dict>
|
||||||
|
<key>public.filename-extension</key>
|
||||||
|
<string>ipa</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
86
source-code/ALTs/AltStore/LaunchViewController.swift
Normal file
86
source-code/ALTs/AltStore/LaunchViewController.swift
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// LaunchViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/30/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
class LaunchViewController: RSTLaunchViewController
|
||||||
|
{
|
||||||
|
private var didFinishLaunching = false
|
||||||
|
|
||||||
|
private var destinationViewController: UIViewController!
|
||||||
|
|
||||||
|
override var launchConditions: [RSTLaunchCondition] {
|
||||||
|
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
|
||||||
|
DatabaseManager.shared.start(completionHandler: completionHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [isDatabaseStarted]
|
||||||
|
}
|
||||||
|
|
||||||
|
override var childForStatusBarStyle: UIViewController? {
|
||||||
|
return self.children.first
|
||||||
|
}
|
||||||
|
|
||||||
|
override var childForStatusBarHidden: UIViewController? {
|
||||||
|
return self.children.first
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
||||||
|
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LaunchViewController
|
||||||
|
{
|
||||||
|
override func handleLaunchError(_ error: Error)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
catch let error as NSError
|
||||||
|
{
|
||||||
|
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch AltStore", comment: "")
|
||||||
|
|
||||||
|
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert)
|
||||||
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
|
||||||
|
self.handleLaunchConditions()
|
||||||
|
}))
|
||||||
|
self.present(alertController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func finishLaunching()
|
||||||
|
{
|
||||||
|
super.finishLaunching()
|
||||||
|
|
||||||
|
guard !self.didFinishLaunching else { return }
|
||||||
|
|
||||||
|
AppManager.shared.update()
|
||||||
|
PatreonAPI.shared.refreshPatreonAccount()
|
||||||
|
|
||||||
|
// Add view controller as child (rather than presenting modally)
|
||||||
|
// so tint adjustment + card presentations works correctly.
|
||||||
|
self.destinationViewController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
|
||||||
|
self.destinationViewController.view.alpha = 0.0
|
||||||
|
self.addChild(self.destinationViewController)
|
||||||
|
self.view.addSubview(self.destinationViewController.view, pinningEdgesWith: .zero)
|
||||||
|
self.destinationViewController.didMove(toParent: self)
|
||||||
|
|
||||||
|
UIView.animate(withDuration: 0.2) {
|
||||||
|
self.destinationViewController.view.alpha = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
self.didFinishLaunching = true
|
||||||
|
}
|
||||||
|
}
|
||||||
1343
source-code/ALTs/AltStore/Managing Apps/AppManager.swift
Normal file
1343
source-code/ALTs/AltStore/Managing Apps/AppManager.swift
Normal file
File diff suppressed because it is too large
Load Diff
66
source-code/ALTs/AltStore/Model/Account.swift
Normal file
66
source-code/ALTs/AltStore/Model/Account.swift
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
//
|
||||||
|
// Account.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 6/5/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
@objc(Account)
|
||||||
|
class Account: NSManagedObject, Fetchable
|
||||||
|
{
|
||||||
|
var localizedName: String {
|
||||||
|
var components = PersonNameComponents()
|
||||||
|
components.givenName = self.firstName
|
||||||
|
components.familyName = self.lastName
|
||||||
|
|
||||||
|
let name = PersonNameComponentsFormatter.localizedString(from: components, style: .default)
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Properties */
|
||||||
|
@NSManaged var appleID: String
|
||||||
|
@NSManaged var identifier: String
|
||||||
|
|
||||||
|
@NSManaged var firstName: String
|
||||||
|
@NSManaged var lastName: String
|
||||||
|
|
||||||
|
@NSManaged var isActiveAccount: Bool
|
||||||
|
|
||||||
|
/* Relationships */
|
||||||
|
@NSManaged var teams: Set<Team>
|
||||||
|
|
||||||
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
|
{
|
||||||
|
super.init(entity: entity, insertInto: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ account: ALTAccount, context: NSManagedObjectContext)
|
||||||
|
{
|
||||||
|
super.init(entity: Account.entity(), insertInto: context)
|
||||||
|
|
||||||
|
self.update(account: account)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(account: ALTAccount)
|
||||||
|
{
|
||||||
|
self.appleID = account.appleID
|
||||||
|
self.identifier = account.identifier
|
||||||
|
|
||||||
|
self.firstName = account.firstName
|
||||||
|
self.lastName = account.lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Account
|
||||||
|
{
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<Account>
|
||||||
|
{
|
||||||
|
return NSFetchRequest<Account>(entityName: "Account")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>_XCCurrentVersionName</key>
|
||||||
|
<string>AltStore 6.xcdatamodel</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14492.1" systemVersion="18G95" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
|
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||||
|
<attribute name="appleID" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="firstName" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="lastName" attributeType="String" syncable="YES"/>
|
||||||
|
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||||
|
<attribute name="type" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="usageDescription" attributeType="String" syncable="YES"/>
|
||||||
|
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp" syncable="YES"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="resignedBundleIdentifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="version" attributeType="String" syncable="YES"/>
|
||||||
|
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="bundleIdentifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||||
|
<attribute name="appID" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="caption" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="date" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="externalURL" optional="YES" attributeType="URI" syncable="YES"/>
|
||||||
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="imageURL" optional="YES" attributeType="URI" syncable="YES"/>
|
||||||
|
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="tintColor" optional="YES" attributeType="Transformable" syncable="YES"/>
|
||||||
|
<attribute name="title" attributeType="String" syncable="YES"/>
|
||||||
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source" syncable="YES"/>
|
||||||
|
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||||
|
<attribute name="firstName" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||||
|
<attribute name="date" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="errorDescription" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||||
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="sourceURL" attributeType="URI" syncable="YES"/>
|
||||||
|
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp" syncable="YES"/>
|
||||||
|
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="developerName" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="downloadURL" attributeType="URI" syncable="YES"/>
|
||||||
|
<attribute name="iconURL" attributeType="URI" syncable="YES"/>
|
||||||
|
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="localizedDescription" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="screenshotURLs" attributeType="Transformable" syncable="YES"/>
|
||||||
|
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="tintColor" optional="YES" attributeType="Transformable" syncable="YES"/>
|
||||||
|
<attribute name="version" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="versionDescription" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
|
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp" syncable="YES"/>
|
||||||
|
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem" syncable="YES"/>
|
||||||
|
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission" syncable="YES"/>
|
||||||
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="bundleIdentifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||||
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<elements>
|
||||||
|
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||||
|
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||||
|
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/>
|
||||||
|
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
|
||||||
|
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
|
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
|
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
|
||||||
|
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
||||||
|
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
||||||
|
</elements>
|
||||||
|
</model>
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
|
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||||
|
<attribute name="appleID" attributeType="String"/>
|
||||||
|
<attribute name="firstName" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="lastName" attributeType="String"/>
|
||||||
|
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||||
|
<attribute name="type" attributeType="String"/>
|
||||||
|
<attribute name="usageDescription" attributeType="String"/>
|
||||||
|
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="version" attributeType="String"/>
|
||||||
|
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||||
|
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||||
|
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="bundleIdentifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="version" attributeType="String"/>
|
||||||
|
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||||
|
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="caption" attributeType="String"/>
|
||||||
|
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||||
|
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||||
|
<attribute name="title" attributeType="String"/>
|
||||||
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||||
|
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||||
|
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||||
|
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="sourceURL" attributeType="URI"/>
|
||||||
|
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||||
|
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="developerName" attributeType="String"/>
|
||||||
|
<attribute name="downloadURL" attributeType="URI"/>
|
||||||
|
<attribute name="iconURL" attributeType="URI"/>
|
||||||
|
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="localizedDescription" attributeType="String"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
||||||
|
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||||
|
<attribute name="version" attributeType="String"/>
|
||||||
|
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||||
|
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||||
|
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||||
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="bundleIdentifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||||
|
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<elements>
|
||||||
|
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||||
|
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||||
|
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="193"/>
|
||||||
|
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
||||||
|
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
|
||||||
|
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
|
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
|
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
|
||||||
|
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
||||||
|
<element name="Team" positionX="-45" positionY="81" width="128" height="133"/>
|
||||||
|
</elements>
|
||||||
|
</model>
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
|
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||||
|
<attribute name="appleID" attributeType="String"/>
|
||||||
|
<attribute name="firstName" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="lastName" attributeType="String"/>
|
||||||
|
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="features" attributeType="Transformable"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||||
|
<attribute name="type" attributeType="String"/>
|
||||||
|
<attribute name="usageDescription" attributeType="String"/>
|
||||||
|
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="version" attributeType="String"/>
|
||||||
|
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||||
|
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||||
|
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="bundleIdentifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="version" attributeType="String"/>
|
||||||
|
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||||
|
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="caption" attributeType="String"/>
|
||||||
|
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||||
|
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||||
|
<attribute name="title" attributeType="String"/>
|
||||||
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||||
|
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||||
|
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||||
|
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="sourceURL" attributeType="URI"/>
|
||||||
|
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||||
|
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="developerName" attributeType="String"/>
|
||||||
|
<attribute name="downloadURL" attributeType="URI"/>
|
||||||
|
<attribute name="iconURL" attributeType="URI"/>
|
||||||
|
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="localizedDescription" attributeType="String"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
||||||
|
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||||
|
<attribute name="version" attributeType="String"/>
|
||||||
|
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||||
|
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||||
|
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||||
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="bundleIdentifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||||
|
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||||
|
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<elements>
|
||||||
|
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||||
|
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||||
|
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="193"/>
|
||||||
|
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
||||||
|
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
|
||||||
|
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
|
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
|
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
|
||||||
|
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
||||||
|
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
|
||||||
|
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
|
||||||
|
</elements>
|
||||||
|
</model>
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16117.1" systemVersion="19D76" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
|
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||||
|
<attribute name="appleID" attributeType="String"/>
|
||||||
|
<attribute name="firstName" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="lastName" attributeType="String"/>
|
||||||
|
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="features" attributeType="Transformable"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||||
|
<attribute name="type" attributeType="String"/>
|
||||||
|
<attribute name="usageDescription" attributeType="String"/>
|
||||||
|
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="version" attributeType="String"/>
|
||||||
|
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||||
|
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||||
|
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="bundleIdentifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="version" attributeType="String"/>
|
||||||
|
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||||
|
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="caption" attributeType="String"/>
|
||||||
|
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||||
|
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||||
|
<attribute name="title" attributeType="String"/>
|
||||||
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||||
|
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||||
|
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||||
|
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="sourceURL" attributeType="URI"/>
|
||||||
|
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||||
|
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="developerName" attributeType="String"/>
|
||||||
|
<attribute name="downloadURL" attributeType="URI"/>
|
||||||
|
<attribute name="iconURL" attributeType="URI"/>
|
||||||
|
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="localizedDescription" attributeType="String"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
||||||
|
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||||
|
<attribute name="version" attributeType="String"/>
|
||||||
|
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||||
|
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||||
|
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||||
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="bundleIdentifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||||
|
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||||
|
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<elements>
|
||||||
|
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||||
|
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
|
||||||
|
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||||
|
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="223"/>
|
||||||
|
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
||||||
|
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
|
||||||
|
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
|
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
|
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
|
||||||
|
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
||||||
|
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
|
||||||
|
</elements>
|
||||||
|
</model>
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16117.1" systemVersion="19D76" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
|
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||||
|
<attribute name="appleID" attributeType="String"/>
|
||||||
|
<attribute name="firstName" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="lastName" attributeType="String"/>
|
||||||
|
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="features" attributeType="Transformable"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||||
|
<attribute name="type" attributeType="String"/>
|
||||||
|
<attribute name="usageDescription" attributeType="String"/>
|
||||||
|
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="version" attributeType="String"/>
|
||||||
|
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||||
|
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||||
|
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="bundleIdentifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="version" attributeType="String"/>
|
||||||
|
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||||
|
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="caption" attributeType="String"/>
|
||||||
|
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||||
|
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||||
|
<attribute name="title" attributeType="String"/>
|
||||||
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||||
|
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
<constraint value="sourceIdentifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||||
|
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||||
|
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="sourceURL" attributeType="URI"/>
|
||||||
|
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||||
|
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||||
|
<attribute name="developerName" attributeType="String"/>
|
||||||
|
<attribute name="downloadURL" attributeType="URI"/>
|
||||||
|
<attribute name="iconURL" attributeType="URI"/>
|
||||||
|
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="localizedDescription" attributeType="String"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
||||||
|
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||||
|
<attribute name="version" attributeType="String"/>
|
||||||
|
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||||
|
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||||
|
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||||
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="sourceIdentifier"/>
|
||||||
|
<constraint value="bundleIdentifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||||
|
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||||
|
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<elements>
|
||||||
|
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||||
|
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
|
||||||
|
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||||
|
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="223"/>
|
||||||
|
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
||||||
|
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="238"/>
|
||||||
|
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
|
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
|
<element name="Source" positionX="-45" positionY="99" width="128" height="118"/>
|
||||||
|
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="343"/>
|
||||||
|
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
|
||||||
|
</elements>
|
||||||
|
</model>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.99" systemVersion="18F203" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
|
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||||
|
<attribute name="appleID" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="firstName" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="lastName" attributeType="String" syncable="YES"/>
|
||||||
|
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||||
|
<attribute name="type" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="usageDescription" attributeType="String" syncable="YES"/>
|
||||||
|
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp" syncable="YES"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="resignedBundleIdentifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="version" attributeType="String" syncable="YES"/>
|
||||||
|
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="bundleIdentifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||||
|
<attribute name="date" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="errorDescription" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||||
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="sourceURL" attributeType="URI" syncable="YES"/>
|
||||||
|
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||||
|
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="developerName" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="downloadURL" attributeType="URI" syncable="YES"/>
|
||||||
|
<attribute name="iconName" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="localizedDescription" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="screenshotNames" attributeType="Transformable" syncable="YES"/>
|
||||||
|
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="tintColor" optional="YES" attributeType="Transformable" syncable="YES"/>
|
||||||
|
<attribute name="version" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="versionDescription" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
|
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp" syncable="YES"/>
|
||||||
|
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission" syncable="YES"/>
|
||||||
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="bundleIdentifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||||
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<elements>
|
||||||
|
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||||
|
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="300"/>
|
||||||
|
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||||
|
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/>
|
||||||
|
<element name="Source" positionX="-45" positionY="99" width="128" height="105"/>
|
||||||
|
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
||||||
|
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
|
</elements>
|
||||||
|
</model>
|
||||||
52
source-code/ALTs/AltStore/Model/AppID.swift
Normal file
52
source-code/ALTs/AltStore/Model/AppID.swift
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// AppID.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 1/27/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
@objc(AppID)
|
||||||
|
class AppID: NSManagedObject, Fetchable
|
||||||
|
{
|
||||||
|
/* Properties */
|
||||||
|
@NSManaged var name: String
|
||||||
|
@NSManaged var identifier: String
|
||||||
|
@NSManaged var bundleIdentifier: String
|
||||||
|
@NSManaged var features: [ALTFeature: Any]
|
||||||
|
@NSManaged var expirationDate: Date?
|
||||||
|
|
||||||
|
/* Relationships */
|
||||||
|
@NSManaged private(set) var team: Team?
|
||||||
|
|
||||||
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
|
{
|
||||||
|
super.init(entity: entity, insertInto: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ appID: ALTAppID, team: Team, context: NSManagedObjectContext)
|
||||||
|
{
|
||||||
|
super.init(entity: AppID.entity(), insertInto: context)
|
||||||
|
|
||||||
|
self.name = appID.name
|
||||||
|
self.identifier = appID.identifier
|
||||||
|
self.bundleIdentifier = appID.bundleIdentifier
|
||||||
|
self.features = appID.features
|
||||||
|
self.expirationDate = appID.expirationDate
|
||||||
|
|
||||||
|
self.team = team
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppID
|
||||||
|
{
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<AppID>
|
||||||
|
{
|
||||||
|
return NSFetchRequest<AppID>(entityName: "AppID")
|
||||||
|
}
|
||||||
|
}
|
||||||
88
source-code/ALTs/AltStore/Model/AppPermission.swift
Normal file
88
source-code/ALTs/AltStore/Model/AppPermission.swift
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
//
|
||||||
|
// AppPermission.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/23/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension ALTAppPermissionType
|
||||||
|
{
|
||||||
|
var localizedShortName: String? {
|
||||||
|
switch self
|
||||||
|
{
|
||||||
|
case .photos: return NSLocalizedString("Photos", comment: "")
|
||||||
|
case .backgroundAudio: return NSLocalizedString("Audio (BG)", comment: "")
|
||||||
|
case .backgroundFetch: return NSLocalizedString("Fetch (BG)", comment: "")
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var localizedName: String? {
|
||||||
|
switch self
|
||||||
|
{
|
||||||
|
case .photos: return NSLocalizedString("Photos", comment: "")
|
||||||
|
case .backgroundAudio: return NSLocalizedString("Background Audio", comment: "")
|
||||||
|
case .backgroundFetch: return NSLocalizedString("Background Fetch", comment: "")
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var icon: UIImage? {
|
||||||
|
switch self
|
||||||
|
{
|
||||||
|
case .photos: return UIImage(named: "PhotosPermission")
|
||||||
|
case .backgroundAudio: return UIImage(named: "BackgroundAudioPermission")
|
||||||
|
case .backgroundFetch: return UIImage(named: "BackgroundFetchPermission")
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(AppPermission)
|
||||||
|
class AppPermission: NSManagedObject, Decodable, Fetchable
|
||||||
|
{
|
||||||
|
/* Properties */
|
||||||
|
@NSManaged var type: ALTAppPermissionType
|
||||||
|
@NSManaged var usageDescription: String
|
||||||
|
|
||||||
|
/* Relationships */
|
||||||
|
@NSManaged private(set) var app: StoreApp!
|
||||||
|
|
||||||
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
|
{
|
||||||
|
super.init(entity: entity, insertInto: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey
|
||||||
|
{
|
||||||
|
case type
|
||||||
|
case usageDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws
|
||||||
|
{
|
||||||
|
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||||
|
|
||||||
|
super.init(entity: AppPermission.entity(), insertInto: nil)
|
||||||
|
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.usageDescription = try container.decode(String.self, forKey: .usageDescription)
|
||||||
|
|
||||||
|
let rawType = try container.decode(String.self, forKey: .type)
|
||||||
|
self.type = ALTAppPermissionType(rawValue: rawType)
|
||||||
|
|
||||||
|
context.insert(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppPermission
|
||||||
|
{
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<AppPermission>
|
||||||
|
{
|
||||||
|
return NSFetchRequest<AppPermission>(entityName: "AppPermission")
|
||||||
|
}
|
||||||
|
}
|
||||||
231
source-code/ALTs/AltStore/Model/DatabaseManager.swift
Normal file
231
source-code/ALTs/AltStore/Model/DatabaseManager.swift
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
//
|
||||||
|
// DatabaseManager.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 5/20/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
public class DatabaseManager
|
||||||
|
{
|
||||||
|
public static let shared = DatabaseManager()
|
||||||
|
|
||||||
|
public let persistentContainer: RSTPersistentContainer
|
||||||
|
|
||||||
|
public private(set) var isStarted = false
|
||||||
|
|
||||||
|
private var startCompletionHandlers = [(Error?) -> Void]()
|
||||||
|
|
||||||
|
private init()
|
||||||
|
{
|
||||||
|
self.persistentContainer = RSTPersistentContainer(name: "AltStore")
|
||||||
|
self.persistentContainer.preferredMergePolicy = MergePolicy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension DatabaseManager
|
||||||
|
{
|
||||||
|
func start(completionHandler: @escaping (Error?) -> Void)
|
||||||
|
{
|
||||||
|
self.startCompletionHandlers.append(completionHandler)
|
||||||
|
|
||||||
|
guard self.startCompletionHandlers.count == 1 else { return }
|
||||||
|
|
||||||
|
func finish(_ error: Error?)
|
||||||
|
{
|
||||||
|
self.startCompletionHandlers.forEach { $0(error) }
|
||||||
|
self.startCompletionHandlers.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !self.isStarted else { return finish(nil) }
|
||||||
|
|
||||||
|
self.persistentContainer.loadPersistentStores { (description, error) in
|
||||||
|
guard error == nil else { return finish(error!) }
|
||||||
|
|
||||||
|
self.prepareDatabase() { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error):
|
||||||
|
finish(error)
|
||||||
|
|
||||||
|
case .success:
|
||||||
|
self.isStarted = true
|
||||||
|
finish(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func signOut(completionHandler: @escaping (Error?) -> Void)
|
||||||
|
{
|
||||||
|
self.persistentContainer.performBackgroundTask { (context) in
|
||||||
|
if let account = self.activeAccount(in: context)
|
||||||
|
{
|
||||||
|
account.isActiveAccount = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let team = self.activeTeam(in: context)
|
||||||
|
{
|
||||||
|
team.isActiveTeam = false
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
try context.save()
|
||||||
|
|
||||||
|
Keychain.shared.reset()
|
||||||
|
|
||||||
|
completionHandler(nil)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to save when signing out.", error)
|
||||||
|
completionHandler(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension DatabaseManager
|
||||||
|
{
|
||||||
|
var viewContext: NSManagedObjectContext {
|
||||||
|
return self.persistentContainer.viewContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DatabaseManager
|
||||||
|
{
|
||||||
|
func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account?
|
||||||
|
{
|
||||||
|
let predicate = NSPredicate(format: "%K == YES", #keyPath(Account.isActiveAccount))
|
||||||
|
|
||||||
|
let activeAccount = Account.first(satisfying: predicate, in: context)
|
||||||
|
return activeAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeTeam(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Team?
|
||||||
|
{
|
||||||
|
let predicate = NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam))
|
||||||
|
|
||||||
|
let activeTeam = Team.first(satisfying: predicate, in: context)
|
||||||
|
return activeTeam
|
||||||
|
}
|
||||||
|
|
||||||
|
func patreonAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> PatreonAccount?
|
||||||
|
{
|
||||||
|
let patronAccount = PatreonAccount.first(in: context)
|
||||||
|
return patronAccount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension DatabaseManager
|
||||||
|
{
|
||||||
|
func prepareDatabase(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let context = self.persistentContainer.newBackgroundContext()
|
||||||
|
context.performAndWait {
|
||||||
|
guard let localApp = ALTApplication(fileURL: Bundle.main.bundleURL) else { return }
|
||||||
|
|
||||||
|
let altStoreSource: Source
|
||||||
|
|
||||||
|
if let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
|
||||||
|
{
|
||||||
|
altStoreSource = source
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
altStoreSource = Source.makeAltStoreSource(in: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure to always update source URL to be current.
|
||||||
|
altStoreSource.sourceURL = Source.altStoreSourceURL
|
||||||
|
|
||||||
|
let storeApp: StoreApp
|
||||||
|
|
||||||
|
if let app = StoreApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: context)
|
||||||
|
{
|
||||||
|
storeApp = app
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
storeApp = StoreApp.makeAltStoreApp(in: context)
|
||||||
|
storeApp.version = localApp.version
|
||||||
|
storeApp.source = altStoreSource
|
||||||
|
}
|
||||||
|
|
||||||
|
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String
|
||||||
|
let installedApp: InstalledApp
|
||||||
|
|
||||||
|
if let app = storeApp.installedApp
|
||||||
|
{
|
||||||
|
installedApp = app
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, context: context)
|
||||||
|
installedApp.storeApp = storeApp
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileURL = installedApp.fileURL
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
let replaceCachedApp = true
|
||||||
|
#else
|
||||||
|
let replaceCachedApp = !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if replaceCachedApp
|
||||||
|
{
|
||||||
|
FileManager.default.prepareTemporaryURL() { (temporaryFileURL) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
try FileManager.default.copyItem(at: Bundle.main.bundleURL, to: temporaryFileURL)
|
||||||
|
|
||||||
|
let infoPlistURL = temporaryFileURL.appendingPathComponent("Info.plist")
|
||||||
|
|
||||||
|
guard var infoDictionary = Bundle.main.infoDictionary else { throw ALTError(.missingInfoPlist) }
|
||||||
|
infoDictionary[kCFBundleIdentifierKey as String] = StoreApp.altstoreAppID
|
||||||
|
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
|
||||||
|
|
||||||
|
try FileManager.default.copyItem(at: temporaryFileURL, to: fileURL, shouldReplace: true)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to copy AltStore app bundle to its proper location.", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedRefreshedDate = installedApp.refreshedDate
|
||||||
|
let cachedExpirationDate = installedApp.expirationDate
|
||||||
|
|
||||||
|
// Must go after comparing versions to see if we need to update our cached AltStore app bundle.
|
||||||
|
installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber)
|
||||||
|
|
||||||
|
if installedApp.refreshedDate < cachedRefreshedDate
|
||||||
|
{
|
||||||
|
// Embedded provisioning profile has a creation date older than our refreshed date.
|
||||||
|
// This most likely means we've refreshed the app since then, and profile is now outdated,
|
||||||
|
// so use cached dates instead (i.e. not the dates updated from provisioning profile).
|
||||||
|
|
||||||
|
installedApp.refreshedDate = cachedRefreshedDate
|
||||||
|
installedApp.expirationDate = cachedExpirationDate
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
try context.save()
|
||||||
|
completionHandler(.success(()))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
280
source-code/ALTs/AltStore/Model/InstalledApp.swift
Normal file
280
source-code/ALTs/AltStore/Model/InstalledApp.swift
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
//
|
||||||
|
// InstalledApp.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 5/20/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
// Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1.
|
||||||
|
let ALTActiveAppsLimit = 3
|
||||||
|
|
||||||
|
protocol InstalledAppProtocol: Fetchable
|
||||||
|
{
|
||||||
|
var name: String { get }
|
||||||
|
var bundleIdentifier: String { get }
|
||||||
|
var resignedBundleIdentifier: String { get }
|
||||||
|
var version: String { get }
|
||||||
|
|
||||||
|
var refreshedDate: Date { get }
|
||||||
|
var expirationDate: Date { get }
|
||||||
|
var installedDate: Date { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(InstalledApp)
|
||||||
|
class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||||
|
{
|
||||||
|
/* Properties */
|
||||||
|
@NSManaged var name: String
|
||||||
|
@NSManaged var bundleIdentifier: String
|
||||||
|
@NSManaged var resignedBundleIdentifier: String
|
||||||
|
@NSManaged var version: String
|
||||||
|
|
||||||
|
@NSManaged var refreshedDate: Date
|
||||||
|
@NSManaged var expirationDate: Date
|
||||||
|
@NSManaged var installedDate: Date
|
||||||
|
|
||||||
|
@NSManaged var isActive: Bool
|
||||||
|
|
||||||
|
@NSManaged var certificateSerialNumber: String?
|
||||||
|
|
||||||
|
/* Relationships */
|
||||||
|
@NSManaged var storeApp: StoreApp?
|
||||||
|
@NSManaged var team: Team?
|
||||||
|
@NSManaged var appExtensions: Set<InstalledExtension>
|
||||||
|
|
||||||
|
var isSideloaded: Bool {
|
||||||
|
return self.storeApp == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var appIDCount: Int {
|
||||||
|
return 1 + self.appExtensions.count
|
||||||
|
}
|
||||||
|
|
||||||
|
var requiredActiveSlots: Int {
|
||||||
|
let requiredActiveSlots = UserDefaults.standard.activeAppLimitIncludesExtensions ? self.appIDCount : 1
|
||||||
|
return requiredActiveSlots
|
||||||
|
}
|
||||||
|
|
||||||
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
|
{
|
||||||
|
super.init(entity: entity, insertInto: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, context: NSManagedObjectContext)
|
||||||
|
{
|
||||||
|
super.init(entity: InstalledApp.entity(), insertInto: context)
|
||||||
|
|
||||||
|
self.bundleIdentifier = originalBundleIdentifier
|
||||||
|
|
||||||
|
self.refreshedDate = Date()
|
||||||
|
self.installedDate = Date()
|
||||||
|
|
||||||
|
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
|
||||||
|
|
||||||
|
self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(resignedApp: ALTApplication, certificateSerialNumber: String?)
|
||||||
|
{
|
||||||
|
self.name = resignedApp.name
|
||||||
|
|
||||||
|
self.resignedBundleIdentifier = resignedApp.bundleIdentifier
|
||||||
|
self.version = resignedApp.version
|
||||||
|
|
||||||
|
self.certificateSerialNumber = certificateSerialNumber
|
||||||
|
|
||||||
|
if let provisioningProfile = resignedApp.provisioningProfile
|
||||||
|
{
|
||||||
|
self.update(provisioningProfile: provisioningProfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(provisioningProfile: ALTProvisioningProfile)
|
||||||
|
{
|
||||||
|
self.refreshedDate = provisioningProfile.creationDate
|
||||||
|
self.expirationDate = provisioningProfile.expirationDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InstalledApp
|
||||||
|
{
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledApp>
|
||||||
|
{
|
||||||
|
return NSFetchRequest<InstalledApp>(entityName: "InstalledApp")
|
||||||
|
}
|
||||||
|
|
||||||
|
class func updatesFetchRequest() -> NSFetchRequest<InstalledApp>
|
||||||
|
{
|
||||||
|
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||||
|
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K != nil AND %K != %K",
|
||||||
|
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.version))
|
||||||
|
return fetchRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
class func activeAppsFetchRequest() -> NSFetchRequest<InstalledApp>
|
||||||
|
{
|
||||||
|
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||||
|
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(InstalledApp.isActive))
|
||||||
|
return fetchRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
class func fetchAltStore(in context: NSManagedObjectContext) -> InstalledApp?
|
||||||
|
{
|
||||||
|
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||||
|
|
||||||
|
let altStore = InstalledApp.first(satisfying: predicate, in: context)
|
||||||
|
return altStore
|
||||||
|
}
|
||||||
|
|
||||||
|
class func fetchActiveApps(in context: NSManagedObjectContext) -> [InstalledApp]
|
||||||
|
{
|
||||||
|
let activeApps = InstalledApp.fetch(InstalledApp.activeAppsFetchRequest(), in: context)
|
||||||
|
return activeApps
|
||||||
|
}
|
||||||
|
|
||||||
|
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
|
||||||
|
{
|
||||||
|
var predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||||
|
|
||||||
|
if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||||
|
{
|
||||||
|
// No additional predicate
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
|
||||||
|
NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
|
||||||
|
}
|
||||||
|
|
||||||
|
var installedApps = InstalledApp.all(satisfying: predicate,
|
||||||
|
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
||||||
|
in: context)
|
||||||
|
|
||||||
|
if let altStoreApp = InstalledApp.fetchAltStore(in: context)
|
||||||
|
{
|
||||||
|
// Refresh AltStore last since it causes app to quit.
|
||||||
|
installedApps.append(altStoreApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return installedApps
|
||||||
|
}
|
||||||
|
|
||||||
|
class func fetchAppsForBackgroundRefresh(in context: NSManagedObjectContext) -> [InstalledApp]
|
||||||
|
{
|
||||||
|
// Date 6 hours before now.
|
||||||
|
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
|
||||||
|
|
||||||
|
var predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@)",
|
||||||
|
#keyPath(InstalledApp.isActive),
|
||||||
|
#keyPath(InstalledApp.refreshedDate), date as NSDate,
|
||||||
|
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||||
|
|
||||||
|
if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||||
|
{
|
||||||
|
// No additional predicate
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
|
||||||
|
NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
|
||||||
|
}
|
||||||
|
|
||||||
|
var installedApps = InstalledApp.all(satisfying: predicate,
|
||||||
|
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
||||||
|
in: context)
|
||||||
|
|
||||||
|
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.refreshedDate < date
|
||||||
|
{
|
||||||
|
// Refresh AltStore last since it may cause app to quit.
|
||||||
|
installedApps.append(altStoreApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return installedApps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InstalledApp
|
||||||
|
{
|
||||||
|
var openAppURL: URL {
|
||||||
|
let openAppURL = URL(string: "altstore-" + self.bundleIdentifier + "://")!
|
||||||
|
return openAppURL
|
||||||
|
}
|
||||||
|
|
||||||
|
class func openAppURL(for app: AppProtocol) -> URL
|
||||||
|
{
|
||||||
|
let openAppURL = URL(string: "altstore-" + app.bundleIdentifier + "://")!
|
||||||
|
return openAppURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InstalledApp
|
||||||
|
{
|
||||||
|
class var appsDirectoryURL: URL {
|
||||||
|
let appsDirectoryURL = FileManager.default.applicationSupportDirectory.appendingPathComponent("Apps")
|
||||||
|
|
||||||
|
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) }
|
||||||
|
catch { print(error) }
|
||||||
|
|
||||||
|
return appsDirectoryURL
|
||||||
|
}
|
||||||
|
|
||||||
|
class func fileURL(for app: AppProtocol) -> URL
|
||||||
|
{
|
||||||
|
let appURL = self.directoryURL(for: app).appendingPathComponent("App.app")
|
||||||
|
return appURL
|
||||||
|
}
|
||||||
|
|
||||||
|
class func refreshedIPAURL(for app: AppProtocol) -> URL
|
||||||
|
{
|
||||||
|
let ipaURL = self.directoryURL(for: app).appendingPathComponent("Refreshed.ipa")
|
||||||
|
return ipaURL
|
||||||
|
}
|
||||||
|
|
||||||
|
class func directoryURL(for app: AppProtocol) -> URL
|
||||||
|
{
|
||||||
|
let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.bundleIdentifier)
|
||||||
|
|
||||||
|
do { try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) }
|
||||||
|
catch { print(error) }
|
||||||
|
|
||||||
|
return directoryURL
|
||||||
|
}
|
||||||
|
|
||||||
|
class func installedAppUTI(forBundleIdentifier bundleIdentifier: String) -> String
|
||||||
|
{
|
||||||
|
let installedAppUTI = "io.altstore.Installed." + bundleIdentifier
|
||||||
|
return installedAppUTI
|
||||||
|
}
|
||||||
|
|
||||||
|
class func installedBackupAppUTI(forBundleIdentifier bundleIdentifier: String) -> String
|
||||||
|
{
|
||||||
|
let installedBackupAppUTI = InstalledApp.installedAppUTI(forBundleIdentifier: bundleIdentifier) + ".backup"
|
||||||
|
return installedBackupAppUTI
|
||||||
|
}
|
||||||
|
|
||||||
|
var directoryURL: URL {
|
||||||
|
return InstalledApp.directoryURL(for: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileURL: URL {
|
||||||
|
return InstalledApp.fileURL(for: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
var refreshedIPAURL: URL {
|
||||||
|
return InstalledApp.refreshedIPAURL(for: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
var installedAppUTI: String {
|
||||||
|
return InstalledApp.installedAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
var installedBackupAppUTI: String {
|
||||||
|
return InstalledApp.installedBackupAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
75
source-code/ALTs/AltStore/Model/InstalledExtension.swift
Normal file
75
source-code/ALTs/AltStore/Model/InstalledExtension.swift
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
//
|
||||||
|
// InstalledExtension.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 1/7/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
@objc(InstalledExtension)
|
||||||
|
class InstalledExtension: NSManagedObject, InstalledAppProtocol
|
||||||
|
{
|
||||||
|
/* Properties */
|
||||||
|
@NSManaged var name: String
|
||||||
|
@NSManaged var bundleIdentifier: String
|
||||||
|
@NSManaged var resignedBundleIdentifier: String
|
||||||
|
@NSManaged var version: String
|
||||||
|
|
||||||
|
@NSManaged var refreshedDate: Date
|
||||||
|
@NSManaged var expirationDate: Date
|
||||||
|
@NSManaged var installedDate: Date
|
||||||
|
|
||||||
|
/* Relationships */
|
||||||
|
@NSManaged var parentApp: InstalledApp?
|
||||||
|
|
||||||
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
|
{
|
||||||
|
super.init(entity: entity, insertInto: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(resignedAppExtension: ALTApplication, originalBundleIdentifier: String, context: NSManagedObjectContext)
|
||||||
|
{
|
||||||
|
super.init(entity: InstalledExtension.entity(), insertInto: context)
|
||||||
|
|
||||||
|
self.bundleIdentifier = originalBundleIdentifier
|
||||||
|
|
||||||
|
self.refreshedDate = Date()
|
||||||
|
self.installedDate = Date()
|
||||||
|
|
||||||
|
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
|
||||||
|
|
||||||
|
self.update(resignedAppExtension: resignedAppExtension)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(resignedAppExtension: ALTApplication)
|
||||||
|
{
|
||||||
|
self.name = resignedAppExtension.name
|
||||||
|
|
||||||
|
self.resignedBundleIdentifier = resignedAppExtension.bundleIdentifier
|
||||||
|
self.version = resignedAppExtension.version
|
||||||
|
|
||||||
|
if let provisioningProfile = resignedAppExtension.provisioningProfile
|
||||||
|
{
|
||||||
|
self.update(provisioningProfile: provisioningProfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(provisioningProfile: ALTProvisioningProfile)
|
||||||
|
{
|
||||||
|
self.refreshedDate = provisioningProfile.creationDate
|
||||||
|
self.expirationDate = provisioningProfile.expirationDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InstalledExtension
|
||||||
|
{
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledExtension>
|
||||||
|
{
|
||||||
|
return NSFetchRequest<InstalledExtension>(entityName: "InstalledExtension")
|
||||||
|
}
|
||||||
|
}
|
||||||
88
source-code/ALTs/AltStore/Model/MergePolicy.swift
Normal file
88
source-code/ALTs/AltStore/Model/MergePolicy.swift
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
//
|
||||||
|
// MergePolicy.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/23/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||||
|
{
|
||||||
|
open override func resolve(constraintConflicts conflicts: [NSConstraintConflict]) throws
|
||||||
|
{
|
||||||
|
guard conflicts.allSatisfy({ $0.databaseObject != nil }) else {
|
||||||
|
for conflict in conflicts
|
||||||
|
{
|
||||||
|
switch conflict.conflictingObjects.first
|
||||||
|
{
|
||||||
|
case is StoreApp where conflict.conflictingObjects.count == 2:
|
||||||
|
// Modified cached StoreApp while replacing it with new one, causing context-level conflict.
|
||||||
|
// Most likely, we set up a relationship between the new StoreApp and a NewsItem,
|
||||||
|
// causing cached StoreApp to delete it's NewsItem relationship, resulting in (resolvable) conflict.
|
||||||
|
|
||||||
|
if let previousApp = conflict.conflictingObjects.first(where: { !$0.isInserted }) as? StoreApp
|
||||||
|
{
|
||||||
|
// Delete previous permissions (same as below).
|
||||||
|
for permission in previousApp.permissions
|
||||||
|
{
|
||||||
|
permission.managedObjectContext?.delete(permission)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unknown context-level conflict.
|
||||||
|
assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try super.resolve(constraintConflicts: conflicts)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for conflict in conflicts
|
||||||
|
{
|
||||||
|
switch conflict.databaseObject
|
||||||
|
{
|
||||||
|
case let databaseObject as StoreApp:
|
||||||
|
// Delete previous permissions
|
||||||
|
for permission in databaseObject.permissions
|
||||||
|
{
|
||||||
|
permission.managedObjectContext?.delete(permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
case let databaseObject as Source:
|
||||||
|
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
|
||||||
|
|
||||||
|
let bundleIdentifiers = Set(conflictedObject.apps.map { $0.bundleIdentifier })
|
||||||
|
let newsItemIdentifiers = Set(conflictedObject.newsItems.map { $0.identifier })
|
||||||
|
|
||||||
|
for app in databaseObject.apps
|
||||||
|
{
|
||||||
|
if !bundleIdentifiers.contains(app.bundleIdentifier)
|
||||||
|
{
|
||||||
|
// No longer listed in Source, so remove it from database.
|
||||||
|
app.managedObjectContext?.delete(app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for newsItem in databaseObject.newsItems
|
||||||
|
{
|
||||||
|
if !newsItemIdentifiers.contains(newsItem.identifier)
|
||||||
|
{
|
||||||
|
// No longer listed in Source, so remove it from database.
|
||||||
|
newsItem.managedObjectContext?.delete(newsItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try super.resolve(constraintConflicts: conflicts)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,58 @@
|
|||||||
|
//
|
||||||
|
// InstalledAppPolicy.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 1/24/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
@objc(InstalledAppToInstalledAppMigrationPolicy)
|
||||||
|
class InstalledAppToInstalledAppMigrationPolicy: NSEntityMigrationPolicy
|
||||||
|
{
|
||||||
|
override func createRelationships(forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws
|
||||||
|
{
|
||||||
|
try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager)
|
||||||
|
|
||||||
|
// Entity must be in manager.destinationContext.
|
||||||
|
let entity = NSEntityDescription.entity(forEntityName: "Team", in: manager.destinationContext)
|
||||||
|
|
||||||
|
let fetchRequest = NSFetchRequest<NSManagedObject>()
|
||||||
|
fetchRequest.entity = entity
|
||||||
|
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam))
|
||||||
|
|
||||||
|
let teams = try manager.destinationContext.fetch(fetchRequest)
|
||||||
|
|
||||||
|
// Cannot use NSManagedObject subclasses during migration, so fallback to using KVC instead.
|
||||||
|
dInstance.setValue(teams.first, forKey: #keyPath(InstalledApp.team))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(defaultIsActiveForBundleID:team:)
|
||||||
|
func defaultIsActive(for bundleID: String, team: NSManagedObject?) -> NSNumber
|
||||||
|
{
|
||||||
|
let isActive: Bool
|
||||||
|
|
||||||
|
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
|
||||||
|
if !ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
|
||||||
|
{
|
||||||
|
isActive = true
|
||||||
|
}
|
||||||
|
else if let team = team, let type = team.value(forKey: #keyPath(Team.type)) as? Int16, type != ALTTeamType.free.rawValue
|
||||||
|
{
|
||||||
|
isActive = true
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// AltStore should always be active, but deactivate all other apps.
|
||||||
|
isActive = (bundleID == StoreApp.altstoreAppID)
|
||||||
|
|
||||||
|
// We can assume there is an active app limit,
|
||||||
|
// but will confirm next time user authenticates.
|
||||||
|
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSNumber(value: isActive)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// StoreAppPolicy.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 9/14/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
@objc(StoreAppToStoreAppMigrationPolicy)
|
||||||
|
class StoreAppToStoreAppMigrationPolicy: NSEntityMigrationPolicy
|
||||||
|
{
|
||||||
|
@objc(migrateIconURL)
|
||||||
|
func migrateIconURL() -> URL
|
||||||
|
{
|
||||||
|
return URL(string: "https://via.placeholder.com/150")!
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(migrateScreenshotURLs)
|
||||||
|
func migrateScreenshotURLs() -> NSCopying
|
||||||
|
{
|
||||||
|
return [] as NSArray
|
||||||
|
}
|
||||||
|
}
|
||||||
91
source-code/ALTs/AltStore/Model/NewsItem.swift
Normal file
91
source-code/ALTs/AltStore/Model/NewsItem.swift
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// NewsItem.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 8/29/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
@objc(NewsItem)
|
||||||
|
class NewsItem: NSManagedObject, Decodable, Fetchable
|
||||||
|
{
|
||||||
|
/* Properties */
|
||||||
|
@NSManaged var identifier: String
|
||||||
|
@NSManaged var date: Date
|
||||||
|
|
||||||
|
@NSManaged var title: String
|
||||||
|
@NSManaged var caption: String
|
||||||
|
@NSManaged var tintColor: UIColor
|
||||||
|
@NSManaged var sortIndex: Int32
|
||||||
|
@NSManaged var isSilent: Bool
|
||||||
|
|
||||||
|
@NSManaged var imageURL: URL?
|
||||||
|
@NSManaged var externalURL: URL?
|
||||||
|
|
||||||
|
@NSManaged var appID: String?
|
||||||
|
@NSManaged var sourceIdentifier: String?
|
||||||
|
|
||||||
|
/* Relationships */
|
||||||
|
@NSManaged var storeApp: StoreApp?
|
||||||
|
@NSManaged var source: Source?
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey
|
||||||
|
{
|
||||||
|
case identifier
|
||||||
|
case date
|
||||||
|
case title
|
||||||
|
case caption
|
||||||
|
case tintColor
|
||||||
|
case imageURL
|
||||||
|
case externalURL = "url"
|
||||||
|
case appID
|
||||||
|
case notify
|
||||||
|
}
|
||||||
|
|
||||||
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
|
{
|
||||||
|
super.init(entity: entity, insertInto: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws
|
||||||
|
{
|
||||||
|
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||||
|
|
||||||
|
super.init(entity: NewsItem.entity(), insertInto: context)
|
||||||
|
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.identifier = try container.decode(String.self, forKey: .identifier)
|
||||||
|
self.date = try container.decode(Date.self, forKey: .date)
|
||||||
|
|
||||||
|
self.title = try container.decode(String.self, forKey: .title)
|
||||||
|
self.caption = try container.decode(String.self, forKey: .caption)
|
||||||
|
|
||||||
|
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.imageURL = try container.decodeIfPresent(URL.self, forKey: .imageURL)
|
||||||
|
self.externalURL = try container.decodeIfPresent(URL.self, forKey: .externalURL)
|
||||||
|
|
||||||
|
self.appID = try container.decodeIfPresent(String.self, forKey: .appID)
|
||||||
|
|
||||||
|
let notify = try container.decodeIfPresent(Bool.self, forKey: .notify) ?? false
|
||||||
|
self.isSilent = !notify
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NewsItem
|
||||||
|
{
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<NewsItem>
|
||||||
|
{
|
||||||
|
return NSFetchRequest<NewsItem>(entityName: "NewsItem")
|
||||||
|
}
|
||||||
|
}
|
||||||
74
source-code/ALTs/AltStore/Model/PatreonAccount.swift
Normal file
74
source-code/ALTs/AltStore/Model/PatreonAccount.swift
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
//
|
||||||
|
// PatreonAccount.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 8/20/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
extension PatreonAPI
|
||||||
|
{
|
||||||
|
struct AccountResponse: Decodable
|
||||||
|
{
|
||||||
|
struct Data: Decodable
|
||||||
|
{
|
||||||
|
struct Attributes: Decodable
|
||||||
|
{
|
||||||
|
var first_name: String?
|
||||||
|
var full_name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
var id: String
|
||||||
|
var attributes: Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
var data: Data
|
||||||
|
var included: [PatronResponse]?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(PatreonAccount)
|
||||||
|
class PatreonAccount: NSManagedObject, Fetchable
|
||||||
|
{
|
||||||
|
@NSManaged var identifier: String
|
||||||
|
|
||||||
|
@NSManaged var name: String
|
||||||
|
@NSManaged var firstName: String?
|
||||||
|
|
||||||
|
@NSManaged var isPatron: Bool
|
||||||
|
|
||||||
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
|
{
|
||||||
|
super.init(entity: entity, insertInto: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(response: PatreonAPI.AccountResponse, context: NSManagedObjectContext)
|
||||||
|
{
|
||||||
|
super.init(entity: PatreonAccount.entity(), insertInto: context)
|
||||||
|
|
||||||
|
self.identifier = response.data.id
|
||||||
|
self.name = response.data.attributes.full_name
|
||||||
|
self.firstName = response.data.attributes.first_name
|
||||||
|
|
||||||
|
if let patronResponse = response.included?.first
|
||||||
|
{
|
||||||
|
let patron = Patron(response: patronResponse)
|
||||||
|
self.isPatron = (patron.status == .active)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.isPatron = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PatreonAccount
|
||||||
|
{
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<PatreonAccount>
|
||||||
|
{
|
||||||
|
return NSFetchRequest<PatreonAccount>(entityName: "PatreonAccount")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
59
source-code/ALTs/AltStore/Model/RefreshAttempt.swift
Normal file
59
source-code/ALTs/AltStore/Model/RefreshAttempt.swift
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
//
|
||||||
|
// RefreshAttempt.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/31/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
@objc(RefreshAttempt)
|
||||||
|
class RefreshAttempt: NSManagedObject, Fetchable
|
||||||
|
{
|
||||||
|
@NSManaged var identifier: String
|
||||||
|
@NSManaged var date: Date
|
||||||
|
|
||||||
|
@NSManaged var isSuccess: Bool
|
||||||
|
@NSManaged var errorDescription: String?
|
||||||
|
|
||||||
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
|
{
|
||||||
|
super.init(entity: entity, insertInto: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(identifier: String, result: Result<[String: Result<InstalledApp, Error>], Error>, context: NSManagedObjectContext)
|
||||||
|
{
|
||||||
|
super.init(entity: RefreshAttempt.entity(), insertInto: context)
|
||||||
|
|
||||||
|
self.identifier = identifier
|
||||||
|
self.date = Date()
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let results = try result.get()
|
||||||
|
|
||||||
|
for (_, result) in results
|
||||||
|
{
|
||||||
|
guard case let .failure(error) = result else { continue }
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isSuccess = true
|
||||||
|
self.errorDescription = nil
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
self.isSuccess = false
|
||||||
|
self.errorDescription = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RefreshAttempt
|
||||||
|
{
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<RefreshAttempt>
|
||||||
|
{
|
||||||
|
return NSFetchRequest<RefreshAttempt>(entityName: "RefreshAttempt")
|
||||||
|
}
|
||||||
|
}
|
||||||
162
source-code/ALTs/AltStore/Model/Source.swift
Normal file
162
source-code/ALTs/AltStore/Model/Source.swift
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
//
|
||||||
|
// Source.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/30/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
extension Source
|
||||||
|
{
|
||||||
|
#if ALPHA
|
||||||
|
static let altStoreIdentifier = "com.rileytestut.AltStore.Alpha"
|
||||||
|
#else
|
||||||
|
static let altStoreIdentifier = "com.rileytestut.AltStore"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if STAGING
|
||||||
|
|
||||||
|
#if ALPHA
|
||||||
|
static let altStoreSourceURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/sources/alpha/apps-alpha-staging.json")!
|
||||||
|
#else
|
||||||
|
static let altStoreSourceURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/apps-staging.json")!
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#if ALPHA
|
||||||
|
static let altStoreSourceURL = URL(string: "https://alpha.altstore.io/")!
|
||||||
|
#else
|
||||||
|
static let altStoreSourceURL = URL(string: "https://apps.altstore.io/")!
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(Source)
|
||||||
|
class Source: NSManagedObject, Fetchable, Decodable
|
||||||
|
{
|
||||||
|
/* Properties */
|
||||||
|
@NSManaged var name: String
|
||||||
|
@NSManaged var identifier: String
|
||||||
|
@NSManaged var sourceURL: URL
|
||||||
|
|
||||||
|
/* Non-Core Data Properties */
|
||||||
|
var userInfo: [ALTSourceUserInfoKey: String]?
|
||||||
|
|
||||||
|
/* Relationships */
|
||||||
|
@objc(apps) @NSManaged private(set) var _apps: NSOrderedSet
|
||||||
|
@objc(newsItems) @NSManaged private(set) var _newsItems: NSOrderedSet
|
||||||
|
|
||||||
|
@nonobjc var apps: [StoreApp] {
|
||||||
|
get {
|
||||||
|
return self._apps.array as! [StoreApp]
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
self._apps = NSOrderedSet(array: newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@nonobjc var newsItems: [NewsItem] {
|
||||||
|
get {
|
||||||
|
return self._newsItems.array as! [NewsItem]
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
self._newsItems = NSOrderedSet(array: newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey
|
||||||
|
{
|
||||||
|
case name
|
||||||
|
case identifier
|
||||||
|
case sourceURL
|
||||||
|
case userInfo
|
||||||
|
case apps
|
||||||
|
case news
|
||||||
|
}
|
||||||
|
|
||||||
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
|
{
|
||||||
|
super.init(entity: entity, insertInto: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws
|
||||||
|
{
|
||||||
|
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||||
|
guard let sourceURL = decoder.sourceURL else { preconditionFailure("Decoder must have non-nil sourceURL.") }
|
||||||
|
|
||||||
|
super.init(entity: Source.entity(), insertInto: nil)
|
||||||
|
|
||||||
|
self.sourceURL = sourceURL
|
||||||
|
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.name = try container.decode(String.self, forKey: .name)
|
||||||
|
self.identifier = try container.decode(String.self, forKey: .identifier)
|
||||||
|
|
||||||
|
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
|
||||||
|
self.userInfo = userInfo?.reduce(into: [:]) { $0[ALTSourceUserInfoKey($1.key)] = $1.value }
|
||||||
|
|
||||||
|
let apps = try container.decodeIfPresent([StoreApp].self, forKey: .apps) ?? []
|
||||||
|
let appsByID = Dictionary(apps.map { ($0.bundleIdentifier, $0) }, uniquingKeysWith: { (a, b) in return a })
|
||||||
|
|
||||||
|
for (index, app) in apps.enumerated()
|
||||||
|
{
|
||||||
|
app.sourceIdentifier = self.identifier
|
||||||
|
app.sortIndex = Int32(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
|
||||||
|
for (index, item) in newsItems.enumerated()
|
||||||
|
{
|
||||||
|
item.sourceIdentifier = self.identifier
|
||||||
|
item.sortIndex = Int32(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.insert(self)
|
||||||
|
|
||||||
|
for newsItem in newsItems
|
||||||
|
{
|
||||||
|
guard let appID = newsItem.appID else { continue }
|
||||||
|
|
||||||
|
if let storeApp = appsByID[appID]
|
||||||
|
{
|
||||||
|
newsItem.storeApp = storeApp
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newsItem.storeApp = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must assign after we're inserted into context.
|
||||||
|
self._apps = NSMutableOrderedSet(array: apps)
|
||||||
|
self._newsItems = NSMutableOrderedSet(array: newsItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Source
|
||||||
|
{
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<Source>
|
||||||
|
{
|
||||||
|
return NSFetchRequest<Source>(entityName: "Source")
|
||||||
|
}
|
||||||
|
|
||||||
|
class func makeAltStoreSource(in context: NSManagedObjectContext) -> Source
|
||||||
|
{
|
||||||
|
let source = Source(context: context)
|
||||||
|
source.name = "AltStore"
|
||||||
|
source.identifier = Source.altStoreIdentifier
|
||||||
|
source.sourceURL = Source.altStoreSourceURL
|
||||||
|
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
class func fetchAltStoreSource(in context: NSManagedObjectContext) -> Source?
|
||||||
|
{
|
||||||
|
let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
}
|
||||||
173
source-code/ALTs/AltStore/Model/StoreApp.swift
Normal file
173
source-code/ALTs/AltStore/Model/StoreApp.swift
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
extension StoreApp
|
||||||
|
{
|
||||||
|
#if ALPHA
|
||||||
|
static let altstoreAppID = "com.rileytestut.AltStore.Alpha"
|
||||||
|
static let alternativeAltStoreAppIDs: Set<String> = ["com.rileytestut.AltStore", "com.rileytestut.AltStore.Beta"]
|
||||||
|
#elseif BETA
|
||||||
|
static let altstoreAppID = "com.rileytestut.AltStore.Beta"
|
||||||
|
static let alternativeAltStoreAppIDs: Set<String> = ["com.rileytestut.AltStore", "com.rileytestut.AltStore.Alpha"]
|
||||||
|
#else
|
||||||
|
static let altstoreAppID = "com.rileytestut.AltStore"
|
||||||
|
static let alternativeAltStoreAppIDs: Set<String> = ["com.rileytestut.AltStore.Beta", "com.rileytestut.AltStore.Alpha"]
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static let dolphinAppID = "me.oatmealdome.dolphinios-njb"
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(StoreApp)
|
||||||
|
class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||||
|
{
|
||||||
|
/* Properties */
|
||||||
|
@NSManaged private(set) var name: String
|
||||||
|
@NSManaged private(set) var bundleIdentifier: String
|
||||||
|
@NSManaged private(set) var subtitle: String?
|
||||||
|
|
||||||
|
@NSManaged private(set) var developerName: String
|
||||||
|
@NSManaged private(set) var localizedDescription: String
|
||||||
|
@NSManaged private(set) var size: Int32
|
||||||
|
|
||||||
|
@NSManaged private(set) var iconURL: URL
|
||||||
|
@NSManaged private(set) var screenshotURLs: [URL]
|
||||||
|
|
||||||
|
@NSManaged var version: String
|
||||||
|
@NSManaged private(set) var versionDate: Date
|
||||||
|
@NSManaged private(set) var versionDescription: String?
|
||||||
|
|
||||||
|
@NSManaged private(set) var downloadURL: URL
|
||||||
|
@NSManaged private(set) var tintColor: UIColor?
|
||||||
|
@NSManaged private(set) var isBeta: Bool
|
||||||
|
|
||||||
|
@NSManaged var sourceIdentifier: String?
|
||||||
|
|
||||||
|
@NSManaged var sortIndex: Int32
|
||||||
|
|
||||||
|
/* Relationships */
|
||||||
|
@NSManaged var installedApp: InstalledApp?
|
||||||
|
@NSManaged var newsItems: Set<NewsItem>
|
||||||
|
|
||||||
|
@NSManaged @objc(source) var _source: Source?
|
||||||
|
@NSManaged @objc(permissions) var _permissions: NSOrderedSet
|
||||||
|
|
||||||
|
@nonobjc var source: Source? {
|
||||||
|
set {
|
||||||
|
self._source = newValue
|
||||||
|
self.sourceIdentifier = newValue?.identifier
|
||||||
|
}
|
||||||
|
get {
|
||||||
|
return self._source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@nonobjc var permissions: [AppPermission] {
|
||||||
|
return self._permissions.array as! [AppPermission]
|
||||||
|
}
|
||||||
|
|
||||||
|
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 tintColor
|
||||||
|
case subtitle
|
||||||
|
case permissions
|
||||||
|
case size
|
||||||
|
case isBeta = "beta"
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws
|
||||||
|
{
|
||||||
|
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||||
|
|
||||||
|
super.init(entity: StoreApp.entity(), insertInto: nil)
|
||||||
|
|
||||||
|
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.version = try container.decode(String.self, forKey: .version)
|
||||||
|
self.versionDate = try container.decode(Date.self, forKey: .versionDate)
|
||||||
|
self.versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
|
||||||
|
|
||||||
|
self.iconURL = try container.decode(URL.self, forKey: .iconURL)
|
||||||
|
self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
|
||||||
|
|
||||||
|
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||||
|
|
||||||
|
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.size = try container.decode(Int32.self, forKey: .size)
|
||||||
|
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
||||||
|
|
||||||
|
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
|
||||||
|
|
||||||
|
context.insert(self)
|
||||||
|
|
||||||
|
// Must assign after we're inserted into context.
|
||||||
|
self._permissions = NSOrderedSet(array: permissions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StoreApp
|
||||||
|
{
|
||||||
|
@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 = "AltStore"
|
||||||
|
app.bundleIdentifier = StoreApp.altstoreAppID
|
||||||
|
app.developerName = "Riley Testut"
|
||||||
|
app.localizedDescription = "AltStore 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.version = "1.0"
|
||||||
|
app.versionDate = Date()
|
||||||
|
app.downloadURL = URL(string: "http://rileytestut.com")!
|
||||||
|
|
||||||
|
#if BETA
|
||||||
|
app.isBeta = true
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
}
|
||||||
80
source-code/ALTs/AltStore/Model/Team.swift
Normal file
80
source-code/ALTs/AltStore/Model/Team.swift
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
//
|
||||||
|
// Team.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 5/31/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
extension ALTTeamType
|
||||||
|
{
|
||||||
|
var localizedDescription: String {
|
||||||
|
switch self
|
||||||
|
{
|
||||||
|
case .free: return NSLocalizedString("Free Developer Account", comment: "")
|
||||||
|
case .individual: return NSLocalizedString("Developer", comment: "")
|
||||||
|
case .organization: return NSLocalizedString("Organization", comment: "")
|
||||||
|
case .unknown: fallthrough
|
||||||
|
@unknown default: return NSLocalizedString("Unknown", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Team
|
||||||
|
{
|
||||||
|
static let maximumFreeAppIDs = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(Team)
|
||||||
|
class Team: NSManagedObject, Fetchable
|
||||||
|
{
|
||||||
|
/* Properties */
|
||||||
|
@NSManaged var name: String
|
||||||
|
@NSManaged var identifier: String
|
||||||
|
@NSManaged var type: ALTTeamType
|
||||||
|
|
||||||
|
@NSManaged var isActiveTeam: Bool
|
||||||
|
|
||||||
|
/* Relationships */
|
||||||
|
@NSManaged private(set) var account: Account!
|
||||||
|
@NSManaged var installedApps: Set<InstalledApp>
|
||||||
|
@NSManaged private(set) var appIDs: Set<AppID>
|
||||||
|
|
||||||
|
var altTeam: ALTTeam?
|
||||||
|
|
||||||
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
|
{
|
||||||
|
super.init(entity: entity, insertInto: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ team: ALTTeam, account: Account, context: NSManagedObjectContext)
|
||||||
|
{
|
||||||
|
super.init(entity: Team.entity(), insertInto: context)
|
||||||
|
|
||||||
|
self.account = account
|
||||||
|
|
||||||
|
self.update(team: team)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(team: ALTTeam)
|
||||||
|
{
|
||||||
|
self.altTeam = team
|
||||||
|
|
||||||
|
self.name = team.name
|
||||||
|
self.identifier = team.identifier
|
||||||
|
self.type = team.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Team
|
||||||
|
{
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<Team>
|
||||||
|
{
|
||||||
|
return NSFetchRequest<Team>(entityName: "Team")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// InstalledAppsCollectionHeaderView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/9/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class InstalledAppsCollectionHeaderView: UICollectionReusableView
|
||||||
|
{
|
||||||
|
let textLabel: UILabel
|
||||||
|
let button: UIButton
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
self.textLabel = UILabel()
|
||||||
|
self.textLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
self.textLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold)
|
||||||
|
|
||||||
|
self.button = UIButton(type: .system)
|
||||||
|
self.button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
self.button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.textLabel)
|
||||||
|
self.addSubview(self.button)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([self.textLabel.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor),
|
||||||
|
self.textLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor)])
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([self.button.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor),
|
||||||
|
self.button.firstBaselineAnchor.constraint(equalTo: self.textLabel.firstBaselineAnchor)])
|
||||||
|
|
||||||
|
self.preservesSuperviewLayoutMargins = true
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionReusableView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="InstalledAppsHeader" id="eyV-eW-aLi" customClass="InstalledAppsCollectionHeaderView" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="50"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Installed" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zhW-Re-WNf">
|
||||||
|
<rect key="frame" x="20" y="21" width="96.5" height="29"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="24"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="iMf-wr-wRV">
|
||||||
|
<rect key="frame" x="274" y="23" width="81" height="32"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="16"/>
|
||||||
|
<state key="normal" title="Refresh All"/>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="zhW-Re-WNf" firstAttribute="leading" secondItem="eyV-eW-aLi" secondAttribute="leading" constant="20" id="Fo0-fL-UpD"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="zhW-Re-WNf" secondAttribute="bottom" id="OWw-FY-KOh"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="iMf-wr-wRV" secondAttribute="trailing" constant="20" id="dJM-7c-k31"/>
|
||||||
|
<constraint firstItem="iMf-wr-wRV" firstAttribute="firstBaseline" secondItem="zhW-Re-WNf" secondAttribute="firstBaseline" id="iU7-F2-XDu"/>
|
||||||
|
</constraints>
|
||||||
|
<viewLayoutGuide key="safeArea" id="N3q-SZ-Vyv"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="button" destination="iMf-wr-wRV" id="kWT-cc-BjS"/>
|
||||||
|
<outlet property="textLabel" destination="zhW-Re-WNf" id="UOg-4X-rWx"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="19.565217391304348" y="30.803571428571427"/>
|
||||||
|
</collectionReusableView>
|
||||||
|
</objects>
|
||||||
|
</document>
|
||||||
97
source-code/ALTs/AltStore/My Apps/MyAppsComponents.swift
Normal file
97
source-code/ALTs/AltStore/My Apps/MyAppsComponents.swift
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
//
|
||||||
|
// MyAppsComponents.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/17/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
class InstalledAppCollectionViewCell: UICollectionViewCell
|
||||||
|
{
|
||||||
|
private(set) var deactivateBadge: UIView?
|
||||||
|
|
||||||
|
@IBOutlet var bannerView: AppBannerView!
|
||||||
|
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
self.contentView.preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *)
|
||||||
|
{
|
||||||
|
let deactivateBadge = UIView()
|
||||||
|
deactivateBadge.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
deactivateBadge.isHidden = true
|
||||||
|
self.addSubview(deactivateBadge)
|
||||||
|
|
||||||
|
// Solid background to make the X opaque white.
|
||||||
|
let backgroundView = UIView()
|
||||||
|
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
backgroundView.backgroundColor = .white
|
||||||
|
deactivateBadge.addSubview(backgroundView)
|
||||||
|
|
||||||
|
let badgeView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
|
||||||
|
badgeView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(scale: .large)
|
||||||
|
badgeView.tintColor = .systemRed
|
||||||
|
deactivateBadge.addSubview(badgeView, pinningEdgesWith: .zero)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
deactivateBadge.centerXAnchor.constraint(equalTo: self.bannerView.iconImageView.trailingAnchor),
|
||||||
|
deactivateBadge.centerYAnchor.constraint(equalTo: self.bannerView.iconImageView.topAnchor),
|
||||||
|
|
||||||
|
backgroundView.centerXAnchor.constraint(equalTo: badgeView.centerXAnchor),
|
||||||
|
backgroundView.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor),
|
||||||
|
backgroundView.widthAnchor.constraint(equalTo: badgeView.widthAnchor, multiplier: 0.5),
|
||||||
|
backgroundView.heightAnchor.constraint(equalTo: badgeView.heightAnchor, multiplier: 0.5)
|
||||||
|
])
|
||||||
|
|
||||||
|
self.deactivateBadge = deactivateBadge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InstalledAppsCollectionFooterView: UICollectionReusableView
|
||||||
|
{
|
||||||
|
@IBOutlet var textLabel: UILabel!
|
||||||
|
@IBOutlet var button: UIButton!
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoUpdatesCollectionViewCell: UICollectionViewCell
|
||||||
|
{
|
||||||
|
@IBOutlet var blurView: UIVisualEffectView!
|
||||||
|
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.contentView.preservesSuperviewLayoutMargins = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdatesCollectionHeaderView: UICollectionReusableView
|
||||||
|
{
|
||||||
|
let button = PillButton(type: .system)
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
self.button.setTitle(">", for: .normal)
|
||||||
|
self.addSubview(self.button)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([self.button.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20),
|
||||||
|
self.button.topAnchor.constraint(equalTo: self.topAnchor),
|
||||||
|
self.button.widthAnchor.constraint(equalToConstant: 50),
|
||||||
|
self.button.heightAnchor.constraint(equalToConstant: 26)])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
1977
source-code/ALTs/AltStore/My Apps/MyAppsViewController.swift
Normal file
1977
source-code/ALTs/AltStore/My Apps/MyAppsViewController.swift
Normal file
File diff suppressed because it is too large
Load Diff
108
source-code/ALTs/AltStore/My Apps/UpdateCollectionViewCell.swift
Normal file
108
source-code/ALTs/AltStore/My Apps/UpdateCollectionViewCell.swift
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
//
|
||||||
|
// UpdateCollectionViewCell.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/16/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UpdateCollectionViewCell
|
||||||
|
{
|
||||||
|
enum Mode
|
||||||
|
{
|
||||||
|
case collapsed
|
||||||
|
case expanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc class UpdateCollectionViewCell: UICollectionViewCell
|
||||||
|
{
|
||||||
|
var mode: Mode = .expanded {
|
||||||
|
didSet {
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBOutlet var bannerView: AppBannerView!
|
||||||
|
@IBOutlet var versionDescriptionTitleLabel: UILabel!
|
||||||
|
@IBOutlet var versionDescriptionTextView: CollapsingTextView!
|
||||||
|
|
||||||
|
@IBOutlet private var blurView: UIVisualEffectView!
|
||||||
|
|
||||||
|
private var originalTintColor: UIColor?
|
||||||
|
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
// Prevent temporary unsatisfiable constraint errors due to UIView-Encapsulated-Layout constraints.
|
||||||
|
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
self.contentView.preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
|
self.bannerView.backgroundEffectView.isHidden = true
|
||||||
|
self.bannerView.button.setTitle(NSLocalizedString("UPDATE", comment: ""), for: .normal)
|
||||||
|
|
||||||
|
self.blurView.layer.cornerRadius = 20
|
||||||
|
self.blurView.layer.masksToBounds = true
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tintColorDidChange()
|
||||||
|
{
|
||||||
|
super.tintColorDidChange()
|
||||||
|
|
||||||
|
if self.tintAdjustmentMode != .dimmed
|
||||||
|
{
|
||||||
|
self.originalTintColor = self.tintColor
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes)
|
||||||
|
{
|
||||||
|
// Animates transition to new attributes.
|
||||||
|
let animator = UIViewPropertyAnimator(springTimingParameters: UISpringTimingParameters()) {
|
||||||
|
self.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
animator.startAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
|
||||||
|
{
|
||||||
|
let view = super.hitTest(point, with: event)
|
||||||
|
|
||||||
|
if view == self.versionDescriptionTextView
|
||||||
|
{
|
||||||
|
// Forward touches on the text view (but not on the nested "more" button)
|
||||||
|
// so cell selection works as expected.
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UpdateCollectionViewCell
|
||||||
|
{
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
switch self.mode
|
||||||
|
{
|
||||||
|
case .collapsed: self.versionDescriptionTextView.isCollapsed = true
|
||||||
|
case .expanded: self.versionDescriptionTextView.isCollapsed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
self.versionDescriptionTitleLabel.textColor = self.originalTintColor ?? self.tintColor
|
||||||
|
self.blurView.backgroundColor = self.originalTintColor ?? self.tintColor
|
||||||
|
self.bannerView.button.progressTintColor = self.originalTintColor ?? self.tintColor
|
||||||
|
|
||||||
|
self.setNeedsLayout()
|
||||||
|
self.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
121
source-code/ALTs/AltStore/My Apps/UpdateCollectionViewCell.xib
Normal file
121
source-code/ALTs/AltStore/My Apps/UpdateCollectionViewCell.xib
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dmf-hv-bwx">
|
||||||
|
<rect key="frame" x="16" y="0.0" width="343" height="125"/>
|
||||||
|
<subviews>
|
||||||
|
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1xN-9h-DFd">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="9iq-CR-Xc4">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="uYl-PH-DuP">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
|
||||||
|
<subviews>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="88" id="EPP-7O-1Ad"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="firstBaseline" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="RSR-5W-7tt" userLabel="Release Notes">
|
||||||
|
<rect key="frame" x="0.0" y="88" width="343" height="37"/>
|
||||||
|
<subviews>
|
||||||
|
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RKU-pY-wmQ">
|
||||||
|
<rect key="frame" x="15" y="0.0" width="65" height="22"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="4GQ-XP-i7X">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="65" height="22"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="251" text="What's New" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h1u-nj-qsP">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="65" height="22"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="65" id="C7Y-nh-TKJ"/>
|
||||||
|
</constraints>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="11"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="h1u-nj-qsP" firstAttribute="leading" secondItem="4GQ-XP-i7X" secondAttribute="leading" id="3cO-Mj-Yua"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="h1u-nj-qsP" secondAttribute="trailing" id="Hek-OE-YMc"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="h1u-nj-qsP" secondAttribute="bottom" id="bLg-Ut-aEb"/>
|
||||||
|
<constraint firstItem="h1u-nj-qsP" firstAttribute="top" secondItem="4GQ-XP-i7X" secondAttribute="top" id="beL-ob-CQ7"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<vibrancyEffect style="secondaryLabel">
|
||||||
|
<blurEffect style="systemChromeMaterial"/>
|
||||||
|
</vibrancyEffect>
|
||||||
|
</visualEffectView>
|
||||||
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="90" y="0.0" width="238" height="22"/>
|
||||||
|
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||||
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
|
</textView>
|
||||||
|
</subviews>
|
||||||
|
<edgeInsets key="layoutMargins" top="0.0" left="15" bottom="15" right="15"/>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" name="BlurTint"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="uYl-PH-DuP" secondAttribute="trailing" id="51O-j6-eoh"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="uYl-PH-DuP" secondAttribute="bottom" id="IGs-MS-vnM"/>
|
||||||
|
<constraint firstItem="uYl-PH-DuP" firstAttribute="top" secondItem="9iq-CR-Xc4" secondAttribute="top" id="hnr-wG-XRY"/>
|
||||||
|
<constraint firstItem="uYl-PH-DuP" firstAttribute="leading" secondItem="9iq-CR-Xc4" secondAttribute="leading" id="usR-Ia-LMy"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<blurEffect style="systemChromeMaterial"/>
|
||||||
|
</visualEffectView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="1xN-9h-DFd" firstAttribute="top" secondItem="dmf-hv-bwx" secondAttribute="top" id="6rb-Bw-UVn"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="1xN-9h-DFd" secondAttribute="bottom" id="dnI-NB-BKv"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="1xN-9h-DFd" secondAttribute="trailing" id="kbY-Z6-V86"/>
|
||||||
|
<constraint firstItem="1xN-9h-DFd" firstAttribute="leading" secondItem="dmf-hv-bwx" secondAttribute="leading" id="ofk-a7-m0Y"/>
|
||||||
|
</constraints>
|
||||||
|
<edgeInsets key="layoutMargins" top="20" left="20" bottom="20" right="20"/>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="dmf-hv-bwx" firstAttribute="top" secondItem="Kqf-Pv-ca3" secondAttribute="top" id="7yY-05-eHt"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="dmf-hv-bwx" secondAttribute="bottom" id="Rrx-0k-6He"/>
|
||||||
|
<constraint firstItem="dmf-hv-bwx" firstAttribute="leading" secondItem="Kqf-Pv-ca3" secondAttribute="leadingMargin" id="W0V-sT-tXo"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="dmf-hv-bwx" secondAttribute="trailing" id="tgy-Zi-iZF"/>
|
||||||
|
</constraints>
|
||||||
|
<connections>
|
||||||
|
<outlet property="bannerView" destination="Nop-pL-Icx" id="GiX-K1-5oz"/>
|
||||||
|
<outlet property="blurView" destination="1xN-9h-DFd" id="HBI-nT-xYh"/>
|
||||||
|
<outlet property="versionDescriptionTextView" destination="rNs-2O-k3V" id="4TC-A3-oxb"/>
|
||||||
|
<outlet property="versionDescriptionTitleLabel" destination="h1u-nj-qsP" id="dnz-Yv-BdY"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="618.39999999999998" y="96.251874062968525"/>
|
||||||
|
</collectionViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<namedColor name="BlurTint">
|
||||||
|
<color red="1" green="1" blue="1" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
30
source-code/ALTs/AltStore/News/NewsCollectionViewCell.swift
Normal file
30
source-code/ALTs/AltStore/News/NewsCollectionViewCell.swift
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// NewsCollectionViewCell.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 8/29/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class NewsCollectionViewCell: UICollectionViewCell
|
||||||
|
{
|
||||||
|
@IBOutlet var titleLabel: UILabel!
|
||||||
|
@IBOutlet var captionLabel: UILabel!
|
||||||
|
@IBOutlet var imageView: UIImageView!
|
||||||
|
@IBOutlet var contentBackgroundView: UIView!
|
||||||
|
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.contentView.preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
|
self.contentBackgroundView.layer.cornerRadius = 30
|
||||||
|
self.contentBackgroundView.clipsToBounds = true
|
||||||
|
|
||||||
|
self.imageView.layer.cornerRadius = 30
|
||||||
|
self.imageView.clipsToBounds = true
|
||||||
|
}
|
||||||
|
}
|
||||||
84
source-code/ALTs/AltStore/News/NewsCollectionViewCell.xib
Normal file
84
source-code/ALTs/AltStore/News/NewsCollectionViewCell.xib
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="wRF-2R-NUG" customClass="NewsCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="335" height="299"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="335" height="299"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="azr-Ea-luN">
|
||||||
|
<rect key="frame" x="16" y="0.0" width="303" height="299"/>
|
||||||
|
</view>
|
||||||
|
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Xba-Qs-SQo">
|
||||||
|
<rect key="frame" x="16" y="0.0" width="303" height="299"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="tNk-9u-1tk">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="303" height="298.5"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="akF-Tr-G5M">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="303" height="117.5"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" text="Delta" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="AkN-BE-I1a">
|
||||||
|
<rect key="frame" x="25" y="25" width="54.5" height="26.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="22"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" alpha="0.75" contentMode="left" horizontalHuggingPriority="251" verticalCompressionResistancePriority="999" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SHB-kk-YhL">
|
||||||
|
<rect key="frame" x="25" y="61.5" width="35.5" height="36"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<edgeInsets key="layoutMargins" top="25" left="25" bottom="20" right="25"/>
|
||||||
|
</stackView>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" placeholderIntrinsicWidth="335" placeholderIntrinsicHeight="200" translatesAutoresizingMaskIntoConstraints="NO" id="l36-Bm-De0">
|
||||||
|
<rect key="frame" x="0.0" y="117.5" width="303" height="181"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" secondItem="l36-Bm-De0" secondAttribute="height" multiplier="67:40" priority="999" id="QGD-YE-Hw2"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="tNk-9u-1tk" firstAttribute="top" secondItem="Xba-Qs-SQo" secondAttribute="top" id="Dw8-lF-Fzl"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="tNk-9u-1tk" secondAttribute="trailing" id="Zt8-Wa-oB9"/>
|
||||||
|
<constraint firstItem="tNk-9u-1tk" firstAttribute="leading" secondItem="Xba-Qs-SQo" secondAttribute="leading" id="m6p-Ee-dTh"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="tNk-9u-1tk" secondAttribute="bottom" constant="0.5" id="v9g-yC-db9"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="Xba-Qs-SQo" firstAttribute="top" secondItem="wRF-2R-NUG" secondAttribute="top" id="0xe-Rt-MhF"/>
|
||||||
|
<constraint firstItem="Xba-Qs-SQo" firstAttribute="leading" secondItem="wRF-2R-NUG" secondAttribute="leadingMargin" id="5MO-c0-5rG"/>
|
||||||
|
<constraint firstItem="azr-Ea-luN" firstAttribute="leading" secondItem="wRF-2R-NUG" secondAttribute="leadingMargin" id="8Ck-dI-nJy"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="Xba-Qs-SQo" secondAttribute="trailing" id="DNL-Jj-3By"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="Xba-Qs-SQo" secondAttribute="bottom" id="Ecj-fN-hZv"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="azr-Ea-luN" secondAttribute="bottom" priority="999" id="e56-UD-DRT"/>
|
||||||
|
<constraint firstItem="azr-Ea-luN" firstAttribute="top" secondItem="wRF-2R-NUG" secondAttribute="top" id="h2k-WE-Esg"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="azr-Ea-luN" secondAttribute="trailing" priority="999" id="hsS-zC-A58"/>
|
||||||
|
</constraints>
|
||||||
|
<connections>
|
||||||
|
<outlet property="captionLabel" destination="SHB-kk-YhL" id="zY3-qQ-9oY"/>
|
||||||
|
<outlet property="contentBackgroundView" destination="azr-Ea-luN" id="2Pl-11-YvR"/>
|
||||||
|
<outlet property="imageView" destination="l36-Bm-De0" id="3do-aQ-5r4"/>
|
||||||
|
<outlet property="titleLabel" destination="AkN-BE-I1a" id="hA2-3O-q5J"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="138" y="153"/>
|
||||||
|
</collectionViewCell>
|
||||||
|
</objects>
|
||||||
|
</document>
|
||||||
497
source-code/ALTs/AltStore/News/NewsViewController.swift
Normal file
497
source-code/ALTs/AltStore/News/NewsViewController.swift
Normal file
@@ -0,0 +1,497 @@
|
|||||||
|
//
|
||||||
|
// NewsViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 8/29/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SafariServices
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
import Nuke
|
||||||
|
|
||||||
|
private class AppBannerFooterView: UICollectionReusableView
|
||||||
|
{
|
||||||
|
let bannerView = AppBannerView(frame: .zero)
|
||||||
|
let tapGestureRecognizer = UITapGestureRecognizer(target: nil, action: nil)
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addGestureRecognizer(self.tapGestureRecognizer)
|
||||||
|
|
||||||
|
self.bannerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
self.addSubview(self.bannerView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
self.bannerView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||||
|
self.bannerView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||||
|
self.bannerView.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor),
|
||||||
|
self.bannerView.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NewsViewController: UICollectionViewController
|
||||||
|
{
|
||||||
|
private lazy var dataSource = self.makeDataSource()
|
||||||
|
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
|
||||||
|
|
||||||
|
private var prototypeCell: NewsCollectionViewCell!
|
||||||
|
|
||||||
|
private var loadingState: LoadingState = .loading {
|
||||||
|
didSet {
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache
|
||||||
|
private var cachedCellSizes = [String: CGSize]()
|
||||||
|
|
||||||
|
required init?(coder: NSCoder)
|
||||||
|
{
|
||||||
|
super.init(coder: coder)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(NewsViewController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.prototypeCell = NewsCollectionViewCell.instantiate(with: NewsCollectionViewCell.nib!)
|
||||||
|
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
self.collectionView.dataSource = self.dataSource
|
||||||
|
self.collectionView.prefetchDataSource = self.dataSource
|
||||||
|
|
||||||
|
self.collectionView.register(NewsCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||||
|
self.collectionView.register(AppBannerFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "AppBanner")
|
||||||
|
|
||||||
|
self.registerForPreviewing(with: self, sourceView: self.collectionView)
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
self.fetchSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillLayoutSubviews()
|
||||||
|
{
|
||||||
|
super.viewWillLayoutSubviews()
|
||||||
|
|
||||||
|
if self.collectionView.contentInset.bottom != 20
|
||||||
|
{
|
||||||
|
// Triggers collection view update in iOS 13, which crashes if we do it in viewDidLoad()
|
||||||
|
// since the database might not be loaded yet.
|
||||||
|
self.collectionView.contentInset.bottom = 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NewsViewController
|
||||||
|
{
|
||||||
|
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>
|
||||||
|
{
|
||||||
|
let fetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
||||||
|
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \NewsItem.date, ascending: false),
|
||||||
|
NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: true),
|
||||||
|
NSSortDescriptor(keyPath: \NewsItem.sourceIdentifier, ascending: true)]
|
||||||
|
|
||||||
|
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(NewsItem.objectID), cacheName: nil)
|
||||||
|
|
||||||
|
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>(fetchedResultsController: fetchedResultsController)
|
||||||
|
dataSource.proxy = self
|
||||||
|
dataSource.cellConfigurationHandler = { (cell, newsItem, indexPath) in
|
||||||
|
let cell = cell as! NewsCollectionViewCell
|
||||||
|
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||||
|
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
|
cell.titleLabel.text = newsItem.title
|
||||||
|
cell.captionLabel.text = newsItem.caption
|
||||||
|
cell.contentBackgroundView.backgroundColor = newsItem.tintColor
|
||||||
|
|
||||||
|
cell.imageView.image = nil
|
||||||
|
|
||||||
|
if newsItem.imageURL != nil
|
||||||
|
{
|
||||||
|
cell.imageView.isIndicatingActivity = true
|
||||||
|
cell.imageView.isHidden = false
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cell.imageView.isIndicatingActivity = false
|
||||||
|
cell.imageView.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.prefetchHandler = { (newsItem, indexPath, completionHandler) in
|
||||||
|
guard let imageURL = newsItem.imageURL else { return nil }
|
||||||
|
|
||||||
|
return RSTAsyncBlockOperation() { (operation) in
|
||||||
|
ImagePipeline.shared.loadImage(with: imageURL, progress: nil, completion: { (response, error) in
|
||||||
|
guard !operation.isCancelled else { return operation.finish() }
|
||||||
|
|
||||||
|
if let image = response?.image
|
||||||
|
{
|
||||||
|
completionHandler(image, nil)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completionHandler(nil, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
|
let cell = cell as! NewsCollectionViewCell
|
||||||
|
cell.imageView.isIndicatingActivity = false
|
||||||
|
cell.imageView.image = image
|
||||||
|
|
||||||
|
if let error = error
|
||||||
|
{
|
||||||
|
print("Error loading image:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSource.placeholderView = self.placeholderView
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchSource()
|
||||||
|
{
|
||||||
|
self.loadingState = .loading
|
||||||
|
|
||||||
|
AppManager.shared.fetchSources() { (result) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let sources = try result.get()
|
||||||
|
try sources.first?.managedObjectContext?.save()
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.loadingState = .finished(.success(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch let error as NSError
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if self.dataSource.itemCount > 0
|
||||||
|
{
|
||||||
|
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Fetch Sources", comment: ""))
|
||||||
|
|
||||||
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.show(in: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.loadingState = .finished(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
switch self.loadingState
|
||||||
|
{
|
||||||
|
case .loading:
|
||||||
|
self.placeholderView.textLabel.isHidden = true
|
||||||
|
self.placeholderView.detailTextLabel.isHidden = false
|
||||||
|
|
||||||
|
self.placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "")
|
||||||
|
|
||||||
|
self.placeholderView.activityIndicatorView.startAnimating()
|
||||||
|
|
||||||
|
case .finished(.failure(let error)):
|
||||||
|
self.placeholderView.textLabel.isHidden = false
|
||||||
|
self.placeholderView.detailTextLabel.isHidden = false
|
||||||
|
|
||||||
|
self.placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch News", comment: "")
|
||||||
|
self.placeholderView.detailTextLabel.text = error.localizedDescription
|
||||||
|
|
||||||
|
self.placeholderView.activityIndicatorView.stopAnimating()
|
||||||
|
|
||||||
|
case .finished(.success):
|
||||||
|
self.placeholderView.textLabel.isHidden = true
|
||||||
|
self.placeholderView.detailTextLabel.isHidden = true
|
||||||
|
|
||||||
|
self.placeholderView.activityIndicatorView.stopAnimating()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NewsViewController
|
||||||
|
{
|
||||||
|
@objc func handleTapGesture(_ gestureRecognizer: UITapGestureRecognizer)
|
||||||
|
{
|
||||||
|
guard let footerView = gestureRecognizer.view as? UICollectionReusableView else { return }
|
||||||
|
|
||||||
|
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
|
||||||
|
|
||||||
|
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
|
||||||
|
let supplementaryView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath)
|
||||||
|
return supplementaryView == footerView
|
||||||
|
}) else { return }
|
||||||
|
|
||||||
|
let item = self.dataSource.item(at: indexPath)
|
||||||
|
guard let storeApp = item.storeApp else { return }
|
||||||
|
|
||||||
|
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||||
|
self.navigationController?.pushViewController(appViewController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func performAppAction(_ sender: PillButton)
|
||||||
|
{
|
||||||
|
let point = self.collectionView.convert(sender.center, from: sender.superview)
|
||||||
|
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
|
||||||
|
|
||||||
|
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
|
||||||
|
let supplementaryView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath)
|
||||||
|
return supplementaryView?.frame.contains(point) ?? false
|
||||||
|
}) else { return }
|
||||||
|
|
||||||
|
let app = self.dataSource.item(at: indexPath)
|
||||||
|
guard let storeApp = app.storeApp else { return }
|
||||||
|
|
||||||
|
if let installedApp = app.storeApp?.installedApp
|
||||||
|
{
|
||||||
|
self.open(installedApp)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.install(storeApp, at: indexPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func install(_ storeApp: StoreApp, at indexPath: IndexPath)
|
||||||
|
{
|
||||||
|
let previousProgress = AppManager.shared.installationProgress(for: storeApp)
|
||||||
|
guard previousProgress == nil else {
|
||||||
|
previousProgress?.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = AppManager.shared.install(storeApp, presentingViewController: self) { (result) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(OperationError.cancelled): break // Ignore
|
||||||
|
case .failure(let error):
|
||||||
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.show(in: self)
|
||||||
|
|
||||||
|
case .success: print("Installed app:", storeApp.bundleIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.collectionView.reloadSections(IndexSet(integer: indexPath.section))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.collectionView.reloadSections(IndexSet(integer: indexPath.section))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func open(_ installedApp: InstalledApp)
|
||||||
|
{
|
||||||
|
UIApplication.shared.open(installedApp.openAppURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NewsViewController
|
||||||
|
{
|
||||||
|
@objc func importApp(_ notification: Notification)
|
||||||
|
{
|
||||||
|
self.presentedViewController?.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NewsViewController
|
||||||
|
{
|
||||||
|
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||||
|
{
|
||||||
|
let newsItem = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
if let externalURL = newsItem.externalURL
|
||||||
|
{
|
||||||
|
let safariViewController = SFSafariViewController(url: externalURL)
|
||||||
|
safariViewController.preferredControlTintColor = newsItem.tintColor
|
||||||
|
self.present(safariViewController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
else if let storeApp = newsItem.storeApp
|
||||||
|
{
|
||||||
|
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||||
|
self.navigationController?.pushViewController(appViewController, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||||
|
{
|
||||||
|
let item = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "AppBanner", for: indexPath) as! AppBannerFooterView
|
||||||
|
guard let storeApp = item.storeApp else { return footerView }
|
||||||
|
|
||||||
|
footerView.layoutMargins.left = self.view.layoutMargins.left
|
||||||
|
footerView.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
|
footerView.bannerView.titleLabel.text = storeApp.name
|
||||||
|
footerView.bannerView.subtitleLabel.text = storeApp.developerName
|
||||||
|
footerView.bannerView.tintColor = storeApp.tintColor
|
||||||
|
footerView.bannerView.betaBadgeView.isHidden = !storeApp.isBeta
|
||||||
|
footerView.bannerView.button.addTarget(self, action: #selector(NewsViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||||
|
footerView.tapGestureRecognizer.addTarget(self, action: #selector(NewsViewController.handleTapGesture(_:)))
|
||||||
|
|
||||||
|
footerView.bannerView.button.isIndicatingActivity = false
|
||||||
|
|
||||||
|
if storeApp.installedApp == nil
|
||||||
|
{
|
||||||
|
footerView.bannerView.button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
|
||||||
|
|
||||||
|
let progress = AppManager.shared.installationProgress(for: storeApp)
|
||||||
|
footerView.bannerView.button.progress = progress
|
||||||
|
|
||||||
|
if Date() < storeApp.versionDate
|
||||||
|
{
|
||||||
|
footerView.bannerView.button.countdownDate = storeApp.versionDate
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
footerView.bannerView.button.countdownDate = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
footerView.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||||
|
footerView.bannerView.button.progress = nil
|
||||||
|
footerView.bannerView.button.countdownDate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
Nuke.loadImage(with: storeApp.iconURL, into: footerView.bannerView.iconImageView)
|
||||||
|
|
||||||
|
return footerView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NewsViewController: UICollectionViewDelegateFlowLayout
|
||||||
|
{
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||||
|
{
|
||||||
|
let item = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
if let previousSize = self.cachedCellSizes[item.identifier]
|
||||||
|
{
|
||||||
|
return previousSize
|
||||||
|
}
|
||||||
|
|
||||||
|
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
|
||||||
|
NSLayoutConstraint.activate([widthConstraint])
|
||||||
|
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
|
||||||
|
|
||||||
|
self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
|
||||||
|
|
||||||
|
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||||
|
self.cachedCellSizes[item.identifier] = size
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
|
||||||
|
{
|
||||||
|
let item = self.dataSource.item(at: IndexPath(row: 0, section: section))
|
||||||
|
|
||||||
|
if item.storeApp != nil
|
||||||
|
{
|
||||||
|
return CGSize(width: 88, height: 88)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
|
||||||
|
{
|
||||||
|
var insets = UIEdgeInsets(top: 30, left: 0, bottom: 13, right: 0)
|
||||||
|
|
||||||
|
if section == 0
|
||||||
|
{
|
||||||
|
insets.top = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NewsViewController: UIViewControllerPreviewingDelegate
|
||||||
|
{
|
||||||
|
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
|
||||||
|
{
|
||||||
|
if let indexPath = self.collectionView.indexPathForItem(at: location), let cell = self.collectionView.cellForItem(at: indexPath)
|
||||||
|
{
|
||||||
|
// Previewing news item.
|
||||||
|
|
||||||
|
previewingContext.sourceRect = cell.frame
|
||||||
|
|
||||||
|
let newsItem = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
if let externalURL = newsItem.externalURL
|
||||||
|
{
|
||||||
|
let safariViewController = SFSafariViewController(url: externalURL)
|
||||||
|
safariViewController.preferredControlTintColor = newsItem.tintColor
|
||||||
|
return safariViewController
|
||||||
|
}
|
||||||
|
else if let storeApp = newsItem.storeApp
|
||||||
|
{
|
||||||
|
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||||
|
return appViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Previewing app banner (or nothing).
|
||||||
|
|
||||||
|
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
|
||||||
|
|
||||||
|
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
|
||||||
|
let layoutAttributes = self.collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionFooter, at: indexPath)
|
||||||
|
return layoutAttributes?.frame.contains(location) ?? false
|
||||||
|
}) else { return nil }
|
||||||
|
|
||||||
|
guard let layoutAttributes = self.collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionFooter, at: indexPath) else { return nil }
|
||||||
|
previewingContext.sourceRect = layoutAttributes.frame
|
||||||
|
|
||||||
|
let item = self.dataSource.item(at: indexPath)
|
||||||
|
guard let storeApp = item.storeApp else { return nil }
|
||||||
|
|
||||||
|
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||||
|
return appViewController
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
|
||||||
|
{
|
||||||
|
if let safariViewController = viewControllerToCommit as? SFSafariViewController
|
||||||
|
{
|
||||||
|
self.present(safariViewController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.navigationController?.pushViewController(viewControllerToCommit, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,683 @@
|
|||||||
|
//
|
||||||
|
// AuthenticationOperation.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 6/5/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Roxas
|
||||||
|
import Network
|
||||||
|
|
||||||
|
import AltKit
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
enum AuthenticationError: LocalizedError
|
||||||
|
{
|
||||||
|
case noTeam
|
||||||
|
case noCertificate
|
||||||
|
|
||||||
|
case missingPrivateKey
|
||||||
|
case missingCertificate
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
|
||||||
|
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
|
||||||
|
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
|
||||||
|
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(AuthenticationOperation)
|
||||||
|
class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
|
||||||
|
{
|
||||||
|
let context: AuthenticatedOperationContext
|
||||||
|
|
||||||
|
private weak var presentingViewController: UIViewController?
|
||||||
|
|
||||||
|
private lazy var navigationController: UINavigationController = {
|
||||||
|
let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
|
||||||
|
if #available(iOS 13.0, *)
|
||||||
|
{
|
||||||
|
navigationController.isModalInPresentation = true
|
||||||
|
}
|
||||||
|
return navigationController
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
|
||||||
|
|
||||||
|
private var appleIDPassword: String?
|
||||||
|
private var shouldShowInstructions = false
|
||||||
|
|
||||||
|
private let operationQueue = OperationQueue()
|
||||||
|
|
||||||
|
private var submitCodeAction: UIAlertAction?
|
||||||
|
|
||||||
|
init(context: AuthenticatedOperationContext, presentingViewController: UIViewController?)
|
||||||
|
{
|
||||||
|
self.context = context
|
||||||
|
self.presentingViewController = presentingViewController
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.context.authenticationOperation = self
|
||||||
|
self.operationQueue.name = "com.altstore.AuthenticationOperation"
|
||||||
|
self.progress.totalUnitCount = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
override func main()
|
||||||
|
{
|
||||||
|
super.main()
|
||||||
|
|
||||||
|
if let error = self.context.error
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign In
|
||||||
|
self.signIn() { (result) in
|
||||||
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): self.finish(.failure(error))
|
||||||
|
case .success((let account, let session)):
|
||||||
|
self.context.session = session
|
||||||
|
self.progress.completedUnitCount += 1
|
||||||
|
|
||||||
|
// Fetch Team
|
||||||
|
self.fetchTeam(for: account, session: session) { (result) in
|
||||||
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): self.finish(.failure(error))
|
||||||
|
case .success(let team):
|
||||||
|
self.context.team = team
|
||||||
|
self.progress.completedUnitCount += 1
|
||||||
|
|
||||||
|
// Fetch Certificate
|
||||||
|
self.fetchCertificate(for: team, session: session) { (result) in
|
||||||
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): self.finish(.failure(error))
|
||||||
|
case .success(let certificate):
|
||||||
|
self.context.certificate = certificate
|
||||||
|
self.progress.completedUnitCount += 1
|
||||||
|
|
||||||
|
// Register Device
|
||||||
|
self.registerCurrentDevice(for: team, session: session) { (result) in
|
||||||
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): self.finish(.failure(error))
|
||||||
|
case .success:
|
||||||
|
self.progress.completedUnitCount += 1
|
||||||
|
|
||||||
|
// Save account/team to disk.
|
||||||
|
self.save(team) { (result) in
|
||||||
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): self.finish(.failure(error))
|
||||||
|
case .success:
|
||||||
|
// Must cache App IDs _after_ saving account/team to disk.
|
||||||
|
self.cacheAppIDs(team: team, session: session) { (result) in
|
||||||
|
let result = result.map { _ in (team, certificate, session) }
|
||||||
|
self.finish(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(_ altTeam: ALTTeam, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||||
|
context.performAndWait {
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let account: Account
|
||||||
|
let team: Team
|
||||||
|
|
||||||
|
if let tempAccount = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context)
|
||||||
|
{
|
||||||
|
account = tempAccount
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
account = Account(altTeam.account, context: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let tempTeam = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
|
||||||
|
{
|
||||||
|
team = tempTeam
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
team = Team(altTeam, account: account, context: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
account.update(account: altTeam.account)
|
||||||
|
team.update(team: altTeam)
|
||||||
|
|
||||||
|
try context.save()
|
||||||
|
|
||||||
|
completionHandler(.success(()))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func finish(_ result: Result<(ALTTeam, ALTCertificate, ALTAppleAPISession), Error>)
|
||||||
|
{
|
||||||
|
guard !self.isFinished else { return }
|
||||||
|
|
||||||
|
print("Finished authenticating with result:", result.error?.localizedDescription ?? "success")
|
||||||
|
|
||||||
|
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||||
|
context.perform {
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let (altTeam, altCertificate, session) = try result.get()
|
||||||
|
|
||||||
|
guard
|
||||||
|
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
|
||||||
|
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
|
||||||
|
else { throw AuthenticationError.noTeam }
|
||||||
|
|
||||||
|
// Account
|
||||||
|
account.isActiveAccount = true
|
||||||
|
|
||||||
|
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
|
||||||
|
otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
|
||||||
|
|
||||||
|
let otherAccounts = try context.fetch(otherAccountsFetchRequest)
|
||||||
|
for account in otherAccounts
|
||||||
|
{
|
||||||
|
account.isActiveAccount = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Team
|
||||||
|
team.isActiveTeam = true
|
||||||
|
|
||||||
|
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
|
||||||
|
otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
|
||||||
|
|
||||||
|
let otherTeams = try context.fetch(otherTeamsFetchRequest)
|
||||||
|
for team in otherTeams
|
||||||
|
{
|
||||||
|
team.isActiveTeam = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
|
||||||
|
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
|
||||||
|
{
|
||||||
|
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UserDefaults.standard.activeAppsLimit = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
try context.save()
|
||||||
|
|
||||||
|
// Update keychain
|
||||||
|
Keychain.shared.appleIDEmailAddress = altTeam.account.appleID
|
||||||
|
Keychain.shared.appleIDPassword = self.appleIDPassword
|
||||||
|
|
||||||
|
Keychain.shared.signingCertificate = altCertificate.p12Data()
|
||||||
|
Keychain.shared.signingCertificatePassword = altCertificate.machineIdentifier
|
||||||
|
|
||||||
|
self.showInstructionsIfNecessary() { (didShowInstructions) in
|
||||||
|
|
||||||
|
let signer = ALTSigner(team: altTeam, certificate: altCertificate)
|
||||||
|
// Refresh screen must go last since a successful refresh will cause the app to quit.
|
||||||
|
self.showRefreshScreenIfNecessary(signer: signer, session: session) { (didShowRefreshAlert) in
|
||||||
|
super.finish(result)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.navigationController.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
super.finish(result)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.navigationController.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AuthenticationOperation
|
||||||
|
{
|
||||||
|
func present(_ viewController: UIViewController) -> Bool
|
||||||
|
{
|
||||||
|
guard let presentingViewController = self.presentingViewController else { return false }
|
||||||
|
|
||||||
|
self.navigationController.view.tintColor = .white
|
||||||
|
|
||||||
|
if self.navigationController.viewControllers.isEmpty
|
||||||
|
{
|
||||||
|
guard presentingViewController.presentedViewController == nil else { return false }
|
||||||
|
|
||||||
|
self.navigationController.setViewControllers([viewController], animated: false)
|
||||||
|
presentingViewController.present(self.navigationController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
viewController.navigationItem.leftBarButtonItem = nil
|
||||||
|
self.navigationController.pushViewController(viewController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AuthenticationOperation
|
||||||
|
{
|
||||||
|
func signIn(completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
|
||||||
|
{
|
||||||
|
func authenticate()
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
|
||||||
|
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
|
||||||
|
self.authenticate(appleID: appleID, password: password) { (result) in
|
||||||
|
completionHandler(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authenticationViewController.completionHandler = { (result) in
|
||||||
|
if let (account, session, password) = result
|
||||||
|
{
|
||||||
|
// We presented the Auth UI and the user signed in.
|
||||||
|
// In this case, we'll assume we should show the instructions again.
|
||||||
|
self.shouldShowInstructions = true
|
||||||
|
|
||||||
|
self.appleIDPassword = password
|
||||||
|
completionHandler(.success((account, session)))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completionHandler(.failure(OperationError.cancelled))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.present(authenticationViewController)
|
||||||
|
{
|
||||||
|
completionHandler(.failure(OperationError.notAuthenticated))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
|
||||||
|
{
|
||||||
|
self.authenticate(appleID: appleID, password: password) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .success((let account, let session)):
|
||||||
|
self.appleIDPassword = password
|
||||||
|
completionHandler(.success((account, session)))
|
||||||
|
|
||||||
|
case .failure(ALTAppleAPIError.incorrectCredentials), .failure(ALTAppleAPIError.appSpecificPasswordRequired):
|
||||||
|
authenticate()
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
authenticate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
|
||||||
|
{
|
||||||
|
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
|
||||||
|
fetchAnisetteDataOperation.resultHandler = { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): completionHandler(.failure(error))
|
||||||
|
case .success(let anisetteData):
|
||||||
|
let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
|
||||||
|
|
||||||
|
if let presentingViewController = self.presentingViewController
|
||||||
|
{
|
||||||
|
verificationHandler = { (completionHandler) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
|
||||||
|
alertController.addTextField { (textField) in
|
||||||
|
textField.autocorrectionType = .no
|
||||||
|
textField.autocapitalizationType = .none
|
||||||
|
textField.keyboardType = .numberPad
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
|
||||||
|
}
|
||||||
|
|
||||||
|
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
|
||||||
|
let textField = alertController.textFields?.first
|
||||||
|
|
||||||
|
let code = textField?.text ?? ""
|
||||||
|
completionHandler(code)
|
||||||
|
}
|
||||||
|
submitAction.isEnabled = false
|
||||||
|
alertController.addAction(submitAction)
|
||||||
|
self.submitCodeAction = submitAction
|
||||||
|
|
||||||
|
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
|
||||||
|
completionHandler(nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.navigationController.presentingViewController != nil
|
||||||
|
{
|
||||||
|
self.navigationController.present(alertController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
presentingViewController.present(alertController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No view controller to present security code alert, so don't provide verificationHandler.
|
||||||
|
verificationHandler = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
|
||||||
|
verificationHandler: verificationHandler) { (account, session, error) in
|
||||||
|
if let account = account, let session = session
|
||||||
|
{
|
||||||
|
completionHandler(.success((account, session)))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error ?? OperationError.unknown))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.operationQueue.addOperation(fetchAnisetteDataOperation)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
|
||||||
|
{
|
||||||
|
func selectTeam(from teams: [ALTTeam])
|
||||||
|
{
|
||||||
|
if let team = teams.first(where: { $0.type == .free })
|
||||||
|
{
|
||||||
|
return completionHandler(.success(team))
|
||||||
|
}
|
||||||
|
else if let team = teams.first(where: { $0.type == .individual })
|
||||||
|
{
|
||||||
|
return completionHandler(.success(team))
|
||||||
|
}
|
||||||
|
else if let team = teams.first
|
||||||
|
{
|
||||||
|
return completionHandler(.success(team))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return completionHandler(.failure(AuthenticationError.noTeam))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
|
||||||
|
switch Result(teams, error)
|
||||||
|
{
|
||||||
|
case .failure(let error): completionHandler(.failure(error))
|
||||||
|
case .success(let teams):
|
||||||
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
|
if let activeTeam = DatabaseManager.shared.activeTeam(in: context), let altTeam = teams.first(where: { $0.identifier == activeTeam.identifier })
|
||||||
|
{
|
||||||
|
completionHandler(.success(altTeam))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
selectTeam(from: teams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTCertificate, Swift.Error>) -> Void)
|
||||||
|
{
|
||||||
|
func requestCertificate()
|
||||||
|
{
|
||||||
|
let machineName = "AltStore - " + UIDevice.current.name
|
||||||
|
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let certificate = try Result(certificate, error).get()
|
||||||
|
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
|
||||||
|
|
||||||
|
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let certificates = try Result(certificates, error).get()
|
||||||
|
|
||||||
|
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
|
||||||
|
throw AuthenticationError.missingCertificate
|
||||||
|
}
|
||||||
|
|
||||||
|
certificate.privateKey = privateKey
|
||||||
|
completionHandler(.success(certificate))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceCertificate(from certificates: [ALTCertificate])
|
||||||
|
{
|
||||||
|
guard let certificate = certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
|
||||||
|
|
||||||
|
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
|
||||||
|
if let error = error, !success
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
requestCertificate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let certificates = try Result(certificates, error).get()
|
||||||
|
|
||||||
|
if
|
||||||
|
let data = Keychain.shared.signingCertificate,
|
||||||
|
let localCertificate = ALTCertificate(p12Data: data, password: nil),
|
||||||
|
let certificate = certificates.first(where: { $0.serialNumber == localCertificate.serialNumber })
|
||||||
|
{
|
||||||
|
// We have a certificate stored in the keychain and it hasn't been revoked.
|
||||||
|
localCertificate.machineIdentifier = certificate.machineIdentifier
|
||||||
|
completionHandler(.success(localCertificate))
|
||||||
|
}
|
||||||
|
else if
|
||||||
|
let serialNumber = Keychain.shared.signingCertificateSerialNumber,
|
||||||
|
let privateKey = Keychain.shared.signingCertificatePrivateKey,
|
||||||
|
let certificate = certificates.first(where: { $0.serialNumber == serialNumber })
|
||||||
|
{
|
||||||
|
// LEGACY
|
||||||
|
// We have the private key for one of the certificates, so add it to certificate and use it.
|
||||||
|
certificate.privateKey = privateKey
|
||||||
|
completionHandler(.success(certificate))
|
||||||
|
}
|
||||||
|
else if
|
||||||
|
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String,
|
||||||
|
let certificate = certificates.first(where: { $0.serialNumber == serialNumber }),
|
||||||
|
let machineIdentifier = certificate.machineIdentifier,
|
||||||
|
FileManager.default.fileExists(atPath: Bundle.main.certificateURL.path),
|
||||||
|
let data = try? Data(contentsOf: Bundle.main.certificateURL),
|
||||||
|
let localCertificate = ALTCertificate(p12Data: data, password: machineIdentifier)
|
||||||
|
{
|
||||||
|
// We have an embedded certificate that hasn't been revoked.
|
||||||
|
localCertificate.machineIdentifier = machineIdentifier
|
||||||
|
completionHandler(.success(localCertificate))
|
||||||
|
}
|
||||||
|
else if certificates.isEmpty
|
||||||
|
{
|
||||||
|
// No certificates, so request a new one.
|
||||||
|
requestCertificate()
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We don't have private keys for any of the certificates,
|
||||||
|
// so we need to revoke one and create a new one.
|
||||||
|
replaceCertificate(from: certificates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||||
|
{
|
||||||
|
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
|
||||||
|
return completionHandler(.failure(OperationError.unknownUDID))
|
||||||
|
}
|
||||||
|
|
||||||
|
ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (devices, error) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let devices = try Result(devices, error).get()
|
||||||
|
|
||||||
|
if let device = devices.first(where: { $0.identifier == udid })
|
||||||
|
{
|
||||||
|
completionHandler(.success(device))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team, session: session) { (device, error) in
|
||||||
|
completionHandler(Result(device, error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheAppIDs(team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let fetchAppIDsOperation = FetchAppIDsOperation(context: self.context)
|
||||||
|
fetchAppIDsOperation.resultHandler = { (result) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let (_, context) = try result.get()
|
||||||
|
try context.save()
|
||||||
|
|
||||||
|
completionHandler(.success(()))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.operationQueue.addOperation(fetchAppIDsOperation)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
|
||||||
|
{
|
||||||
|
guard self.shouldShowInstructions else { return completionHandler(false) }
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
|
||||||
|
instructionsViewController.showsBottomButton = true
|
||||||
|
instructionsViewController.completionHandler = {
|
||||||
|
completionHandler(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.present(instructionsViewController)
|
||||||
|
{
|
||||||
|
completionHandler(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void)
|
||||||
|
{
|
||||||
|
guard let application = ALTApplication(fileURL: Bundle.main.bundleURL), let provisioningProfile = application.provisioningProfile else { return completionHandler(false) }
|
||||||
|
|
||||||
|
// If we're not using the same certificate used to install AltStore, warn user that they need to refresh.
|
||||||
|
guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) }
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
completionHandler(false)
|
||||||
|
#else
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let context = AuthenticatedOperationContext(context: self.context)
|
||||||
|
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
|
||||||
|
|
||||||
|
let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController
|
||||||
|
refreshViewController.context = context
|
||||||
|
refreshViewController.completionHandler = { _ in
|
||||||
|
completionHandler(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.present(refreshViewController)
|
||||||
|
{
|
||||||
|
completionHandler(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AuthenticationOperation
|
||||||
|
{
|
||||||
|
@objc func textFieldTextDidChange(_ notification: Notification)
|
||||||
|
{
|
||||||
|
guard let textField = notification.object as? UITextField else { return }
|
||||||
|
|
||||||
|
self.submitCodeAction?.isEnabled = (textField.text ?? "").count == 6
|
||||||
|
}
|
||||||
|
}
|
||||||
183
source-code/ALTs/AltStore/Operations/BackupAppOperation.swift
Normal file
183
source-code/ALTs/AltStore/Operations/BackupAppOperation.swift
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
//
|
||||||
|
// BackupAppOperation.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 5/12/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import AltKit
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
extension BackupAppOperation
|
||||||
|
{
|
||||||
|
enum Action: String
|
||||||
|
{
|
||||||
|
case backup
|
||||||
|
case restore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(BackupAppOperation)
|
||||||
|
class BackupAppOperation: ResultOperation<Void>
|
||||||
|
{
|
||||||
|
let action: Action
|
||||||
|
let context: InstallAppOperationContext
|
||||||
|
|
||||||
|
private var appName: String?
|
||||||
|
private var timeoutTimer: Timer?
|
||||||
|
|
||||||
|
init(action: Action, context: InstallAppOperationContext)
|
||||||
|
{
|
||||||
|
self.action = action
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func main()
|
||||||
|
{
|
||||||
|
super.main()
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if let error = self.context.error
|
||||||
|
{
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else { throw OperationError.invalidParameters }
|
||||||
|
context.perform {
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let appName = installedApp.name
|
||||||
|
self.appName = appName
|
||||||
|
|
||||||
|
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { throw OperationError.appNotFound }
|
||||||
|
let altstoreOpenURL = altstoreApp.openAppURL
|
||||||
|
|
||||||
|
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
|
||||||
|
returnURLComponents?.host = "appBackupResponse"
|
||||||
|
guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) }
|
||||||
|
|
||||||
|
var openURLComponents = URLComponents()
|
||||||
|
openURLComponents.scheme = installedApp.openAppURL.scheme
|
||||||
|
openURLComponents.host = self.action.rawValue
|
||||||
|
openURLComponents.queryItems = [URLQueryItem(name: "returnURL", value: returnURL.absoluteString)]
|
||||||
|
|
||||||
|
guard let openURL = openURLComponents.url else { throw OperationError.openAppFailed(name: appName) }
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
let currentTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
|
||||||
|
UIApplication.shared.open(openURL, options: [:]) { (success) in
|
||||||
|
let elapsedTime = CFAbsoluteTimeGetCurrent() - currentTime
|
||||||
|
|
||||||
|
if success
|
||||||
|
{
|
||||||
|
self.registerObservers()
|
||||||
|
}
|
||||||
|
else if elapsedTime < 0.5
|
||||||
|
{
|
||||||
|
// Failed too quickly for human to respond to alert, possibly still finalizing installation.
|
||||||
|
// Try again in a couple seconds.
|
||||||
|
|
||||||
|
print("Failed too quickly, retrying after a few seconds...")
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||||
|
UIApplication.shared.open(openURL, options: [:]) { (success) in
|
||||||
|
if success
|
||||||
|
{
|
||||||
|
self.registerObservers()
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.finish(.failure(OperationError.openAppFailed(name: appName)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.finish(.failure(OperationError.openAppFailed(name: appName)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func finish(_ result: Result<Void, Error>)
|
||||||
|
{
|
||||||
|
let result = result.mapError { (error) -> Error in
|
||||||
|
let appName = self.appName ?? self.context.bundleIdentifier
|
||||||
|
|
||||||
|
switch (error, self.action)
|
||||||
|
{
|
||||||
|
case (let error as NSError, _) where (self.context.error as NSError?) == error: fallthrough
|
||||||
|
case (OperationError.cancelled, _):
|
||||||
|
return error
|
||||||
|
|
||||||
|
case (let error as NSError, .backup):
|
||||||
|
let localizedFailure = String(format: NSLocalizedString("Could not back up “%@”.", comment: ""), appName)
|
||||||
|
return error.withLocalizedFailure(localizedFailure)
|
||||||
|
|
||||||
|
case (let error as NSError, .restore):
|
||||||
|
let localizedFailure = String(format: NSLocalizedString("Could not restore “%@”.", comment: ""), appName)
|
||||||
|
return error.withLocalizedFailure(localizedFailure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .success: self.progress.completedUnitCount += 1
|
||||||
|
case .failure: break
|
||||||
|
}
|
||||||
|
|
||||||
|
super.finish(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension BackupAppOperation
|
||||||
|
{
|
||||||
|
func registerObservers()
|
||||||
|
{
|
||||||
|
var applicationWillReturnObserver: NSObjectProtocol!
|
||||||
|
applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in
|
||||||
|
guard let self = self, !self.isFinished else { return }
|
||||||
|
|
||||||
|
self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] (timer) in
|
||||||
|
// Final delay to ensure we don't prematurely return failure
|
||||||
|
// in case timer expired while we were in background, but
|
||||||
|
// are now returning to app with success response.
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||||
|
guard let self = self, !self.isFinished else { return }
|
||||||
|
self.finish(.failure(OperationError.timedOut))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.removeObserver(applicationWillReturnObserver!)
|
||||||
|
}
|
||||||
|
|
||||||
|
var backupResponseObserver: NSObjectProtocol!
|
||||||
|
backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in
|
||||||
|
self?.timeoutTimer?.invalidate()
|
||||||
|
|
||||||
|
let result = notification.userInfo?[AppDelegate.appBackupResultKey] as? Result<Void, Error> ?? .failure(OperationError.unknownResult)
|
||||||
|
self?.finish(result)
|
||||||
|
|
||||||
|
NotificationCenter.default.removeObserver(backupResponseObserver!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
//
|
||||||
|
// DeactivateAppOperation.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/4/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
import AltKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
@objc(DeactivateAppOperation)
|
||||||
|
class DeactivateAppOperation: ResultOperation<InstalledApp>
|
||||||
|
{
|
||||||
|
let app: InstalledApp
|
||||||
|
let context: OperationContext
|
||||||
|
|
||||||
|
init(app: InstalledApp, context: OperationContext)
|
||||||
|
{
|
||||||
|
self.app = app
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func main()
|
||||||
|
{
|
||||||
|
super.main()
|
||||||
|
|
||||||
|
if let error = self.context.error
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
|
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return self.finish(.failure(OperationError.unknownUDID)) }
|
||||||
|
|
||||||
|
ServerManager.shared.connect(to: server) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): self.finish(.failure(error))
|
||||||
|
case .success(let connection):
|
||||||
|
print("Sending deactivate app request...")
|
||||||
|
|
||||||
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
|
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
|
||||||
|
|
||||||
|
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
|
||||||
|
let allIdentifiers = [installedApp.resignedBundleIdentifier] + appExtensionProfiles
|
||||||
|
|
||||||
|
let request = RemoveProvisioningProfilesRequest(udid: udid, bundleIdentifiers: Set(allIdentifiers))
|
||||||
|
connection.send(request) { (result) in
|
||||||
|
print("Sent deactive app request!")
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): self.finish(.failure(error))
|
||||||
|
case .success:
|
||||||
|
print("Waiting for deactivate app response...")
|
||||||
|
connection.receiveResponse() { (result) in
|
||||||
|
print("Receiving deactivate app response:", result)
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): self.finish(.failure(error))
|
||||||
|
case .success(.error(let response)): self.finish(.failure(response.error))
|
||||||
|
case .success(.removeProvisioningProfiles):
|
||||||
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
|
self.progress.completedUnitCount += 1
|
||||||
|
|
||||||
|
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
|
||||||
|
installedApp.isActive = false
|
||||||
|
self.finish(.success(installedApp))
|
||||||
|
}
|
||||||
|
|
||||||
|
case .success: self.finish(.failure(ALTServerError(.unknownResponse)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
source-code/ALTs/AltStore/Operations/DownloadAppOperation.swift
Normal file
132
source-code/ALTs/AltStore/Operations/DownloadAppOperation.swift
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
//
|
||||||
|
// DownloadAppOperation.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 6/10/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
|
||||||
|
@objc(DownloadAppOperation)
|
||||||
|
class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||||
|
{
|
||||||
|
let app: AppProtocol
|
||||||
|
let context: AppOperationContext
|
||||||
|
|
||||||
|
private let bundleIdentifier: String
|
||||||
|
private let sourceURL: URL
|
||||||
|
private let destinationURL: URL
|
||||||
|
|
||||||
|
private let session = URLSession(configuration: .default)
|
||||||
|
|
||||||
|
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
|
||||||
|
{
|
||||||
|
self.app = app
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
self.bundleIdentifier = app.bundleIdentifier
|
||||||
|
self.sourceURL = app.url
|
||||||
|
self.destinationURL = destinationURL
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.progress.totalUnitCount = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override func main()
|
||||||
|
{
|
||||||
|
super.main()
|
||||||
|
|
||||||
|
if let error = self.context.error
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Downloading App:", self.bundleIdentifier)
|
||||||
|
|
||||||
|
func finishOperation(_ result: Result<URL, Error>)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let fileURL = try result.get()
|
||||||
|
|
||||||
|
var isDirectory: ObjCBool = false
|
||||||
|
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound }
|
||||||
|
|
||||||
|
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||||
|
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
defer { try? FileManager.default.removeItem(at: temporaryDirectory) }
|
||||||
|
|
||||||
|
let appBundleURL: URL
|
||||||
|
|
||||||
|
if isDirectory.boolValue
|
||||||
|
{
|
||||||
|
// Directory, so assuming this is .app bundle.
|
||||||
|
guard Bundle(url: fileURL) != nil else { throw OperationError.invalidApp }
|
||||||
|
|
||||||
|
appBundleURL = fileURL
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// File, so assuming this is a .ipa file.
|
||||||
|
appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let application = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||||
|
|
||||||
|
guard ProcessInfo.processInfo.isOperatingSystemAtLeast(application.minimumiOSVersion) else { throw OperationError.iOSVersionNotSupported(application) }
|
||||||
|
|
||||||
|
try FileManager.default.copyItem(at: appBundleURL, to: self.destinationURL, shouldReplace: true)
|
||||||
|
|
||||||
|
if self.context.bundleIdentifier == StoreApp.dolphinAppID, self.context.bundleIdentifier != application.bundleIdentifier
|
||||||
|
{
|
||||||
|
let infoPlistURL = self.destinationURL.appendingPathComponent("Info.plist")
|
||||||
|
|
||||||
|
if var infoPlist = NSDictionary(contentsOf: infoPlistURL) as? [String: Any]
|
||||||
|
{
|
||||||
|
// Manually update the app's bundle identifier to match the one specified in the source.
|
||||||
|
// This allows people who previously installed the app to still update and refresh normally.
|
||||||
|
infoPlist[kCFBundleIdentifierKey as String] = StoreApp.dolphinAppID
|
||||||
|
(infoPlist as NSDictionary).write(to: infoPlistURL, atomically: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let copiedApplication = ALTApplication(fileURL: self.destinationURL) else { throw OperationError.invalidApp }
|
||||||
|
self.finish(.success(copiedApplication))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.sourceURL.isFileURL
|
||||||
|
{
|
||||||
|
finishOperation(.success(self.sourceURL))
|
||||||
|
|
||||||
|
self.progress.completedUnitCount += 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let downloadTask = self.session.downloadTask(with: self.sourceURL) { (fileURL, response, error) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||||
|
finishOperation(.success(fileURL))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
finishOperation(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
|
||||||
|
|
||||||
|
downloadTask.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
//
|
||||||
|
// FetchAnisetteDataOperation.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 1/7/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
import AltKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
@objc(FetchAnisetteDataOperation)
|
||||||
|
class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
|
||||||
|
{
|
||||||
|
let context: OperationContext
|
||||||
|
|
||||||
|
init(context: OperationContext)
|
||||||
|
{
|
||||||
|
self.context = context
|
||||||
|
}
|
||||||
|
|
||||||
|
override func main()
|
||||||
|
{
|
||||||
|
super.main()
|
||||||
|
|
||||||
|
if let error = self.context.error
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
|
|
||||||
|
ServerManager.shared.connect(to: server) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error):
|
||||||
|
self.finish(.failure(error))
|
||||||
|
case .success(let connection):
|
||||||
|
print("Sending anisette data request...")
|
||||||
|
|
||||||
|
let request = AnisetteDataRequest()
|
||||||
|
connection.send(request) { (result) in
|
||||||
|
print("Sent anisette data request!")
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): self.finish(.failure(error))
|
||||||
|
case .success:
|
||||||
|
print("Waiting for anisette data...")
|
||||||
|
connection.receiveResponse() { (result) in
|
||||||
|
print("Receiving anisette data:", result.error?.localizedDescription ?? "success")
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): self.finish(.failure(error))
|
||||||
|
case .success(.error(let response)): self.finish(.failure(response.error))
|
||||||
|
case .success(.anisetteData(let response)): self.finish(.success(response.anisetteData))
|
||||||
|
case .success: self.finish(.failure(ALTServerError(.unknownRequest)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
//
|
||||||
|
// FetchAppIDsOperation.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 1/27/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
import AltKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
@objc(FetchAppIDsOperation)
|
||||||
|
class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
|
||||||
|
{
|
||||||
|
let context: AuthenticatedOperationContext
|
||||||
|
let managedObjectContext: NSManagedObjectContext
|
||||||
|
|
||||||
|
init(context: AuthenticatedOperationContext, managedObjectContext: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
|
||||||
|
{
|
||||||
|
self.context = context
|
||||||
|
self.managedObjectContext = managedObjectContext
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func main()
|
||||||
|
{
|
||||||
|
super.main()
|
||||||
|
|
||||||
|
if let error = self.context.error
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard
|
||||||
|
let team = self.context.team,
|
||||||
|
let session = self.context.session
|
||||||
|
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
|
|
||||||
|
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
|
||||||
|
self.managedObjectContext.perform {
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let fetchedAppIDs = try Result(appIDs, error).get()
|
||||||
|
|
||||||
|
guard let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), team.identifier), in: self.managedObjectContext) else { throw OperationError.notAuthenticated }
|
||||||
|
|
||||||
|
let fetchedIdentifiers = fetchedAppIDs.map { $0.identifier }
|
||||||
|
|
||||||
|
let deletedAppIDsRequest = AppID.fetchRequest() as NSFetchRequest<AppID>
|
||||||
|
deletedAppIDsRequest.predicate = NSPredicate(format: "%K == %@ AND NOT (%K IN %@)",
|
||||||
|
#keyPath(AppID.team), team,
|
||||||
|
#keyPath(AppID.identifier), fetchedIdentifiers)
|
||||||
|
|
||||||
|
let deletedAppIDs = try self.managedObjectContext.fetch(deletedAppIDsRequest)
|
||||||
|
deletedAppIDs.forEach { self.managedObjectContext.delete($0) }
|
||||||
|
|
||||||
|
let appIDs = fetchedAppIDs.map { AppID($0, team: team, context: self.managedObjectContext) }
|
||||||
|
self.finish(.success((appIDs, self.managedObjectContext)))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user