More improvements and fixes (see commit description)

- put SwiftUI in an unstable feature
- Add Reset adi.pb to SwiftUI settings
- Add localizations to more things such as Error Log and Refresh Attempts
- Move debug logging into Advanced Settings
- Add padding to version text at the bottom of SwiftUI settings
- Add some things to Unstable Features such as nesting the Feature enum in UnstableFeatures and allowing on enable/disable hooks
- Don't use ObservableObject for UnstableFeatures as it's not needed
- fix a bug with unstable features where the toggle would be reverted if you go into another tab and then back
- Use SwiftUI advanced settings in UIKit
This commit is contained in:
naturecodevoid
2023-05-27 21:53:04 -07:00
parent d2c15b5acd
commit 026392dbc7
13 changed files with 341 additions and 199 deletions

View File

@@ -93,6 +93,7 @@
992C896029A6A56500FB3501 /* LocalConsole in Frameworks */ = {isa = PBXBuildFile; productRef = 992C895F29A6A56500FB3501 /* LocalConsole */; }; 992C896029A6A56500FB3501 /* LocalConsole in Frameworks */ = {isa = PBXBuildFile; productRef = 992C895F29A6A56500FB3501 /* LocalConsole */; };
994D6E9B29E326080045B3F7 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99F87D1729D8E4C900B40039 /* minimuxer.swift */; }; 994D6E9B29E326080045B3F7 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99F87D1729D8E4C900B40039 /* minimuxer.swift */; };
994D6EB529E35C130045B3F7 /* StoreApp+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994D6EB429E35C130045B3F7 /* StoreApp+SideStore.swift */; }; 994D6EB529E35C130045B3F7 /* StoreApp+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994D6EB429E35C130045B3F7 /* StoreApp+SideStore.swift */; };
998E3B112A2061850046BBAA /* UnstableFeatures+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 998E3B102A2061850046BBAA /* UnstableFeatures+SwiftUI.swift */; };
99BCB7DF29A2AC050041D1A7 /* AdvancedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99BCB7DE29A2AC050041D1A7 /* AdvancedSettingsView.swift */; }; 99BCB7DF29A2AC050041D1A7 /* AdvancedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99BCB7DE29A2AC050041D1A7 /* AdvancedSettingsView.swift */; };
99C4EF4D2979132100CB538D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = 99C4EF4C2979132100CB538D /* SemanticVersion */; }; 99C4EF4D2979132100CB538D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = 99C4EF4C2979132100CB538D /* SemanticVersion */; };
99D87A60299F1B1100ED09A9 /* DevModeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99D87A5F299F1B1100ED09A9 /* DevModeView.swift */; }; 99D87A60299F1B1100ED09A9 /* DevModeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99D87A5F299F1B1100ED09A9 /* DevModeView.swift */; };
@@ -659,6 +660,7 @@
990D2AFF2A19593F0055D93C /* UnstableFeaturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnstableFeaturesView.swift; sourceTree = "<group>"; }; 990D2AFF2A19593F0055D93C /* UnstableFeaturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnstableFeaturesView.swift; sourceTree = "<group>"; };
994D6EB429E35C130045B3F7 /* StoreApp+SideStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoreApp+SideStore.swift"; sourceTree = "<group>"; }; 994D6EB429E35C130045B3F7 /* StoreApp+SideStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoreApp+SideStore.swift"; sourceTree = "<group>"; };
9961EC2D29BE9F2E00AF2C6F /* minimuxer-helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "minimuxer-helpers.swift"; path = "Dependencies/minimuxer/minimuxer-helpers.swift"; sourceTree = SOURCE_ROOT; }; 9961EC2D29BE9F2E00AF2C6F /* minimuxer-helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "minimuxer-helpers.swift"; path = "Dependencies/minimuxer/minimuxer-helpers.swift"; sourceTree = SOURCE_ROOT; };
998E3B102A2061850046BBAA /* UnstableFeatures+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UnstableFeatures+SwiftUI.swift"; sourceTree = "<group>"; };
99BCB7DE29A2AC050041D1A7 /* AdvancedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsView.swift; sourceTree = "<group>"; }; 99BCB7DE29A2AC050041D1A7 /* AdvancedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsView.swift; sourceTree = "<group>"; };
99D87A5F299F1B1100ED09A9 /* DevModeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevModeView.swift; sourceTree = "<group>"; }; 99D87A5F299F1B1100ED09A9 /* DevModeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevModeView.swift; sourceTree = "<group>"; };
99D87A61299F3EC300ED09A9 /* FileExplorer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExplorer.swift; sourceTree = "<group>"; }; 99D87A61299F3EC300ED09A9 /* FileExplorer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExplorer.swift; sourceTree = "<group>"; };
@@ -1322,6 +1324,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
990D2AE12A1910CD0055D93C /* UnstableFeatures.swift */, 990D2AE12A1910CD0055D93C /* UnstableFeatures.swift */,
998E3B102A2061850046BBAA /* UnstableFeatures+SwiftUI.swift */,
); );
path = "Unstable Features"; path = "Unstable Features";
sourceTree = "<group>"; sourceTree = "<group>";
@@ -2996,6 +2999,7 @@
1F180F94298E7A2500D1C98B /* Source+Trusted.swift in Sources */, 1F180F94298E7A2500D1C98B /* Source+Trusted.swift in Sources */,
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */, BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */, BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */,
998E3B112A2061850046BBAA /* UnstableFeatures+SwiftUI.swift in Sources */,
1F0DD81C2932D2FF007608A4 /* AppScreenshotsScrollView.swift in Sources */, 1F0DD81C2932D2FF007608A4 /* AppScreenshotsScrollView.swift in Sources */,
BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */, BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */,
1F545E85298D84CF00589F68 /* FilePreviewView.swift in Sources */, 1F545E85298D84CF00589F68 /* FilePreviewView.swift in Sources */,

View File

@@ -42,11 +42,14 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
override func viewDidLoad() override func viewDidLoad()
{ {
defer { defer {
// Create destinationViewController now so view controllers can register for receiving Notifications. if UnstableFeatures.enabled(.swiftUI) {
// self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController let rootView = RootView()
let rootView = RootView() .environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext) self.destinationViewController = UIHostingController(rootView: rootView)
self.destinationViewController = UIHostingController(rootView: rootView) } else {
// Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
}
} }
super.viewDidLoad() super.viewDidLoad()
} }

View File

@@ -41,16 +41,16 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
private weak var presentingViewController: UIViewController? private weak var presentingViewController: UIViewController?
// private lazy var navigationController: UINavigationController = { private lazy var navigationController: UINavigationController = {
// let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
// if #available(iOS 13.0, *) if #available(iOS 13.0, *)
// { {
// navigationController.isModalInPresentation = true navigationController.isModalInPresentation = true
// } }
// return navigationController return navigationController
// }() }()
//
// private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil) private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
private var appleIDEmailAddress: String? private var appleIDEmailAddress: String?
private var appleIDPassword: String? private var appleIDPassword: String?
@@ -267,8 +267,11 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
super.finish(result) super.finish(result)
DispatchQueue.main.async { DispatchQueue.main.async {
// self.navigationController.dismiss(animated: true, completion: nil) if UnstableFeatures.enabled(.swiftUI) {
self.dismiss() self.dismiss()
} else {
self.navigationController.dismiss(animated: true, completion: nil)
}
} }
} }
} }
@@ -278,8 +281,11 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
super.finish(result) super.finish(result)
DispatchQueue.main.async { DispatchQueue.main.async {
// self.navigationController.dismiss(animated: true, completion: nil) if UnstableFeatures.enabled(.swiftUI) {
self.dismiss() self.dismiss()
} else {
self.navigationController.dismiss(animated: true, completion: nil)
}
} }
} }
} }
@@ -290,23 +296,26 @@ private extension AuthenticationOperation
{ {
func present(_ viewController: UIViewController) -> Bool func present(_ viewController: UIViewController) -> Bool
{ {
UIApplication.shared.keyWindow?.rootViewController?.present(viewController, animated: true) if UnstableFeatures.enabled(.swiftUI) {
// guard let presentingViewController = self.presentingViewController else { return false } UIApplication.shared.keyWindow?.rootViewController?.present(viewController, animated: true)
// } else {
// self.navigationController.view.tintColor = .white guard let presentingViewController = self.presentingViewController else { return false }
//
// if self.navigationController.viewControllers.isEmpty self.navigationController.view.tintColor = .white
// {
// guard presentingViewController.presentedViewController == nil else { return false } if self.navigationController.viewControllers.isEmpty
// {
// self.navigationController.setViewControllers([viewController], animated: false) guard presentingViewController.presentedViewController == nil else { return false }
// presentingViewController.present(self.navigationController, animated: true, completion: nil)
// } self.navigationController.setViewControllers([viewController], animated: false)
// else presentingViewController.present(self.navigationController, animated: true, completion: nil)
// { }
// viewController.navigationItem.leftBarButtonItem = nil else
// self.navigationController.pushViewController(viewController, animated: true) {
// } viewController.navigationItem.leftBarButtonItem = nil
self.navigationController.pushViewController(viewController, animated: true)
}
}
return true return true
} }
@@ -326,12 +335,37 @@ private extension AuthenticationOperation
func authenticate() func authenticate()
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
let viewController = UIHostingController(rootView: NavigationView { let viewController: UIViewController
ConnectAppleIDView { appleID, password, completionHandler in if UnstableFeatures.enabled(.swiftUI) {
viewController = UIHostingController(rootView: NavigationView {
ConnectAppleIDView { appleID, password, completionHandler in
self.authenticate(appleID: appleID, password: password) { (result) in
completionHandler(result)
}
} completionHandler: { result in
if let (account, session, password) = result
{
// We presented the Auth UI and the user signed in.
// In this case, we'll assume we should show the instructions again.
self.shouldShowInstructions = true
self.appleIDPassword = password
completionHandler(.success((account, session)))
}
else
{
completionHandler(.failure(OperationError.cancelled))
}
}
}.navigationViewStyle(StackNavigationViewStyle()))
} else {
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
self.authenticate(appleID: appleID, password: password) { (result) in self.authenticate(appleID: appleID, password: password) { (result) in
completionHandler(result) completionHandler(result)
} }
} completionHandler: { result in }
authenticationViewController.completionHandler = { (result) in
if let (account, session, password) = result if let (account, session, password) = result
{ {
// We presented the Auth UI and the user signed in. // We presented the Auth UI and the user signed in.
@@ -346,7 +380,8 @@ private extension AuthenticationOperation
completionHandler(.failure(OperationError.cancelled)) completionHandler(.failure(OperationError.cancelled))
} }
} }
}.navigationViewStyle(StackNavigationViewStyle())) viewController = authenticationViewController
}
if !self.present(viewController) if !self.present(viewController)
{ {
@@ -390,50 +425,42 @@ private extension AuthenticationOperation
case .success(let anisetteData): case .success(let anisetteData):
let verificationHandler: ((@escaping (String?) -> Void) -> Void)? let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
// if let presentingViewController = self.presentingViewController verificationHandler = { (completionHandler) in
// { DispatchQueue.main.async {
verificationHandler = { (completionHandler) in let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
DispatchQueue.main.async { alertController.addTextField { (textField) in
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert) textField.autocorrectionType = .no
alertController.addTextField { (textField) in textField.autocapitalizationType = .none
textField.autocorrectionType = .no textField.keyboardType = .numberPad
textField.autocapitalizationType = .none
textField.keyboardType = .numberPad NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
}
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
let textField = alertController.textFields?.first
let code = textField?.text ?? ""
completionHandler(code)
}
submitAction.isEnabled = false
alertController.addAction(submitAction)
self.submitCodeAction = submitAction
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
completionHandler(nil)
})
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
if var topController = keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
} }
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in topController.present(alertController, animated: true, completion: nil)
let textField = alertController.textFields?.first
let code = textField?.text ?? ""
completionHandler(code)
}
submitAction.isEnabled = false
alertController.addAction(submitAction)
self.submitCodeAction = submitAction
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
completionHandler(nil)
})
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
if var topController = keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
topController.present(alertController, animated: true, completion: nil)
}
} }
} }
// } }
// else
// {
// // No view controller to present security code alert, so don't provide verificationHandler.
// verificationHandler = nil
// }
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData, ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
verificationHandler: verificationHandler) { (account, session, error) in verificationHandler: verificationHandler) { (account, session, error) in
@@ -464,15 +491,17 @@ private extension AuthenticationOperation
} }
} else { } else {
DispatchQueue.main.async { DispatchQueue.main.async {
// let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController if !UnstableFeatures.enabled(.swiftUI) {
// let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
// selectTeamViewController.teams = teams
// selectTeamViewController.completionHandler = completionHandler selectTeamViewController.teams = teams
// selectTeamViewController.completionHandler = completionHandler
// if !self.present(selectTeamViewController)
// { if !self.present(selectTeamViewController)
// return completionHandler(.failure(AuthenticationError.noTeam)) {
// } return completionHandler(.failure(AuthenticationError.noTeam))
}
}
} }
} }
} }
@@ -654,21 +683,24 @@ private extension AuthenticationOperation
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void) func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
{ {
return completionHandler(false) if UnstableFeatures.enabled(.swiftUI) {
// guard self.shouldShowInstructions else { return completionHandler(false) } return completionHandler(false)
// } else {
// DispatchQueue.main.async { guard self.shouldShowInstructions else { return completionHandler(false) }
// let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
// instructionsViewController.showsBottomButton = true DispatchQueue.main.async {
// instructionsViewController.completionHandler = { let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
// completionHandler(true) instructionsViewController.showsBottomButton = true
// } instructionsViewController.completionHandler = {
// completionHandler(true)
// if !self.present(instructionsViewController) }
// {
// completionHandler(false) if !self.present(instructionsViewController)
// } {
// } completionHandler(false)
}
}
}
} }
func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void) func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void)

View File

@@ -65,7 +65,14 @@
"SettingsView.title" = "Settings"; "SettingsView.title" = "Settings";
"SettingsView.refreshingAppsFooter" = "Enable Background Refresh to automatically refresh apps in the background when connected to WiFi and with Wireguard active."; "SettingsView.refreshingAppsFooter" = "Enable Background Refresh to automatically refresh apps in the background when connected to WiFi and with Wireguard active.";
"SettingsView.exportLogs" = "Export Logs"; "SettingsView.exportLogs" = "Export Logs";
"SettingsView.debugLogging" = "Debug Logging"; "SettingsView.resetAdiPb" = "Reset adi.pb";
"SettingsView.ResetAdiPb.title" = "Are you sure you want to reset the adi.pb file?";
"SettingsView.ResetAdiPb.description" = "The adi.pb file is used to generate anisette data, which is required to log into an Apple ID. If you are having issues with account related things, you can try this. However, you will be required to do 2FA again. This will do nothing if you are using an older anisette server.";
"SettingsView.resetPairingFile" = "Reset Pairing File";
"SettingsView.ResetPairingFile.title" = "Are you sure to reset the pairing file?";
"SettingsView.ResetPairingFile.description" = "If you are having issues with SideStore not being able to install/refresh apps or enable JIT, you can try resetting the pairing file. You will need to generate a new pairing file after doing this. SideStore will close when the file has been deleted.";
"SettingsView.showRefreshAttempts" = "Show Refresh Attempts";
"SettingsView.showErrorLog" = "Show Error Log";
/* ConnectAppleIDView */ /* ConnectAppleIDView */
"ConnectAppleIDView.startWithSignIn" = "Sign in with your Apple ID to get started."; "ConnectAppleIDView.startWithSignIn" = "Sign in with your Apple ID to get started.";
@@ -198,7 +205,7 @@ You should only enable Developer Mode if you meet one of the following requireme
- It is currently limited to a maximum depth of 3 to ensure it doesn't take too long to iterate over everything when you open it - It is currently limited to a maximum depth of 3 to ensure it doesn't take too long to iterate over everything when you open it
- Very buggy - Very buggy
- There are multiple unimplemented actions"; - There are multiple unimplemented actions";
"DevModeView.unstableFeaturesNightlyOnly" = "Unstable Features are only available on nightly builds or debug builds"; "DevModeView.unstableFeaturesNightlyOnly" = "Unstable Features are only available on nightly builds, PR builds and debug builds.";
/* AsyncFallibleButton */ /* AsyncFallibleButton */
"AsyncFallibleButton.error" = "An error occurred"; "AsyncFallibleButton.error" = "An error occurred";
@@ -211,12 +218,19 @@ You should only enable Developer Mode if you meet one of the following requireme
"AdvancedSettingsView.AnisetteSettings.anisetteURL" = "Anisette URL"; "AdvancedSettingsView.AnisetteSettings.anisetteURL" = "Anisette URL";
"AdvancedSettingsView.AnisetteSettings.footer" = "If you disable \"Use preferred servers\" then SideStore will use the server you input into the \"Anisette URL\" box rather than one selected in \"Anisette Server\"."; "AdvancedSettingsView.AnisetteSettings.footer" = "If you disable \"Use preferred servers\" then SideStore will use the server you input into the \"Anisette URL\" box rather than one selected in \"Anisette Server\".";
"AdvancedSettingsView.dangerZone" = "Danger Zone"; "AdvancedSettingsView.dangerZone" = "Danger Zone";
"AdvancedSettingsView.DangerZone.debugLogging" = "Debug Logging";
/* UnstableFeaturesView */ /* UnstableFeaturesView */
"UnstableFeaturesView.title" = "Unstable Features"; "UnstableFeaturesView.title" = "Unstable Features";
"UnstableFeaturesView.description" = "Unstable Features are features that are currently being tested or still a work-in-progress and not ready for public usage. Because of this, they are only available on nightly builds. By default, all unstable features are off. Additionally, only more stable unstable features are available in Advanced Settings; most are locked behind Developer Mode to ensure normal users don't use them. "UnstableFeaturesView.description" = "Unstable Features are features that are currently being tested or still a work-in-progress and not ready for public usage. Because of this, they are only available on nightly builds, PR builds and debug builds. By default, all unstable features are off. Additionally, only more stable unstable features are available in Advanced Settings; most are locked behind Developer Mode to ensure normal users don't use them as they could contain .
Every unstable feature has a tracking issue, which contains info on what the unstable feature adds and tracks the unstable feature status. To view a tracking issue for an unstable feature, simply click it in the list. **Please use the tracking issue for reporting bugs or giving feedback.** Every unstable feature has a tracking issue, which contains info on what the unstable feature adds and tracks the unstable feature status. To view a tracking issue for an unstable feature, simply click it in the list. **Please use the tracking issue for reporting bugs or giving feedback.**
**Do not ask for support on using unstable features, you will not receive any help.**"; **Do not ask for support on using unstable features, you will not receive any help.**";
"UnstableFeaturesView.noUnstableFeatures" = "There are currently no unstable features available."; "UnstableFeaturesView.noUnstableFeatures" = "There are currently no unstable features available.";
/* ErrorLogView */
"ErrorLogView.title" = "Error Log";
/* RefreshAttemptsView */
"RefreshAttemptsView.title" = "Refresh Attempts";

View File

@@ -53,6 +53,10 @@ internal enum L10n {
/// Use preferred servers /// Use preferred servers
internal static let usePreferred = L10n.tr("Localizable", "AdvancedSettingsView.AnisetteSettings.usePreferred", fallback: "Use preferred servers") internal static let usePreferred = L10n.tr("Localizable", "AdvancedSettingsView.AnisetteSettings.usePreferred", fallback: "Use preferred servers")
} }
internal enum DangerZone {
/// Debug Logging
internal static let debugLogging = L10n.tr("Localizable", "AdvancedSettingsView.DangerZone.debugLogging", fallback: "Debug Logging")
}
} }
internal enum AppAction { internal enum AppAction {
/// Activate /// Activate
@@ -283,8 +287,8 @@ internal enum L10n {
internal static let title = L10n.tr("Localizable", "DevModeView.title", fallback: "Developer Mode") internal static let title = L10n.tr("Localizable", "DevModeView.title", fallback: "Developer Mode")
/// Temporary File Explorer /// Temporary File Explorer
internal static let tmpExplorer = L10n.tr("Localizable", "DevModeView.tmpExplorer", fallback: "Temporary File Explorer") internal static let tmpExplorer = L10n.tr("Localizable", "DevModeView.tmpExplorer", fallback: "Temporary File Explorer")
/// Unstable Features are only available on nightly builds or debug builds /// Unstable Features are only available on nightly builds, PR builds and debug builds.
internal static let unstableFeaturesNightlyOnly = L10n.tr("Localizable", "DevModeView.unstableFeaturesNightlyOnly", fallback: "Unstable Features are only available on nightly builds or debug builds") internal static let unstableFeaturesNightlyOnly = L10n.tr("Localizable", "DevModeView.unstableFeaturesNightlyOnly", fallback: "Unstable Features are only available on nightly builds, PR builds and debug builds.")
internal enum Minimuxer { internal enum Minimuxer {
/// AFC File Explorer (check footer for notes) /// AFC File Explorer (check footer for notes)
internal static let afcExplorer = L10n.tr("Localizable", "DevModeView.Minimuxer.afcExplorer", fallback: "AFC File Explorer (check footer for notes)") internal static let afcExplorer = L10n.tr("Localizable", "DevModeView.Minimuxer.afcExplorer", fallback: "AFC File Explorer (check footer for notes)")
@@ -299,6 +303,10 @@ internal enum L10n {
internal static let footer = L10n.tr("Localizable", "DevModeView.Minimuxer.footer", fallback: "Notes on AFC File Explorer:\n- If nothing shows up, check minimuxer logs for error\n- It is currently extremely very unoptimized and may be very slow; a new AFC client is created for every action\n- It is currently limited to a maximum depth of 3 to ensure it doesn't take too long to iterate over everything when you open it\n- Very buggy\n- There are multiple unimplemented actions") internal static let footer = L10n.tr("Localizable", "DevModeView.Minimuxer.footer", fallback: "Notes on AFC File Explorer:\n- If nothing shows up, check minimuxer logs for error\n- It is currently extremely very unoptimized and may be very slow; a new AFC client is created for every action\n- It is currently limited to a maximum depth of 3 to ensure it doesn't take too long to iterate over everything when you open it\n- Very buggy\n- There are multiple unimplemented actions")
} }
} }
internal enum ErrorLogView {
/// ErrorLogView
internal static let title = L10n.tr("Localizable", "ErrorLogView.title", fallback: "Error Log")
}
internal enum MyAppsView { internal enum MyAppsView {
/// MyAppsView /// MyAppsView
internal static let active = L10n.tr("Localizable", "MyAppsView.active", fallback: "Active") internal static let active = L10n.tr("Localizable", "MyAppsView.active", fallback: "Active")
@@ -339,6 +347,10 @@ internal enum L10n {
} }
} }
} }
internal enum RefreshAttemptsView {
/// RefreshAttemptsView
internal static let title = L10n.tr("Localizable", "RefreshAttemptsView.title", fallback: "Refresh Attempts")
}
internal enum RootView { internal enum RootView {
/// Browse /// Browse
internal static let browse = L10n.tr("Localizable", "RootView.browse", fallback: "Browse") internal static let browse = L10n.tr("Localizable", "RootView.browse", fallback: "Browse")
@@ -360,16 +372,22 @@ internal enum L10n {
internal static let credits = L10n.tr("Localizable", "SettingsView.credits", fallback: "Credits") internal static let credits = L10n.tr("Localizable", "SettingsView.credits", fallback: "Credits")
/// Debug /// Debug
internal static let debug = L10n.tr("Localizable", "SettingsView.debug", fallback: "Debug") internal static let debug = L10n.tr("Localizable", "SettingsView.debug", fallback: "Debug")
/// Debug Logging
internal static let debugLogging = L10n.tr("Localizable", "SettingsView.debugLogging", fallback: "Debug Logging")
/// Export Logs /// Export Logs
internal static let exportLogs = L10n.tr("Localizable", "SettingsView.exportLogs", fallback: "Export Logs") internal static let exportLogs = L10n.tr("Localizable", "SettingsView.exportLogs", fallback: "Export Logs")
/// Refreshing Apps /// Refreshing Apps
internal static let refreshingApps = L10n.tr("Localizable", "SettingsView.refreshingApps", fallback: "Refreshing Apps") internal static let refreshingApps = L10n.tr("Localizable", "SettingsView.refreshingApps", fallback: "Refreshing Apps")
/// Enable Background Refresh to automatically refresh apps in the background when connected to WiFi and with Wireguard active. /// Enable Background Refresh to automatically refresh apps in the background when connected to WiFi and with Wireguard active.
internal static let refreshingAppsFooter = L10n.tr("Localizable", "SettingsView.refreshingAppsFooter", fallback: "Enable Background Refresh to automatically refresh apps in the background when connected to WiFi and with Wireguard active.") internal static let refreshingAppsFooter = L10n.tr("Localizable", "SettingsView.refreshingAppsFooter", fallback: "Enable Background Refresh to automatically refresh apps in the background when connected to WiFi and with Wireguard active.")
/// Reset adi.pb
internal static let resetAdiPb = L10n.tr("Localizable", "SettingsView.resetAdiPb", fallback: "Reset adi.pb")
/// Reset Image Cache /// Reset Image Cache
internal static let resetImageCache = L10n.tr("Localizable", "SettingsView.resetImageCache", fallback: "Reset Image Cache") internal static let resetImageCache = L10n.tr("Localizable", "SettingsView.resetImageCache", fallback: "Reset Image Cache")
/// Reset Pairing File
internal static let resetPairingFile = L10n.tr("Localizable", "SettingsView.resetPairingFile", fallback: "Reset Pairing File")
/// Show Error Log
internal static let showErrorLog = L10n.tr("Localizable", "SettingsView.showErrorLog", fallback: "Show Error Log")
/// Show Refresh Attempts
internal static let showRefreshAttempts = L10n.tr("Localizable", "SettingsView.showRefreshAttempts", fallback: "Show Refresh Attempts")
/// SwiftUI Redesign /// SwiftUI Redesign
internal static let swiftUIRedesign = L10n.tr("Localizable", "SettingsView.swiftUIRedesign", fallback: "SwiftUI Redesign") internal static let swiftUIRedesign = L10n.tr("Localizable", "SettingsView.swiftUIRedesign", fallback: "SwiftUI Redesign")
/// Switch to UIKit /// Switch to UIKit
@@ -394,6 +412,18 @@ internal enum L10n {
internal static let p2 = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.Footer.p2", fallback: "Your credentials are only sent to Apple's servers and are not accessible by the SideStore Team. Once successfully logged in, the login details are stored securely on your device.") internal static let p2 = L10n.tr("Localizable", "SettingsView.ConnectedAppleID.Footer.p2", fallback: "Your credentials are only sent to Apple's servers and are not accessible by the SideStore Team. Once successfully logged in, the login details are stored securely on your device.")
} }
} }
internal enum ResetAdiPb {
/// The adi.pb file is used to generate anisette data, which is required to log into an Apple ID. If you are having issues with account related things, you can try this. However, you will be required to do 2FA again. This will do nothing if you are using an older anisette server.
internal static let description = L10n.tr("Localizable", "SettingsView.ResetAdiPb.description", fallback: "The adi.pb file is used to generate anisette data, which is required to log into an Apple ID. If you are having issues with account related things, you can try this. However, you will be required to do 2FA again. This will do nothing if you are using an older anisette server.")
/// Are you sure you want to reset the adi.pb file?
internal static let title = L10n.tr("Localizable", "SettingsView.ResetAdiPb.title", fallback: "Are you sure you want to reset the adi.pb file?")
}
internal enum ResetPairingFile {
/// If you are having issues with SideStore not being able to install/refresh apps or enable JIT, you can try resetting the pairing file. You will need to generate a new pairing file after doing this. SideStore will close when the file has been deleted.
internal static let description = L10n.tr("Localizable", "SettingsView.ResetPairingFile.description", fallback: "If you are having issues with SideStore not being able to install/refresh apps or enable JIT, you can try resetting the pairing file. You will need to generate a new pairing file after doing this. SideStore will close when the file has been deleted.")
/// Are you sure to reset the pairing file?
internal static let title = L10n.tr("Localizable", "SettingsView.ResetPairingFile.title", fallback: "Are you sure to reset the pairing file?")
}
} }
internal enum SourcesView { internal enum SourcesView {
/// Done /// Done
@@ -410,12 +440,12 @@ internal enum L10n {
internal static let trustedSources = L10n.tr("Localizable", "SourcesView.trustedSources", fallback: "Trusted Sources") internal static let trustedSources = L10n.tr("Localizable", "SourcesView.trustedSources", fallback: "Trusted Sources")
} }
internal enum UnstableFeaturesView { internal enum UnstableFeaturesView {
/// Unstable Features are features that are currently being tested or still a work-in-progress and not ready for public usage. Because of this, they are only available on nightly builds. By default, all unstable features are off. Additionally, only more stable unstable features are available in Advanced Settings; most are locked behind Developer Mode to ensure normal users don't use them. /// Unstable Features are features that are currently being tested or still a work-in-progress and not ready for public usage. Because of this, they are only available on nightly builds, PR builds and debug builds. By default, all unstable features are off. Additionally, only more stable unstable features are available in Advanced Settings; most are locked behind Developer Mode to ensure normal users don't use them as they could contain .
/// ///
/// Every unstable feature has a tracking issue, which contains info on what the unstable feature adds and tracks the unstable feature status. To view a tracking issue for an unstable feature, simply click it in the list. **Please use the tracking issue for reporting bugs or giving feedback.** /// Every unstable feature has a tracking issue, which contains info on what the unstable feature adds and tracks the unstable feature status. To view a tracking issue for an unstable feature, simply click it in the list. **Please use the tracking issue for reporting bugs or giving feedback.**
/// ///
/// **Do not ask for support on using unstable features, you will not receive any help.** /// **Do not ask for support on using unstable features, you will not receive any help.**
internal static let description = L10n.tr("Localizable", "UnstableFeaturesView.description", fallback: "Unstable Features are features that are currently being tested or still a work-in-progress and not ready for public usage. Because of this, they are only available on nightly builds. By default, all unstable features are off. Additionally, only more stable unstable features are available in Advanced Settings; most are locked behind Developer Mode to ensure normal users don't use them.\n\nEvery unstable feature has a tracking issue, which contains info on what the unstable feature adds and tracks the unstable feature status. To view a tracking issue for an unstable feature, simply click it in the list. **Please use the tracking issue for reporting bugs or giving feedback.**\n\n**Do not ask for support on using unstable features, you will not receive any help.**") internal static let description = L10n.tr("Localizable", "UnstableFeaturesView.description", fallback: "Unstable Features are features that are currently being tested or still a work-in-progress and not ready for public usage. Because of this, they are only available on nightly builds, PR builds and debug builds. By default, all unstable features are off. Additionally, only more stable unstable features are available in Advanced Settings; most are locked behind Developer Mode to ensure normal users don't use them as they could contain .\n\nEvery unstable feature has a tracking issue, which contains info on what the unstable feature adds and tracks the unstable feature status. To view a tracking issue for an unstable feature, simply click it in the list. **Please use the tracking issue for reporting bugs or giving feedback.**\n\n**Do not ask for support on using unstable features, you will not receive any help.**")
/// There are currently no unstable features available. /// There are currently no unstable features available.
internal static let noUnstableFeatures = L10n.tr("Localizable", "UnstableFeaturesView.noUnstableFeatures", fallback: "There are currently no unstable features available.") internal static let noUnstableFeatures = L10n.tr("Localizable", "UnstableFeaturesView.noUnstableFeatures", fallback: "There are currently no unstable features available.")
/// UnstableFeaturesView /// UnstableFeaturesView

View File

@@ -7,6 +7,7 @@
// //
import SwiftUI import SwiftUI
import minimuxer
private struct Server: Identifiable { private struct Server: Identifiable {
var id: String { value } var id: String { value }
@@ -37,6 +38,9 @@ struct AdvancedSettingsView: View {
@AppStorage("customAnisetteURL") @AppStorage("customAnisetteURL")
var selectedAnisetteServer: String = "" var selectedAnisetteServer: String = ""
@AppStorage("isDebugLoggingEnabled")
var isDebugLoggingEnabled: Bool = false
var body: some View { var body: some View {
List { List {
Section { Section {
@@ -60,8 +64,13 @@ struct AdvancedSettingsView: View {
Text(L10n.AdvancedSettingsView.AnisetteSettings.footer) Text(L10n.AdvancedSettingsView.AnisetteSettings.footer)
} }
#if UNSTABLE // TODO: remove this once we have more settings for the danger zone.
Section { Section {
Toggle(L10n.AdvancedSettingsView.DangerZone.debugLogging, isOn: self.$isDebugLoggingEnabled)
.onChange(of: self.isDebugLoggingEnabled) { value in
UserDefaults.shared.isDebugLoggingEnabled = value
set_debug(value)
}
#if UNSTABLE #if UNSTABLE
NavigationLink(L10n.UnstableFeaturesView.title) { NavigationLink(L10n.UnstableFeaturesView.title) {
UnstableFeaturesView(inDevMode: false) UnstableFeaturesView(inDevMode: false)
@@ -71,7 +80,6 @@ struct AdvancedSettingsView: View {
} header: { } header: {
Text(L10n.AdvancedSettingsView.dangerZone) Text(L10n.AdvancedSettingsView.dangerZone)
} }
#endif
} }
.navigationTitle(L10n.AdvancedSettingsView.title) .navigationTitle(L10n.AdvancedSettingsView.title)
.enableInjection() .enableInjection()

View File

@@ -107,7 +107,7 @@ struct ErrorLogView: View {
} }
} }
} }
.navigationBarTitle("Error Log") .navigationBarTitle(L10n.ErrorLogView.title)
.toolbar { .toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) { ToolbarItemGroup(placement: .navigationBarTrailing) {
ModalNavigationLink { ModalNavigationLink {

View File

@@ -55,7 +55,7 @@ struct RefreshAttemptsView: View {
} }
} }
.background(self.listBackground) .background(self.listBackground)
.navigationTitle("Refresh Attempts") .navigationTitle(L10n.RefreshAttemptsView.title)
} }
@ViewBuilder @ViewBuilder

View File

@@ -31,13 +31,11 @@ struct SettingsView: View {
@AppStorage("isDevModeEnabled") @AppStorage("isDevModeEnabled")
var isDevModeEnabled: Bool = false var isDevModeEnabled: Bool = false
@AppStorage("isDebugLoggingEnabled")
var isDebugLoggingEnabled: Bool = false
@State var isShowingConnectAppleIDView = false @State var isShowingConnectAppleIDView = false
@State var isShowingResetPairingFileConfirmation = false @State var isShowingResetPairingFileConfirmation = false
@State var isShowingDevModePrompt = false @State var isShowingDevModePrompt = false
@State var isShowingDevModeMenu = false @State var isShowingDevModeMenu = false
@State var isShowingResetAdiPbConfirmation = false
@State var externalURLToShow: URL? @State var externalURLToShow: URL?
@State var quickLookURL: URL? @State var quickLookURL: URL?
@@ -104,7 +102,7 @@ struct SettingsView: View {
} }
Section { Section {
NavigationLink("Show Refresh Attempts") { NavigationLink(L10n.SettingsView.showRefreshAttempts) {
RefreshAttemptsView() RefreshAttemptsView()
} }
@@ -163,7 +161,7 @@ struct SettingsView: View {
} }
Section { Section {
NavigationLink("Show Error Log") { NavigationLink(L10n.SettingsView.showErrorLog) {
ErrorLogView() ErrorLogView()
} }
@@ -171,12 +169,6 @@ struct SettingsView: View {
AdvancedSettingsView() AdvancedSettingsView()
} }
Toggle(L10n.SettingsView.debugLogging, isOn: self.$isDebugLoggingEnabled)
.onChange(of: self.isDebugLoggingEnabled) { value in
UserDefaults.shared.isDebugLoggingEnabled = value
set_debug(value)
}
AsyncFallibleButton(action: self.exportLogs, label: { execute in Text(L10n.SettingsView.exportLogs) }) AsyncFallibleButton(action: self.exportLogs, label: { execute in Text(L10n.SettingsView.exportLogs) })
if MailComposeView.canSendMail { if MailComposeView.canSendMail {
@@ -191,18 +183,27 @@ struct SettingsView: View {
} }
} }
SwiftUI.Button(L10n.SettingsView.switchToUIKit, action: self.switchToUIKit)
SwiftUI.Button(L10n.SettingsView.resetImageCache, action: self.resetImageCache) SwiftUI.Button(L10n.SettingsView.resetImageCache, action: self.resetImageCache)
.foregroundColor(.red) .foregroundColor(.red)
SwiftUI.Button("Reset Pairing File") { SwiftUI.Button(L10n.SettingsView.resetPairingFile) {
self.isShowingResetPairingFileConfirmation = true self.isShowingResetPairingFileConfirmation = true
} }
.foregroundColor(.red) .foregroundColor(.red)
.actionSheet(isPresented: self.$isShowingResetPairingFileConfirmation) { .actionSheet(isPresented: self.$isShowingResetPairingFileConfirmation) {
ActionSheet(title: Text("Are you sure to reset the pairing file?"), message: Text("You can reset the pairing file when you cannot sideload apps or enable JIT. SideStore will close when the file has been deleted."), buttons: [ ActionSheet(title: Text(L10n.SettingsView.ResetPairingFile.title), message: Text(L10n.SettingsView.ResetPairingFile.description), buttons: [
.destructive(Text("Delete and Reset"), action: self.resetPairingFile), .destructive(Text(L10n.SettingsView.resetPairingFile), action: self.resetPairingFile),
.cancel()
])
}
SwiftUI.Button(L10n.SettingsView.resetAdiPb) {
self.isShowingResetAdiPbConfirmation = true
}
.foregroundColor(.red)
.actionSheet(isPresented: self.$isShowingResetAdiPbConfirmation) {
ActionSheet(title: Text(L10n.SettingsView.ResetAdiPb.title), message: Text(L10n.SettingsView.ResetAdiPb.description), buttons: [
.destructive(Text(L10n.SettingsView.resetAdiPb), action: self.resetAdiPb),
.cancel() .cancel()
]) ])
} }
@@ -225,13 +226,11 @@ struct SettingsView: View {
} }
Section { Section {} footer: {
} footer: {
Text("SideStore \(appVersion)") Text("SideStore \(appVersion)")
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }.padding([.bottom], 32)
} }
.listStyle(InsetGroupedListStyle()) .listStyle(InsetGroupedListStyle())
.navigationTitle(L10n.SettingsView.title) .navigationTitle(L10n.SettingsView.title)
@@ -253,13 +252,10 @@ struct SettingsView: View {
.enableInjection() .enableInjection()
} }
// var appleIDSection: some View { // var appleIDSection: some View {
// //
// } // }
func connectAppleID() { func connectAppleID() {
guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else { guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else {
return return
@@ -293,13 +289,6 @@ struct SettingsView: View {
} }
} }
func switchToUIKit() {
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let rootVC = storyboard.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
UIApplication.shared.keyWindow?.rootViewController = rootVC
}
func resetImageCache() { func resetImageCache() {
do { do {
let url = try FileManager.default.url( let url = try FileManager.default.url(
@@ -334,6 +323,13 @@ struct SettingsView: View {
} }
} }
func resetAdiPb() {
if Keychain.shared.adiPb != nil {
Keychain.shared.adiPb = nil
print("Cleared adi.pb from keychain")
}
}
func exportLogs() throws { func exportLogs() throws {
let path = FileManager.default.documentsDirectory.appendingPathComponent("sidestore.log") let path = FileManager.default.documentsDirectory.appendingPathComponent("sidestore.log")
var text = LCManager.shared.currentText var text = LCManager.shared.currentText

View File

@@ -12,13 +12,20 @@ import SwiftUI
struct UnstableFeaturesView: View { struct UnstableFeaturesView: View {
@ObservedObject private var iO = Inject.observer @ObservedObject private var iO = Inject.observer
// Keeping a cache of the features allows us to reload the view every time we change one
// If we don't reload the view there is a bug where the toggle will be reset to previous value if you go to another tab and then back
@State private var featureCache: [(key: UnstableFeatures.Feature, value: Bool)]
var inDevMode: Bool var inDevMode: Bool
init(inDevMode: Bool) {
self.inDevMode = inDevMode
self.featureCache = UnstableFeatures.getFeatures(inDevMode)
}
var body: some View { var body: some View {
List { List {
let features = UnstableFeatures.getFeatures(inDevMode) let description = L10n.UnstableFeaturesView.description + (featureCache.count <= 0 ? "\n\n" + L10n.UnstableFeaturesView.noUnstableFeatures : "")
let description = L10n.UnstableFeaturesView.description + (features.count <= 0 ? "\n\n" + L10n.UnstableFeaturesView.noUnstableFeatures : "")
Section {} footer: { Section {} footer: {
if #available(iOS 15.0, *), if #available(iOS 15.0, *),
let string = try? AttributedString(markdown: description, options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace)) { let string = try? AttributedString(markdown: description, options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace)) {
@@ -28,9 +35,13 @@ struct UnstableFeaturesView: View {
} }
}.listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0)) }.listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))
if features.count > 0 { if featureCache.count > 0 {
ForEach(features.sorted(by: { _, _ in true }), id: \.key) { feature, _ in ForEach(featureCache.sorted(by: { _, _ in true }), id: \.key) { feature, _ in
Toggle(isOn: Binding(get: { UnstableFeatures.enabled(feature) }, set: { newValue in UnstableFeatures.set(feature, enabled: newValue) })) { Toggle(isOn: Binding(get: { UnstableFeatures.enabled(feature) }, set: { newValue in
UnstableFeatures.set(feature, enabled: newValue)
// Update the cache so we reload the view (this fixes the toggle resetting to the previous value if you go to another tab and then back)
featureCache = UnstableFeatures.getFeatures(inDevMode)
})) {
Text(String(describing: feature)) Text(String(describing: feature))
let link = "https://github.com/SideStore/SideStore/issues/\(feature.rawValue)" let link = "https://github.com/SideStore/SideStore/issues/\(feature.rawValue)"
Link(link, destination: URL(string: link)!) Link(link, destination: URL(string: link)!)

View File

@@ -7,6 +7,7 @@
// //
import UIKit import UIKit
import SwiftUI
import SafariServices import SafariServices
import MessageUI import MessageUI
import Intents import Intents
@@ -551,13 +552,9 @@ extension SettingsViewController
self.present(alertController, animated: true) self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true) self.tableView.deselectRow(at: indexPath, animated: true)
case .advancedSettings: case .advancedSettings:
// Create the URL that deep links to your app's custom settings. let controller = UIHostingController(rootView: AdvancedSettingsView())
if let url = URL(string: UIApplication.openSettingsURLString) { navigationController?.pushViewController(controller, animated: true)
// Ask the system to open that URL. self.tableView.deselectRow(at: indexPath, animated: true)
UIApplication.shared.open(url)
} else {
ELOG("UIApplication.openSettingsURLString invalid")
}
case .refreshAttempts, .errorLog: break case .refreshAttempts, .errorLog: break
} }

View File

@@ -0,0 +1,30 @@
//
// UnstableFeatures+SwiftUI.swift
// SideStore
//
// Created by naturecodevoid on 5/25/23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import UIKit
import AltStoreCore
extension UnstableFeatures {
class SwiftUI {
static func onEnable() {
let rootView = RootView()
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
UIApplication.shared.keyWindow?.rootViewController = UIHostingController(rootView: rootView)
}
static func onDisable() {
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let rootVC = storyboard.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
UIApplication.shared.keyWindow?.rootViewController = rootVC
}
}
}

View File

@@ -6,42 +6,60 @@
// Copyright © 2023 SideStore. All rights reserved. // Copyright © 2023 SideStore. All rights reserved.
// //
import SwiftUI import Foundation
// I prefixed it with Available to make UnstableFeatures come up first in autocomplete, feel free to rename it if you know a better name class UnstableFeatures {
enum AvailableUnstableFeature: String, CaseIterable { fileprivate struct Metadata {
// The value will be the GitHub Issue number. For example, "123" would correspond to https://github.com/SideStore/SideStore/issues/123 var availableOutsideDevMode = false
// var onEnable = {}
// Unstable features must have a GitHub Issue for tracking progress, PRs and feedback/commenting. var onDisable = {}
}
case jitUrlScheme = "0" enum Feature: String, CaseIterable {
// The value will be the GitHub Issue number. For example, "123" would correspond to https://github.com/SideStore/SideStore/issues/123
/// Dummy variant to ensure there is always at least one variant. DO NOT USE! //
case dummy = "dummy" // Unstable features must have a GitHub Issue for tracking progress, PRs and feedback/bug reporting/commenting.
//
func availableOutsideDevMode() -> Bool { // Please order the case by the issue number. They will be ordered by issue number (ascending) in the unstable features menu, so please order them the same way here and in `metadata`.
switch self {
// If your unstable feature is stable enough to be used by nightly users who are not alpha testers or developers,
// you may want to have it available in the Unstable Features menu in Advanced Settings (outside of dev mode). To do so, add this:
//case .yourFeature: return true
case .jitUrlScheme: return true
default: return false case swiftUI = "0"
case jitUrlScheme = "00"
/// Dummy variant to ensure there is always at least one variant. DO NOT USE!
case dummy = "dummy"
fileprivate var metadata: Metadata {
switch self {
// If your unstable feature is stable enough to be used by nightly users who are not alpha testers or developers,
// you may want to have it available in the Unstable Features menu in Advanced Settings (outside of dev mode). To do so, add this:
//case .yourFeature: return Metadata(availableOutsideDevMode: true)
// You can also add custom hooks for when your feature is enabled or disabled. However, we strongly recommend moving these to a new file. Example: https://github.com/SideStore/SideStore/blob/feature/unstable-features/AltStore/Unstable%20Features/UnstableFeatures+SwiftUI.swift
// Please keep the ordering of the cases in this switch statement the same as the ordering of the enum variants!
case .swiftUI: return Metadata(availableOutsideDevMode: true, onEnable: SwiftUI.onEnable, onDisable: SwiftUI.onDisable)
case .jitUrlScheme: return Metadata(availableOutsideDevMode: true)
default: return Metadata()
}
} }
} }
}
class UnstableFeatures: ObservableObject {
#if UNSTABLE
private static var features: [AvailableUnstableFeature: Bool] = [:]
static func getFeatures(_ inDevMode: Bool) -> [(key: AvailableUnstableFeature, value: Bool)] { #if UNSTABLE
private static var features: [Feature: Bool] = [:]
static func getFeatures(_ inDevMode: Bool) -> [(key: Feature, value: Bool)] {
// Ensure every feature is in the dictionary
for feature in Feature.allCases {
if features[feature] == nil {
features[feature] = false
}
}
return features return features
.filter { feature, _ in .filter { feature, _ in
feature != .dummy && feature != .dummy &&
(inDevMode || feature.availableOutsideDevMode()) (inDevMode || feature.metadata.availableOutsideDevMode)
}.sorted { a, b in a.key.rawValue > b.key.rawValue } // Convert to array of keys and values }.sorted { a, b in a.key.rawValue > b.key.rawValue } // Convert to array of keys and values (and also sort them by issue number)
} }
static func load() { static func load() {
@@ -50,25 +68,17 @@ class UnstableFeatures: ObservableObject {
if let rawFeatures = UserDefaults.shared.unstableFeatures, if let rawFeatures = UserDefaults.shared.unstableFeatures,
let rawFeatures = try? JSONDecoder().decode([String: Bool].self, from: rawFeatures) { let rawFeatures = try? JSONDecoder().decode([String: Bool].self, from: rawFeatures) {
for rawFeature in rawFeatures { for rawFeature in rawFeatures {
if let feature = AvailableUnstableFeature.allCases.first(where: { feature in String(describing: feature) == rawFeature.key }) { if let feature = Feature.allCases.first(where: { feature in String(describing: feature) == rawFeature.key }) {
features[feature] = rawFeature.value features[feature] = rawFeature.value
} else { } else {
print("Unknown unstable feature: \(rawFeature.key) = \(rawFeature.value)") print("Unknown unstable feature: \(rawFeature.key) = \(rawFeature.value)")
} }
} }
// If there's a new feature that wasn't saved and therefore wasn't loaded, let's set it to false
// Technically we shouldn't have to do this because enabled() will fallback to false
for feature in AvailableUnstableFeature.allCases {
if features[feature] == nil {
features[feature] = false
}
}
save(load: true) save(load: true)
} else { } else {
print("Setting all unstable features to false since we couldn't load them from UserDefaults (either they were never saved or there was an error decoding JSON)") print("Setting all unstable features to false since we couldn't load them from UserDefaults (either they were never saved or there was an error decoding JSON)")
for feature in AvailableUnstableFeature.allCases { for feature in Feature.allCases {
features[feature] = false features[feature] = false
} }
save() save()
@@ -84,14 +94,21 @@ class UnstableFeatures: ObservableObject {
print("\(load ? "Loaded" : "Saved") unstable features: \(String(describing: rawFeatures))") print("\(load ? "Loaded" : "Saved") unstable features: \(String(describing: rawFeatures))")
} }
static func set(_ feature: AvailableUnstableFeature, enabled: Bool) { static func set(_ feature: Feature, enabled: Bool) {
features[feature] = enabled features[feature] = enabled
// Let's save before running the hooks... they might crash the app or something
save() save()
// Should be no-op for features with the default hooks (they do nothing)
if enabled {
feature.metadata.onEnable()
} else {
feature.metadata.onDisable()
}
} }
#endif #endif
@inline(__always) // hopefully this will help the compiler realize that if statements that use this function should be removed on non-unstable builds @inline(__always) // hopefully this will help the compiler realize that if statements that use this function should be removed on non-unstable builds
static func enabled(_ feature: AvailableUnstableFeature) -> Bool { static func enabled(_ feature: Feature) -> Bool {
#if UNSTABLE #if UNSTABLE
features[feature] ?? false features[feature] ?? false
#else #else