mirror of
https://github.com/SideStore/SideStore.git
synced 2026-03-29 23:05:39 +02:00
clean-checkpoint-1
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,11 +5,13 @@
|
||||
# Xcode
|
||||
#
|
||||
|
||||
## CocoaPods
|
||||
Pods/
|
||||
|
||||
## Build generated
|
||||
build/
|
||||
DerivedData
|
||||
|
||||
archive.xcarchive
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
@@ -21,6 +23,7 @@ archive.xcarchive
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
|
||||
## Other
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
|
||||
5
.gitmodules
vendored
5
.gitmodules
vendored
@@ -19,3 +19,8 @@
|
||||
[submodule "Dependencies/libfragmentzip"]
|
||||
path = Dependencies/libfragmentzip
|
||||
url = https://github.com/SideStore/libfragmentzip.git
|
||||
|
||||
[submodule "AltSign"]
|
||||
path = Dependencies/AltSign
|
||||
url = https://github.com/rileytestut/AltSign.git
|
||||
branch = marketplace
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "175",
|
||||
"green" : "4",
|
||||
"red" : "115"
|
||||
"blue" : "0.518",
|
||||
"green" : "0.502",
|
||||
"red" : "0.004"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
@@ -23,9 +23,9 @@
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "150",
|
||||
"green" : "3",
|
||||
"red" : "99"
|
||||
"blue" : "0.404",
|
||||
"green" : "0.322",
|
||||
"red" : "0.008"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<key>ALTAppGroups</key>
|
||||
<array>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
<string>group.com.SideStore.SideStore</string>
|
||||
</array>
|
||||
<key>ALTBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
// Shared
|
||||
#import "ALTWrappedError.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// AltJIT.swift
|
||||
// AltJIT
|
||||
//
|
||||
// Created by Riley Testut on 8/29/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import OSLog
|
||||
import ArgumentParser
|
||||
|
||||
@main
|
||||
struct AltJIT: AsyncParsableCommand
|
||||
{
|
||||
static let configuration = CommandConfiguration(commandName: "altjit",
|
||||
abstract: "Enable JIT for sideloaded apps.",
|
||||
subcommands: [EnableJIT.self, MountDisk.self])
|
||||
}
|
||||
@@ -1,455 +0,0 @@
|
||||
//
|
||||
// EnableJIT.swift
|
||||
// AltPackage
|
||||
//
|
||||
// Created by Riley Testut on 8/29/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
import RegexBuilder
|
||||
|
||||
import ArgumentParser
|
||||
|
||||
struct EnableJIT: PythonCommand
|
||||
{
|
||||
static let configuration = CommandConfiguration(commandName: "enable", abstract: "Enable JIT for a specific app on your device.")
|
||||
|
||||
@Argument(help: "The name or PID of the app to enable JIT for.", transform: AppProcess.init)
|
||||
var process: AppProcess
|
||||
|
||||
@Option(help: "Your iOS device's UDID.")
|
||||
var udid: String
|
||||
|
||||
@Option(name: .shortAndLong, help: "Number of seconds to wait when connecting to an iOS device before operation is cancelled.")
|
||||
var timeout: TimeInterval = 90.0
|
||||
|
||||
// PythonCommand
|
||||
var pythonPath: String?
|
||||
|
||||
mutating func run() async throws
|
||||
{
|
||||
// Use local variables to fix "escaping autoclosure captures mutating self parameter" compiler error.
|
||||
let process = self.process
|
||||
let udid = self.udid
|
||||
|
||||
do
|
||||
{
|
||||
do
|
||||
{
|
||||
Logger.main.info("Enabling JIT for \(process, privacy: .private(mask: .hash)) on device \(udid, privacy: .private(mask: .hash))...")
|
||||
|
||||
try await self.prepare()
|
||||
|
||||
let rsdTunnel = try await self.startRSDTunnel()
|
||||
defer { rsdTunnel.process.terminate() }
|
||||
print("Connected to device \(self.udid)!", rsdTunnel)
|
||||
|
||||
let port = try await self.startDebugServer(rsdTunnel: rsdTunnel)
|
||||
print("Started debugserver on port \(port).")
|
||||
|
||||
print("Attaching debugger...")
|
||||
let lldb = try await self.attachDebugger(ipAddress: rsdTunnel.ipAddress, port: port)
|
||||
defer { lldb.terminate() }
|
||||
print("Attached debugger to \(process).")
|
||||
|
||||
try await self.detachDebugger(lldb)
|
||||
print("Detached debugger from \(process).")
|
||||
|
||||
print("✅ Successfully enabled JIT for \(process) on device \(udid)!")
|
||||
}
|
||||
catch let error as ProcessError
|
||||
{
|
||||
if let output = error.output
|
||||
{
|
||||
print(output)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("❌ Unable to enable JIT for \(process) on device \(udid).")
|
||||
print(error.localizedDescription)
|
||||
|
||||
Logger.main.error("Failed to enable JIT for \(process, privacy: .private(mask: .hash)) on device \(udid, privacy: .private(mask: .hash)). \(error, privacy: .public)")
|
||||
|
||||
throw ExitCode.failure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension EnableJIT
|
||||
{
|
||||
func startRSDTunnel() async throws -> RemoteServiceDiscoveryTunnel
|
||||
{
|
||||
do
|
||||
{
|
||||
Logger.main.info("Starting RSD tunnel with timeout: \(self.timeout)")
|
||||
|
||||
let process = try Process.launch(.python3, arguments: ["-u", "-m", "pymobiledevice3", "remote", "start-quic-tunnel", "--udid", self.udid], environment: self.processEnvironment)
|
||||
|
||||
do
|
||||
{
|
||||
let rsdTunnel = try await withTimeout(seconds: self.timeout) {
|
||||
let regex = Regex {
|
||||
"--rsd"
|
||||
|
||||
OneOrMore(.whitespace)
|
||||
|
||||
Capture {
|
||||
OneOrMore(.anyGraphemeCluster)
|
||||
}
|
||||
|
||||
OneOrMore(.whitespace)
|
||||
|
||||
TryCapture {
|
||||
OneOrMore(.digit)
|
||||
} transform: { match in
|
||||
Int(match)
|
||||
}
|
||||
}
|
||||
|
||||
for try await line in process.outputLines
|
||||
{
|
||||
if let match = line.firstMatch(of: regex)
|
||||
{
|
||||
let rsdTunnel = RemoteServiceDiscoveryTunnel(ipAddress: String(match.1), port: match.2, process: process)
|
||||
return rsdTunnel
|
||||
}
|
||||
}
|
||||
|
||||
throw ProcessError.unexpectedOutput(executableURL: .python3, output: process.output)
|
||||
}
|
||||
|
||||
// MUST close standardOutput in order to stream output later.
|
||||
process.stopOutput()
|
||||
|
||||
return rsdTunnel
|
||||
}
|
||||
catch is TimedOutError
|
||||
{
|
||||
process.terminate()
|
||||
|
||||
let error = ProcessError.timedOut(executableURL: .python3, output: process.output)
|
||||
throw error
|
||||
}
|
||||
catch
|
||||
{
|
||||
process.terminate()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
let localizedFailure = NSLocalizedString("Could not connect to device \(self.udid).", comment: "")
|
||||
throw error.withLocalizedFailure(localizedFailure)
|
||||
}
|
||||
}
|
||||
|
||||
func startDebugServer(rsdTunnel: RemoteServiceDiscoveryTunnel) async throws -> Int
|
||||
{
|
||||
do
|
||||
{
|
||||
Logger.main.info("Starting debugserver with timeout: \(self.timeout)")
|
||||
|
||||
return try await withTimeout(seconds: self.timeout) {
|
||||
let arguments = ["-u", "-m", "pymobiledevice3", "developer", "debugserver", "start-server"] + rsdTunnel.commandArguments
|
||||
|
||||
let output = try await Process.launchAndWait(.python3, arguments: arguments, environment: self.processEnvironment)
|
||||
|
||||
let port = Reference(Int.self)
|
||||
let regex = Regex {
|
||||
"connect://"
|
||||
|
||||
OneOrMore(.anyGraphemeCluster, .eager)
|
||||
|
||||
":"
|
||||
|
||||
TryCapture(as: port) {
|
||||
OneOrMore(.digit)
|
||||
} transform: { match in
|
||||
Int(match)
|
||||
}
|
||||
}
|
||||
|
||||
if let match = output.firstMatch(of: regex)
|
||||
{
|
||||
return match[port]
|
||||
}
|
||||
|
||||
throw ProcessError.unexpectedOutput(executableURL: .python3, output: output)
|
||||
}
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
let localizedFailure = NSLocalizedString("Could not start debugserver on device \(self.udid).", comment: "")
|
||||
throw error.withLocalizedFailure(localizedFailure)
|
||||
}
|
||||
}
|
||||
|
||||
func attachDebugger(ipAddress: String, port: Int) async throws -> Process
|
||||
{
|
||||
do
|
||||
{
|
||||
Logger.main.info("Attaching debugger...")
|
||||
|
||||
let processID: Int
|
||||
|
||||
switch self.process
|
||||
{
|
||||
case .pid(let pid): processID = pid
|
||||
case .name(let name):
|
||||
guard let pid = try await self.getPID(for: name) else { throw JITError.processNotRunning(self.process) }
|
||||
processID = pid
|
||||
}
|
||||
|
||||
let process = try Process.launch(.lldb, environment: self.processEnvironment)
|
||||
|
||||
do
|
||||
{
|
||||
try await withThrowingTaskGroup(of: Void.self) { taskGroup in
|
||||
|
||||
// // Throw error if program terminates.
|
||||
// taskGroup.addTask {
|
||||
// try await withCheckedThrowingContinuation { continuation in
|
||||
// process.terminationHandler = { process in
|
||||
// Task {
|
||||
// // Should NEVER be called unless an error occurs.
|
||||
// continuation.resume(throwing: ProcessError.terminated(executableURL: .lldb, exitCode: process.terminationStatus, output: process.output))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
taskGroup.addTask {
|
||||
do
|
||||
{
|
||||
try await self.sendDebuggerCommand("platform select remote-ios", to: process, timeout: 5) {
|
||||
ChoiceOf {
|
||||
"SDK Roots:"
|
||||
"unable to locate SDK"
|
||||
}
|
||||
}
|
||||
|
||||
let ipAddress = "[\(ipAddress)]"
|
||||
let connectCommand = "process connect connect://\(ipAddress):\(port)"
|
||||
try await self.sendDebuggerCommand(connectCommand, to: process, timeout: 10)
|
||||
|
||||
try await self.sendDebuggerCommand("settings set target.memory-module-load-level minimal", to: process, timeout: 5)
|
||||
|
||||
let attachCommand = "attach -p \(processID)"
|
||||
let failureMessage = "attach failed"
|
||||
let output = try await self.sendDebuggerCommand(attachCommand, to: process, timeout: 120) {
|
||||
|
||||
ChoiceOf {
|
||||
failureMessage
|
||||
|
||||
Regex {
|
||||
"Process "
|
||||
OneOrMore(.digit)
|
||||
" stopped"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if output.contains(failureMessage)
|
||||
{
|
||||
throw ProcessError.failed(executableURL: .lldb, exitCode: -1, output: process.output)
|
||||
}
|
||||
}
|
||||
catch is TimedOutError
|
||||
{
|
||||
let error = ProcessError.timedOut(executableURL: .lldb, output: process.output)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Wait until first child task returns
|
||||
_ = try await taskGroup.next()!
|
||||
|
||||
// Cancel remaining tasks
|
||||
taskGroup.cancelAll()
|
||||
}
|
||||
|
||||
return process
|
||||
}
|
||||
catch
|
||||
{
|
||||
process.terminate()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
let localizedFailure = String(format: NSLocalizedString("Could not attach debugger to %@.", comment: ""), self.process.description)
|
||||
throw error.withLocalizedFailure(localizedFailure)
|
||||
}
|
||||
}
|
||||
|
||||
func detachDebugger(_ process: Process) async throws
|
||||
{
|
||||
do
|
||||
{
|
||||
Logger.main.info("Detaching debugger...")
|
||||
|
||||
try await withThrowingTaskGroup(of: Void.self) { taskGroup in
|
||||
|
||||
// // Throw error if program terminates.
|
||||
// taskGroup.addTask {
|
||||
// try await withCheckedThrowingContinuation { continuation in
|
||||
// process.terminationHandler = { process in
|
||||
// if process.terminationStatus == 0
|
||||
// {
|
||||
// continuation.resume()
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// continuation.resume(throwing: ProcessError.terminated(executableURL: .lldb, exitCode: process.terminationStatus, output: process.output))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
taskGroup.addTask {
|
||||
do
|
||||
{
|
||||
try await self.sendDebuggerCommand("c", to: process, timeout: 10) {
|
||||
"Process "
|
||||
OneOrMore(.digit)
|
||||
" resuming"
|
||||
}
|
||||
|
||||
try await self.sendDebuggerCommand("detach", to: process, timeout: 10) {
|
||||
"Process "
|
||||
OneOrMore(.digit)
|
||||
" detached"
|
||||
}
|
||||
}
|
||||
catch is TimedOutError
|
||||
{
|
||||
let error = ProcessError.timedOut(executableURL: .lldb, output: process.output)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Wait until first child task returns
|
||||
_ = try await taskGroup.next()!
|
||||
|
||||
// Cancel remaining tasks
|
||||
taskGroup.cancelAll()
|
||||
}
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
let localizedFailure = NSLocalizedString("Could not detach debugger from \(self.process).", comment: "")
|
||||
throw error.withLocalizedFailure(localizedFailure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension EnableJIT
|
||||
{
|
||||
func getPID(for name: String) async throws -> Int?
|
||||
{
|
||||
Logger.main.info("Retrieving PID for \(name, privacy: .private(mask: .hash))...")
|
||||
|
||||
let arguments = ["-m", "pymobiledevice3", "processes", "pgrep", name, "--udid", self.udid]
|
||||
let output = try await Process.launchAndWait(.python3, arguments: arguments, environment: self.processEnvironment)
|
||||
|
||||
let regex = Regex {
|
||||
"INFO"
|
||||
|
||||
OneOrMore(.whitespace)
|
||||
|
||||
TryCapture {
|
||||
OneOrMore(.digit)
|
||||
} transform: { match in
|
||||
Int(match)
|
||||
}
|
||||
|
||||
OneOrMore(.whitespace)
|
||||
|
||||
name
|
||||
}
|
||||
|
||||
if let match = output.firstMatch(of: regex)
|
||||
{
|
||||
Logger.main.info("\(name, privacy: .private(mask: .hash)) PID is \(match.1)")
|
||||
return match.1
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func sendDebuggerCommand(_ command: String, to process: Process, timeout: TimeInterval,
|
||||
@RegexComponentBuilder regex: @escaping () -> (some RegexComponent<Substring>)? = { Optional<Regex<Substring>>.none }) async throws -> String
|
||||
{
|
||||
guard let inputPipe = process.standardInput as? Pipe else { preconditionFailure("`process` must have a Pipe as its standardInput") }
|
||||
defer {
|
||||
inputPipe.fileHandleForWriting.writeabilityHandler = nil
|
||||
}
|
||||
|
||||
let initialOutput = process.output
|
||||
|
||||
let data = (command + "\n").data(using: .utf8)! // Will always succeed.
|
||||
Logger.main.info("Sending lldb command: \(command, privacy: .public)")
|
||||
|
||||
let output = try await withTimeout(seconds: timeout) {
|
||||
// Wait until process is ready to receive input.
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
inputPipe.fileHandleForWriting.writeabilityHandler = { fileHandle in
|
||||
inputPipe.fileHandleForWriting.writeabilityHandler = nil
|
||||
|
||||
let result = Result { try fileHandle.write(contentsOf: data) }
|
||||
continuation.resume(with: result)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait until we receive at least one line of output.
|
||||
for try await _ in process.outputLines
|
||||
{
|
||||
break
|
||||
}
|
||||
|
||||
// Keep waiting until output doesn't change.
|
||||
// If regex is provided, we keep waiting until a match is found.
|
||||
var previousOutput = process.output
|
||||
while true
|
||||
{
|
||||
try await Task.sleep(for: .seconds(0.2))
|
||||
|
||||
let output = process.output
|
||||
if output == previousOutput
|
||||
{
|
||||
guard let regex = regex() else {
|
||||
// No regex, so break as soon as output stops changing.
|
||||
break
|
||||
}
|
||||
|
||||
if output.contains(regex)
|
||||
{
|
||||
// Found a match, so exit while loop.
|
||||
break
|
||||
}
|
||||
else
|
||||
{
|
||||
// Output hasn't changed, but regex does not match (yet).
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
previousOutput = output
|
||||
}
|
||||
|
||||
return previousOutput
|
||||
}
|
||||
|
||||
// Subtract initialOutput from output to get just this command's output.
|
||||
let commandOutput = output.replacingOccurrences(of: initialOutput, with: "")
|
||||
return commandOutput
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
//
|
||||
// MountDisk.swift
|
||||
// AltPackage
|
||||
//
|
||||
// Created by Riley Testut on 8/31/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
import ArgumentParser
|
||||
|
||||
typealias MountError = MountErrorCode.Error
|
||||
enum MountErrorCode: Int, ALTErrorEnum
|
||||
{
|
||||
case alreadyMounted
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self
|
||||
{
|
||||
case .alreadyMounted: return NSLocalizedString("A personalized Developer Disk is already mounted.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MountDisk: PythonCommand
|
||||
{
|
||||
static let configuration = CommandConfiguration(commandName: "mount", abstract: "Mount a personalized developer disk image onto an iOS device.")
|
||||
|
||||
@Option(help: "The iOS device's UDID.")
|
||||
var udid: String
|
||||
|
||||
// PythonCommand
|
||||
var pythonPath: String?
|
||||
|
||||
mutating func run() async throws
|
||||
{
|
||||
do
|
||||
{
|
||||
print("Mounting personalized developer disk...")
|
||||
|
||||
try await self.prepare()
|
||||
|
||||
let output = try await Process.launchAndWait(.python3, arguments: ["-m", "pymobiledevice3", "mounter", "auto-mount", "--udid", self.udid])
|
||||
if !output.contains("DeveloperDiskImage")
|
||||
{
|
||||
throw ProcessError.unexpectedOutput(executableURL: .python3, output: output)
|
||||
}
|
||||
|
||||
if output.contains("already mounted")
|
||||
{
|
||||
throw MountError(.alreadyMounted)
|
||||
}
|
||||
|
||||
print("✅ Successfully mounted personalized Developer Disk!")
|
||||
}
|
||||
catch let error as MountError where error.code == .alreadyMounted
|
||||
{
|
||||
// Prepend ⚠️ since this is not really an error.
|
||||
let localizedDescription = "⚠️ " + error.localizedDescription
|
||||
print(localizedDescription)
|
||||
|
||||
throw ExitCode.success
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Output failure message first before error.
|
||||
print("❌ Unable to mount personalized Developer Disk.")
|
||||
print(error.localizedDescription)
|
||||
|
||||
throw ExitCode.failure
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// Logger+AltJIT.swift
|
||||
// AltJIT
|
||||
//
|
||||
// Created by Riley Testut on 8/29/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import OSLog
|
||||
|
||||
public extension Logger
|
||||
{
|
||||
static let altjitSubsystem = Bundle.main.bundleIdentifier!
|
||||
|
||||
static let main = Logger(subsystem: altjitSubsystem, category: "AltJIT")
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
//
|
||||
// Task+Timeout.swift
|
||||
// AltPackage
|
||||
//
|
||||
// Created by Riley Testut on 8/31/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
// Based heavily on https://forums.swift.org/t/running-an-async-task-with-a-timeout/49733/13
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TimedOutError: LocalizedError
|
||||
{
|
||||
var duration: TimeInterval
|
||||
|
||||
public var errorDescription: String? {
|
||||
//TODO: Change pluralization for 1 second.
|
||||
let errorDescription = String(format: NSLocalizedString("The task timed out after %@ seconds.", comment: ""), self.duration.formatted())
|
||||
return errorDescription
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Execute an operation in the current task subject to a timeout.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - seconds: The duration in seconds `operation` is allowed to run before timing out.
|
||||
/// - operation: The async operation to perform.
|
||||
/// - Returns: Returns the result of `operation` if it completed in time.
|
||||
/// - Throws: Throws ``TimedOutError`` if the timeout expires before `operation` completes.
|
||||
/// If `operation` throws an error before the timeout expires, that error is propagated to the caller.
|
||||
func withTimeout<R>(seconds: TimeInterval, file: StaticString = #file, line: Int = #line, operation: @escaping @Sendable () async throws -> R) async throws -> R
|
||||
{
|
||||
return try await withThrowingTaskGroup(of: R.self) { group in
|
||||
let deadline = Date(timeIntervalSinceNow: seconds)
|
||||
|
||||
// Start actual work.
|
||||
group.addTask {
|
||||
return try await operation()
|
||||
}
|
||||
// Start timeout child task.
|
||||
group.addTask {
|
||||
let interval = deadline.timeIntervalSinceNow
|
||||
if interval > 0 {
|
||||
try await Task.sleep(for: .seconds(interval))
|
||||
}
|
||||
try Task.checkCancellation()
|
||||
// We’ve reached the timeout.
|
||||
throw TimedOutError(duration: seconds)
|
||||
}
|
||||
// First finished child task wins, cancel the other task.
|
||||
let result = try await group.next()!
|
||||
group.cancelAll()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// URL+Tools.swift
|
||||
// AltJIT
|
||||
//
|
||||
// Created by Riley Testut on 9/3/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URL
|
||||
{
|
||||
static let python3 = URL(fileURLWithPath: "/usr/bin/python3")
|
||||
static let lldb = URL(fileURLWithPath: "/usr/bin/lldb")
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// PythonCommand.swift
|
||||
// AltJIT
|
||||
//
|
||||
// Created by Riley Testut on 9/6/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import ArgumentParser
|
||||
|
||||
protocol PythonCommand: AsyncParsableCommand
|
||||
{
|
||||
var pythonPath: String? { get set }
|
||||
}
|
||||
|
||||
extension PythonCommand
|
||||
{
|
||||
var processEnvironment: [String: String] {
|
||||
var environment = ProcessInfo.processInfo.environment
|
||||
|
||||
if let pythonPath
|
||||
{
|
||||
environment["PYTHONPATH"] = pythonPath
|
||||
}
|
||||
|
||||
return environment
|
||||
}
|
||||
|
||||
mutating func prepare() async throws
|
||||
{
|
||||
let pythonPath = try await self.readPythonPath()
|
||||
self.pythonPath = pythonPath.path(percentEncoded: false)
|
||||
}
|
||||
}
|
||||
|
||||
private extension PythonCommand
|
||||
{
|
||||
func readPythonPath() async throws -> URL
|
||||
{
|
||||
let processOutput: String
|
||||
|
||||
do
|
||||
{
|
||||
processOutput = try await Process.launchAndWait(.python3, arguments: ["-m", "site", "--user-site"])
|
||||
}
|
||||
catch let error as ProcessError where error.exitCode == 2
|
||||
{
|
||||
// Ignore exit code 2.
|
||||
guard let output = error.output else { throw error }
|
||||
processOutput = output
|
||||
}
|
||||
|
||||
let sanitizedOutput = processOutput.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
let pythonURL = URL(filePath: sanitizedOutput)
|
||||
return pythonURL
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// RemoteServiceDiscoveryTunnel.swift
|
||||
// AltJIT
|
||||
//
|
||||
// Created by Riley Testut on 9/3/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class RemoteServiceDiscoveryTunnel
|
||||
{
|
||||
let ipAddress: String
|
||||
let port: Int
|
||||
|
||||
let process: Process
|
||||
|
||||
var commandArguments: [String] {
|
||||
["--rsd", self.ipAddress, String(self.port)]
|
||||
}
|
||||
|
||||
init(ipAddress: String, port: Int, process: Process)
|
||||
{
|
||||
self.ipAddress = ipAddress
|
||||
self.port = port
|
||||
|
||||
self.process = process
|
||||
}
|
||||
|
||||
deinit
|
||||
{
|
||||
self.process.terminate()
|
||||
}
|
||||
}
|
||||
|
||||
extension RemoteServiceDiscoveryTunnel: CustomStringConvertible
|
||||
{
|
||||
var description: String {
|
||||
"\(self.ipAddress) \(self.port)"
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
//
|
||||
// AnisetteError.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 9/13/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension AnisetteError
|
||||
{
|
||||
enum Code: Int, ALTErrorCode
|
||||
{
|
||||
typealias Error = AnisetteError
|
||||
|
||||
case aosKitFailure
|
||||
case missingValue
|
||||
}
|
||||
|
||||
static func aosKitFailure(file: String = #fileID, line: UInt = #line) -> AnisetteError {
|
||||
AnisetteError(code: .aosKitFailure, sourceFile: file, sourceLine: line)
|
||||
}
|
||||
|
||||
static func missingValue(_ value: String?, file: String = #fileID, line: UInt = #line) -> AnisetteError {
|
||||
AnisetteError(code: .missingValue, value: value, sourceFile: file, sourceLine: line)
|
||||
}
|
||||
}
|
||||
|
||||
struct AnisetteError: ALTLocalizedError
|
||||
{
|
||||
var code: Code
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
|
||||
@UserInfoValue
|
||||
var value: String?
|
||||
|
||||
var sourceFile: String?
|
||||
var sourceLine: UInt?
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self.code
|
||||
{
|
||||
case .aosKitFailure: return NSLocalizedString("AltServer could not retrieve anisette data from AOSKit.", comment: "")
|
||||
case .missingValue:
|
||||
let valueName = self.value.map { "anisette data value “\($0)”" } ?? NSLocalizedString("anisette data values.", comment: "")
|
||||
return String(format: NSLocalizedString("AltServer could not retrieve %@.", comment: ""), valueName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// Logger+AltServer.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 9/6/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import OSLog
|
||||
|
||||
extension Logger
|
||||
{
|
||||
static let altserverSubsystem = Bundle.main.bundleIdentifier!
|
||||
|
||||
static let main = Logger(subsystem: altserverSubsystem, category: "AltServer")
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
//
|
||||
// Process+STPrivilegedTask.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 8/22/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
import OSLog
|
||||
|
||||
import STPrivilegedTask
|
||||
|
||||
extension Process
|
||||
{
|
||||
class func runAsAdmin(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws -> AuthorizationRef?
|
||||
{
|
||||
var launchPath = "/usr/bin/" + program
|
||||
if !FileManager.default.fileExists(atPath: launchPath)
|
||||
{
|
||||
launchPath = "/bin/" + program
|
||||
}
|
||||
|
||||
if !FileManager.default.fileExists(atPath: launchPath)
|
||||
{
|
||||
launchPath = program
|
||||
}
|
||||
|
||||
Logger.main.info("Launching admin process: \(launchPath, privacy: .public)")
|
||||
|
||||
let task = STPrivilegedTask()
|
||||
task.launchPath = launchPath
|
||||
task.arguments = arguments
|
||||
task.freeAuthorizationWhenDone = false
|
||||
|
||||
let errorCode: OSStatus
|
||||
|
||||
if let authorization = authorization
|
||||
{
|
||||
errorCode = task.launch(withAuthorization: authorization)
|
||||
}
|
||||
else
|
||||
{
|
||||
errorCode = task.launch()
|
||||
}
|
||||
|
||||
let executableURL = URL(fileURLWithPath: launchPath)
|
||||
guard errorCode == 0 else { throw ProcessError.failed(executableURL: executableURL, exitCode: errorCode, output: nil) }
|
||||
|
||||
task.waitUntilExit()
|
||||
|
||||
Logger.main.info("Admin process \(launchPath, privacy: .public) terminated with exit code \(task.terminationStatus, privacy: .public).")
|
||||
|
||||
guard task.terminationStatus == 0 else {
|
||||
let executableURL = URL(fileURLWithPath: launchPath)
|
||||
|
||||
let outputData = task.outputFileHandle.readDataToEndOfFile()
|
||||
if let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty
|
||||
{
|
||||
throw ProcessError.failed(executableURL: executableURL, exitCode: task.terminationStatus, output: outputString)
|
||||
}
|
||||
else
|
||||
{
|
||||
throw ProcessError.failed(executableURL: executableURL, exitCode: task.terminationStatus, output: nil)
|
||||
}
|
||||
}
|
||||
|
||||
return task.authorization
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
//
|
||||
// ProcessInfo+Device.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 9/13/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RegexBuilder
|
||||
|
||||
extension ProcessInfo
|
||||
{
|
||||
var deviceModel: String? {
|
||||
let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
|
||||
defer {
|
||||
IOObjectRelease(service)
|
||||
}
|
||||
|
||||
guard
|
||||
let modelData = IORegistryEntryCreateCFProperty(service, "model" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? Data,
|
||||
let cDeviceModel = String(data: modelData, encoding: .utf8)?.cString(using: .utf8) // Remove trailing NULL character
|
||||
else { return nil }
|
||||
|
||||
let deviceModel = String(cString: cDeviceModel)
|
||||
return deviceModel
|
||||
}
|
||||
|
||||
var operatingSystemBuildVersion: String? {
|
||||
let osVersionString = ProcessInfo.processInfo.operatingSystemVersionString
|
||||
let buildVersion: String?
|
||||
|
||||
if #available(macOS 13, *), let match = osVersionString.firstMatch(of: Regex {
|
||||
"(Build "
|
||||
Capture {
|
||||
OneOrMore(.anyNonNewline)
|
||||
}
|
||||
")"
|
||||
})
|
||||
{
|
||||
buildVersion = String(match.1)
|
||||
}
|
||||
else if let build = osVersionString.split(separator: " ").last?.dropLast()
|
||||
{
|
||||
buildVersion = String(build)
|
||||
}
|
||||
else
|
||||
{
|
||||
buildVersion = nil
|
||||
}
|
||||
|
||||
return buildVersion
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
//
|
||||
// JITManager.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 8/30/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import RegexBuilder
|
||||
|
||||
import AltSign
|
||||
|
||||
private extension URL
|
||||
{
|
||||
static let python3 = URL(fileURLWithPath: "/usr/bin/python3")
|
||||
static let altjit = Bundle.main.executableURL!.deletingLastPathComponent().appendingPathComponent("altjit")
|
||||
}
|
||||
|
||||
class JITManager
|
||||
{
|
||||
static let shared = JITManager()
|
||||
|
||||
private let diskManager = DeveloperDiskManager()
|
||||
|
||||
private var authorization: AuthorizationRef?
|
||||
|
||||
private init()
|
||||
{
|
||||
}
|
||||
|
||||
func prepare(_ device: ALTDevice) async throws
|
||||
{
|
||||
let isMounted = try await ALTDeviceManager.shared.isDeveloperDiskImageMounted(for: device)
|
||||
guard !isMounted else { return }
|
||||
|
||||
if #available(macOS 13, *), device.osVersion.majorVersion >= 17
|
||||
{
|
||||
// iOS 17+
|
||||
try await self.installPersonalizedDeveloperDisk(onto: device)
|
||||
}
|
||||
else
|
||||
{
|
||||
try await self.installDeveloperDisk(onto: device)
|
||||
}
|
||||
}
|
||||
|
||||
func enableUnsignedCodeExecution(process: AppProcess, device: ALTDevice) async throws
|
||||
{
|
||||
try await self.prepare(device)
|
||||
|
||||
if #available(macOS 13, *), device.osVersion.majorVersion >= 17
|
||||
{
|
||||
// iOS 17+
|
||||
try await self.enableModernUnsignedCodeExecution(process: process, device: device)
|
||||
}
|
||||
else
|
||||
{
|
||||
try await self.enableLegacyUnsignedCodeExecution(process: process, device: device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension JITManager
|
||||
{
|
||||
func installDeveloperDisk(onto device: ALTDevice) async throws
|
||||
{
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
self.diskManager.downloadDeveloperDisk(for: device) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): continuation.resume(throwing: error)
|
||||
case .success((let diskFileURL, let signatureFileURL)):
|
||||
ALTDeviceManager.shared.installDeveloperDiskImage(at: diskFileURL, signatureURL: signatureFileURL, to: device) { (success, error) in
|
||||
switch Result(success, error)
|
||||
{
|
||||
case .failure(let error as ALTServerError) where error.code == .incompatibleDeveloperDisk:
|
||||
self.diskManager.setDeveloperDiskCompatible(false, with: device)
|
||||
continuation.resume(throwing: error)
|
||||
|
||||
case .failure(let error):
|
||||
// Don't mark developer disk as incompatible because it probably failed for a different reason.
|
||||
continuation.resume(throwing: error)
|
||||
|
||||
case .success:
|
||||
self.diskManager.setDeveloperDiskCompatible(true, with: device)
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func enableLegacyUnsignedCodeExecution(process: AppProcess, device: ALTDevice) async throws
|
||||
{
|
||||
let connection = try await ALTDeviceManager.shared.startDebugConnection(to: device)
|
||||
|
||||
switch process
|
||||
{
|
||||
case .name(let name): try await connection.enableUnsignedCodeExecutionForProcess(withName: name)
|
||||
case .pid(let pid): try await connection.enableUnsignedCodeExecutionForProcess(withID: pid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13, *)
|
||||
private extension JITManager
|
||||
{
|
||||
func installPersonalizedDeveloperDisk(onto device: ALTDevice) async throws
|
||||
{
|
||||
do
|
||||
{
|
||||
_ = try await Process.launchAndWait(.altjit, arguments: ["mount", "--udid", device.identifier])
|
||||
}
|
||||
catch
|
||||
{
|
||||
try self.processAltJITError(error)
|
||||
}
|
||||
}
|
||||
|
||||
func enableModernUnsignedCodeExecution(process: AppProcess, device: ALTDevice) async throws
|
||||
{
|
||||
do
|
||||
{
|
||||
if self.authorization == nil
|
||||
{
|
||||
// runAsAdmin() only returns authorization if the process completes successfully,
|
||||
// so we request authorization for a command that can't fail, then re-use it for the failable command below.
|
||||
self.authorization = try Process.runAsAdmin("echo", arguments: ["altstore"], authorization: self.authorization)
|
||||
}
|
||||
|
||||
var arguments = ["enable"]
|
||||
switch process
|
||||
{
|
||||
case .name(let name): arguments.append(name)
|
||||
case .pid(let pid): arguments.append(String(pid))
|
||||
}
|
||||
arguments += ["--udid", device.identifier]
|
||||
|
||||
if let timeout = UserDefaults.standard.altJITTimeout
|
||||
{
|
||||
arguments += ["--timeout", String(timeout)]
|
||||
}
|
||||
|
||||
self.authorization = try Process.runAsAdmin(URL.altjit.path, arguments: arguments, authorization: self.authorization)
|
||||
}
|
||||
catch
|
||||
{
|
||||
try self.processAltJITError(error)
|
||||
}
|
||||
}
|
||||
|
||||
func processAltJITError(_ error: some Error) throws
|
||||
{
|
||||
do
|
||||
{
|
||||
throw error
|
||||
}
|
||||
catch let error as ProcessError where error.code == .failed
|
||||
{
|
||||
guard let output = error.output else { throw error }
|
||||
|
||||
let dependencyNotFoundRegex = Regex {
|
||||
"No module named"
|
||||
|
||||
OneOrMore(.whitespace)
|
||||
|
||||
Capture {
|
||||
OneOrMore(.anyNonNewline)
|
||||
}
|
||||
}
|
||||
|
||||
let deviceNotFoundRegex = Regex {
|
||||
"Device is not connected"
|
||||
}
|
||||
|
||||
if let match = output.firstMatch(of: dependencyNotFoundRegex)
|
||||
{
|
||||
let dependency = String(match.1)
|
||||
throw JITError.dependencyNotFound(dependency)
|
||||
}
|
||||
else if output.contains(deviceNotFoundRegex)
|
||||
{
|
||||
throw ALTServerError(.deviceNotFound, userInfo: [
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString("Your device must be plugged into your computer to enable JIT on iOS 17 or later.", comment: "")
|
||||
])
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,114 +0,0 @@
|
||||
{
|
||||
"originHash" : "ee46302f91cbb62c5234c36750d40856658e961e191f5536cf4fe74d10fc2c94",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "altsign",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SideStore/AltSign",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "4323ff794e600ce1759cb6ea57275e13b7ea72f2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "appcenter-sdk-apple",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/microsoft/appcenter-sdk-apple.git",
|
||||
"state" : {
|
||||
"revision" : "b2dc99cfedead0bad4e6573d86c5228c89cff332",
|
||||
"version" : "4.4.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "imobiledevice.swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SideStore/iMobileDevice.swift",
|
||||
"state" : {
|
||||
"revision" : "74e481106dd155c0cd21bca6795fd9fe5f751654",
|
||||
"version" : "1.0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "keychainaccess",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
||||
"state" : {
|
||||
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
|
||||
"version" : "4.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "launchatlogin",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sindresorhus/LaunchAtLogin.git",
|
||||
"state" : {
|
||||
"revision" : "e8171b3e38a2816f579f58f3dac1522aa39efe41",
|
||||
"version" : "4.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nuke",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kean/Nuke.git",
|
||||
"state" : {
|
||||
"revision" : "9318d02a8a6d20af56505c9673261c1fd3b3aebe",
|
||||
"version" : "7.6.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "openssl",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/krzyzanowskim/OpenSSL",
|
||||
"state" : {
|
||||
"revision" : "8cb1d641ab5ebce2cd7cf31c93baef07bed672d4",
|
||||
"version" : "1.1.2301"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "plcrashreporter",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/microsoft/PLCrashReporter.git",
|
||||
"state" : {
|
||||
"revision" : "81cdec2b3827feb03286cb297f4c501a8eb98df1",
|
||||
"version" : "1.10.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "semanticversion",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git",
|
||||
"state" : {
|
||||
"revision" : "ea8eea9d89842a29af1b8e6c7677f1c86e72fa42",
|
||||
"version" : "0.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sparkle",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sparkle-project/Sparkle.git",
|
||||
"state" : {
|
||||
"revision" : "0ef1ee0220239b3776f433314515fd849025673f",
|
||||
"version" : "2.6.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "starscream",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/daltoniam/Starscream.git",
|
||||
"state" : {
|
||||
"revision" : "c6bfd1af48efcc9a9ad203665db12375ba6b145a",
|
||||
"version" : "4.0.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "stprivilegedtask",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/JoeMatt/STPrivilegedTask.git",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "10a9150ef32d444af326beba76356ae9af95a3e7"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
16
AltStore.xcworkspace/contents.xcworkspacedata
generated
Normal file
16
AltStore.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:AltStore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Dependencies/AltSign">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Dependencies/Roxas/Roxas.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -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>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
33
AltStore.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
33
AltStore.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"originHash" : "e97900bbb49eb7423d5c5ecbf132d2d5761c401e2444f3c8e6cbfa7014ad9c53",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "launchatlogin",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sindresorhus/LaunchAtLogin.git",
|
||||
"state" : {
|
||||
"revision" : "e8171b3e38a2816f579f58f3dac1522aa39efe41",
|
||||
"version" : "4.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "semanticversion",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion",
|
||||
"state" : {
|
||||
"revision" : "ea8eea9d89842a29af1b8e6c7677f1c86e72fa42",
|
||||
"version" : "0.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-argument-parser",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-argument-parser.git",
|
||||
"state" : {
|
||||
"revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531",
|
||||
"version" : "1.2.3"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
<true/>
|
||||
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
||||
<true/>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.siri</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
|
||||
@@ -14,7 +14,13 @@ import AppCenter
|
||||
import AppCenterAnalytics
|
||||
import AppCenterCrashes
|
||||
|
||||
#if DEBUG
|
||||
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
|
||||
#elseif RELEASE
|
||||
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
|
||||
#else
|
||||
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
|
||||
#endif
|
||||
|
||||
extension AnalyticsManager
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ final class AppContentViewController: UITableViewController
|
||||
{
|
||||
var app: StoreApp!
|
||||
|
||||
private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
|
||||
// private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
|
||||
private lazy var dateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
@@ -186,7 +186,7 @@ extension AppContentViewController
|
||||
switch Row.allCases[indexPath.row]
|
||||
{
|
||||
case .screenshots:
|
||||
guard !self.app.allScreenshots.isEmpty else { return 0.0 }
|
||||
guard !self.app.screenshots.isEmpty else { return 0.0 }
|
||||
return UITableView.automaticDimension
|
||||
|
||||
case .permissions:
|
||||
|
||||
@@ -207,7 +207,21 @@ final class AppViewController: UIViewController
|
||||
self._shouldResetLayout = false
|
||||
}
|
||||
|
||||
let statusBarHeight = (self.view.window ?? self.presentedViewController?.view.window)?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
|
||||
let statusBarHeight: Double
|
||||
|
||||
if let navigationController, navigationController.presentingViewController != nil, navigationController.modalPresentationStyle != .fullScreen
|
||||
{
|
||||
statusBarHeight = 20
|
||||
}
|
||||
else if let statusBarManager = (self.view.window ?? self.presentedViewController?.view.window)?.windowScene?.statusBarManager
|
||||
{
|
||||
statusBarHeight = statusBarManager.statusBarFrame.height
|
||||
}
|
||||
else
|
||||
{
|
||||
statusBarHeight = 0
|
||||
}
|
||||
|
||||
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
||||
|
||||
let inset = 12 as CGFloat
|
||||
@@ -562,8 +576,6 @@ extension AppViewController
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(error: error, opensLog: true)
|
||||
toastView.show(in: self)
|
||||
self.bannerView.button.progress = nil
|
||||
self.navigationBarDownloadButton.progress = nil
|
||||
self.update()
|
||||
|
||||
@@ -93,19 +93,20 @@ private extension AppIDsViewController
|
||||
cell.bannerView.buttonLabel.isHidden = false
|
||||
|
||||
let currentDate = Date()
|
||||
|
||||
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.unitsStyle = .full
|
||||
formatter.includesApproximationPhrase = false
|
||||
formatter.includesTimeRemainingPhrase = false
|
||||
formatter.allowedUnits = [.minute, .hour, .day]
|
||||
formatter.maximumUnitCount = 1
|
||||
|
||||
cell.bannerView.button.setTitle((formatter.string(from: currentDate, to: expirationDate) ?? NSLocalizedString("Unknown", comment: "")).uppercased(), for: .normal)
|
||||
|
||||
let timeInterval = formatter.string(from: currentDate, to: expirationDate)
|
||||
let timeIntervalText = timeInterval ?? NSLocalizedString("Unknown", comment: "")
|
||||
cell.bannerView.button.setTitle(timeIntervalText.uppercased(), for: .normal)
|
||||
|
||||
// formatter.includesTimeRemainingPhrase = true
|
||||
|
||||
// attributedAccessibilityLabel.mutableString.append((formatter.string(from: currentDate, to: expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " ")
|
||||
attributedAccessibilityLabel.mutableString.append(timeIntervalText)
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -63,7 +63,10 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
self.setTintColor()
|
||||
self.prepareImageCache()
|
||||
|
||||
|
||||
// TODO: @mahee96: find if we need to start em_proxy as in altstore?
|
||||
// start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||
|
||||
SecureValueTransformer.register()
|
||||
|
||||
if UserDefaults.standard.firstLaunch == nil
|
||||
@@ -86,7 +89,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func applicationDidEnterBackground(_ application: UIApplication)
|
||||
{
|
||||
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
|
||||
|
||||
// TODO: @mahee96: find if we need to stop em_proxy as in altstore?
|
||||
// stop_em_proxy()
|
||||
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
|
||||
|
||||
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
|
||||
@@ -104,6 +108,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
{
|
||||
AppManager.shared.update()
|
||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||
|
||||
PatreonAPI.shared.refreshPatreonAccount()
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<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="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||
<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"/>
|
||||
@@ -42,13 +42,13 @@
|
||||
<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="64" width="375" height="603"/>
|
||||
<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="603"/>
|
||||
<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"/>
|
||||
@@ -57,13 +57,13 @@
|
||||
<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 SideStore." 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="343" height="41"/>
|
||||
<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="306.5" height="20.5"/>
|
||||
<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"/>
|
||||
@@ -160,7 +160,7 @@
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
|
||||
<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>
|
||||
@@ -179,7 +179,7 @@
|
||||
</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="498.5" width="343" height="96.5"/>
|
||||
<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"/>
|
||||
@@ -198,10 +198,6 @@
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="DBk-rT-ZE8" firstAttribute="leading" secondItem="2wp-qG-f0Z" secondAttribute="leadingMargin" id="5AT-nV-ZP9"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="DBk-rT-ZE8" secondAttribute="bottom" id="HgY-oY-8KM"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="DBk-rT-ZE8" secondAttribute="trailing" id="VCf-bW-2K4"/>
|
||||
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
|
||||
<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>
|
||||
@@ -219,15 +215,19 @@
|
||||
<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>
|
||||
</view>
|
||||
@@ -264,7 +264,7 @@
|
||||
<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="64" width="375" height="544"/>
|
||||
<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"/>
|
||||
@@ -298,7 +298,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
|
||||
<rect key="frame" x="16" y="161" width="343" height="95.5"/>
|
||||
<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"/>
|
||||
@@ -310,7 +310,7 @@
|
||||
<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="17.5" width="264" height="60.5"/>
|
||||
<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 Wi-Fi and VPN" 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"/>
|
||||
@@ -319,7 +319,7 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." 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="35"/>
|
||||
<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"/>
|
||||
@@ -329,7 +329,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
|
||||
<rect key="frame" x="16" y="287.5" width="343" height="95.5"/>
|
||||
<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"/>
|
||||
@@ -341,7 +341,7 @@
|
||||
<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="15.5" width="264" height="64"/>
|
||||
<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"/>
|
||||
@@ -360,7 +360,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
|
||||
<rect key="frame" x="16" y="413.5" width="343" height="95.5"/>
|
||||
<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"/>
|
||||
@@ -372,7 +372,7 @@
|
||||
<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="17" width="264" height="62"/>
|
||||
<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"/>
|
||||
@@ -381,7 +381,7 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background while you are on SideStore VPN!" 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="36.5"/>
|
||||
<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"/>
|
||||
@@ -393,7 +393,7 @@
|
||||
</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="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK">
|
||||
<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>
|
||||
@@ -445,7 +445,7 @@
|
||||
<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="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
|
||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||
<constraints>
|
||||
@@ -460,7 +460,7 @@
|
||||
<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="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ">
|
||||
<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">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
@@ -51,7 +51,7 @@
|
||||
<!--Browse-->
|
||||
<scene sceneID="rXq-UR-qQp">
|
||||
<objects>
|
||||
<collectionViewController storyboardIdentifier="browseViewController" id="e3L-BF-iXp" customClass="BrowseViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionViewController storyboardIdentifier="browseViewController" id="e3L-BF-iXp" customClass="BrowseViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -254,7 +254,7 @@
|
||||
<inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="nI6-wC-H2d">
|
||||
<rect key="frame" x="0.0" y="107" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="107" width="375" height="300"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="nI6-wC-H2d" id="Z4y-vb-Z4Q">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="300"/>
|
||||
@@ -281,7 +281,7 @@
|
||||
<inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="EL5-UC-RIw" customClass="AppContentTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="151" width="375" height="98"/>
|
||||
<rect key="frame" x="0.0" y="407" width="375" height="98"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EL5-UC-RIw" id="D1G-nK-G0Z">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="98"/>
|
||||
@@ -305,7 +305,7 @@
|
||||
<inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="47M-El-a4G" customClass="AppContentTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="249" width="375" height="137.5"/>
|
||||
<rect key="frame" x="0.0" y="505" width="375" height="137.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="47M-El-a4G" id="f9D-OR-oGE">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="137.5"/>
|
||||
@@ -353,7 +353,7 @@
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="59.5" width="335" height="34"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
@@ -394,57 +394,6 @@
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="200" id="HFx-PP-dAt"/>
|
||||
</constraints>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" scrollDirection="horizontal" minimumLineSpacing="10" minimumInteritemSpacing="10" id="2HF-4d-3Im">
|
||||
<size key="itemSize" width="60" height="88"/>
|
||||
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||
<inset key="sectionInset" minX="20" minY="0.0" maxX="20" maxY="0.0"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="WYy-bZ-h3T" customClass="PermissionCollectionViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="0.0" width="60" height="88"/>
|
||||
<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="60" height="88"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="fSx-We-L4W">
|
||||
<rect key="frame" x="0.0" y="0.0" width="60" height="87.5"/>
|
||||
<subviews>
|
||||
<button opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="79g-9q-mE2">
|
||||
<rect key="frame" x="5" y="0.0" width="50" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="50" id="0LZ-4n-COH"/>
|
||||
<constraint firstAttribute="height" constant="50" id="keD-mf-Rga"/>
|
||||
</constraints>
|
||||
</button>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pQi-FD-18P">
|
||||
<rect key="frame" x="12.5" y="56" width="35.5" height="31.5"/>
|
||||
<string key="text">Hello
|
||||
World</string>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</view>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="fSx-We-L4W" secondAttribute="trailing" id="IyD-vD-tA4"/>
|
||||
<constraint firstItem="fSx-We-L4W" firstAttribute="leading" secondItem="WYy-bZ-h3T" secondAttribute="leading" id="bTq-op-ivD"/>
|
||||
<constraint firstItem="fSx-We-L4W" firstAttribute="top" secondItem="WYy-bZ-h3T" secondAttribute="top" id="sMw-NS-jtY"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="button" destination="79g-9q-mE2" id="G5V-SS-vaA"/>
|
||||
<outlet property="textLabel" destination="pQi-FD-18P" id="D5d-20-cm3"/>
|
||||
<segue destination="Ojq-DN-xcF" kind="popoverPresentation" identifier="showPermission" popoverAnchorView="r8T-dj-wQX" id="ftM-H7-Q7G">
|
||||
<popoverArrowDirection key="popoverArrowDirection" down="YES"/>
|
||||
</segue>
|
||||
</connections>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
</collectionView>
|
||||
<connections>
|
||||
<segue destination="OYP-I1-A3i" kind="embed" destinationCreationSelector="makeAppDetailCollectionViewController:sender:" id="Uxh-GM-nzb"/>
|
||||
</connections>
|
||||
@@ -495,7 +444,7 @@ World</string>
|
||||
<!--App Screenshots View Controller-->
|
||||
<scene sceneID="E6k-TI-c4N">
|
||||
<objects>
|
||||
<collectionViewController storyboardIdentifier="appScreenshotsViewController" id="nX2-hQ-qjX" customClass="AppScreenshotsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionViewController storyboardIdentifier="appScreenshotsViewController" id="nX2-hQ-qjX" customClass="AppScreenshotsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" contentInsetAdjustmentBehavior="never" dataMode="prototypes" id="zXl-if-KtH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="300"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -520,7 +469,7 @@ World</string>
|
||||
<!--App Detail Collection View Controller-->
|
||||
<scene sceneID="Pcn-h5-5fk">
|
||||
<objects>
|
||||
<collectionViewController id="OYP-I1-A3i" customClass="AppDetailCollectionViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionViewController id="OYP-I1-A3i" customClass="AppDetailCollectionViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" id="y1V-56-IqS" customClass="SafeAreaIgnoringCollectionView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="200"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -790,7 +739,7 @@ World</string>
|
||||
<!--Featured View Controller-->
|
||||
<scene sceneID="1eF-L7-aZz">
|
||||
<objects>
|
||||
<collectionViewController storyboardIdentifier="featuredViewController" id="KKu-kI-2kg" customClass="FeaturedViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionViewController storyboardIdentifier="featuredViewController" id="KKu-kI-2kg" customClass="FeaturedViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="2HL-eH-weG">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -851,15 +800,16 @@ World</string>
|
||||
<inset key="sectionInset" minX="0.0" minY="10" maxX="0.0" maxY="20"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="XWu-DU-xbh" customClass="BannerCollectionViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="XWu-DU-xbh" customClass="AppBannerCollectionViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="70" width="375" height="80"/>
|
||||
<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="80"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
|
||||
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="8" y="0.0" width="359" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
@@ -990,11 +940,12 @@ World</string>
|
||||
<color key="tintColor" name="Primary"/>
|
||||
<resources>
|
||||
<image name="Back" width="18" height="18"/>
|
||||
<image name="Browse" width="128" height="128"/>
|
||||
<image name="MyApps" width="20" height="20"/>
|
||||
<image name="News" width="19" height="20"/>
|
||||
<image name="Settings" width="20" height="20"/>
|
||||
<namedColor name="Background">
|
||||
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="BlurTint">
|
||||
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
//
|
||||
// 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 final 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
|
||||
let request = ImageRequest(url: imageURL as URL)
|
||||
ImagePipeline.shared.loadImage(with: request, progress: nil) { result in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .success(let response): completionHandler(response.image, nil)
|
||||
case .failure(let error): 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
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<?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>
|
||||
@@ -296,7 +296,6 @@ private extension BrowseViewController
|
||||
|
||||
func updateDataSource()
|
||||
{
|
||||
self.dataSource.predicate = nil
|
||||
let fetchRequest = self.makeFetchRequest()
|
||||
|
||||
let context = self.source?.managedObjectContext ?? DatabaseManager.shared.viewContext
|
||||
|
||||
@@ -10,7 +10,7 @@ import UIKit
|
||||
|
||||
import Roxas
|
||||
|
||||
final class NavigationBar: UINavigationBar
|
||||
class NavigationBarAppearance: UINavigationBarAppearance
|
||||
{
|
||||
// We sometimes need to ignore user interaction so
|
||||
// we can tap items underneath the navigation bar.
|
||||
|
||||
@@ -16,10 +16,13 @@ extension TimeInterval
|
||||
static let longToastViewDuration = 8.0
|
||||
}
|
||||
|
||||
final class ToastView: RSTToastView
|
||||
extension ToastView
|
||||
{
|
||||
static let openErrorLogNotification = Notification.Name("ALTOpenErrorLogNotification")
|
||||
}
|
||||
|
||||
class ToastView: RSTToastView
|
||||
{
|
||||
var preferredDuration: TimeInterval
|
||||
|
||||
var opensErrorLog: Bool = false
|
||||
@@ -72,6 +75,7 @@ final class ToastView: RSTToastView
|
||||
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
|
||||
{
|
||||
// Treat underlyingError as the primary error, but keep localized title + failure.
|
||||
|
||||
let nsError = error as NSError
|
||||
error = unwrappedUnderlyingError as NSError
|
||||
|
||||
@@ -138,12 +142,6 @@ final class ToastView: RSTToastView
|
||||
{
|
||||
self.show(in: view, duration: self.preferredDuration)
|
||||
}
|
||||
|
||||
@objc
|
||||
func showErrorLog() {
|
||||
guard self.opensErrorLog else { return }
|
||||
NotificationCenter.default.post(name: ToastView.openErrorLogNotification, object: self)
|
||||
}
|
||||
}
|
||||
|
||||
private extension ToastView
|
||||
|
||||
@@ -30,7 +30,9 @@ extension AppManager
|
||||
} else if self.errors.count == 1 {
|
||||
guard let source = self.errors.keys.first else { return }
|
||||
localizedTitle = String(format: NSLocalizedString("Failed to refresh Source '%@'", comment: ""), source.name)
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
localizedTitle = String(format: NSLocalizedString("Failed to refresh %@ Sources", comment: ""), NSNumber(value: self.errors.count))
|
||||
}
|
||||
}
|
||||
@@ -42,7 +44,9 @@ extension AppManager
|
||||
return error.localizedDescription
|
||||
} else if let error = self.errors.values.first, self.errors.count == 1 {
|
||||
return error.localizedDescription
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
var localizedDescription: String?
|
||||
|
||||
self.managedObjectContext?.performAndWait {
|
||||
|
||||
@@ -356,11 +356,11 @@ private extension MyAppsViewController
|
||||
formatter.maximumUnitCount = 1
|
||||
|
||||
|
||||
|
||||
cell.bannerView.button.setTitle(formatter.string(from: currentDate, to: installedApp.expirationDate)?.uppercased(), for: .normal)
|
||||
let timeInterval = formatter.string(from: currentDate, to: installedApp.expirationDate)
|
||||
cell.bannerView.button.setTitle(timeInterval?.uppercased(), for: .normal)
|
||||
|
||||
cell.bannerView.button.isIndicatingActivity = false
|
||||
cell.bannerView.configure(for: installedApp, action: .custom(numberOfDaysText.uppercased()))
|
||||
cell.bannerView.configure(for: installedApp, action: .custom((timeInterval?.uppercased())!))
|
||||
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||
|
||||
@@ -387,7 +387,7 @@ private extension MyAppsViewController
|
||||
cell.bannerView.button.alpha = 1.0
|
||||
}
|
||||
|
||||
cell.bannerView.accessibilityLabel? += ". " + String(format: NSLocalizedString("Expires in %@", comment: ""), numberOfDaysText)
|
||||
cell.bannerView.accessibilityLabel? += ". " + String(format: NSLocalizedString("Expires in %@", comment: ""), timeInterval!)
|
||||
|
||||
// Make sure refresh button is correct size.
|
||||
cell.layoutIfNeeded()
|
||||
@@ -529,11 +529,10 @@ private extension MyAppsViewController
|
||||
print("[ALTLog] Failed to fetch updates:", error)
|
||||
}
|
||||
|
||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isAltStorePatron, PatreonAPI.shared.isAuthenticated
|
||||
{
|
||||
self.dataSource.predicate = nil
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1023,7 +1022,7 @@ private extension MyAppsViewController
|
||||
{
|
||||
message = NSLocalizedString("Non-developer Apple IDs are limited to 3 apps. Inactive apps are backed up and uninstalled so they don't count towards your total, but will be reinstalled with all their data when activated again.", comment: "")
|
||||
|
||||
if UserDefaults.standard.ignoreActiveAppsLimit
|
||||
if UserDefaults.standard.isAppLimitDisabled
|
||||
{
|
||||
message += "\n\n"
|
||||
message += NSLocalizedString("If you're using the MacDirtyCow exploit to remove the 3-app limit, you can install up to 10 apps and app extensions instead.", comment: "")
|
||||
|
||||
@@ -360,7 +360,9 @@ private extension NewsViewController
|
||||
{
|
||||
case .failure(OperationError.cancelled): break // Ignore
|
||||
case .failure(let error):
|
||||
ToastView(error: error, opensLog: true).show(in: self)
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.opensErrorLog = true
|
||||
toastView.show(in: self)
|
||||
|
||||
case .success: print("Installed app:", storeApp.bundleIdentifier)
|
||||
}
|
||||
|
||||
@@ -244,14 +244,23 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
{
|
||||
team.isActiveTeam = false
|
||||
}
|
||||
|
||||
|
||||
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
|
||||
if team.type == .free, !UserDefaults.standard.isAppLimitDisabled, ProcessInfo().sparseRestorePatched {
|
||||
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
|
||||
} else if UserDefaults.standard.isAppLimitDisabled, !ProcessInfo().sparseRestorePatched {
|
||||
UserDefaults.standard.activeAppsLimit = 10
|
||||
} else {
|
||||
UserDefaults.standard.activeAppsLimit = nil
|
||||
|
||||
let isMinimumVersionMatching = ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
|
||||
let isSparseRestorePatched = ProcessInfo().sparseRestorePatched
|
||||
let isAppLimitDisabled = UserDefaults.standard.isAppLimitDisabled
|
||||
|
||||
UserDefaults.standard.activeAppsLimit = nil
|
||||
// TODO: @mahee96: is the minimum ver match for ios 13.3.1 check required?
|
||||
// if so what is the app limit? As nil app limit specifies unlimited apps?!
|
||||
if team.type == .free//, isMinimumVersionMatching
|
||||
{
|
||||
if (!isAppLimitDisabled && isSparseRestorePatched) ||
|
||||
(isAppLimitDisabled && !isSparseRestorePatched)
|
||||
{
|
||||
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
|
||||
}
|
||||
}
|
||||
|
||||
// Save
|
||||
@@ -620,6 +629,8 @@ private extension AuthenticationOperation
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ private extension BackgroundRefreshAppsOperation
|
||||
do
|
||||
{
|
||||
let results = try result.get()
|
||||
shouldPresentAlert = false
|
||||
shouldPresentAlert = !results.isEmpty
|
||||
|
||||
for (_, result) in results
|
||||
{
|
||||
@@ -242,6 +242,8 @@ private extension BackgroundRefreshAppsOperation
|
||||
|
||||
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
|
||||
content.body = error.localizedDescription
|
||||
|
||||
shouldPresentAlert = true
|
||||
}
|
||||
|
||||
if shouldPresentAlert
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import AltStoreCore
|
||||
/*
|
||||
|
||||
|
||||
import Nuke
|
||||
|
||||
@@ -41,7 +41,7 @@ struct BatchError: ALTLocalizedError
|
||||
return message
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@objc(ClearAppCacheOperation)
|
||||
class ClearAppCacheOperation: ResultOperation<Void>
|
||||
{
|
||||
|
||||
@@ -31,7 +31,11 @@ final class DeactivateAppOperation: ResultOperation<InstalledApp>
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.context.error { return self.finish(.failure(error)) }
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
|
||||
|
||||
@@ -19,24 +19,27 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
{
|
||||
@Managed
|
||||
private(set) var app: AppProtocol
|
||||
|
||||
|
||||
let context: InstallAppOperationContext
|
||||
|
||||
|
||||
private let appName: String
|
||||
private let bundleIdentifier: String
|
||||
private var sourceURL: URL?
|
||||
private let destinationURL: URL
|
||||
|
||||
private let session = URLSession(configuration: .default)
|
||||
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||
|
||||
|
||||
private var downloadPatreonAppContinuation: CheckedContinuation<URL, Error>?
|
||||
|
||||
|
||||
init(app: AppProtocol, destinationURL: URL, context: InstallAppOperationContext)
|
||||
{
|
||||
self.app = app
|
||||
self.context = context
|
||||
|
||||
self.appName = app.name
|
||||
self.bundleIdentifier = app.bundleIdentifier
|
||||
self.sourceURL = app.url
|
||||
self.destinationURL = destinationURL
|
||||
|
||||
super.init()
|
||||
@@ -57,23 +60,14 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
|
||||
print("Downloading App:", self.bundleIdentifier)
|
||||
|
||||
|
||||
Logger.sideload.notice("Downloading app \(self.bundleIdentifier, privacy: .public)...")
|
||||
|
||||
// Set _after_ checking self.context.error to prevent overwriting localized failure for previous errors.
|
||||
self.localizedFailure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), self.appName)
|
||||
|
||||
guard let storeApp = self.app as? StoreApp else {
|
||||
// Only StoreApp allows falling back to previous versions.
|
||||
// AppVersion can only install itself, and ALTApplication doesn't have previous versions.
|
||||
return self.download(self.app)
|
||||
}
|
||||
|
||||
|
||||
self.$app.perform { app in
|
||||
do
|
||||
{
|
||||
var appVersion: AppVersion?
|
||||
|
||||
|
||||
if let version = app as? AppVersion
|
||||
{
|
||||
appVersion = version
|
||||
@@ -83,33 +77,35 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
guard let latestVersion = storeApp.latestAvailableVersion else {
|
||||
let failureReason = String(format: NSLocalizedString("The latest version of %@ could not be determined.", comment: ""), self.appName)
|
||||
throw OperationError.unknown(failureReason: failureReason)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Attempt to download latest _available_ version, and fall back to older versions if necessary.
|
||||
appVersion = latestVersion
|
||||
}
|
||||
|
||||
|
||||
if let appVersion
|
||||
{
|
||||
try self.verify(appVersion)
|
||||
}
|
||||
|
||||
|
||||
self.download(appVersion ?? app)
|
||||
}
|
||||
catch let error as VerificationError where error.code == .iOSVersionNotSupported
|
||||
{
|
||||
guard let presentingViewController = self.context.presentingViewController, let storeApp = app.storeApp, let latestSupportedVersion = storeApp.latestSupportedVersion
|
||||
guard let presentingViewController = self.context.presentingViewController, let storeApp = app.storeApp, let latestSupportedVersion = storeApp.latestSupportedVersion,
|
||||
case let version = latestSupportedVersion.version,
|
||||
version != storeApp.installedApp?.version
|
||||
else { return self.finish(.failure(error)) }
|
||||
|
||||
|
||||
if let installedApp = storeApp.installedApp
|
||||
{
|
||||
guard !installedApp.matches(latestSupportedVersion) else { return self.finish(.failure(error)) }
|
||||
}
|
||||
|
||||
|
||||
let title = NSLocalizedString("Unsupported iOS Version", comment: "")
|
||||
let message = error.localizedDescription + "\n\n" + NSLocalizedString("Would you like to download the last version compatible with this device instead?", comment: "")
|
||||
let localizedVersion = latestSupportedVersion.localizedVersion
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in
|
||||
@@ -120,19 +116,25 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
})
|
||||
presentingViewController.present(alertController, animated: true)
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<ALTApplication, any Error>) {
|
||||
do {
|
||||
override func finish(_ result: Result<ALTApplication, Error>)
|
||||
{
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: self.temporaryDirectory)
|
||||
} catch {
|
||||
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
|
||||
Logger.sideload.error("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
|
||||
}
|
||||
|
||||
super.finish(result)
|
||||
}
|
||||
}
|
||||
@@ -153,274 +155,273 @@ private extension DownloadAppOperation
|
||||
|
||||
func download(@Managed _ app: AppProtocol)
|
||||
{
|
||||
guard let sourceURL = $app.url else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) }
|
||||
|
||||
if let appVersion = app as? AppVersion
|
||||
{
|
||||
// All downloads go through this path, and `app` is
|
||||
// always an AppVersion if downloading from a source,
|
||||
// so context.appVersion != nil means downloading from source.
|
||||
self.context.appVersion = appVersion
|
||||
}
|
||||
|
||||
self.downloadIPA(from: sourceURL) { result in
|
||||
do
|
||||
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound(name: self.appName)))
|
||||
|
||||
if let appVersion = app as? AppVersion
|
||||
{
|
||||
let application = try result.get()
|
||||
|
||||
if self.context.bundleIdentifier == StoreApp.dolphinAppID, self.context.bundleIdentifier != application.bundleIdentifier
|
||||
{
|
||||
if var infoPlist = NSDictionary(contentsOf: application.bundle.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: application.bundle.infoPlistURL, atomically: true)
|
||||
}
|
||||
}
|
||||
|
||||
self.downloadDependencies(for: application) { result in
|
||||
do
|
||||
{
|
||||
_ = try result.get()
|
||||
|
||||
try FileManager.default.copyItem(at: application.fileURL, to: self.destinationURL, shouldReplace: true)
|
||||
|
||||
guard let copiedApplication = ALTApplication(fileURL: self.destinationURL) else { throw OperationError.invalidApp }
|
||||
|
||||
Logger.sideload.notice("Downloaded app \(copiedApplication.bundleIdentifier, privacy: .public) from \(sourceURL, privacy: .public)")
|
||||
|
||||
self.finish(.success(copiedApplication))
|
||||
|
||||
self.progress.completedUnitCount += 1
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
// All downloads go through this path, and `app` is
|
||||
// always an AppVersion if downloading from a source,
|
||||
// so context.appVersion != nil means downloading from source.
|
||||
self.context.appVersion = appVersion
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
|
||||
{
|
||||
Task<Void, Never>.detached(priority: .userInitiated) {
|
||||
do
|
||||
{
|
||||
let fileURL: URL
|
||||
|
||||
if sourceURL.isFileURL
|
||||
{
|
||||
fileURL = sourceURL
|
||||
self.progress.completedUnitCount += 3
|
||||
}
|
||||
else if let host = sourceURL.host, host.lowercased().hasSuffix("patreon.com") && sourceURL.path.lowercased() == "/file"
|
||||
{
|
||||
// Patreon app
|
||||
fileURL = try await self.downloadPatreonApp(from: sourceURL)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regular app
|
||||
fileURL = try await self.downloadFile(from: sourceURL)
|
||||
}
|
||||
|
||||
defer {
|
||||
if !sourceURL.isFileURL
|
||||
{
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound(name: self.appName) }
|
||||
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let appBundleURL: URL
|
||||
|
||||
if isDirectory.boolValue
|
||||
{
|
||||
// Directory, so assuming this is .app bundle.
|
||||
guard Bundle(url: fileURL) != nil else { throw OperationError.invalidApp }
|
||||
|
||||
appBundleURL = self.temporaryDirectory.appendingPathComponent(fileURL.lastPathComponent)
|
||||
try FileManager.default.copyItem(at: fileURL, to: appBundleURL)
|
||||
}
|
||||
else
|
||||
{
|
||||
// File, so assuming this is a .ipa file.
|
||||
appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: self.temporaryDirectory)
|
||||
|
||||
// Use context's temporaryDirectory to ensure .ipa isn't deleted before we're done installing.
|
||||
let ipaURL = self.context.temporaryDirectory.appendingPathComponent("App.ipa")
|
||||
try FileManager.default.copyItem(at: fileURL, to: ipaURL)
|
||||
|
||||
self.context.ipaURL = ipaURL
|
||||
}
|
||||
|
||||
guard let application = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||
completionHandler(.success(application))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFile(from downloadURL: URL) async throws -> URL
|
||||
{
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
let downloadTask = self.session.downloadTask(with: downloadURL) { (fileURL, response, error) in
|
||||
downloadIPA(from: sourceURL!) { result in
|
||||
do
|
||||
{
|
||||
if let response = response as? HTTPURLResponse
|
||||
let application = try result.get()
|
||||
|
||||
if self.context.bundleIdentifier == StoreApp.dolphinAppID, self.context.bundleIdentifier != application.bundleIdentifier
|
||||
{
|
||||
guard response.statusCode != 403 else { throw URLError(.noPermissionsToReadFile) }
|
||||
guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: downloadURL]) }
|
||||
if var infoPlist = NSDictionary(contentsOf: application.bundle.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: application.bundle.infoPlistURL, atomically: true)
|
||||
}
|
||||
}
|
||||
|
||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||
continuation.resume(returning: fileURL)
|
||||
self.downloadDependencies(for: application) { result in
|
||||
do
|
||||
{
|
||||
_ = try result.get()
|
||||
|
||||
try FileManager.default.copyItem(at: application.fileURL, to: self.destinationURL, shouldReplace: true)
|
||||
|
||||
guard let copiedApplication = ALTApplication(fileURL: self.destinationURL) else { throw OperationError.invalidApp }
|
||||
self.finish(.success(copiedApplication))
|
||||
|
||||
self.progress.completedUnitCount += 1
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
continuation.resume(throwing: error)
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 3)
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func downloadPatreonApp(from patreonURL: URL) async throws -> URL
|
||||
{
|
||||
guard !UserDefaults.shared.skipPatreonDownloads else {
|
||||
// Skip all hacks, take user straight to Patreon post.
|
||||
return try await downloadFromPatreonPost()
|
||||
}
|
||||
|
||||
do
|
||||
func downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
|
||||
{
|
||||
// User is pledged to this app, attempt to download.
|
||||
|
||||
let fileURL = try await self.downloadFile(from: patreonURL)
|
||||
return fileURL
|
||||
Task<Void, Never>.detached(priority: .userInitiated) {
|
||||
do
|
||||
{
|
||||
let fileURL: URL
|
||||
|
||||
if sourceURL.isFileURL
|
||||
{
|
||||
fileURL = sourceURL
|
||||
self.progress.completedUnitCount += 3
|
||||
}
|
||||
else if let host = sourceURL.host, host.lowercased().hasSuffix("patreon.com") && sourceURL.path.lowercased() == "/file"
|
||||
{
|
||||
// Patreon app
|
||||
fileURL = try await downloadPatreonApp(from: sourceURL)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regular app
|
||||
fileURL = try await downloadFile(from: sourceURL)
|
||||
}
|
||||
|
||||
defer {
|
||||
if !sourceURL.isFileURL
|
||||
{
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound(name: self.appName) }
|
||||
|
||||
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let appBundleURL: URL
|
||||
|
||||
if isDirectory.boolValue
|
||||
{
|
||||
// Directory, so assuming this is .app bundle.
|
||||
guard Bundle(url: fileURL) != nil else { throw OperationError.invalidApp }
|
||||
|
||||
appBundleURL = self.temporaryDirectory.appendingPathComponent(fileURL.lastPathComponent)
|
||||
try FileManager.default.copyItem(at: fileURL, to: appBundleURL)
|
||||
}
|
||||
else
|
||||
{
|
||||
// File, so assuming this is a .ipa file.
|
||||
appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: self.temporaryDirectory)
|
||||
|
||||
// Use context's temporaryDirectory to ensure .ipa isn't deleted before we're done installing.
|
||||
let ipaURL = self.context.temporaryDirectory.appendingPathComponent("App.ipa")
|
||||
try FileManager.default.copyItem(at: fileURL, to: ipaURL)
|
||||
|
||||
self.context.ipaURL = ipaURL
|
||||
}
|
||||
|
||||
guard let application = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||
completionHandler(.success(application))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch URLError.noPermissionsToReadFile
|
||||
|
||||
func downloadFile(from downloadURL: URL) async throws -> URL
|
||||
{
|
||||
guard let presentingViewController = self.context.presentingViewController else { throw OperationError.pledgeRequired(appName: self.appName) }
|
||||
|
||||
// Attempt to sign-in again in case our Patreon session has expired.
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
PatreonAPI.shared.authenticate(presentingViewController: presentingViewController) { result in
|
||||
let downloadTask = self.session.downloadTask(with: downloadURL) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
let account = try result.get()
|
||||
try account.managedObjectContext?.save()
|
||||
if let response = response as? HTTPURLResponse
|
||||
{
|
||||
guard response.statusCode != 403 else { throw URLError(.noPermissionsToReadFile) }
|
||||
guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: downloadURL]) }
|
||||
}
|
||||
|
||||
continuation.resume()
|
||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
continuation.resume(returning: fileURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 3)
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func downloadPatreonApp(from patreonURL: URL) async throws -> URL
|
||||
{
|
||||
guard !UserDefaults.shared.skipPatreonDownloads else {
|
||||
// Skip all hacks, take user straight to Patreon post.
|
||||
return try await downloadFromPatreonPost()
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
// Success, so try to download once more now that we're definitely authenticated.
|
||||
// User is pledged to this app, attempt to download.
|
||||
|
||||
let fileURL = try await self.downloadFile(from: patreonURL)
|
||||
let fileURL = try await downloadFile(from: patreonURL)
|
||||
return fileURL
|
||||
}
|
||||
catch URLError.noPermissionsToReadFile
|
||||
{
|
||||
// We know authentication succeeded, so failure must mean user isn't patron/on the correct tier,
|
||||
// or that our hacky workaround for downloading Patreon attachments has failed.
|
||||
// Either way, taking them directly to the post serves as a decent fallback.
|
||||
guard let presentingViewController = self.context.presentingViewController else { throw OperationError.pledgeRequired(appName: self.appName) }
|
||||
|
||||
return try await downloadFromPatreonPost()
|
||||
// Attempt to sign-in again in case our Patreon session has expired.
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
PatreonAPI.shared.authenticate(presentingViewController: presentingViewController) { result in
|
||||
do
|
||||
{
|
||||
let account = try result.get()
|
||||
try account.managedObjectContext?.save()
|
||||
|
||||
continuation.resume()
|
||||
}
|
||||
catch
|
||||
{
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
// Success, so try to download once more now that we're definitely authenticated.
|
||||
|
||||
let fileURL = try await downloadFile(from: patreonURL)
|
||||
return fileURL
|
||||
}
|
||||
catch URLError.noPermissionsToReadFile
|
||||
{
|
||||
// We know authentication succeeded, so failure must mean user isn't patron/on the correct tier,
|
||||
// or that our hacky workaround for downloading Patreon attachments has failed.
|
||||
// Either way, taking them directly to the post serves as a decent fallback.
|
||||
|
||||
return try await downloadFromPatreonPost()
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFromPatreonPost() async throws -> URL
|
||||
{
|
||||
guard let presentingViewController = self.context.presentingViewController else { throw OperationError.pledgeRequired(appName: self.appName) }
|
||||
|
||||
let downloadURL: URL
|
||||
|
||||
if let components = URLComponents(url: patreonURL, resolvingAgainstBaseURL: false),
|
||||
let postItem = components.queryItems?.first(where: { $0.name == "h" }),
|
||||
let postID = postItem.value,
|
||||
let patreonPostURL = URL(string: "https://www.patreon.com/posts/" + postID)
|
||||
{
|
||||
downloadURL = patreonPostURL
|
||||
}
|
||||
else
|
||||
{
|
||||
downloadURL = patreonURL
|
||||
}
|
||||
|
||||
return try await downloadFromPatreon(downloadURL, presentingViewController: presentingViewController)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFromPatreonPost() async throws -> URL
|
||||
@MainActor
|
||||
func downloadFromPatreon(_ patreonURL: URL, presentingViewController: UIViewController) async throws -> URL
|
||||
{
|
||||
guard let presentingViewController = self.context.presentingViewController else { throw OperationError.pledgeRequired(appName: self.appName) }
|
||||
let webViewController = WebViewController(url: patreonURL)
|
||||
webViewController.delegate = self
|
||||
webViewController.webView.navigationDelegate = self
|
||||
|
||||
let navigationController = UINavigationController(rootViewController: webViewController)
|
||||
presentingViewController.present(navigationController, animated: true)
|
||||
|
||||
let downloadURL: URL
|
||||
|
||||
if let components = URLComponents(url: patreonURL, resolvingAgainstBaseURL: false),
|
||||
let postItem = components.queryItems?.first(where: { $0.name == "h" }),
|
||||
let postID = postItem.value,
|
||||
let patreonPostURL = URL(string: "https://www.patreon.com/posts/" + postID)
|
||||
do
|
||||
{
|
||||
downloadURL = patreonPostURL
|
||||
}
|
||||
else
|
||||
{
|
||||
downloadURL = patreonURL
|
||||
defer {
|
||||
navigationController.dismiss(animated: true)
|
||||
}
|
||||
|
||||
downloadURL = try await withCheckedThrowingContinuation { continuation in
|
||||
self.downloadPatreonAppContinuation = continuation
|
||||
}
|
||||
}
|
||||
|
||||
return try await self.downloadFromPatreon(downloadURL, presentingViewController: presentingViewController)
|
||||
let fileURL = try await downloadFile(from: downloadURL)
|
||||
return fileURL
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func downloadFromPatreon(_ patreonURL: URL, presentingViewController: UIViewController) async throws -> URL
|
||||
{
|
||||
let webViewController = WebViewController(url: patreonURL)
|
||||
webViewController.delegate = self
|
||||
webViewController.webView.navigationDelegate = self
|
||||
|
||||
let navigationController = UINavigationController(rootViewController: webViewController)
|
||||
presentingViewController.present(navigationController, animated: true)
|
||||
|
||||
let downloadURL: URL
|
||||
|
||||
do
|
||||
{
|
||||
defer {
|
||||
navigationController.dismiss(animated: true)
|
||||
}
|
||||
|
||||
downloadURL = try await withCheckedThrowingContinuation { continuation in
|
||||
self.downloadPatreonAppContinuation = continuation
|
||||
}
|
||||
}
|
||||
|
||||
let fileURL = try await self.downloadFile(from: downloadURL)
|
||||
return fileURL
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadAppOperation: WebViewControllerDelegate
|
||||
{
|
||||
func webViewControllerDidFinish(_ webViewController: WebViewController)
|
||||
func webViewControllerDidFinish(_ webViewController: WebViewController)
|
||||
{
|
||||
guard let continuation = self.downloadPatreonAppContinuation else { return }
|
||||
self.downloadPatreonAppContinuation = nil
|
||||
|
||||
|
||||
continuation.resume(throwing: CancellationError())
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadAppOperation: WKNavigationDelegate
|
||||
{
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy
|
||||
{
|
||||
guard #available(iOS 14.5, *), navigationAction.shouldPerformDownload else { return .allow }
|
||||
|
||||
|
||||
guard let continuation = self.downloadPatreonAppContinuation else { return .allow }
|
||||
self.downloadPatreonAppContinuation = nil
|
||||
|
||||
|
||||
if let downloadURL = navigationAction.request.url
|
||||
{
|
||||
continuation.resume(returning: downloadURL)
|
||||
@@ -429,19 +430,19 @@ extension DownloadAppOperation: WKNavigationDelegate
|
||||
{
|
||||
continuation.resume(throwing: URLError(.badURL))
|
||||
}
|
||||
|
||||
|
||||
return .cancel
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy
|
||||
{
|
||||
// Called for Patreon attachments
|
||||
|
||||
|
||||
guard !navigationResponse.canShowMIMEType else { return .allow }
|
||||
|
||||
|
||||
guard let continuation = self.downloadPatreonAppContinuation else { return .allow }
|
||||
self.downloadPatreonAppContinuation = nil
|
||||
|
||||
|
||||
guard let response = navigationResponse.response as? HTTPURLResponse, let responseURL = response.url,
|
||||
let mimeType = response.mimeType, let type = UTType(mimeType: mimeType),
|
||||
type.conforms(to: .ipa) || type.conforms(to: .zip) || type.conforms(to: .application)
|
||||
@@ -449,9 +450,9 @@ extension DownloadAppOperation: WKNavigationDelegate
|
||||
continuation.resume(throwing: OperationError.invalidApp)
|
||||
return .cancel
|
||||
}
|
||||
|
||||
|
||||
continuation.resume(returning: responseURL)
|
||||
|
||||
|
||||
return .cancel
|
||||
}
|
||||
}
|
||||
@@ -546,7 +547,7 @@ private extension DownloadAppOperation
|
||||
}
|
||||
catch let error as DecodingError
|
||||
{
|
||||
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("The dependencies for %@ could not be determined.", comment: ""), application.name))
|
||||
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not determine dependencies for %@.", comment: ""), application.name))
|
||||
completionHandler(.failure(nsError))
|
||||
}
|
||||
catch
|
||||
@@ -578,7 +579,7 @@ private extension DownloadAppOperation
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
let localizedFailure = String(format: NSLocalizedString("The dependency “%@” could not be downloaded.", comment: ""), dependency.preferredFilename)
|
||||
let localizedFailure = String(format: NSLocalizedString("The dependency '%@' could not be downloaded.", comment: ""), dependency.preferredFilename)
|
||||
completionHandler(.failure(error.withLocalizedFailure(localizedFailure)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,17 +15,12 @@ extension OperationError
|
||||
{
|
||||
enum Code: Int, ALTErrorCode, CaseIterable {
|
||||
typealias Error = OperationError
|
||||
|
||||
|
||||
// General
|
||||
case unknown = 1000
|
||||
case unknownResult = 1001
|
||||
// case cancelled = 1002
|
||||
case cancelled = 1002
|
||||
case timedOut = 1003
|
||||
case unableToConnectSideJIT
|
||||
case unableToRespondSideJITDevice
|
||||
case wrongSideJITIP
|
||||
case SideJITIssue // (error: String)
|
||||
case refreshsidejit
|
||||
case notAuthenticated = 1004
|
||||
case appNotFound = 1005
|
||||
case unknownUDID = 1006
|
||||
@@ -35,18 +30,11 @@ extension OperationError
|
||||
case noSources = 1010
|
||||
case openAppFailed = 1011
|
||||
case missingAppGroup = 1012
|
||||
case refreshAppFailed
|
||||
|
||||
// Connection
|
||||
case noWiFi = 1200
|
||||
case tooNewError
|
||||
case anisetteV1Error//(message: String)
|
||||
case provisioningError//(result: String, message: String?)
|
||||
case anisetteV3Error//(message: String)
|
||||
|
||||
case cacheClearError//(errors: [String])
|
||||
case forbidden = 1013
|
||||
case sourceNotAdded = 1014
|
||||
|
||||
|
||||
// Connection
|
||||
|
||||
/* Connection */
|
||||
case serverNotFound = 1200
|
||||
@@ -56,6 +44,20 @@ extension OperationError
|
||||
/* Pledges */
|
||||
case pledgeRequired = 1401
|
||||
case pledgeInactive = 1402
|
||||
|
||||
/* SideStore Only */
|
||||
case unableToConnectSideJIT
|
||||
case unableToRespondSideJITDevice
|
||||
case wrongSideJITIP
|
||||
case SideJITIssue // (error: String)
|
||||
case refreshsidejit
|
||||
case refreshAppFailed
|
||||
case tooNewError
|
||||
case anisetteV1Error//(message: String)
|
||||
case provisioningError//(result: String, message: String?)
|
||||
case anisetteV3Error//(message: String)
|
||||
case cacheClearError//(errors: [String])
|
||||
case noWiFi
|
||||
}
|
||||
|
||||
static var cancelled: CancellationError { CancellationError() }
|
||||
@@ -70,13 +72,13 @@ extension OperationError
|
||||
static let invalidApp: OperationError = .init(code: .invalidApp)
|
||||
static let noSources: OperationError = .init(code: .noSources)
|
||||
static let missingAppGroup: OperationError = .init(code: .missingAppGroup)
|
||||
|
||||
|
||||
static let noWiFi: OperationError = .init(code: .noWiFi)
|
||||
static let tooNewError: OperationError = .init(code: .tooNewError)
|
||||
static let provisioningError: OperationError = .init(code: .provisioningError)
|
||||
static let anisetteV1Error: OperationError = .init(code: .anisetteV1Error)
|
||||
static let anisetteV3Error: OperationError = .init(code: .anisetteV3Error)
|
||||
|
||||
|
||||
static let cacheClearError: OperationError = .init(code: .cacheClearError)
|
||||
|
||||
static func unknown(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
|
||||
@@ -126,6 +128,7 @@ extension OperationError
|
||||
|
||||
static func invalidParameters(_ message: String? = nil) -> OperationError {
|
||||
OperationError(code: .invalidParameters, failureReason: message)
|
||||
}
|
||||
|
||||
static func forbidden(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
|
||||
OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line)
|
||||
@@ -168,8 +171,8 @@ struct OperationError: ALTLocalizedError {
|
||||
private var _failureReason: String?
|
||||
|
||||
private init(code: Code, failureReason: String? = nil,
|
||||
appName: String? = nil, requiredAppIDs: Int? = nil, availableAppIDs: Int? = nil,
|
||||
expirationDate: Date? = nil, sourceFile: String? = nil, sourceLine: UInt? = nil){
|
||||
appName: String? = nil, sourceName: String? = nil, requiredAppIDs: Int? = nil,
|
||||
availableAppIDs: Int? = nil, expirationDate: Date? = nil, sourceFile: String? = nil, sourceLine: UInt? = nil){
|
||||
self.code = code
|
||||
self._failureReason = failureReason
|
||||
|
||||
@@ -245,7 +248,6 @@ struct OperationError: ALTLocalizedError {
|
||||
}
|
||||
|
||||
}
|
||||
private var _failureReason: String?
|
||||
|
||||
var recoverySuggestion: String? {
|
||||
switch self.code
|
||||
|
||||
@@ -16,7 +16,7 @@ extension VerificationError
|
||||
typealias Error = VerificationError
|
||||
|
||||
// Legacy
|
||||
// case privateEntitlements = 0
|
||||
// case privateEntitlements = 0
|
||||
|
||||
case mismatchedBundleIdentifiers = 1
|
||||
case iOSVersionNotSupported = 2
|
||||
@@ -28,7 +28,11 @@ extension VerificationError
|
||||
case undeclaredPermissions = 6
|
||||
case addedPermissions = 7
|
||||
}
|
||||
|
||||
|
||||
// static func privateEntitlements(_ entitlements: [String: Any], app: ALTApplication) -> VerificationError {
|
||||
// VerificationError(code: .privateEntitlements, app: app, entitlements: entitlements)
|
||||
// }
|
||||
|
||||
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
|
||||
VerificationError(code: .mismatchedBundleIdentifiers, app: app, sourceBundleID: sourceBundleID)
|
||||
}
|
||||
@@ -108,6 +112,10 @@ struct VerificationError: ALTLocalizedError
|
||||
var errorFailureReason: String {
|
||||
switch self.code
|
||||
{
|
||||
// case .privateEntitlements:
|
||||
// let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
||||
// return String(formatted: "“%@” requires private permissions.", appName)
|
||||
|
||||
case .mismatchedBundleIdentifiers:
|
||||
if let appBundleID = self.$app.bundleIdentifier, let bundleID = self.sourceBundleID
|
||||
{
|
||||
|
||||
@@ -46,9 +46,7 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
else {
|
||||
return self.finish(.failure(OperationError.invalidParameters("InstallAppOperation.main: self.context.certificate or self.context.resignedApp or self.context.provisioningProfiles is nil")))
|
||||
}
|
||||
|
||||
Logger.sideload.notice("Installing resigned app \(resignedApp.bundleIdentifier, privacy: .public)...")
|
||||
|
||||
|
||||
@Managed var appVersion = self.context.appVersion
|
||||
let storeBuildVersion = $appVersion.buildVersion
|
||||
|
||||
@@ -257,7 +255,6 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
catch
|
||||
{
|
||||
print("Failed to remove refreshed .ipa: \(error)")
|
||||
Logger.sideload.error("Failed to remove refreshed .ipa: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,43 +264,6 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
|
||||
private extension InstallAppOperation
|
||||
{
|
||||
func receive(from connection: ServerConnection, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
connection.receiveResponse() { (result) in
|
||||
do
|
||||
{
|
||||
let response = try result.get()
|
||||
|
||||
switch response
|
||||
{
|
||||
case .installationProgress(let response):
|
||||
Logger.sideload.debug("Installing \(self.context.resignedApp?.bundleIdentifier ?? self.context.bundleIdentifier, privacy: .public)... \((response.progress * 100).rounded())%")
|
||||
|
||||
if response.progress == 1.0
|
||||
{
|
||||
self.progress.completedUnitCount = self.progress.totalUnitCount
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.progress.completedUnitCount = Int64(response.progress * 100)
|
||||
self.receive(from: connection, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
case .error(let response):
|
||||
completionHandler(.failure(response.error))
|
||||
|
||||
default:
|
||||
completionHandler(.failure(ALTServerError(.unknownRequest)))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cleanUp()
|
||||
{
|
||||
guard !self.didCleanUp else { return }
|
||||
@@ -315,7 +275,7 @@ private extension InstallAppOperation
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.sideload.error("Failed to remove temporary directory: \(error.localizedDescription, privacy: .public)")
|
||||
print("Failed to remove temporary directory.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@ final class SendAppOperation: ResultOperation<()>
|
||||
guard let resignedApp = self.context.resignedApp else {
|
||||
return self.finish(.failure(OperationError.invalidParameters("SendAppOperation.main: self.resignedApp is nil")))
|
||||
}
|
||||
|
||||
Logger.sideload.notice("Sending app \(self.context.bundleIdentifier, privacy: .public) to AltServer \(server.localizedName ?? "nil", privacy: .public)...")
|
||||
|
||||
|
||||
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
|
||||
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL, storeApp: nil)
|
||||
let fileURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
|
||||
@@ -13,87 +13,6 @@ import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
extension VerificationError
|
||||
{
|
||||
enum Code: Int, ALTErrorCode, CaseIterable {
|
||||
typealias Error = VerificationError
|
||||
|
||||
case privateEntitlements
|
||||
case mismatchedBundleIdentifiers
|
||||
case iOSVersionNotSupported
|
||||
}
|
||||
|
||||
static func privateEntitlements(_ entitlements: [String: Any], app: ALTApplication) -> VerificationError {
|
||||
VerificationError(code: .privateEntitlements, app: app, entitlements: entitlements)
|
||||
}
|
||||
|
||||
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
|
||||
VerificationError(code: .mismatchedBundleIdentifiers, app: app, sourceBundleID: sourceBundleID)
|
||||
}
|
||||
|
||||
static func iOSVersionNotSupported(app: AppProtocol, osVersion: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion, requiredOSVersion: OperatingSystemVersion?) -> VerificationError {
|
||||
VerificationError(code: .iOSVersionNotSupported, app: app)
|
||||
}
|
||||
}
|
||||
|
||||
struct VerificationError: ALTLocalizedError {
|
||||
let code: Code
|
||||
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
@Managed var app: AppProtocol?
|
||||
var sourceBundleID: String?
|
||||
var deviceOSVersion: OperatingSystemVersion?
|
||||
var requiredOSVersion: OperatingSystemVersion?
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self.code {
|
||||
case .iOSVersionNotSupported:
|
||||
guard let deviceOSVersion else { return nil }
|
||||
|
||||
var failureReason = self.errorFailureReason
|
||||
if self.app == nil {
|
||||
let firstLetter = failureReason.prefix(1).lowercased()
|
||||
failureReason = firstLetter + failureReason.dropFirst()
|
||||
}
|
||||
|
||||
return String(formatted: "This device is running iOS %@, but %@", deviceOSVersion.stringValue, failureReason)
|
||||
default: return nil
|
||||
}
|
||||
|
||||
return self.errorFailureReason
|
||||
}
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self.code
|
||||
{
|
||||
case .privateEntitlements:
|
||||
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
||||
return String(formatted: "“%@” requires private permissions.", appName)
|
||||
|
||||
case .mismatchedBundleIdentifiers:
|
||||
if let appBundleID = self.$app.bundleIdentifier, let bundleID = self.sourceBundleID {
|
||||
return String(formatted: "The bundle ID '%@' does not match the one specified by the source ('%@').", appBundleID, bundleID)
|
||||
} else {
|
||||
return NSLocalizedString("The bundle ID does not match the one specified by the source.", comment: "")
|
||||
}
|
||||
|
||||
case .iOSVersionNotSupported:
|
||||
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
||||
let deviceOSVersion = self.deviceOSVersion ?? ProcessInfo.processInfo.operatingSystemVersion
|
||||
|
||||
guard let requiredOSVersion else {
|
||||
return String(formatted: "%@ does not support iOS %@.", appName, deviceOSVersion.stringValue)
|
||||
}
|
||||
if deviceOSVersion > requiredOSVersion {
|
||||
return String(formatted: "%@ requires iOS %@ or earlier", appName, requiredOSVersion.stringValue)
|
||||
} else {
|
||||
return String(formatted: "%@ requires iOS %@ or later", appName, requiredOSVersion.stringValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import RegexBuilder
|
||||
|
||||
private extension ALTEntitlement
|
||||
|
||||
Binary file not shown.
22
AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/Contents.json
vendored
Normal file
22
AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "sound@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "sound@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/sound@2x.png
vendored
Normal file
BIN
AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/sound@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/sound@3x.png
vendored
Normal file
BIN
AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/sound@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
22
AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/Contents.json
vendored
Normal file
22
AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "fetch@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "fetch@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/fetch@2x.png
vendored
Normal file
BIN
AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/fetch@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/fetch@3x.png
vendored
Normal file
BIN
AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/fetch@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
22
AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/Contents.json
vendored
Normal file
22
AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "photos@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "photos@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/photos@2x.png
vendored
Normal file
BIN
AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/photos@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/photos@3x.png
vendored
Normal file
BIN
AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/photos@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -44,7 +44,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate
|
||||
|
||||
PatreonAPI.shared.refreshPatreonAccount()
|
||||
}
|
||||
|
||||
|
||||
func sceneDidEnterBackground(_ scene: UIScene)
|
||||
{
|
||||
// Called as the scene transitions from the foreground to the background.
|
||||
@@ -54,7 +54,10 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate
|
||||
guard UIApplication.shared.applicationState == .background else { return }
|
||||
|
||||
// Make sure to update AppDelegate.applicationDidEnterBackground() as well.
|
||||
|
||||
|
||||
// TODO: @mahee96: find if we need to stop em_proxy as in altstore?
|
||||
// stop_em_proxy()
|
||||
|
||||
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
|
||||
|
||||
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
|
||||
|
||||
@@ -11,17 +11,17 @@
|
||||
<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="AboutHeader" id="xq2-Pl-zaG" customClass="AboutPatreonHeaderView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="390" height="682"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="445"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="25" translatesAutoresizingMaskIntoConstraints="NO" id="XiA-Jf-XMp">
|
||||
<rect key="frame" x="16" y="2" width="358" height="630"/>
|
||||
<rect key="frame" x="16" y="2" width="343" height="393"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="5Ol-zN-wYv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="358" height="426"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="253"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="f7H-EV-7Sx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="358" height="55"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="55"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="SideStore" translatesAutoresizingMaskIntoConstraints="NO" id="pn6-Ic-MJm">
|
||||
<rect key="frame" x="0.0" y="0.0" width="55" height="55"/>
|
||||
@@ -31,19 +31,19 @@
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="si2-MA-3RH">
|
||||
<rect key="frame" x="65" y="0.0" width="293" height="55"/>
|
||||
<rect key="frame" x="65" y="7.5" width="213" height="40.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="hkS-oz-wiT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="83" height="55"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="100.5" height="40.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideTeam" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="DTd-Yu-HXr">
|
||||
<rect key="frame" x="0.0" y="0.0" width="83" height="21.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="100.5" height="21.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="18"/>
|
||||
<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="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nPA-DN-RTG" userLabel=" ">
|
||||
<rect key="frame" x="0.0" y="23.5" width="83" height="31.5"/>
|
||||
<rect key="frame" x="0.0" y="23.5" width="100.5" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="1" alpha="0.65000000000000002" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -51,16 +51,16 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="TFB-qo-cbh">
|
||||
<rect key="frame" x="210" y="0.0" width="83" height="55"/>
|
||||
<rect key="frame" x="127" y="0.0" width="86" height="40.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Zpb-k3-y7l">
|
||||
<rect key="frame" x="0.0" y="0.0" width="83" height="50"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="86" height="21.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="18"/>
|
||||
<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" ambiguous="YES" text="" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VOu-b8-uEL">
|
||||
<rect key="frame" x="0.0" y="52" width="83" height="3"/>
|
||||
<rect key="frame" x="0.0" y="23.5" width="86" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="1" alpha="0.65000000000000002" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -69,13 +69,21 @@
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Shane" translatesAutoresizingMaskIntoConstraints="NO" id="F6g-4g-Gr2">
|
||||
<rect key="frame" x="288" y="0.0" width="55" height="55"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="55" id="CaK-rR-Zjy"/>
|
||||
<constraint firstAttribute="width" secondItem="F6g-4g-Gr2" secondAttribute="height" id="geK-Xu-ybL"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="55" id="Uiy-9X-WSO"/>
|
||||
<constraint firstItem="F6g-4g-Gr2" firstAttribute="width" secondItem="F6g-4g-Gr2" secondAttribute="height" id="cCw-He-Yyc"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FeG-e5-LJl">
|
||||
<rect key="frame" x="0.0" y="65" width="358" height="361"/>
|
||||
<rect key="frame" x="0.0" y="65" width="343" height="188"/>
|
||||
<color key="backgroundColor" white="1" alpha="0.13" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<string key="text">Thank you for using SideStore!
|
||||
|
||||
@@ -91,10 +99,10 @@ Following us on social media allows us to give quick updates and spread the word
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="13" translatesAutoresizingMaskIntoConstraints="NO" id="QS9-vO-bj8">
|
||||
<rect key="frame" x="0.0" y="451" width="358" height="179"/>
|
||||
<rect key="frame" x="0.0" y="278" width="343" height="115"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="yEi-L6-kQ8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="358" height="51"/>
|
||||
<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="l4o-vb-cMy"/>
|
||||
@@ -105,7 +113,7 @@ Following us on social media allows us to give quick updates and spread the word
|
||||
</state>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hov-Ce-LaM" userLabel="Twitter Button">
|
||||
<rect key="frame" x="0.0" y="64" width="358" height="51"/>
|
||||
<rect key="frame" x="0.0" y="64" width="343" height="51"/>
|
||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="51" id="m0M-GX-KKG"/>
|
||||
@@ -146,7 +154,7 @@ Following us on social media allows us to give quick updates and spread the word
|
||||
<outlet property="textView" destination="FeG-e5-LJl" id="K0M-lF-I6u"/>
|
||||
<outlet property="twitterButton" destination="hov-Ce-LaM" id="gib-Lt-qtY"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="147.82608695652175" y="58.258928571428569"/>
|
||||
<point key="canvasLocation" x="138" y="138"/>
|
||||
</collectionReusableView>
|
||||
</objects>
|
||||
<resources>
|
||||
|
||||
@@ -17,7 +17,7 @@ import Nuke
|
||||
|
||||
import QuickLook
|
||||
|
||||
final class ErrorLogViewController: UITableViewController
|
||||
final class ErrorLogViewController: UITableViewController, QLPreviewControllerDelegate
|
||||
{
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
private var expandedErrorIDs = Set<NSManagedObjectID>()
|
||||
@@ -28,6 +28,7 @@ final class ErrorLogViewController: UITableViewController
|
||||
self.updateButtonInteractivity()
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var timeFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .none
|
||||
@@ -126,11 +127,18 @@ private extension ErrorLogViewController
|
||||
])
|
||||
|
||||
cell.menuButton.menu = menu
|
||||
cell.menuButton.showsMenuAsPrimaryAction = self.isScrolling ? false : true
|
||||
cell.selectionStyle = .none
|
||||
} else {
|
||||
cell.menuButton.isUserInteractionEnabled = false
|
||||
|
||||
if self.isScrolling
|
||||
{
|
||||
cell.menuButton.showsMenuAsPrimaryAction = false
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.menuButton.showsMenuAsPrimaryAction = true
|
||||
}
|
||||
|
||||
cell.selectionStyle = .none
|
||||
|
||||
// Include errorDescriptionTextView's text in cell summary.
|
||||
cell.accessibilityLabel = [cell.errorFailureLabel.text, cell.dateLabel.text, cell.errorCodeLabel.text, cell.errorDescriptionTextView.text].compactMap { $0 }.joined(separator: ". ")
|
||||
|
||||
@@ -429,38 +437,45 @@ extension ErrorLogViewController
|
||||
{
|
||||
for case let cell as ErrorLogTableViewCell in self.tableView.visibleCells
|
||||
{
|
||||
cell.menuButton.showsMenuAsPrimaryAction = self.isScrolling ? false : true
|
||||
if self.isScrolling
|
||||
{
|
||||
cell.menuButton.showsMenuAsPrimaryAction = false
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.menuButton.showsMenuAsPrimaryAction = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ErrorLogViewController: QLPreviewControllerDataSource, QLPreviewControllerDelegate
|
||||
{
|
||||
func numberOfPreviewItems(in controller: QLPreviewController) -> Int
|
||||
{
|
||||
return 1
|
||||
}
|
||||
|
||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem
|
||||
{
|
||||
return (_exportedLogURL as? NSURL) ?? NSURL()
|
||||
}
|
||||
|
||||
func previewControllerDidDismiss(_ controller: QLPreviewController)
|
||||
{
|
||||
guard let exportedLogURL = _exportedLogURL else { return }
|
||||
|
||||
let parentDirectory = exportedLogURL.deletingLastPathComponent()
|
||||
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: parentDirectory)
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.main.error("Failed to remove temporary log directory \(parentDirectory.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
||||
_exportedLogURL = nil
|
||||
}
|
||||
}
|
||||
//extension ErrorLogViewController: QLPreviewControllerDataSource, QLPreviewControllerDelegate
|
||||
//{
|
||||
// func numberOfPreviewItems(in controller: QLPreviewController) -> Int
|
||||
// {
|
||||
// return 1
|
||||
// }
|
||||
//
|
||||
// func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem
|
||||
// {
|
||||
// return (_exportedLogURL as? NSURL) ?? NSURL()
|
||||
// }
|
||||
//
|
||||
// func previewControllerDidDismiss(_ controller: QLPreviewController)
|
||||
// {
|
||||
// guard let exportedLogURL = _exportedLogURL else { return }
|
||||
//
|
||||
// let parentDirectory = exportedLogURL.deletingLastPathComponent()
|
||||
//
|
||||
// do
|
||||
// {
|
||||
// try FileManager.default.removeItem(at: parentDirectory)
|
||||
// }
|
||||
// catch
|
||||
// {
|
||||
// Logger.main.error("Failed to remove temporary log directory \(parentDirectory.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
||||
// }
|
||||
//
|
||||
// _exportedLogURL = nil
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -76,13 +76,13 @@ final class AboutPatreonHeaderView: UICollectionReusableView
|
||||
self.textView.layer.cornerRadius = 20
|
||||
self.textView.textContainer.lineFragmentPadding = 0
|
||||
|
||||
for imageView in [self.rileyImageView, self.shaneImageView].compactMap({ $0 })
|
||||
for imageView in [self.rileyImageView!, self.shaneImageView!]
|
||||
{
|
||||
imageView.clipsToBounds = true
|
||||
imageView.layer.cornerRadius = imageView.bounds.midY
|
||||
}
|
||||
|
||||
for button in [self.supportButton, self.accountButton, self.twitterButton, self.instagramButton].compactMap({ $0 })
|
||||
for button in [self.supportButton!, self.accountButton!]
|
||||
{
|
||||
button.clipsToBounds = true
|
||||
button.layer.cornerRadius = 16
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
<tableViewSection headerTitle="" id="flW-d4-bco">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="7lu-Yk-87t" rowHeight="51" style="IBUITableViewCellStyleDefault" id="DzJ-TL-jvR" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="22" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="39.5" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="DzJ-TL-jvR" id="XnZ-bO-peM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -133,7 +133,7 @@
|
||||
<tableViewSection headerTitle="" id="CAI-9J-8fR">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="xvY-lN-Toz" detailTextLabel="CnN-M1-AYK" rowHeight="51" style="IBUITableViewCellStyleValue1" id="kCH-yh-bMk" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="113" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="130.5" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="kCH-yh-bMk" id="MQ9-Qn-bWg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -147,7 +147,7 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Side Team" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" id="CnN-M1-AYK">
|
||||
<rect key="frame" x="262.5" y="16" width="82.5" height="20.5"/>
|
||||
<rect key="frame" x="252.5" y="16" width="92.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -165,7 +165,7 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="rAc-lQ-B1k" detailTextLabel="0uP-Cd-tNX" rowHeight="51" style="IBUITableViewCellStyleValue1" id="q11-3k-oIm" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="164" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="181.5" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="q11-3k-oIm" id="QCY-a8-Lhx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -179,7 +179,7 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="SideTeam@SideStore.io" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" id="0uP-Cd-tNX">
|
||||
<rect key="frame" x="156" y="16" width="189" height="20.5"/>
|
||||
<rect key="frame" x="215.5" y="16" width="129.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -197,7 +197,7 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="Sge-cM-Fw9" detailTextLabel="434-MW-Den" rowHeight="51" style="IBUITableViewCellStyleValue1" id="vuc-eX-w3f" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="215" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="232.5" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="vuc-eX-w3f" id="wpD-YB-mrf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -211,7 +211,7 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Developers" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" id="434-MW-Den">
|
||||
<rect key="frame" x="255" y="16" width="90" height="20.5"/>
|
||||
<rect key="frame" x="264" y="16" width="81" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -233,14 +233,14 @@
|
||||
<tableViewSection id="YHi-gR-wed">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="R1C-Gr-xD4" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="302" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="319.5" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="R1C-Gr-xD4" id="Ojx-7f-z7E">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Support the team" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3Il-5a-5Zp">
|
||||
<rect key="frame" x="30" y="15.5" width="142.5" height="20.5"/>
|
||||
<rect key="frame" x="30" y="15.5" width="140.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -313,14 +313,14 @@
|
||||
<tableViewSection headerTitle="" id="2dM-lg-cRI">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Rra-U5-kCd" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="393" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="480" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Rra-U5-kCd" id="8gV-kx-lGe">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Background Refresh" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="EbG-HB-IOn">
|
||||
<rect key="frame" x="30" y="15.5" width="166" height="20.5"/>
|
||||
<rect key="frame" x="30" y="15" width="166" height="21"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -412,15 +412,16 @@
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="7PQ-AW-GcV" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="495" width="375" height="51"/>
|
||||
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="amC-sE-8O0" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="531" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="7PQ-AW-GcV" id="wQ8-9w-iiw">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Disable App Limit" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="W95-fD-NAd" userLabel="Disable App Limit">
|
||||
<rect key="frame" x="30" y="15.5" width="142.5" height="20.5"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Allow Siri To Refresh Apps…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c6K-fI-CVr">
|
||||
<rect key="frame" x="30" y="15" width="101" height="21"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -453,14 +454,14 @@
|
||||
<tableViewSection headerTitle="" id="eHy-qI-w5w">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="30h-59-88f" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="637" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="622" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="30h-59-88f" id="7qD-DW-Jls">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="How it works" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2CC-iw-3bd">
|
||||
<rect key="frame" x="30" y="15.5" width="105" height="20.5"/>
|
||||
<rect key="frame" x="30" y="15" width="105" height="21"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -561,29 +562,29 @@
|
||||
<tableViewSection headerTitle="" id="J90-vn-u2O">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="i4T-2q-jF3" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="728" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="851" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="i4T-2q-jF3" id="VTQ-H4-aCM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Developers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hRA-OK-Vjw">
|
||||
<rect key="frame" x="30" y="15.5" width="86" height="20.5"/>
|
||||
<rect key="frame" x="30" y="15.5" width="78" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="lx9-35-OSk">
|
||||
<rect key="frame" x="187.5" y="15.5" width="157.5" height="20.5"/>
|
||||
<rect key="frame" x="220.5" y="15.5" width="124.5" height="20.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="SideStore Team" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JAA-iZ-VGb">
|
||||
<rect key="frame" x="0.0" y="0.0" width="125.5" height="20.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="92.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb">
|
||||
<rect key="frame" x="139.5" y="0.0" width="18" height="20.5"/>
|
||||
<rect key="frame" x="106.5" y="0.0" width="18" height="20.5"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
@@ -612,22 +613,22 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf">
|
||||
<rect key="frame" x="30" y="15.5" width="89" height="20.5"/>
|
||||
<rect key="frame" x="30" y="15.5" width="84" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
|
||||
<rect key="frame" x="198" y="15.5" width="147" height="20.5"/>
|
||||
<rect key="frame" x="233.5" y="15.5" width="111.5" height="20.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq">
|
||||
<rect key="frame" x="0.0" y="0.0" width="115" height="20.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="79.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
|
||||
<rect key="frame" x="129" y="0.0" width="18" height="20.5"/>
|
||||
<rect key="frame" x="93.5" y="0.0" width="18" height="20.5"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
@@ -656,22 +657,22 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Asset Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fGU-Fp-XgM">
|
||||
<rect key="frame" x="30" y="15.5" width="115.5" height="20.5"/>
|
||||
<rect key="frame" x="30" y="15.5" width="68.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY">
|
||||
<rect key="frame" x="206" y="15.5" width="139" height="20.5"/>
|
||||
<rect key="frame" x="192.5" y="15.5" width="152.5" height="20.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Chris (LitRitt)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hId-3P-41T">
|
||||
<rect key="frame" x="0.0" y="0.0" width="107" height="20.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="120.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY">
|
||||
<rect key="frame" x="121" y="0.0" width="18" height="20.5"/>
|
||||
<rect key="frame" x="134.5" y="0.0" width="18" height="20.5"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
@@ -700,7 +701,7 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Licenses" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="D6b-cd-pVK">
|
||||
<rect key="frame" x="30" y="15.5" width="67.5" height="20.5"/>
|
||||
<rect key="frame" x="30" y="15" width="67.5" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -780,7 +781,7 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Send Feedback" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pMI-Aj-nQF">
|
||||
<rect key="frame" x="30" y="15.5" width="125.5" height="20.5"/>
|
||||
<rect key="frame" x="30" y="15" width="126" height="21"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -806,14 +807,14 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Qca-pU-sJh" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1023" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="1237" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qca-pU-sJh" id="QtU-8J-VQN">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="View Refresh Attempts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sni-07-q0M">
|
||||
<rect key="frame" x="30" y="15.5" width="187.5" height="20.5"/>
|
||||
<rect key="frame" x="30" y="15" width="187.5" height="21"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -854,151 +855,6 @@
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="VfB-c5-5wG">
|
||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="PWC-OG-5jx" firstAttribute="leading" secondItem="qIT-rz-ZUb" secondAttribute="leadingMargin" id="BQr-Nx-fIq"/>
|
||||
<constraint firstItem="PWC-OG-5jx" firstAttribute="centerY" secondItem="qIT-rz-ZUb" secondAttribute="centerY" id="IDa-ov-tmK"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="VfB-c5-5wG" secondAttribute="trailing" id="Q6c-iP-6bi"/>
|
||||
<constraint firstItem="VfB-c5-5wG" firstAttribute="centerY" secondItem="qIT-rz-ZUb" secondAttribute="centerY" id="WuL-ax-fFw"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||
<integer key="value" value="2"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<segue destination="g8a-Rf-zWa" kind="show" identifier="showErrorLog" id="vFC-Id-Ww6"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VrV-qI-zXF" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1125" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VrV-qI-zXF" id="w1r-uY-4pD">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="SideJITServer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="46q-DB-5nc">
|
||||
<rect key="frame" x="30" y="15.5" width="183" height="21"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="wvD-eZ-nQI">
|
||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="wvD-eZ-nQI" firstAttribute="centerY" secondItem="w1r-uY-4pD" secondAttribute="centerY" id="O6Y-Y1-yxv"/>
|
||||
<constraint firstItem="46q-DB-5nc" firstAttribute="centerY" secondItem="w1r-uY-4pD" secondAttribute="centerY" id="ROS-YF-6jb"/>
|
||||
<constraint firstItem="46q-DB-5nc" firstAttribute="leading" secondItem="w1r-uY-4pD" secondAttribute="leadingMargin" id="acd-O8-WTI"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="wvD-eZ-nQI" secondAttribute="trailing" id="taB-EP-QMM"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||
<integer key="value" value="2"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="eZ3-BT-q4D" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1176" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="eZ3-BT-q4D" id="17m-VV-hzf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Clear Cache" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="IbH-V1-ce3">
|
||||
<rect key="frame" x="30" y="15.5" width="98.5" 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>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="FZe-BJ-fOm">
|
||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="FZe-BJ-fOm" firstAttribute="centerY" secondItem="17m-VV-hzf" secondAttribute="centerY" id="bGv-Np-5aO"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="FZe-BJ-fOm" secondAttribute="trailing" id="ccb-JP-Eqi"/>
|
||||
<constraint firstItem="IbH-V1-ce3" firstAttribute="centerY" secondItem="17m-VV-hzf" secondAttribute="centerY" id="iQJ-gN-sRF"/>
|
||||
<constraint firstItem="IbH-V1-ce3" firstAttribute="leading" secondItem="17m-VV-hzf" secondAttribute="leadingMargin" id="m1g-Y6-aT5"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||
<integer key="value" value="2"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VNn-u4-cN8" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1227" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VNn-u4-cN8" id="4bh-qe-l2N">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Reset Pairing File" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ysS-9s-dXm">
|
||||
<rect key="frame" x="30" y="15.5" width="140" 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>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="r09-mH-pOD">
|
||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="r09-mH-pOD" firstAttribute="centerY" secondItem="4bh-qe-l2N" secondAttribute="centerY" id="02u-Os-L7r"/>
|
||||
<constraint firstItem="ysS-9s-dXm" firstAttribute="centerY" secondItem="4bh-qe-l2N" secondAttribute="centerY" id="QOA-3E-85e"/>
|
||||
<constraint firstItem="ysS-9s-dXm" firstAttribute="leading" secondItem="4bh-qe-l2N" secondAttribute="leadingMargin" id="gRE-CM-w21"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="r09-mH-pOD" secondAttribute="trailing" id="udf-VS-o6t"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||
<integer key="value" value="2"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="e7s-hL-kv9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1278" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="e7s-hL-kv9" id="yjL-Mu-HTk">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Anisette Servers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y">
|
||||
<rect key="frame" x="30" y="15.5" width="135.5" 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>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="0dh-yd-7i9">
|
||||
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="0dh-yd-7i9" firstAttribute="centerY" secondItem="yjL-Mu-HTk" secondAttribute="centerY" id="8OI-PI-weT"/>
|
||||
<constraint firstItem="eds-Dj-36y" firstAttribute="leading" secondItem="yjL-Mu-HTk" secondAttribute="leadingMargin" id="BqG-Ef-xQo"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="0dh-yd-7i9" secondAttribute="trailing" id="TFW-nu-jo4"/>
|
||||
<constraint firstItem="eds-Dj-36y" firstAttribute="centerY" secondItem="yjL-Mu-HTk" secondAttribute="centerY" id="YiJ-OF-FXE"/>
|
||||
</constraints>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="e30-w4-5fk">
|
||||
<rect key="frame" x="296" y="10" width="51" height="31"/>
|
||||
<connections>
|
||||
@@ -1027,6 +883,7 @@
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="aMk-Xp-UL8" id="c6c-fR-8C4"/>
|
||||
<outlet property="delegate" destination="aMk-Xp-UL8" id="moP-1B-lRq"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Settings" id="Ddg-UQ-KJ8"/>
|
||||
@@ -1039,7 +896,6 @@
|
||||
<outlet property="disableAppLimitSwitch" destination="1aa-og-ZXD" id="oVL-Md-yZ8"/>
|
||||
<outlet property="noIdleTimeoutSwitch" destination="iQA-wm-5ag" id="jHC-js-q0Y"/>
|
||||
<outlet property="disableResponseCachingSwitch" destination="e30-w4-5fk" id="Duy-Yb-eo1"/>
|
||||
<outlet property="enforceThreeAppLimitSwitch" destination="Oie-te-KSQ" id="jKn-t1-gyk"/>
|
||||
<outlet property="githubButton" destination="oqj-4S-I9l" id="sxB-LE-gA2"/>
|
||||
<outlet property="mastodonButton" destination="B8Q-e7-beR" id="Kbe-Og-rsg"/>
|
||||
<outlet property="threadsButton" destination="AWk-yE-9LI" id="SOc-ei-4gK"/>
|
||||
@@ -1059,7 +915,7 @@
|
||||
<toolbarItems/>
|
||||
<simulatedTabBarMetrics key="simulatedBottomBarMetrics"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Jtn-cs-Tvp" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||
<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"/>
|
||||
@@ -1092,7 +948,7 @@
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="8Xf-RE-QJx" customClass="RefreshAttemptTableViewCell">
|
||||
<rect key="frame" x="0.0" y="50" width="375" height="65"/>
|
||||
<rect key="frame" x="0.0" y="28" width="375" height="65"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="8Xf-RE-QJx" id="r3G-oh-AyQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="65"/>
|
||||
@@ -1160,7 +1016,7 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" layoutMarginsFollowReadableWidth="YES" contentInsetAdjustmentBehavior="never" indicatorStyle="white" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oQQ-pR-oKc">
|
||||
<rect key="frame" x="0.0" y="64" width="375" height="554"/>
|
||||
<rect key="frame" x="0.0" y="44" width="375" height="574"/>
|
||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||
<string key="text">Jay Freeman (ldid)
|
||||
Copyright (C) 2007-2012 Jay Freeman (saurik)
|
||||
@@ -1263,6 +1119,7 @@ Settings by i cons from the Noun Project</string>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1697" y="313"/>
|
||||
</scene>
|
||||
|
||||
<!--Support us-->
|
||||
<scene sceneID="Lnh-9P-HnL">
|
||||
<objects>
|
||||
@@ -1328,7 +1185,7 @@ Settings by i cons from the Noun Project</string>
|
||||
<scene sceneID="Htu-2V-dbE">
|
||||
<objects>
|
||||
<tableViewController id="g8a-Rf-zWa" customClass="ErrorLogViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="BBn-tI-e0e">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="107" sectionHeaderHeight="18" sectionFooterHeight="18" id="BBn-tI-e0e">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<prototypes>
|
||||
@@ -1382,7 +1239,7 @@ Settings by i cons from the Noun Project</string>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Error Description" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1df-ri-hKN" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Error Description" textAlignment="natural" selectable="NO" layoutManager="textKit1" translatesAutoresizingMaskIntoConstraints="NO" id="1df-ri-hKN" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="51.5" width="311" height="34"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
@@ -1525,6 +1382,8 @@ Settings by i cons from the Noun Project</string>
|
||||
<image name="Next" width="18" height="18"/>
|
||||
<image name="Settings" width="20" height="20"/>
|
||||
<image name="ladybug" catalog="system" width="128" height="122"/>
|
||||
<image name="Threads" width="130" height="130"/>
|
||||
<image name="Twitter" width="130" height="130"/>
|
||||
<namedColor name="SettingsBackground">
|
||||
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
|
||||
@@ -97,7 +97,6 @@ final class SettingsViewController: UITableViewController
|
||||
@IBOutlet private var disableAppLimitSwitch: UISwitch!
|
||||
|
||||
@IBOutlet private var refreshSideJITServer: UILabel!
|
||||
@IBOutlet private var enforceThreeAppLimitSwitch: UISwitch!
|
||||
@IBOutlet private var disableResponseCachingSwitch: UISwitch!
|
||||
|
||||
@IBOutlet private var mastodonButton: UIButton!
|
||||
@@ -133,7 +132,8 @@ final class SettingsViewController: UITableViewController
|
||||
debugModeGestureRecognizer.direction = .up
|
||||
debugModeGestureRecognizer.numberOfTouchesRequired = 3
|
||||
self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
|
||||
|
||||
|
||||
var versionString: String = ""
|
||||
if let installedApp = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext)
|
||||
{
|
||||
#if BETA
|
||||
@@ -247,7 +247,6 @@ private extension SettingsViewController
|
||||
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
self.noIdleTimeoutSwitch.isOn = UserDefaults.standard.isIdleTimeoutDisableEnabled
|
||||
self.disableAppLimitSwitch.isOn = UserDefaults.standard.isAppLimitDisabled
|
||||
self.enforceThreeAppLimitSwitch.isOn = !UserDefaults.standard.ignoreActiveAppsLimit
|
||||
self.disableResponseCachingSwitch.isOn = UserDefaults.standard.responseCachingDisabled
|
||||
|
||||
if self.isViewLoaded
|
||||
@@ -421,6 +420,10 @@ private extension SettingsViewController
|
||||
|
||||
@IBAction func toggleDisableAppLimit(_ sender: UISwitch) {
|
||||
UserDefaults.standard.isAppLimitDisabled = sender.isOn
|
||||
if UserDefaults.standard.activeAppsLimit != nil
|
||||
{
|
||||
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func toggleIsBackgroundRefreshEnabled(_ sender: UISwitch)
|
||||
@@ -433,16 +436,6 @@ private extension SettingsViewController
|
||||
UserDefaults.standard.isIdleTimeoutDisableEnabled = sender.isOn
|
||||
}
|
||||
|
||||
@IBAction func toggleEnforceThreeAppLimit(_ sender: UISwitch)
|
||||
{
|
||||
UserDefaults.standard.ignoreActiveAppsLimit = !sender.isOn
|
||||
|
||||
if UserDefaults.standard.activeAppsLimit != nil
|
||||
{
|
||||
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func toggleDisableResponseCaching(_ sender: UISwitch)
|
||||
{
|
||||
UserDefaults.standard.responseCachingDisabled = sender.isOn
|
||||
|
||||
@@ -13,26 +13,26 @@ import AltStoreCore
|
||||
import Roxas
|
||||
import Nuke
|
||||
|
||||
struct SourceError: ALTLocalizedError
|
||||
{
|
||||
enum Code: Int, ALTErrorCode
|
||||
{
|
||||
typealias Error = SourceError
|
||||
case unsupported
|
||||
}
|
||||
|
||||
var code: Code
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
@Managed var source: Source
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self.code
|
||||
{
|
||||
case .unsupported: return String(format: NSLocalizedString("The source “%@” is not supported by this version of SideStore.", comment: ""), self.$source.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
//struct SourceError: ALTLocalizedError
|
||||
//{
|
||||
// enum Code: Int, ALTErrorCode
|
||||
// {
|
||||
// typealias Error = SourceError
|
||||
// case unsupported
|
||||
// }
|
||||
//
|
||||
// var code: Code
|
||||
// var errorTitle: String?
|
||||
// var errorFailure: String?
|
||||
// @Managed var source: Source
|
||||
//
|
||||
// var errorFailureReason: String {
|
||||
// switch self.code
|
||||
// {
|
||||
// case .unsupported: return String(format: NSLocalizedString("The source “%@” is not supported by this version of SideStore.", comment: ""), self.$source.name)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
@objc(SourcesFooterView)
|
||||
private final class SourcesFooterView: TextCollectionReusableView
|
||||
@@ -454,14 +454,14 @@ private extension SourcesViewController
|
||||
completionHandler?(false)
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
|
||||
var sourcesByURL = [URL: Source]()
|
||||
var fetchError: Error?
|
||||
|
||||
|
||||
for sourceURL in featuredSourceURLs
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
|
||||
AppManager.shared.fetchSource(sourceURL: sourceURL, managedObjectContext: context) { result in
|
||||
// Serialize access to sourcesByURL.
|
||||
context.performAndWait {
|
||||
@@ -470,12 +470,12 @@ private extension SourcesViewController
|
||||
case .failure(let error): fetchError = error
|
||||
case .success(let source): sourcesByURL[source.sourceURL] = source
|
||||
}
|
||||
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
if let error = fetchError
|
||||
{
|
||||
@@ -547,98 +547,6 @@ extension SourcesViewController
|
||||
let source = self.dataSource.item(at: indexPath)
|
||||
self.showSourceDetails(for: source)
|
||||
}
|
||||
|
||||
// override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||
// {
|
||||
// let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: kind, for: indexPath) as! UICollectionViewListCell
|
||||
|
||||
// var configuation = UIListContentConfiguration.cell()
|
||||
// configuation.text = NSLocalizedString("Sources control what apps are available to download through AltStore.", comment: "")
|
||||
// configuation.textProperties.color = .secondaryLabel
|
||||
// configuation.textProperties.alignment = .natural
|
||||
|
||||
// headerView.contentConfiguration = configuation
|
||||
|
||||
// switch kind
|
||||
// {
|
||||
// case UICollectionView.elementKindSectionHeader:
|
||||
// switch Section.allCases[indexPath.section]
|
||||
// {
|
||||
// case .added:
|
||||
// headerView.textLabel.text = NSLocalizedString("Sources control what apps are available to download through SideStore.", comment: "")
|
||||
// headerView.textLabel.font = UIFont.preferredFont(forTextStyle: .callout)
|
||||
// headerView.textLabel.textAlignment = .natural
|
||||
|
||||
// headerView.topLayoutConstraint.constant = 14
|
||||
// headerView.bottomLayoutConstraint.constant = 30
|
||||
|
||||
// case .trusted:
|
||||
// switch self.fetchTrustedSourcesResult
|
||||
// {
|
||||
// case .failure: headerView.textLabel.text = NSLocalizedString("Error Loading Trusted Sources", comment: "")
|
||||
// case .success, .none: headerView.textLabel.text = NSLocalizedString("Trusted Sources", comment: "")
|
||||
// }
|
||||
|
||||
// let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .callout).withSymbolicTraits(.traitBold)!
|
||||
// headerView.textLabel.font = UIFont(descriptor: descriptor, size: 0)
|
||||
// headerView.textLabel.textAlignment = .center
|
||||
|
||||
// headerView.topLayoutConstraint.constant = 54
|
||||
// headerView.bottomLayoutConstraint.constant = 15
|
||||
// }
|
||||
|
||||
// case UICollectionView.elementKindSectionFooter:
|
||||
// let footerView = headerView as! SourcesFooterView
|
||||
// let font = UIFont.preferredFont(forTextStyle: .subheadline)
|
||||
|
||||
// switch self.fetchTrustedSourcesResult
|
||||
// {
|
||||
// case .failure(let error):
|
||||
// footerView.textView.font = font
|
||||
// footerView.textView.text = error.localizedDescription
|
||||
|
||||
// footerView.activityIndicatorView.stopAnimating()
|
||||
// footerView.topLayoutConstraint.constant = 0
|
||||
// footerView.textView.textAlignment = .center
|
||||
|
||||
// case .success, .none:
|
||||
// footerView.textView.delegate = self
|
||||
|
||||
// let attributedText = NSMutableAttributedString(
|
||||
// string: NSLocalizedString("SideStore has reviewed these sources to make sure they meet our safety standards.", comment: ""),
|
||||
// attributes: [.font: font, .foregroundColor: UIColor.gray]
|
||||
// )
|
||||
// //attributedText.mutableString.append(" ")
|
||||
|
||||
// //let boldedFont = UIFont(descriptor: font.fontDescriptor.withSymbolicTraits(.traitBold)!, size: font.pointSize)
|
||||
// //let openPatreonURL = URL(string: "https://SideStore.io/")!
|
||||
|
||||
// // let joinPatreonText = NSAttributedString(
|
||||
// // string: NSLocalizedString("", comment: ""),
|
||||
// // attributes: [.font: boldedFont, .link: openPatreonURL, .underlineColor: UIColor.clear]
|
||||
// //)
|
||||
// //attributedText.append(joinPatreonText)
|
||||
|
||||
// footerView.textView.attributedText = attributedText
|
||||
// footerView.textView.textAlignment = .natural
|
||||
|
||||
// if self.fetchTrustedSourcesResult != nil
|
||||
// {
|
||||
// footerView.activityIndicatorView.stopAnimating()
|
||||
// footerView.topLayoutConstraint.constant = 20
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// footerView.activityIndicatorView.startAnimating()
|
||||
// footerView.topLayoutConstraint.constant = 0
|
||||
// }
|
||||
// }
|
||||
|
||||
// default: break
|
||||
// }
|
||||
|
||||
// return headerView
|
||||
// }
|
||||
}
|
||||
|
||||
extension SourcesViewController: NSFetchedResultsControllerDelegate
|
||||
|
||||
@@ -136,7 +136,9 @@ private extension TabBarController
|
||||
{
|
||||
self.selectedIndex = Tab.myApps.rawValue
|
||||
}
|
||||
@objc func openErrorLog(_: Notification){
|
||||
|
||||
@objc func openErrorLog(_ notification: Notification)
|
||||
{
|
||||
self.selectedIndex = Tab.settings.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,6 @@ public extension UserDefaults
|
||||
}
|
||||
@NSManaged @objc(activeAppsLimit) private var _activeAppsLimit: NSNumber?
|
||||
|
||||
@NSManaged var ignoreActiveAppsLimit: Bool
|
||||
|
||||
// Including "MacDirtyCow" in name triggers false positives with malware detectors 🤷♂️
|
||||
@NSManaged var isCowExploitSupported: Bool
|
||||
|
||||
@@ -129,8 +127,7 @@ public extension UserDefaults
|
||||
#keyPath(UserDefaults.requiresAppGroupMigration): true,
|
||||
#keyPath(UserDefaults.menuAnisetteList): "https://servers.sidestore.io/servers.json",
|
||||
#keyPath(UserDefaults.menuAnisetteURL): "https://ani.sidestore.io",
|
||||
#keyPath(UserDefaults.ignoreActiveAppsLimit): false,
|
||||
#keyPath(UserDefaults.isMacDirtyCowSupported): isMacDirtyCowSupported
|
||||
#keyPath(UserDefaults.isCowExploitSupported): isMacDirtyCowSupported,
|
||||
#keyPath(UserDefaults.permissionCheckingDisabled): permissionCheckingDisabled,
|
||||
#keyPath(UserDefaults._preferredAppSorting): preferredAppSorting.rawValue,
|
||||
] as [String: Any]
|
||||
@@ -140,8 +137,8 @@ public extension UserDefaults
|
||||
|
||||
if !isMacDirtyCowSupported
|
||||
{
|
||||
// Disable ignoreActiveAppsLimit if running iOS version that doesn't support MacDirtyCow.
|
||||
UserDefaults.standard.ignoreActiveAppsLimit = false
|
||||
// Disable isAppLimitDisabled if running iOS version that doesn't support MacDirtyCow.
|
||||
UserDefaults.standard.isAppLimitDisabled = false
|
||||
}
|
||||
|
||||
#if !BETA
|
||||
|
||||
@@ -10,67 +10,6 @@ import CoreData
|
||||
import UIKit
|
||||
|
||||
import AltSign
|
||||
public 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 .camera: return NSLocalizedString("Camera", comment: "")
|
||||
case .location: return NSLocalizedString("Location", comment: "")
|
||||
case .contacts: return NSLocalizedString("Contacts", comment: "")
|
||||
case .reminders: return NSLocalizedString("Reminders", comment: "")
|
||||
case .appleMusic: return NSLocalizedString("Apple Music", comment: "")
|
||||
case .microphone: return NSLocalizedString("Microphone", comment: "")
|
||||
case .speechRecognition: return NSLocalizedString("Speech Recognition", comment: "")
|
||||
case .backgroundAudio: return NSLocalizedString("Background Audio", comment: "")
|
||||
case .backgroundFetch: return NSLocalizedString("Background Fetch", comment: "")
|
||||
case .bluetooth: return NSLocalizedString("Bluetooth", comment: "")
|
||||
case .network: return NSLocalizedString("Network", comment: "")
|
||||
case .calendars: return NSLocalizedString("Calendars", comment: "")
|
||||
case .touchID: return NSLocalizedString("Touch ID", comment: "")
|
||||
case .faceID: return NSLocalizedString("Face ID", comment: "")
|
||||
case .siri: return NSLocalizedString("Siri", comment: "")
|
||||
case .motion: return NSLocalizedString("Motion", comment: "")
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var icon: UIImage? {
|
||||
switch self
|
||||
{
|
||||
case .photos: return UIImage(systemName: "photo.on.rectangle.angled")
|
||||
case .camera: return UIImage(systemName: "camera.fill")
|
||||
case .location: return UIImage(systemName: "location.fill")
|
||||
case .contacts: return UIImage(systemName: "person.2.fill")
|
||||
case .reminders: return UIImage(systemName: "checklist")
|
||||
case .appleMusic: return UIImage(systemName: "music.note")
|
||||
case .microphone: return UIImage(systemName: "mic.fill")
|
||||
case .speechRecognition: return UIImage(systemName: "waveform.and.mic")
|
||||
case .backgroundAudio: return UIImage(systemName: "speaker.fill")
|
||||
case .backgroundFetch: return UIImage(systemName: "square.and.arrow.down")
|
||||
case .bluetooth: return UIImage(systemName: "wave.3.right")
|
||||
case .network: return UIImage(systemName: "network")
|
||||
case .calendars: return UIImage(systemName: "calendar")
|
||||
case .touchID: return UIImage(systemName: "touchid")
|
||||
case .faceID: return UIImage(systemName: "faceid")
|
||||
case .siri: return UIImage(systemName: "mic.and.signal.meter.fill")
|
||||
case .motion: return UIImage(systemName: "figure.walk.motion")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(AppPermission) @dynamicMemberLookup
|
||||
public class AppPermission: NSManagedObject, Fetchable
|
||||
|
||||
@@ -157,11 +157,15 @@ public extension AppVersion
|
||||
}
|
||||
|
||||
var isSupported: Bool {
|
||||
if let minOSVersion = self.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) {
|
||||
return false
|
||||
} else if let maxOSVersion = self.maxOSVersion, ProcessInfo.processInfo.operatingSystemVersion > maxOSVersion {
|
||||
if let minOSVersion = self.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion)
|
||||
{
|
||||
return false
|
||||
}
|
||||
else if let maxOSVersion = self.maxOSVersion, ProcessInfo.processInfo.operatingSystemVersion > maxOSVersion
|
||||
{
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ public class DatabaseManager
|
||||
private let coordinatorQueue = OperationQueue()
|
||||
|
||||
private var ignoreWillMigrateDatabaseNotification = false
|
||||
|
||||
private init()
|
||||
{
|
||||
self.persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
|
||||
@@ -108,6 +109,7 @@ public extension DatabaseManager
|
||||
self.ignoreWillMigrateDatabaseNotification = true
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willMigrateDatabase, nil, nil, true)
|
||||
}
|
||||
|
||||
self.migrateDatabaseToAppGroupIfNeeded { (result) in
|
||||
switch result
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ import SemanticVersion
|
||||
extension InstalledApp
|
||||
{
|
||||
public static var freeAccountActiveAppsLimit: Int {
|
||||
if UserDefaults.standard.ignoreActiveAppsLimit
|
||||
if UserDefaults.standard.isAppLimitDisabled
|
||||
{
|
||||
// MacDirtyCow exploit allows users to remove 3-app limit, so return 10 to match App ID limit per-week.
|
||||
// Don't return nil because that implies there is no limit, which isn't quite true due to App ID limit.
|
||||
@@ -159,15 +159,17 @@ public extension InstalledApp
|
||||
|
||||
func loadIcon(completion: @escaping (Result<UIImage?, Error>) -> Void)
|
||||
{
|
||||
if self.bundleIdentifier == StoreApp.altstoreAppID, let iconName = UIApplication.alt_shared?.alternateIconName
|
||||
{
|
||||
// Use alternate app icon for AltStore, if one is chosen.
|
||||
|
||||
let image = UIImage(named: iconName)
|
||||
completion(.success(image))
|
||||
|
||||
return
|
||||
}
|
||||
// TODO: @mahee96: Fix this later (reason: alternateIcon is not available for appEx)
|
||||
// if self.bundleIdentifier == StoreApp.altstoreAppID,
|
||||
// let iconName = UIApplication.alt_shared?.alternateIconName
|
||||
// {
|
||||
// // Use alternate app icon for AltStore, if one is chosen.
|
||||
//
|
||||
// let image = UIImage(named: iconName)
|
||||
// completion(.success(image))
|
||||
//
|
||||
// return
|
||||
// }
|
||||
|
||||
let hasAlternateIcon = self.hasAlternateIcon
|
||||
let alternateIconURL = self.alternateIconURL
|
||||
@@ -211,30 +213,34 @@ public extension InstalledApp
|
||||
{
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
|
||||
let predicateFormat = [
|
||||
// isActive && storeApp != nil && latestSupportedVersion != nil
|
||||
"%K == YES AND %K != nil AND %K != nil",
|
||||
// let predicateFormat = [
|
||||
// // isActive && storeApp != nil && latestSupportedVersion != nil
|
||||
// "%K == YES AND %K != nil AND %K != nil",
|
||||
|
||||
"AND",
|
||||
// "AND",
|
||||
|
||||
// latestSupportedVersion.version != installedApp.version || latestSupportedVersion.buildVersion != installedApp.storeBuildVersion
|
||||
//
|
||||
// We have to also check !(latestSupportedVersion.buildVersion == '' && installedApp.storeBuildVersion == nil)
|
||||
// because latestSupportedVersion.buildVersion stores an empty string for nil, while installedApp.storeBuildVersion uses NULL.
|
||||
"(%K != %K OR (%K != %K AND NOT (%K == '' AND %K == nil)))",
|
||||
// // latestSupportedVersion.version != installedApp.version || latestSupportedVersion.buildVersion != installedApp.storeBuildVersion
|
||||
// //
|
||||
// // We have to also check !(latestSupportedVersion.buildVersion == '' && installedApp.storeBuildVersion == nil)
|
||||
// // because latestSupportedVersion.buildVersion stores an empty string for nil, while installedApp.storeBuildVersion uses NULL.
|
||||
// "(%K != %K OR (%K != %K AND NOT (%K == '' AND %K == nil)))",
|
||||
|
||||
"AND",
|
||||
// "AND",
|
||||
|
||||
// !isPledgeRequired || isPledged
|
||||
"(%K == NO OR %K == YES)"
|
||||
].joined(separator: " ")
|
||||
// // !isPledgeRequired || isPledged
|
||||
// "(%K == NO OR %K == YES)"
|
||||
// ].joined(separator: " ")
|
||||
|
||||
fetchRequest.predicate = NSPredicate(format: predicateFormat,
|
||||
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.latestSupportedVersion),
|
||||
#keyPath(InstalledApp.storeApp.latestSupportedVersion.version), #keyPath(InstalledApp.version),
|
||||
#keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeBuildVersion),
|
||||
#keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeBuildVersion),
|
||||
#keyPath(InstalledApp.storeApp.isPledgeRequired), #keyPath(InstalledApp.storeApp.isPledged))
|
||||
// fetchRequest.predicate = NSPredicate(format: predicateFormat,
|
||||
// #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.latestSupportedVersion),
|
||||
// #keyPath(InstalledApp.storeApp.latestSupportedVersion.version), #keyPath(InstalledApp.version),
|
||||
// #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeBuildVersion),
|
||||
// #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeBuildVersion),
|
||||
// #keyPath(InstalledApp.storeApp.isPledgeRequired), #keyPath(InstalledApp.storeApp.isPledged))
|
||||
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K == YES",
|
||||
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.hasUpdate))
|
||||
|
||||
return fetchRequest
|
||||
}
|
||||
|
||||
|
||||
@@ -356,11 +356,11 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
// Screenshots
|
||||
if let sortedScreenshotIDs = sortedScreenshotIDsByGlobalAppID[globallyUniqueID],
|
||||
let sortedScreenshotIDsArray = sortedScreenshotIDs.array as? [String],
|
||||
case let databaseScreenshotIDs = databaseObject.allScreenshots.map({ $0.screenshotID }),
|
||||
case let databaseScreenshotIDs = databaseObject.screenshots.map({ $0.screenshotID }),
|
||||
databaseScreenshotIDs != sortedScreenshotIDsArray
|
||||
{
|
||||
// Screenshot order is incorrect, so attempt to fix by re-sorting.
|
||||
let fixedScreenshots = databaseObject.allScreenshots.sorted { (screenshotA, screenshotB) in
|
||||
let fixedScreenshots = databaseObject.screenshots.sorted { (screenshotA, screenshotB) in
|
||||
let indexA = sortedScreenshotIDs.index(of: screenshotA.screenshotID)
|
||||
let indexB = sortedScreenshotIDs.index(of: screenshotB.screenshotID)
|
||||
return indexA < indexB
|
||||
|
||||
@@ -45,7 +45,8 @@ public class PatreonAccount: NSManagedObject, Fetchable
|
||||
// {
|
||||
// self.isPatron = false
|
||||
// }
|
||||
self.isPatron = true
|
||||
// self.isPatron = true
|
||||
self.isAltStorePatron = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -462,7 +462,7 @@ public extension Source
|
||||
let source = Source(context: context)
|
||||
source.name = "SideStore Offical"
|
||||
source.identifier = Source.altStoreIdentifier
|
||||
source.sourceURL = Source.altStoreSourceURL
|
||||
try! source.setSourceURL(Source.altStoreSourceURL)
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
permission.sourceID = self.sourceIdentifier ?? ""
|
||||
}
|
||||
|
||||
for screenshot in self.allScreenshots
|
||||
for screenshot in self.screenshots
|
||||
{
|
||||
screenshot.sourceID = self.sourceIdentifier ?? ""
|
||||
}
|
||||
@@ -282,8 +282,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
case developerName
|
||||
case localizedDescription
|
||||
case iconURL
|
||||
case screenshotURLs
|
||||
case downloadURL
|
||||
case platformURLs
|
||||
case screenshots
|
||||
case tintColor
|
||||
@@ -606,7 +604,7 @@ public extension StoreApp
|
||||
func screenshots(for deviceType: ALTDeviceType) -> [AppScreenshot]
|
||||
{
|
||||
//TODO: Support multiple device types
|
||||
let filteredScreenshots = self.allScreenshots.filter { $0.deviceType == deviceType }
|
||||
let filteredScreenshots = self.screenshots.filter { $0.deviceType == deviceType }
|
||||
return filteredScreenshots
|
||||
}
|
||||
|
||||
@@ -626,7 +624,7 @@ public extension StoreApp
|
||||
let preferredScreenshots = self.screenshots(for: deviceType)
|
||||
guard !preferredScreenshots.isEmpty else {
|
||||
// There are no screenshots for deviceType, so return _all_ screenshots instead.
|
||||
return self.allScreenshots
|
||||
return self.screenshots
|
||||
}
|
||||
|
||||
return preferredScreenshots
|
||||
|
||||
@@ -300,7 +300,7 @@ public extension PatreonAPI
|
||||
{
|
||||
let account = try result.get()
|
||||
|
||||
if let context = account.managedObjectContext, !account.isPatron
|
||||
if let context = account.managedObjectContext, !account.isAltStorePatron
|
||||
{
|
||||
// Deactivate all beta apps now that we're no longer a patron.
|
||||
//self.deactivateBetaApps(in: context)
|
||||
|
||||
@@ -63,15 +63,16 @@ extension InstalledApp: AppProtocol
|
||||
}
|
||||
}
|
||||
|
||||
extension AppVersion: AppProtocol {
|
||||
extension AppVersion: AppProtocol
|
||||
{
|
||||
public var name: String {
|
||||
return self.app?.name ?? self.bundleIdentifier
|
||||
}
|
||||
|
||||
|
||||
public var bundleIdentifier: String {
|
||||
return self.appBundleID
|
||||
}
|
||||
|
||||
|
||||
public var url: URL? {
|
||||
return self.downloadURL
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,249 +0,0 @@
|
||||
//
|
||||
// Provider.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/26/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import CoreData
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
struct AppEntry: TimelineEntry
|
||||
{
|
||||
var date: Date
|
||||
var relevance: TimelineEntryRelevance?
|
||||
|
||||
var app: AppSnapshot?
|
||||
var isPlaceholder: Bool = false
|
||||
}
|
||||
|
||||
struct Provider: IntentTimelineProvider
|
||||
{
|
||||
typealias Intent = ViewAppIntent
|
||||
typealias Entry = AppEntry
|
||||
|
||||
func placeholder(in context: Context) -> AppEntry
|
||||
{
|
||||
return AppEntry(date: Date(), app: nil, isPlaceholder: true)
|
||||
}
|
||||
|
||||
func getSnapshot(for configuration: ViewAppIntent, in context: Context, completion: @escaping (AppEntry) -> Void)
|
||||
{
|
||||
self.prepare { (result) in
|
||||
do
|
||||
{
|
||||
let context = try result.get()
|
||||
let snapshot = InstalledApp.fetchAltStore(in: context).map(AppSnapshot.init)
|
||||
|
||||
let entry = AppEntry(date: Date(), app: snapshot)
|
||||
completion(entry)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error preparing widget snapshot:", error)
|
||||
|
||||
let entry = AppEntry(date: Date(), app: nil)
|
||||
completion(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTimeline(for configuration: ViewAppIntent, in context: Context, completion: @escaping (Timeline<AppEntry>) -> Void) {
|
||||
self.prepare { (result) in
|
||||
autoreleasepool {
|
||||
do
|
||||
{
|
||||
let context = try result.get()
|
||||
|
||||
let installedApp: InstalledApp?
|
||||
|
||||
if let identifier = configuration.app?.identifier
|
||||
{
|
||||
let app = InstalledApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), identifier),
|
||||
in: context)
|
||||
installedApp = app
|
||||
}
|
||||
else
|
||||
{
|
||||
installedApp = InstalledApp.fetchAltStore(in: context)
|
||||
}
|
||||
|
||||
guard let snapshot = installedApp.map(AppSnapshot.init) else { throw ALTError(.invalidApp) }
|
||||
|
||||
let currentDate = Calendar.current.startOfDay(for: Date())
|
||||
let numberOfDays = snapshot.expirationDate.numberOfCalendarDays(since: currentDate)
|
||||
|
||||
// Generate a timeline consisting of one entry per day.
|
||||
var entries: [AppEntry] = []
|
||||
|
||||
switch numberOfDays
|
||||
{
|
||||
case ..<0:
|
||||
let entry = AppEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 0.0), app: snapshot)
|
||||
entries.append(entry)
|
||||
|
||||
case 0:
|
||||
let entry = AppEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 1.0), app: snapshot)
|
||||
entries.append(entry)
|
||||
|
||||
default:
|
||||
// To reduce memory consumption, we only generate entries for the next week. This includes:
|
||||
// * 1 for each day the app is valid (up to 7)
|
||||
// * 1 "0 days remaining"
|
||||
// * 1 "Expired"
|
||||
let numberOfEntries = min(numberOfDays, 7) + 2
|
||||
|
||||
let appEntries = (0 ..< numberOfEntries).map { (dayOffset) -> AppEntry in
|
||||
let entryDate = Calendar.current.date(byAdding: .day, value: dayOffset, to: currentDate) ?? currentDate.addingTimeInterval(Double(dayOffset) * 60 * 60 * 24)
|
||||
|
||||
let daysSinceRefresh = entryDate.numberOfCalendarDays(since: snapshot.refreshedDate)
|
||||
let totalNumberOfDays = snapshot.expirationDate.numberOfCalendarDays(since: snapshot.refreshedDate)
|
||||
|
||||
let score = (entryDate <= snapshot.expirationDate) ? Float(daysSinceRefresh + 1) / Float(totalNumberOfDays + 1) : 0 // Expired apps have a score of 0.
|
||||
let entry = AppEntry(date: entryDate, relevance: TimelineEntryRelevance(score: score), app: snapshot)
|
||||
return entry
|
||||
}
|
||||
|
||||
entries.append(contentsOf: appEntries)
|
||||
}
|
||||
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error preparing widget timeline:", error)
|
||||
|
||||
let entry = AppEntry(date: Date(), app: nil)
|
||||
let timeline = Timeline(entries: [entry], policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func prepare(completion: @escaping (Result<NSManagedObjectContext, Error>) -> Void)
|
||||
{
|
||||
DatabaseManager.shared.start { (error) in
|
||||
if let error = error
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
DatabaseManager.shared.viewContext.perform {
|
||||
completion(.success(DatabaseManager.shared.viewContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeScreenWidget: Widget
|
||||
{
|
||||
private let kind: String = "AppDetail"
|
||||
|
||||
public var body: some WidgetConfiguration {
|
||||
let configuration = IntentConfiguration(kind: kind,
|
||||
intent: ViewAppIntent.self,
|
||||
provider: Provider()) { (entry) in
|
||||
WidgetView(entry: entry)
|
||||
}
|
||||
.supportedFamilies([.systemSmall])
|
||||
.configurationDisplayName("AltWidget")
|
||||
.description("View remaining days until your sideloaded apps expire.")
|
||||
|
||||
if #available(iOS 17, *)
|
||||
{
|
||||
return configuration
|
||||
.contentMarginsDisabled()
|
||||
}
|
||||
else
|
||||
{
|
||||
return configuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TextLockScreenWidget: Widget
|
||||
{
|
||||
private let kind: String = "TextLockAppDetail"
|
||||
|
||||
public var body: some WidgetConfiguration {
|
||||
if #available(iOSApplicationExtension 16, *)
|
||||
{
|
||||
return IntentConfiguration(kind: kind,
|
||||
intent: ViewAppIntent.self,
|
||||
provider: Provider()) { (entry) in
|
||||
ComplicationView(entry: entry, style: .text)
|
||||
}
|
||||
.supportedFamilies([.accessoryCircular])
|
||||
.configurationDisplayName("AltWidget (Text)")
|
||||
.description("View remaining days until SideStore expires.")
|
||||
}
|
||||
else
|
||||
{
|
||||
return EmptyWidgetConfiguration()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IconLockScreenWidget: Widget
|
||||
{
|
||||
private let kind: String = "IconLockAppDetail"
|
||||
|
||||
public var body: some WidgetConfiguration {
|
||||
if #available(iOSApplicationExtension 16, *)
|
||||
{
|
||||
return IntentConfiguration(kind: kind,
|
||||
intent: ViewAppIntent.self,
|
||||
provider: Provider()) { (entry) in
|
||||
ComplicationView(entry: entry, style: .icon)
|
||||
}
|
||||
.supportedFamilies([.accessoryCircular])
|
||||
.configurationDisplayName("AltWidget (Icon)")
|
||||
.description("View remaining days until SideStore expires.")
|
||||
}
|
||||
else
|
||||
{
|
||||
return EmptyWidgetConfiguration()
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
//struct LockScreenWidget: Widget
|
||||
//{
|
||||
// private let kind: String = "LockAppDetail"
|
||||
//
|
||||
// public var body: some WidgetConfiguration {
|
||||
// if #available(iOSApplicationExtension 16, *)
|
||||
// {
|
||||
// return IntentConfiguration(kind: kind,
|
||||
// intent: ViewAppIntent.self,
|
||||
// provider: Provider()) { (entry) in
|
||||
// ComplicationView(entry: entry, style: .icon)
|
||||
// }
|
||||
// .supportedFamilies([.accessoryCircular])
|
||||
// .configurationDisplayName("AltWidget")
|
||||
// .description("View remaining days until SideStore expires.")
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// return EmptyWidgetConfiguration()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
@main
|
||||
struct AltWidgets: WidgetBundle
|
||||
{
|
||||
var body: some Widget {
|
||||
HomeScreenWidget()
|
||||
IconLockScreenWidget()
|
||||
TextLockScreenWidget()
|
||||
}
|
||||
}
|
||||
1
Dependencies/AltSign
vendored
Submodule
1
Dependencies/AltSign
vendored
Submodule
Submodule Dependencies/AltSign added at 790b9ccdaf
22
Podfile
22
Podfile
@@ -1,6 +1,6 @@
|
||||
inhibit_all_warnings!
|
||||
|
||||
target 'AltStore' do
|
||||
target 'SideStore' do
|
||||
platform :ios, '14.0'
|
||||
|
||||
use_frameworks!
|
||||
@@ -8,17 +8,7 @@ target 'AltStore' do
|
||||
# Pods for AltStore
|
||||
pod 'Nuke', '~> 10.0'
|
||||
pod 'AppCenter', '~> 5.0'
|
||||
|
||||
end
|
||||
|
||||
target 'AltServer' do
|
||||
platform :macos, '11'
|
||||
|
||||
use_frameworks!
|
||||
|
||||
# Pods for AltServer
|
||||
pod 'STPrivilegedTask', :git => 'https://github.com/rileytestut/STPrivilegedTask.git'
|
||||
pod 'Sparkle', '~> 2.3'
|
||||
pod 'Starscream', '~> 4.0.0'
|
||||
|
||||
end
|
||||
|
||||
@@ -27,8 +17,12 @@ target 'AltStoreCore' do
|
||||
|
||||
use_frameworks!
|
||||
|
||||
# Pods for AltServer
|
||||
# Pods for AltStoreCore
|
||||
pod 'KeychainAccess', '~> 4.2.0'
|
||||
# pod 'SemanticVersion', '~> 0.3.5'
|
||||
# Add the Swift Package using the repository URL
|
||||
# pod 'SemanticVersion', :git => 'https://github.com/SwiftPackageIndex/SemanticVersion.git', :tag => '0.4.0'
|
||||
|
||||
|
||||
end
|
||||
|
||||
@@ -39,4 +33,4 @@ post_install do |installer|
|
||||
config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '11.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
24
Podfile.lock
24
Podfile.lock
@@ -9,39 +9,27 @@ PODS:
|
||||
- AppCenter/Core
|
||||
- KeychainAccess (4.2.2)
|
||||
- Nuke (10.7.1)
|
||||
- Sparkle (2.3.2)
|
||||
- STPrivilegedTask (1.0.8)
|
||||
- Starscream (4.0.8)
|
||||
|
||||
DEPENDENCIES:
|
||||
- AppCenter (~> 5.0)
|
||||
- KeychainAccess (~> 4.2.0)
|
||||
- Nuke (~> 10.0)
|
||||
- Sparkle (~> 2.3)
|
||||
- STPrivilegedTask (from `https://github.com/rileytestut/STPrivilegedTask.git`)
|
||||
- Starscream (~> 4.0.0)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- AppCenter
|
||||
- KeychainAccess
|
||||
- Nuke
|
||||
- Sparkle
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
STPrivilegedTask:
|
||||
:git: https://github.com/rileytestut/STPrivilegedTask.git
|
||||
|
||||
CHECKOUT OPTIONS:
|
||||
STPrivilegedTask:
|
||||
:commit: 02ab5081c4f1d7f6a70f5413c88d32dbbea66f4c
|
||||
:git: https://github.com/rileytestut/STPrivilegedTask.git
|
||||
- Starscream
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
AppCenter: 18153bb6bc4241d14c8cce57466ac1c88136b476
|
||||
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||
Nuke: 279f17a599fd1c83cf51de5e0e1f2db143a287b0
|
||||
Sparkle: b36a51855e81585a1c38e32e53101d36c00f4304
|
||||
STPrivilegedTask: 3a3f6add7c567b1be8c326328eb3dd6dc5daed91
|
||||
Starscream: 19b5533ddb925208db698f0ac508a100b884a1b9
|
||||
|
||||
PODFILE CHECKSUM: 3ca028c93d6c7f9f71be0028419da64855dba982
|
||||
PODFILE CHECKSUM: a8db3d0bf5da636c63e459c1ca0453c0937e1c1d
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -8,12 +8,6 @@
|
||||
|
||||
#import "NSError+ALTServerError.h"
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
#import "AltServer-Swift.h"
|
||||
#else
|
||||
#import "AltStoreCore/AltStoreCore-Swift.h"
|
||||
#endif
|
||||
|
||||
#if ALTJIT
|
||||
#import "AltJIT-Swift.h"
|
||||
@import AltSign;
|
||||
@@ -26,10 +20,9 @@
|
||||
#endif
|
||||
|
||||
NSErrorDomain const AltServerErrorDomain = @"AltServer.ServerError";
|
||||
NSErrorDomain const AltServerInstallationErrorDomain = @"AltServer.InstallationError";
|
||||
NSErrorDomain const AltServerInstallationErrorDomain = @"Apple.InstallationError";
|
||||
NSErrorDomain const AltServerConnectionErrorDomain = @"AltServer.ConnectionError";
|
||||
|
||||
|
||||
NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey = @"underlyingErrorDomain";
|
||||
NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey = @"underlyingErrorCode";
|
||||
NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdentifier";
|
||||
@@ -219,7 +212,7 @@ NSErrorUserInfoKey const ALTNSCodingPathKey = @"NSCodingPath";
|
||||
if (underlyingError != nil) {
|
||||
return underlyingError.localizedFailureReason ?: underlyingError.localizedDescription;
|
||||
}
|
||||
return NSLocalizedString(@"An error occured while installing the app.", @"");
|
||||
return NSLocalizedString(@"An error occurred while installing the app.", @"");
|
||||
}
|
||||
|
||||
case ALTServerErrorMaximumFreeAppLimitReached:
|
||||
@@ -289,7 +282,9 @@ NSErrorUserInfoKey const ALTNSCodingPathKey = @"NSCodingPath";
|
||||
if (underlyingError.localizedRecoverySuggestion != nil){
|
||||
return underlyingError.localizedRecoverySuggestion;
|
||||
}
|
||||
// If there is no underlying error found, fall through to AltServerErrorDeviceNotFound
|
||||
|
||||
// If there is no underlying error, fall through to ALTServerErrorDeviceNotFound.
|
||||
// return nil;
|
||||
}
|
||||
case ALTServerErrorDeviceNotFound:
|
||||
return NSLocalizedString(@"Make sure you have trusted this device with your computer and Wi-Fi sync is enabled.", @"");
|
||||
|
||||
@@ -75,8 +75,8 @@ public extension ALTLocalizedError
|
||||
var userInfo: [String: Any?] = [
|
||||
NSLocalizedFailureErrorKey: self.errorFailure,
|
||||
ALTLocalizedTitleErrorKey: self.errorTitle,
|
||||
// ALTSourceFileErrorKey: self.sourceFile, // TODO: Figure out where these come from
|
||||
// ALTSourceLineErrorKey: self.sourceLine,
|
||||
ALTSourceFileErrorKey: self.sourceFile,
|
||||
ALTSourceLineErrorKey: self.sourceLine,
|
||||
]
|
||||
|
||||
userInfo.merge(self.userInfoValues) { (_, new) in new }
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
_wrappedError = [error copy];
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
//
|
||||
// JITError.swift
|
||||
// AltJIT
|
||||
//
|
||||
// Created by Riley Testut on 9/3/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension JITError
|
||||
{
|
||||
enum Code: Int, ALTErrorCode
|
||||
{
|
||||
typealias Error = JITError
|
||||
|
||||
case processNotRunning
|
||||
case dependencyNotFound
|
||||
}
|
||||
|
||||
static func processNotRunning(_ process: AppProcess, file: StaticString = #file, line: Int = #line) -> JITError {
|
||||
JITError(code: .processNotRunning, process: process, sourceFile: file, sourceLine: UInt(line))
|
||||
}
|
||||
|
||||
static func dependencyNotFound(_ dependency: String?, file: StaticString = #file, line: Int = #line) -> JITError {
|
||||
let errorFailure = NSLocalizedString("AltServer requires additional dependencies to enable JIT on iOS 17.", comment: "")
|
||||
return JITError(code: .dependencyNotFound, errorFailure: errorFailure, dependency: dependency, faq: "https://faq.altstore.io/how-to-use-altstore/altjit", sourceFile: file, sourceLine: UInt(line))
|
||||
}
|
||||
}
|
||||
|
||||
struct JITError: ALTLocalizedError
|
||||
{
|
||||
let code: Code
|
||||
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
|
||||
@UserInfoValue var process: AppProcess?
|
||||
|
||||
@UserInfoValue var dependency: String?
|
||||
@UserInfoValue var faq: String? // Show user FAQ URL in AltStore error log.
|
||||
|
||||
var sourceFile: StaticString?
|
||||
var sourceLine: UInt?
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self.code
|
||||
{
|
||||
case .processNotRunning:
|
||||
let targetName = self.process?.description ?? NSLocalizedString("The target app", comment: "")
|
||||
return String(format: NSLocalizedString("%@ is not running.", comment: ""), targetName)
|
||||
|
||||
case .dependencyNotFound:
|
||||
let dependencyName = self.dependency.map { "'\($0)'" } ?? NSLocalizedString("A required dependency", comment: "")
|
||||
return String(format: NSLocalizedString("%@ is not installed.", comment: ""), dependencyName)
|
||||
}
|
||||
}
|
||||
|
||||
var recoverySuggestion: String? {
|
||||
switch self.code
|
||||
{
|
||||
case .processNotRunning: return NSLocalizedString("Make sure the app is running in the foreground on your device then try again.", comment: "")
|
||||
case .dependencyNotFound: return NSLocalizedString("Please follow the instructions on the AltStore FAQ to install all required dependencies, then try again.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ public extension ALTServerError
|
||||
// because it'll still be accessible via error.underlyingError.underlyingError.
|
||||
var userInfo = error.userInfo
|
||||
userInfo[NSUnderlyingErrorKey] = error
|
||||
|
||||
self = ALTServerError(.underlyingError, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ public extension Bundle
|
||||
public static let certificateID = "ALTCertificateID"
|
||||
public static let appGroups = "ALTAppGroups"
|
||||
public static let altBundleID = "ALTBundleIdentifier"
|
||||
|
||||
public static let orgbundleIdentifier = "com.SideStore"
|
||||
public static let appbundleIdentifier = orgbundleIdentifier + ".SideStore"
|
||||
public static let devicePairingString = "ALTPairingFile"
|
||||
|
||||
@@ -34,7 +34,8 @@ public extension NSError
|
||||
|
||||
@objc(alt_localizedTitle)
|
||||
var localizedTitle: String? {
|
||||
return self.userInfo[ALTLocalizedTitleErrorKey] as? String
|
||||
let localizedTitle = self.userInfo[ALTLocalizedTitleErrorKey] as? String
|
||||
return localizedTitle
|
||||
}
|
||||
|
||||
@objc(alt_errorWithLocalizedFailure:)
|
||||
@@ -48,14 +49,17 @@ public extension NSError
|
||||
|
||||
default:
|
||||
var userInfo = self.userInfo
|
||||
userInfo[NSLocalizedFailureReasonErrorKey] = failure
|
||||
userInfo[NSLocalizedFailureErrorKey] = failure
|
||||
|
||||
return ALTWrappedError(error: self, userInfo: userInfo)
|
||||
let error = ALTWrappedError(error: self, userInfo: userInfo)
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
@objc(alt_errorWithLocalizedTitle:)
|
||||
func withLocalizedTitle(_ title: String) -> NSError {
|
||||
switch self
|
||||
func withLocalizedTitle(_ title: String) -> NSError
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case var error as any ALTLocalizedError:
|
||||
error.errorTitle = title
|
||||
@@ -64,8 +68,9 @@ public extension NSError
|
||||
default:
|
||||
var userInfo = self.userInfo
|
||||
userInfo[ALTLocalizedTitleErrorKey] = title
|
||||
return ALTWrappedError(error: self, userInfo: userInfo)
|
||||
|
||||
let error = ALTWrappedError(error: self, userInfo: userInfo)
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +82,7 @@ public extension NSError
|
||||
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason
|
||||
userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion
|
||||
userInfo[NSDebugDescriptionErrorKey] = self.localizedDebugDescription
|
||||
|
||||
// Remove userInfo values that don't conform to NSSecureEncoding.
|
||||
userInfo = userInfo.filter { (key, value) in
|
||||
guard let secureCodable = value as? NSSecureCoding else { return false }
|
||||
@@ -111,13 +117,16 @@ public extension NSError
|
||||
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
|
||||
return error
|
||||
}
|
||||
func formattedDetailedDescription(with font: ALTFont) -> NSAttributedString {
|
||||
#if canImport(UIKit)
|
||||
|
||||
func formattedDetailedDescription(with font: ALTFont) -> NSAttributedString
|
||||
{
|
||||
#if canImport(UIKit)
|
||||
let boldFontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) ?? font.fontDescriptor
|
||||
#else
|
||||
let boldFont = ALTFont(descriptor: boldFontDescriptor, size: font.pointSize)
|
||||
#else
|
||||
let boldFontDescriptor = font.fontDescriptor.withSymbolicTraits(.bold)
|
||||
#endif
|
||||
let boldFont = ALTFont(descriptor: boldFontDescriptor, size: font.pointSize) ?? font
|
||||
#endif
|
||||
|
||||
var preferredKeyOrder = [
|
||||
NSDebugDescriptionErrorKey,
|
||||
@@ -126,12 +135,13 @@ public extension NSError
|
||||
NSLocalizedFailureReasonErrorKey,
|
||||
NSLocalizedRecoverySuggestionErrorKey,
|
||||
ALTLocalizedTitleErrorKey,
|
||||
// ALTSourceFileErrorKey,
|
||||
// ALTSourceLineErrorKey,
|
||||
ALTSourceFileErrorKey,
|
||||
ALTSourceLineErrorKey,
|
||||
NSUnderlyingErrorKey
|
||||
]
|
||||
|
||||
if #available(iOS 14.5, macOS 11.3, *) {
|
||||
if #available(iOS 14.5, macOS 11.3, *)
|
||||
{
|
||||
preferredKeyOrder.append(NSMultipleUnderlyingErrorsKey)
|
||||
}
|
||||
|
||||
@@ -142,14 +152,16 @@ public extension NSError
|
||||
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason
|
||||
userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion
|
||||
|
||||
let sortedUserInfo = userInfo.sorted { a, b in
|
||||
let sortedUserInfo = userInfo.sorted { (a, b) in
|
||||
let indexA = preferredKeyOrder.firstIndex(of: a.key)
|
||||
let indexB = preferredKeyOrder.firstIndex(of: b.key)
|
||||
switch (indexA, indexB) {
|
||||
|
||||
switch (indexA, indexB)
|
||||
{
|
||||
case (let indexA?, let indexB?): return indexA < indexB
|
||||
case (_?, nil): return true // indexA exists, indexB is nil, indexA should come first
|
||||
case (nil, _?): return false // indexB exists, indexB is nil, indexB should come first
|
||||
case (nil, nil): return a.key < b.key // both nil, so sort alphabetically
|
||||
case (_?, nil): return true // indexA exists, indexB is nil, so A should come first.
|
||||
case (nil, _?): return false // indexA is nil, indexB exists, so B should come first.
|
||||
case (nil, nil): return a.key < b.key // both indexes are nil, so sort alphabetically.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,8 +178,8 @@ public extension NSError
|
||||
case NSLocalizedFailureReasonErrorKey: keyName = NSLocalizedString("Failure Reason", comment: "")
|
||||
case NSLocalizedRecoverySuggestionErrorKey: keyName = NSLocalizedString("Recovery Suggestion", comment: "")
|
||||
case ALTLocalizedTitleErrorKey: keyName = NSLocalizedString("Title", comment: "")
|
||||
// case ALTSourceFileErrorKey: keyName = NSLocalizedString("Source File", comment: "")
|
||||
// case ALTSourceLineErrorKey: keyName = NSLocalizedString("Source Line", comment: "")
|
||||
case ALTSourceFileErrorKey: keyName = NSLocalizedString("Source File", comment: "")
|
||||
case ALTSourceLineErrorKey: keyName = NSLocalizedString("Source Line", comment: "")
|
||||
case NSUnderlyingErrorKey: keyName = NSLocalizedString("Underlying Error", comment: "")
|
||||
default:
|
||||
if #available(iOS 14.5, macOS 11.3, *), key == NSMultipleUnderlyingErrorsKey
|
||||
@@ -214,27 +226,32 @@ public extension NSError
|
||||
typealias UserInfoProvider = (Error, String) -> Any?
|
||||
|
||||
@objc
|
||||
class func alt_setUserInfoValueProvider(forDomain domain: String, provider: UserInfoProvider?) {
|
||||
NSError.setUserInfoValueProvider(forDomain: domain) { error, key in
|
||||
class func alt_setUserInfoValueProvider(forDomain domain: String, provider: UserInfoProvider?)
|
||||
{
|
||||
NSError.setUserInfoValueProvider(forDomain: domain) { (error, key) in
|
||||
let nsError = error as NSError
|
||||
|
||||
switch key{
|
||||
switch key
|
||||
{
|
||||
case NSLocalizedDescriptionKey:
|
||||
if nsError.localizedFailure != nil {
|
||||
// Error has localizedFailure, so return nil to construct localizedDescription from it + localizedFailureReason
|
||||
if nsError.localizedFailure != nil
|
||||
{
|
||||
// Error has localizedFailure, so return nil to construct localizedDescription from it + localizedFailureReason.
|
||||
return nil
|
||||
} else if let localizedDescription = provider?(error, NSLocalizedDescriptionKey) as? String {
|
||||
// Only call provider() if there is no localizedFailure
|
||||
}
|
||||
else if let localizedDescription = provider?(error, NSLocalizedDescriptionKey) as? String
|
||||
{
|
||||
// Only call provider() if there is no localizedFailure.
|
||||
return localizedDescription
|
||||
}
|
||||
|
||||
/* Otherwise return failureReason for localizedDescription to avoid system prepending "Operation Failed" message
|
||||
Do NOT return provider(NSLocalizedFailureReason) which might be unexpectedly nil if unrecognized error code. */
|
||||
|
||||
// Otherwise, return failureReason for localizedDescription to avoid system prepending "Operation Failed" message.
|
||||
// Do NOT return provider(NSLocalizedFailureReason), which might be unexpectedly nil if unrecognized error code.
|
||||
return nsError.localizedFailureReason
|
||||
|
||||
default:
|
||||
return provider?(error, key)
|
||||
let value = provider?(error, key)
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,21 +260,27 @@ public extension NSError
|
||||
public extension Error
|
||||
{
|
||||
var underlyingError: Error? {
|
||||
return (self as NSError).userInfo[NSUnderlyingErrorKey] as? Error
|
||||
let underlyingError = (self as NSError).userInfo[NSUnderlyingErrorKey] as? Error
|
||||
return underlyingError
|
||||
}
|
||||
|
||||
var localizedErrorCode: String {
|
||||
return String(format: NSLocalizedString("%@ %@", comment: ""), (self as NSError).domain, self.displayCode as NSNumber)
|
||||
let nsError = self as NSError
|
||||
let localizedErrorCode = String(format: NSLocalizedString("%@ %@", comment: ""), nsError.domain, self.displayCode as NSNumber)
|
||||
return localizedErrorCode
|
||||
}
|
||||
|
||||
var displayCode: Int {
|
||||
guard let serverError = self as? ALTServerError else {
|
||||
// Not ALTServerError, so display regular code.
|
||||
return (self as NSError).code
|
||||
}
|
||||
|
||||
/* We want AltServerError codes to start at 2000, but we can't change them without breaking AltServer compatibility.
|
||||
Instead we just add 2000 when displaying code to the user to make it appear as if the codes start at 2000 anyway.
|
||||
*/
|
||||
return 2000 + serverError.code.rawValue
|
||||
// We want ALTServerError codes to start at 2000,
|
||||
// but we can't change them without breaking AltServer compatibility.
|
||||
// Instead, we just add 2000 when displaying code to user
|
||||
// to make it appear as if codes start at 2000 normally.
|
||||
let code = 2000 + serverError.code.rawValue
|
||||
return code
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,21 @@
|
||||
// OperatingSystemVersion+Comparable.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by nythepegasus on 5/9/24.
|
||||
// Created by Riley Testut on 11/15/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension OperatingSystemVersion: Comparable {
|
||||
public static func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool {
|
||||
extension OperatingSystemVersion: Comparable
|
||||
{
|
||||
public static func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool
|
||||
{
|
||||
return lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion
|
||||
}
|
||||
|
||||
public static func <(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool {
|
||||
public static func <(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool
|
||||
{
|
||||
return lhs.stringValue.compare(rhs.stringValue, options: .numeric) == .orderedAscending
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ extension CodableError
|
||||
case codableError(CodableError)
|
||||
indirect case array([UserInfoValue])
|
||||
indirect case dictionary([String: UserInfoValue])
|
||||
|
||||
var value: Any? {
|
||||
switch self
|
||||
{
|
||||
@@ -42,6 +43,7 @@ extension CodableError
|
||||
case .dictionary(let dictionary): return dictionary.compactMapValues { $0.value } // .compactMapValues instead of .mapValues to ensure nil values are removed.
|
||||
}
|
||||
}
|
||||
|
||||
var codableValue: Codable? {
|
||||
switch self
|
||||
{
|
||||
@@ -52,10 +54,12 @@ extension CodableError
|
||||
let sanitizedError = nsError.sanitizedForSerialization()
|
||||
let data = try? NSKeyedArchiver.archivedData(withRootObject: sanitizedError, requiringSecureCoding: true)
|
||||
return data
|
||||
|
||||
case .array(let array): return array
|
||||
case .dictionary(let dictionary): return dictionary
|
||||
}
|
||||
}
|
||||
|
||||
init(_ rawValue: Any?)
|
||||
{
|
||||
switch rawValue
|
||||
@@ -69,6 +73,7 @@ extension CodableError
|
||||
default: self = .unknown(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.singleValueContainer()
|
||||
@@ -104,11 +109,11 @@ extension CodableError
|
||||
self = .unknown(nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func encode(to encoder: Encoder) throws
|
||||
{
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
|
||||
if let value = self.codableValue
|
||||
{
|
||||
try container.encode(value)
|
||||
@@ -127,11 +132,11 @@ struct CodableError: Codable
|
||||
return self.rawError ?? NSError(domain: self.errorDomain, code: self.errorCode, userInfo: self.userInfo ?? [:])
|
||||
}
|
||||
private var rawError: Error?
|
||||
|
||||
|
||||
private var errorDomain: String
|
||||
private var errorCode: Int
|
||||
private var userInfo: [String: Any]?
|
||||
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case errorDomain
|
||||
@@ -143,58 +148,63 @@ struct CodableError: Codable
|
||||
init(error: Error)
|
||||
{
|
||||
self.rawError = error
|
||||
|
||||
|
||||
let nsError = error as NSError
|
||||
self.errorDomain = nsError.domain
|
||||
self.errorCode = nsError.code
|
||||
|
||||
|
||||
if !nsError.userInfo.isEmpty
|
||||
{
|
||||
self.userInfo = nsError.userInfo
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
|
||||
// Assume ALTServerError.errorDomain if no explicit domain provided.
|
||||
self.errorDomain = try container.decodeIfPresent(String.self, forKey: .errorDomain) ?? ALTServerError.errorDomain
|
||||
self.errorCode = try container.decode(Int.self, forKey: .errorCode)
|
||||
|
||||
|
||||
if let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .errorUserInfo)
|
||||
{
|
||||
// Attempt decoding from .errorUserInfo first, because it will gracefully handle unknown user info values.
|
||||
|
||||
|
||||
// Copy ALTLocalized... values to NSLocalized... if provider is nil or if error is unrecognized.
|
||||
// This ensures we preserve error messages if receiving an unknown error.
|
||||
var userInfo = rawUserInfo.compactMapValues { $0.value }
|
||||
|
||||
|
||||
// Recognized == the provider returns value for NSLocalizedFailureReasonErrorKey, or error is ALTServerError.underlyingError.
|
||||
let provider = NSError.userInfoValueProvider(forDomain: self.errorDomain)
|
||||
let isRecognizedError = (
|
||||
provider?(self.error, NSLocalizedFailureReasonErrorKey) != nil ||
|
||||
(self.error._domain == ALTServerError.errorDomain && self.error._code == ALTServerError.underlyingError.rawValue)
|
||||
)
|
||||
|
||||
if !isRecognizedError
|
||||
{
|
||||
// Error not recognized, so copy over NSLocalizedDescriptionKey and NSLocalizedFailureReasonErrorKey.
|
||||
userInfo[NSLocalizedDescriptionKey] = userInfo[ErrorUserInfoKey.altLocalizedDescription]
|
||||
userInfo[NSLocalizedFailureReasonErrorKey] = userInfo[ErrorUserInfoKey.altLocalizedFailureReason]
|
||||
}
|
||||
|
||||
// Copy over NSLocalizedRecoverySuggestionErrorKey and NSDebugDescriptionErrorKey if provider returns nil.
|
||||
if provider?(self.error, NSLocalizedRecoverySuggestionErrorKey) == nil
|
||||
{
|
||||
userInfo[NSLocalizedRecoverySuggestionErrorKey] = userInfo[ErrorUserInfoKey.altLocalizedRecoverySuggestion]
|
||||
}
|
||||
|
||||
if provider?(self.error, NSDebugDescriptionErrorKey) == nil
|
||||
{
|
||||
userInfo[NSDebugDescriptionErrorKey] = userInfo[ErrorUserInfoKey.altDebugDescription]
|
||||
}
|
||||
|
||||
userInfo[ErrorUserInfoKey.altLocalizedDescription] = nil
|
||||
userInfo[ErrorUserInfoKey.altLocalizedFailureReason] = nil
|
||||
userInfo[ErrorUserInfoKey.altLocalizedRecoverySuggestion] = nil
|
||||
userInfo[ErrorUserInfoKey.altDebugDescription] = nil
|
||||
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
else if let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .legacyUserInfo)
|
||||
@@ -204,11 +214,13 @@ struct CodableError: Codable
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws
|
||||
{
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.errorDomain, forKey: .errorDomain)
|
||||
try container.encode(self.errorCode, forKey: .errorCode)
|
||||
|
||||
let rawLegacyUserInfo = self.userInfo?.compactMapValues { (value) -> UserInfoValue? in
|
||||
// .legacyUserInfo only supports String and NSError values.
|
||||
switch value
|
||||
@@ -219,19 +231,19 @@ struct CodableError: Codable
|
||||
}
|
||||
}
|
||||
try container.encodeIfPresent(rawLegacyUserInfo, forKey: .legacyUserInfo)
|
||||
|
||||
|
||||
let nsError = self.error as NSError
|
||||
|
||||
|
||||
var userInfo = self.userInfo ?? [:]
|
||||
userInfo[ErrorUserInfoKey.altLocalizedDescription] = nsError.localizedDescription
|
||||
userInfo[ErrorUserInfoKey.altLocalizedFailureReason] = nsError.localizedFailureReason
|
||||
userInfo[ErrorUserInfoKey.altLocalizedRecoverySuggestion] = nsError.localizedRecoverySuggestion
|
||||
userInfo[ErrorUserInfoKey.altDebugDescription] = nsError.localizedDebugDescription
|
||||
|
||||
|
||||
// No need to use alternate key. This is a no-op if userInfo already contains localizedFailure,
|
||||
// but it caches the UserInfoProvider value if one exists.
|
||||
userInfo[NSLocalizedFailureErrorKey] = nsError.localizedFailure
|
||||
|
||||
|
||||
let rawUserInfo = userInfo.compactMapValues { UserInfoValue($0) }
|
||||
try container.encodeIfPresent(rawUserInfo, forKey: .errorUserInfo)
|
||||
}
|
||||
|
||||
@@ -201,6 +201,7 @@ public struct ErrorResponse: ServerMessageProtocol
|
||||
public var identifier = "ErrorResponse"
|
||||
|
||||
public var error: ALTServerError {
|
||||
// Must be ALTServerError
|
||||
return self.serverError.map { ALTServerError($0.error) } ?? ALTServerError(self.errorCode)
|
||||
}
|
||||
private var serverError: CodableError?
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// AltXPCProtocol.h
|
||||
// AltXPC
|
||||
//
|
||||
// Created by Riley Testut on 12/2/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class ALTAnisetteData;
|
||||
|
||||
@protocol AltXPCProtocol
|
||||
|
||||
- (void)ping:(void (^_Nonnull)(void))completionHandler;
|
||||
- (void)requestAnisetteDataWithCompletionHandler:(void (^_Nonnull)(ALTAnisetteData *_Nullable anisetteData, NSError *_Nullable error))completionHandler;
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user