mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
iOS26: added support for iOS 26 deployment target + CI (fixed layout issues, added splash screen, fixed nav title insets).
This commit is contained in:
@@ -3472,7 +3472,7 @@
|
|||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||||
@@ -3500,7 +3500,7 @@
|
|||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||||
@@ -3527,7 +3527,7 @@
|
|||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||||
@@ -3554,7 +3554,7 @@
|
|||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||||
|
|||||||
@@ -532,6 +532,7 @@
|
|||||||
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<color key="tintColor" name="Primary"/>
|
<color key="tintColor" name="Primary"/>
|
||||||
|
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
<connections>
|
<connections>
|
||||||
@@ -561,6 +562,7 @@
|
|||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" 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="20" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
<connections>
|
<connections>
|
||||||
@@ -913,6 +915,7 @@
|
|||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
<connections>
|
<connections>
|
||||||
|
|||||||
@@ -10,85 +10,67 @@ import UIKit
|
|||||||
import Roxas
|
import Roxas
|
||||||
import minimuxer
|
import minimuxer
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
let pairingFileName = "ALTPairingFile.mobiledevicepairing"
|
let pairingFileName = "ALTPairingFile.mobiledevicepairing"
|
||||||
|
|
||||||
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
final class LaunchViewController: UIViewController, UIDocumentPickerDelegate {
|
||||||
{
|
|
||||||
private var didFinishLaunching = false
|
private var didFinishLaunching = false
|
||||||
|
private var retries = 0
|
||||||
|
private var maxRetries = 3
|
||||||
|
private var splashView: SplashView!
|
||||||
|
private var destinationViewController: TabBarController?
|
||||||
|
private var startTime: Date!
|
||||||
|
|
||||||
private var destinationViewController: TabBarController!
|
override func viewDidLoad() {
|
||||||
|
|
||||||
override var launchConditions: [RSTLaunchCondition] {
|
|
||||||
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
|
|
||||||
DatabaseManager.shared.start(completionHandler: completionHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
return [isDatabaseStarted]
|
|
||||||
}
|
|
||||||
|
|
||||||
override var childForStatusBarStyle: UIViewController? {
|
|
||||||
return self.children.first
|
|
||||||
}
|
|
||||||
|
|
||||||
override var childForStatusBarHidden: UIViewController? {
|
|
||||||
return self.children.first
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad()
|
|
||||||
{
|
|
||||||
defer {
|
|
||||||
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
|
||||||
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
|
||||||
}
|
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
splashView = SplashView(frame: view.bounds, appName: "SideStore")
|
||||||
|
destinationViewController = storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController
|
||||||
|
view.addSubview(splashView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(true)
|
super.viewDidAppear(animated)
|
||||||
if #available(iOS 17, *), !UserDefaults.standard.sidejitenable {
|
guard !didFinishLaunching else { return }
|
||||||
DispatchQueue.global().async {
|
Task {
|
||||||
self.isSideJITServerDetected() { result in
|
startTime = Date()
|
||||||
DispatchQueue.main.async {
|
await runLaunchSequence()
|
||||||
switch result {
|
doPostLaunch()
|
||||||
case .success():
|
}
|
||||||
let dialogMessage = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert)
|
}
|
||||||
|
|
||||||
// Create OK button with action handler
|
private func runLaunchSequence() async {
|
||||||
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
|
guard retries < maxRetries else { return }
|
||||||
UserDefaults.standard.sidejitenable = true
|
retries += 1
|
||||||
})
|
|
||||||
|
|
||||||
let cancel = UIAlertAction(title: "Cancel", style: .cancel)
|
await Task.detached {
|
||||||
//Add OK button to a dialog message
|
if !DatabaseManager.shared.isStarted {
|
||||||
dialogMessage.addAction(ok)
|
await withCheckedContinuation { continuation in
|
||||||
dialogMessage.addAction(cancel)
|
DatabaseManager.shared.start { error in
|
||||||
|
if let error {
|
||||||
// Present Alert to
|
Task { await self.handleLaunchError(error, retryCallback: self.runLaunchSequence) }
|
||||||
self.present(dialogMessage, animated: true, completion: nil)
|
} else {
|
||||||
case .failure(_):
|
Task { await self.finishLaunching() }
|
||||||
print("Cannot find sideJITServer")
|
|
||||||
}
|
}
|
||||||
|
continuation.resume(returning: ())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
await self.finishLaunching()
|
||||||
}
|
}
|
||||||
}
|
}.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private func doPostLaunch() {
|
||||||
|
SideJITManager.shared.checkAndPromptIfNeeded(presentingVC: self)
|
||||||
if #available(iOS 17, *), UserDefaults.standard.sidejitenable {
|
if #available(iOS 17, *), UserDefaults.standard.sidejitenable {
|
||||||
DispatchQueue.global().async {
|
DispatchQueue.global().async { SideJITManager.shared.askForNetwork() }
|
||||||
self.askfornetwork()
|
|
||||||
}
|
|
||||||
print("SideJITServer Enabled")
|
print("SideJITServer Enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#if !targetEnvironment(simulator)
|
#if !targetEnvironment(simulator)
|
||||||
|
guard let pf = PairingFileManager.shared.fetchPairingFile(presentingVC: self) else {
|
||||||
guard let pf = fetchPairingFile() else {
|
|
||||||
displayError("Device pairing file not found.")
|
displayError("Device pairing file not found.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -96,118 +78,25 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func askfornetwork() {
|
func start_minimuxer_threads(_ pairing_file: String) {
|
||||||
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
target_minimuxer_address()
|
||||||
|
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||||
var SJSURL = address
|
do {
|
||||||
|
let loggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
|
||||||
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
try minimuxer.startWithLogger(pairing_file, documentsDirectory, loggingEnabled)
|
||||||
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
} catch {
|
||||||
|
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName))
|
||||||
|
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR")")
|
||||||
}
|
}
|
||||||
|
start_auto_mounter(documentsDirectory)
|
||||||
// Create a network operation at launch to Refresh SideJITServer
|
|
||||||
let url = URL(string: "\(SJSURL)/re/")!
|
|
||||||
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
|
|
||||||
print(data)
|
|
||||||
}
|
|
||||||
task.resume()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSideJITServerDetected(completion: @escaping (Result<Void, Error>) -> Void) {
|
func fetchPairingFile() -> String? { PairingFileManager.shared.fetchPairingFile(presentingVC: self) }
|
||||||
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
|
||||||
|
|
||||||
var SJSURL = address
|
|
||||||
|
|
||||||
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
|
||||||
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a network operation at launch to Refresh SideJITServer
|
|
||||||
let url = URL(string: SJSURL)!
|
|
||||||
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
|
|
||||||
if let error = error {
|
|
||||||
print("No SideJITServer on Network")
|
|
||||||
completion(.failure(error))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
|
||||||
task.resume()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchPairingFile() -> String? {
|
|
||||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
|
||||||
let fm = FileManager.default
|
|
||||||
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
|
||||||
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
|
|
||||||
print("Loaded ALTPairingFile from \(documentsPath.path)")
|
|
||||||
return contents
|
|
||||||
} else if
|
|
||||||
let appResourcePath = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"),
|
|
||||||
fm.fileExists(atPath: appResourcePath.path),
|
|
||||||
let data = fm.contents(atPath: appResourcePath.path),
|
|
||||||
let contents = String(data: data, encoding: .utf8),
|
|
||||||
!contents.isEmpty,
|
|
||||||
!UserDefaults.standard.isPairingReset {
|
|
||||||
print("Loaded ALTPairingFile from \(appResourcePath.path)")
|
|
||||||
return contents
|
|
||||||
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset{
|
|
||||||
print("Loaded ALTPairingFile from Info.plist")
|
|
||||||
return plistString
|
|
||||||
} else {
|
|
||||||
// Show an alert explaining the pairing file
|
|
||||||
// Create new Alert
|
|
||||||
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert)
|
|
||||||
|
|
||||||
// Create OK button with action handler
|
|
||||||
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
|
|
||||||
// Try to load it from a file picker
|
|
||||||
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
|
|
||||||
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
|
|
||||||
types.append(.xml)
|
|
||||||
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
|
|
||||||
documentPickerController.shouldShowFileExtensions = true
|
|
||||||
documentPickerController.delegate = self
|
|
||||||
self.present(documentPickerController, animated: true, completion: nil)
|
|
||||||
UserDefaults.standard.isPairingReset = false
|
|
||||||
})
|
|
||||||
|
|
||||||
//Add "help" button to take user to wiki
|
|
||||||
let wikiOption = UIAlertAction(title: "Help", style: .default) { (action) in
|
|
||||||
let wikiURL: String = "https://docs.sidestore.io/docs/installation/pairing-file"
|
|
||||||
if let url = URL(string: wikiURL) {
|
|
||||||
UIApplication.shared.open(url)
|
|
||||||
}
|
|
||||||
sleep(2)
|
|
||||||
exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
//Add buttons to dialog message
|
|
||||||
dialogMessage.addAction(wikiOption)
|
|
||||||
dialogMessage.addAction(ok)
|
|
||||||
|
|
||||||
// Present Alert to
|
|
||||||
self.present(dialogMessage, animated: true, completion: nil)
|
|
||||||
|
|
||||||
let dialogMessage2 = UIAlertController(title: "Analytics", message: "This app contains anonymous analytics for research and project development. By continuing to use this app, you are consenting to this data collection", preferredStyle: .alert)
|
|
||||||
|
|
||||||
let ok2 = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in})
|
|
||||||
|
|
||||||
dialogMessage2.addAction(ok2)
|
|
||||||
self.present(dialogMessage2, animated: true, completion: nil)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func displayError(_ msg: String) {
|
func displayError(_ msg: String) {
|
||||||
print(msg)
|
print(msg)
|
||||||
// Create a new alert
|
let alert = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
|
||||||
let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
|
self.present(alert, animated: true)
|
||||||
|
|
||||||
// Present alert to user
|
|
||||||
self.present(dialogMessage, animated: true, completion: nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||||
@@ -215,96 +104,58 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
|||||||
let isSecuredURL = url.startAccessingSecurityScopedResource() == true
|
let isSecuredURL = url.startAccessingSecurityScopedResource() == true
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// Read to a string
|
let data = try Data(contentsOf: url)
|
||||||
let data1 = try Data(contentsOf: urls[0])
|
guard let pairingString = String(data: data, encoding: .utf8) else {
|
||||||
let pairing_string = String(bytes: data1, encoding: .utf8)
|
|
||||||
if pairing_string == nil {
|
|
||||||
displayError("Unable to read pairing file")
|
displayError("Unable to read pairing file")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
try pairingString.write(to: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName), atomically: true, encoding: .utf8)
|
||||||
// Save to a file for next launch
|
start_minimuxer_threads(pairingString)
|
||||||
let pairingFile = FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")
|
|
||||||
try pairing_string?.write(to: pairingFile, atomically: true, encoding: String.Encoding.utf8)
|
|
||||||
|
|
||||||
// Start minimuxer now that we have a file
|
|
||||||
start_minimuxer_threads(pairing_string!)
|
|
||||||
} catch {
|
} catch {
|
||||||
displayError("Unable to read pairing file")
|
displayError("Unable to read pairing file")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSecuredURL) {
|
if isSecuredURL { url.stopAccessingSecurityScopedResource() }
|
||||||
url.stopAccessingSecurityScopedResource()
|
controller.dismiss(animated: true)
|
||||||
}
|
|
||||||
controller.dismiss(animated: true, completion: nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||||
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
|
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func start_minimuxer_threads(_ pairing_file: String) {
|
|
||||||
target_minimuxer_address()
|
|
||||||
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
|
||||||
do {
|
|
||||||
// enable minimuxer console logging only if enabled in settings
|
|
||||||
let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
|
|
||||||
try minimuxer.startWithLogger(pairing_file, documentsDirectory, isMinimuxerConsoleLoggingEnabled)
|
|
||||||
} catch {
|
|
||||||
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
|
|
||||||
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
|
|
||||||
}
|
|
||||||
start_auto_mounter(documentsDirectory)
|
|
||||||
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
|
||||||
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LaunchViewController
|
extension LaunchViewController {
|
||||||
{
|
@MainActor
|
||||||
override func handleLaunchError(_ error: Error)
|
func handleLaunchError(_ error: Error, retryCallback: (() async -> Void)? = nil) {
|
||||||
{
|
do { throw error } catch let error as NSError {
|
||||||
do
|
|
||||||
{
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
catch let error as NSError
|
|
||||||
{
|
|
||||||
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
|
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
|
||||||
|
let desc: String
|
||||||
let errorDescription: String
|
if #available(iOS 14.5, *) {
|
||||||
|
desc = ([error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }).joined(separator: "\n\n")
|
||||||
if #available(iOS 14.5, *)
|
} else {
|
||||||
{
|
desc = error.debugDescription
|
||||||
let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
|
|
||||||
errorDescription = errorMessages.joined(separator: "\n\n")
|
|
||||||
}
|
}
|
||||||
else
|
let alert = UIAlertController(title: title, message: desc, preferredStyle: .alert)
|
||||||
{
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default) { _ in
|
||||||
errorDescription = error.debugDescription
|
Task { await retryCallback?() }
|
||||||
}
|
})
|
||||||
|
present(alert, animated: true)
|
||||||
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert)
|
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
|
|
||||||
self.handleLaunchConditions()
|
|
||||||
}))
|
|
||||||
self.present(alertController, animated: true, completion: nil)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func finishLaunching()
|
@MainActor
|
||||||
{
|
func finishLaunching() async {
|
||||||
super.finishLaunching()
|
guard !didFinishLaunching else { return }
|
||||||
|
didFinishLaunching = true
|
||||||
guard !self.didFinishLaunching else { return }
|
|
||||||
|
|
||||||
AppManager.shared.update()
|
AppManager.shared.update()
|
||||||
AppManager.shared.updatePatronsIfNeeded()
|
AppManager.shared.updatePatronsIfNeeded()
|
||||||
PatreonAPI.shared.refreshPatreonAccount()
|
PatreonAPI.shared.refreshPatreonAccount()
|
||||||
|
|
||||||
AppManager.shared.updateAllSources { result in
|
AppManager.shared.updateAllSources { result in
|
||||||
guard case .failure(let error) = result else { return }
|
guard case .failure(let error) = result else { return }
|
||||||
Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)")
|
Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)")
|
||||||
|
|
||||||
|
|
||||||
let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError)
|
let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError)
|
||||||
print("Failed to update sources on launch. \(errorDesc)")
|
print("Failed to update sources on launch. \(errorDesc)")
|
||||||
|
|
||||||
@@ -312,63 +163,64 @@ extension LaunchViewController
|
|||||||
if String(describing: error).contains("The Internet connection appears to be offline"){
|
if String(describing: error).contains("The Internet connection appears to be offline"){
|
||||||
mode = .localizedDescription // dont make noise!
|
mode = .localizedDescription // dont make noise!
|
||||||
}
|
}
|
||||||
|
|
||||||
let toastView = ToastView(error: error, mode: mode)
|
let toastView = ToastView(error: error, mode: mode)
|
||||||
toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside)
|
toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside)
|
||||||
toastView.show(in: self.destinationViewController.selectedViewController ?? self.destinationViewController)
|
toastView.show(in: self.destinationViewController!.selectedViewController ?? self.destinationViewController!)
|
||||||
}
|
}
|
||||||
|
updateKnownSources()
|
||||||
self.updateKnownSources()
|
|
||||||
|
|
||||||
// Ask widgets to be refreshed
|
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
didFinishLaunching = true
|
||||||
|
|
||||||
// Add view controller as child (rather than presenting modally)
|
let destinationVC = destinationViewController!
|
||||||
// so tint adjustment + card presentations works correctly.
|
|
||||||
self.destinationViewController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
|
|
||||||
self.destinationViewController.view.alpha = 0.0
|
|
||||||
self.addChild(self.destinationViewController)
|
|
||||||
self.view.addSubview(self.destinationViewController.view, pinningEdgesWith: .zero)
|
|
||||||
self.destinationViewController.didMove(toParent: self)
|
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.2) {
|
let elapsed = abs(startTime.timeIntervalSinceNow)
|
||||||
self.destinationViewController.view.alpha = 1.0
|
let remaining = elapsed >= 1 ? 0 : 1 - elapsed
|
||||||
|
try? await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000))
|
||||||
|
|
||||||
|
destinationVC.loadViewIfNeeded()
|
||||||
|
addChild(destinationVC)
|
||||||
|
destinationVC.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(destinationVC.view)
|
||||||
|
destinationVC.didMove(toParent: self)
|
||||||
|
|
||||||
|
// Pin edges BEFORE animation
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
destinationVC.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
destinationVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
destinationVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
destinationVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
// Set initial alpha for fade-in
|
||||||
|
destinationVC.view.alpha = 0
|
||||||
|
|
||||||
|
UIView.transition(with: view, duration: 0.3, options: .transitionCrossDissolve) { [self] in
|
||||||
|
self.splashView.alpha = 0
|
||||||
|
destinationVC.view.alpha = 1
|
||||||
|
} completion: { _ in
|
||||||
|
self.splashView.removeFromSuperview()
|
||||||
|
self.destinationViewController = destinationVC
|
||||||
}
|
}
|
||||||
|
|
||||||
self.didFinishLaunching = true
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private extension LaunchViewController
|
func updateKnownSources() {
|
||||||
{
|
|
||||||
func updateKnownSources()
|
|
||||||
{
|
|
||||||
AppManager.shared.updateKnownSources { result in
|
AppManager.shared.updateKnownSources { result in
|
||||||
switch result
|
switch result {
|
||||||
{
|
|
||||||
case .failure(let error): print("[ALTLog] Failed to update known sources:", error)
|
case .failure(let error): print("[ALTLog] Failed to update known sources:", error)
|
||||||
case .success((_, let blockedSources)):
|
case .success((_, let blockedSources)):
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
||||||
let blockedSourceIDs = Set(blockedSources.lazy.map { $0.identifier })
|
let blockedSourceIDs = Set(blockedSources.lazy.map { $0.identifier })
|
||||||
let blockedSourceURLs = Set(blockedSources.lazy.compactMap { $0.sourceURL })
|
let blockedSourceURLs = Set(blockedSources.lazy.compactMap { $0.sourceURL })
|
||||||
|
let predicate = NSPredicate(format: "%K IN %@ OR %K IN %@", #keyPath(Source.identifier), blockedSourceIDs, #keyPath(Source.sourceURL), blockedSourceURLs)
|
||||||
let predicate = NSPredicate(format: "%K IN %@ OR %K IN %@",
|
let sourceErrors = Source.all(satisfying: predicate, in: context).map { source in
|
||||||
#keyPath(Source.identifier), blockedSourceIDs,
|
let blocked = blockedSources.first { $0.identifier == source.identifier }
|
||||||
#keyPath(Source.sourceURL), blockedSourceURLs)
|
return SourceError.blocked(source, bundleIDs: blocked?.bundleIDs, existingSource: source)
|
||||||
|
|
||||||
let sourceErrors = Source.all(satisfying: predicate, in: context).map { (source) in
|
|
||||||
let blockedSource = blockedSources.first { $0.identifier == source.identifier }
|
|
||||||
return SourceError.blocked(source, bundleIDs: blockedSource?.bundleIDs, existingSource: source)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard !sourceErrors.isEmpty else { return }
|
guard !sourceErrors.isEmpty else { return }
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
for error in sourceErrors
|
for error in sourceErrors {
|
||||||
{
|
|
||||||
let title = String(format: NSLocalizedString("“%@” Blocked", comment: ""), error.$source.name)
|
let title = String(format: NSLocalizedString("“%@” Blocked", comment: ""), error.$source.name)
|
||||||
let message = [error.localizedDescription, error.recoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
|
let message = [error.localizedDescription, error.recoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
|
||||||
|
|
||||||
await self.presentAlert(title: title, message: message)
|
await self.presentAlert(title: title, message: message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,3 +229,142 @@ private extension LaunchViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - SplashView
|
||||||
|
final class SplashView: UIView {
|
||||||
|
let iconView = UIImageView()
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
|
||||||
|
init(frame: CGRect, appName: String) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = .systemBackground
|
||||||
|
setupIcon()
|
||||||
|
setupTitle(appName: appName)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError() }
|
||||||
|
|
||||||
|
private func setupIcon() {
|
||||||
|
let container = UIView()
|
||||||
|
container.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.layer.shadowColor = UIColor.black.cgColor
|
||||||
|
container.layer.shadowOpacity = 0.25
|
||||||
|
container.layer.shadowOffset = CGSize(width: 0, height: 4)
|
||||||
|
container.layer.shadowRadius = 8
|
||||||
|
addSubview(container)
|
||||||
|
|
||||||
|
iconView.image = UIImage(named: "AppIcon") ?? UIImage(named: "AppIcon60x60") ?? UIImage(systemName: "app.fill")
|
||||||
|
iconView.contentMode = .scaleAspectFit
|
||||||
|
iconView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
iconView.layer.cornerRadius = 24
|
||||||
|
iconView.clipsToBounds = true
|
||||||
|
container.addSubview(iconView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
container.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
container.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -20),
|
||||||
|
container.widthAnchor.constraint(equalToConstant: 120),
|
||||||
|
container.heightAnchor.constraint(equalToConstant: 120),
|
||||||
|
iconView.topAnchor.constraint(equalTo: container.topAnchor),
|
||||||
|
iconView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||||
|
iconView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||||
|
iconView.trailingAnchor.constraint(equalTo: container.trailingAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupTitle(appName: String) {
|
||||||
|
titleLabel.text = appName
|
||||||
|
titleLabel.font = .systemFont(ofSize: 24, weight: .bold)
|
||||||
|
titleLabel.textColor = .label
|
||||||
|
titleLabel.textAlignment = .center
|
||||||
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(titleLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
titleLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 12),
|
||||||
|
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - PairingFileManager
|
||||||
|
final class PairingFileManager {
|
||||||
|
static let shared = PairingFileManager()
|
||||||
|
func fetchPairingFile(presentingVC: UIViewController) -> String? {
|
||||||
|
let fm = FileManager.default
|
||||||
|
let filename = pairingFileName
|
||||||
|
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
||||||
|
if fm.fileExists(atPath: documentsPath.path),
|
||||||
|
let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
|
||||||
|
return contents
|
||||||
|
}
|
||||||
|
if let url = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"),
|
||||||
|
fm.fileExists(atPath: url.path),
|
||||||
|
let data = fm.contents(atPath: url.path),
|
||||||
|
let contents = String(data: data, encoding: .utf8),
|
||||||
|
!contents.isEmpty, !UserDefaults.standard.isPairingReset { return contents }
|
||||||
|
if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String,
|
||||||
|
!plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset { return plistString }
|
||||||
|
|
||||||
|
presentPairingFileAlert(on: presentingVC)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentPairingFileAlert(on vc: UIViewController) {
|
||||||
|
let alert = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "Help", style: .default) { _ in
|
||||||
|
if let url = URL(string: "https://docs.sidestore.io/docs/installation/pairing-file") { UIApplication.shared.open(url) }
|
||||||
|
sleep(2); exit(0)
|
||||||
|
})
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
|
||||||
|
var types = UTType.types(tag: "plist", tagClass: .filenameExtension, conformingTo: nil)
|
||||||
|
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: .filenameExtension, conformingTo: .data))
|
||||||
|
types.append(.xml)
|
||||||
|
let picker = UIDocumentPickerViewController(forOpeningContentTypes: types)
|
||||||
|
picker.delegate = vc as? UIDocumentPickerDelegate
|
||||||
|
picker.shouldShowFileExtensions = true
|
||||||
|
vc.present(picker, animated: true)
|
||||||
|
UserDefaults.standard.isPairingReset = false
|
||||||
|
})
|
||||||
|
vc.present(alert, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SideJITManager
|
||||||
|
final class SideJITManager {
|
||||||
|
static let shared = SideJITManager()
|
||||||
|
func checkAndPromptIfNeeded(presentingVC: UIViewController) {
|
||||||
|
guard #available(iOS 17, *), !UserDefaults.standard.sidejitenable else { return }
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
self.isSideJITServerDetected { result in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
switch result {
|
||||||
|
case .success():
|
||||||
|
let alert = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in UserDefaults.standard.sidejitenable = true })
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
presentingVC.present(alert, animated: true)
|
||||||
|
case .failure(_): print("Cannot find sideJITServer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func askForNetwork() {
|
||||||
|
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
||||||
|
let SJSURL = address.isEmpty ? "http://sidejitserver._http._tcp.local:8080" : address
|
||||||
|
URLSession.shared.dataTask(with: URL(string: "\(SJSURL)/re/")!) { data, resp, err in
|
||||||
|
print("data: \(String(describing: data)), response: \(String(describing: resp)), error: \(String(describing: err))")
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSideJITServerDetected(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
||||||
|
let SJSURL = address.isEmpty ? "http://sidejitserver._http._tcp.local:8080" : address
|
||||||
|
guard let url = URL(string: SJSURL) else { return }
|
||||||
|
URLSession.shared.dataTask(with: url) { _, _, error in
|
||||||
|
if let error = error { completion(.failure(error)); return }
|
||||||
|
completion(.success(()))
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1450,6 +1450,8 @@
|
|||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<color key="barTintColor" name="SettingsBackground"/>
|
<color key="barTintColor" name="SettingsBackground"/>
|
||||||
|
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
||||||
|
|
||||||
<textAttributes key="titleTextAttributes">
|
<textAttributes key="titleTextAttributes">
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
</textAttributes>
|
</textAttributes>
|
||||||
@@ -1923,6 +1925,7 @@ Settings by i cons from the Noun Project</string>
|
|||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" id="dI0-sh-yGf">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" id="dI0-sh-yGf">
|
||||||
<rect key="frame" x="0.0" y="16" width="402" height="54"/>
|
<rect key="frame" x="0.0" y="16" width="402" height="54"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
<connections>
|
<connections>
|
||||||
|
|||||||
@@ -202,6 +202,16 @@ final class SettingsViewController: UITableViewController
|
|||||||
{
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
// --- iOS 26 fix ---
|
||||||
|
if #available(iOS 26.0, *) {
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
// appearance.configureWithOpaqueBackground() // or .defaultBackground if you want blur
|
||||||
|
// appearance.backgroundColor = UIColor(named: "SettingsBackground")
|
||||||
|
appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
|
||||||
|
appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
|
||||||
|
navigationController?.navigationBar.standardAppearance = appearance
|
||||||
|
navigationController?.navigationBar.scrollEdgeAppearance = appearance // required for iOS 26, maybe enforce it in storyboard?
|
||||||
|
}
|
||||||
let nib = UINib(nibName: "SettingsHeaderFooterView", bundle: nil)
|
let nib = UINib(nibName: "SettingsHeaderFooterView", bundle: nil)
|
||||||
self.prototypeHeaderFooterView = nib.instantiate(withOwner: nil, options: nil)[0] as? SettingsHeaderFooterView
|
self.prototypeHeaderFooterView = nib.instantiate(withOwner: nil, options: nil)[0] as? SettingsHeaderFooterView
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="xWh-1U-u0q" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="xWh-1U-u0q" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="59" width="393" height="96"/>
|
<rect key="frame" x="0.0" y="59" width="393" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
<connections>
|
<connections>
|
||||||
@@ -248,6 +249,7 @@
|
|||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" largeTitles="YES" id="HLe-3g-P8I" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" largeTitles="YES" id="HLe-3g-P8I" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="393" height="108"/>
|
<rect key="frame" x="0.0" y="0.0" width="393" height="108"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
<connections>
|
<connections>
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ final class SourcesViewController: UICollectionViewController
|
|||||||
{
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
// Ensure large titles
|
||||||
|
navigationController?.navigationBar.prefersLargeTitles = true
|
||||||
|
navigationItem.largeTitleDisplayMode = .automatic
|
||||||
|
|
||||||
|
// Set title
|
||||||
|
navigationItem.title = "Sources"
|
||||||
|
navigationController?.navigationBar.layoutMargins.left = 20
|
||||||
|
|
||||||
let layout = self.makeLayout()
|
let layout = self.makeLayout()
|
||||||
self.collectionView.collectionViewLayout = layout
|
self.collectionView.collectionViewLayout = layout
|
||||||
|
|
||||||
|
|||||||
18
Makefile
18
Makefile
@@ -201,7 +201,7 @@ build-and-test:
|
|||||||
@echo ""
|
@echo ""
|
||||||
@echo "Performing a build and running tests..."
|
@echo "Performing a build and running tests..."
|
||||||
@xcodebuild test \
|
@xcodebuild test \
|
||||||
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
|
-destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.0' \
|
||||||
-resultBundlePath build/tests/test-results.xcresult \
|
-resultBundlePath build/tests/test-results.xcresult \
|
||||||
-enableCodeCoverage YES \
|
-enableCodeCoverage YES \
|
||||||
$(COMMON_BUILD_SETTINGS)
|
$(COMMON_BUILD_SETTINGS)
|
||||||
@@ -213,7 +213,7 @@ build-tests:
|
|||||||
@echo "Performing a build-for-testing..."
|
@echo "Performing a build-for-testing..."
|
||||||
@xcodebuild build-for-testing \
|
@xcodebuild build-for-testing \
|
||||||
-enableCodeCoverage YES \
|
-enableCodeCoverage YES \
|
||||||
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
|
-destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.0' \
|
||||||
$(COMMON_BUILD_SETTINGS)
|
$(COMMON_BUILD_SETTINGS)
|
||||||
|
|
||||||
run-tests:
|
run-tests:
|
||||||
@@ -224,22 +224,22 @@ run-tests:
|
|||||||
@xcodebuild test-without-building \
|
@xcodebuild test-without-building \
|
||||||
-enableCodeCoverage YES \
|
-enableCodeCoverage YES \
|
||||||
-resultBundlePath build/tests/test-results.xcresult \
|
-resultBundlePath build/tests/test-results.xcresult \
|
||||||
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
|
-destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.0' \
|
||||||
$(COMMON_BUILD_SETTINGS)
|
$(COMMON_BUILD_SETTINGS)
|
||||||
|
|
||||||
boot-sim-async:
|
boot-sim-async:
|
||||||
@if xcrun simctl list devices "iPhone 16 Pro" | grep -q "Booted"; then \
|
@if xcrun simctl list devices "iPhone 17 Pro" | grep -q "Booted"; then \
|
||||||
echo "Simulator 'iPhone 16 Pro' is already booted."; \
|
echo "Simulator 'iPhone 17 Pro' is already booted."; \
|
||||||
else \
|
else \
|
||||||
echo "Booting simulator 'iPhone 16 Pro' asynchronously..."; \
|
echo "Booting simulator 'iPhone 17 Pro' asynchronously..."; \
|
||||||
xcrun simctl boot "iPhone 16 Pro" & \
|
xcrun simctl boot "iPhone 17 Pro" & \
|
||||||
echo "Simulator boot command dispatched."; \
|
echo "Simulator boot command dispatched."; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sim-boot-check:
|
sim-boot-check:
|
||||||
@echo "Checking simulator boot status..."
|
@echo "Checking simulator boot status..."
|
||||||
@if xcrun simctl list devices "iPhone 16 Pro" | grep -q "Booted"; then \
|
@if xcrun simctl list devices "iPhone 17 Pro" | grep -q "Booted"; then \
|
||||||
echo "Simulator 'iPhone 16 Pro' is booted."; \
|
echo "Simulator 'iPhone 17 Pro' is booted."; \
|
||||||
else \
|
else \
|
||||||
echo "Simulator bootup failed or is not booted yet."; \
|
echo "Simulator bootup failed or is not booted yet."; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
|
|||||||
Reference in New Issue
Block a user