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 */; };
994D6E9B29E326080045B3F7 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99F87D1729D8E4C900B40039 /* minimuxer.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 */; };
99C4EF4D2979132100CB538D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = 99C4EF4C2979132100CB538D /* SemanticVersion */; };
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>"; };
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; };
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>"; };
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>"; };
@@ -1322,6 +1324,7 @@
isa = PBXGroup;
children = (
990D2AE12A1910CD0055D93C /* UnstableFeatures.swift */,
998E3B102A2061850046BBAA /* UnstableFeatures+SwiftUI.swift */,
);
path = "Unstable Features";
sourceTree = "<group>";
@@ -2996,6 +2999,7 @@
1F180F94298E7A2500D1C98B /* Source+Trusted.swift in Sources */,
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */,
998E3B112A2061850046BBAA /* UnstableFeatures+SwiftUI.swift in Sources */,
1F0DD81C2932D2FF007608A4 /* AppScreenshotsScrollView.swift in Sources */,
BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */,
1F545E85298D84CF00589F68 /* FilePreviewView.swift in Sources */,

View File

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

View File

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

View File

@@ -65,7 +65,14 @@
"SettingsView.title" = "Settings";
"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.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.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
- Very buggy
- 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.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.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.debugLogging" = "Debug Logging";
/* UnstableFeaturesView */
"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.**
**Do not ask for support on using unstable features, you will not receive any help.**";
"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
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 {
/// Activate
@@ -283,8 +287,8 @@ internal enum L10n {
internal static let title = L10n.tr("Localizable", "DevModeView.title", fallback: "Developer Mode")
/// 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
internal static let unstableFeaturesNightlyOnly = L10n.tr("Localizable", "DevModeView.unstableFeaturesNightlyOnly", fallback: "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, PR builds and debug builds.")
internal enum Minimuxer {
/// 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 enum ErrorLogView {
/// ErrorLogView
internal static let title = L10n.tr("Localizable", "ErrorLogView.title", fallback: "Error Log")
}
internal enum MyAppsView {
/// MyAppsView
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 {
/// 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")
/// 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
internal static let exportLogs = L10n.tr("Localizable", "SettingsView.exportLogs", fallback: "Export Logs")
/// 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.
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
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
internal static let swiftUIRedesign = L10n.tr("Localizable", "SettingsView.swiftUIRedesign", fallback: "SwiftUI Redesign")
/// 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 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 {
/// Done
@@ -410,12 +440,12 @@ internal enum L10n {
internal static let trustedSources = L10n.tr("Localizable", "SourcesView.trustedSources", fallback: "Trusted Sources")
}
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.**
///
/// **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.
internal static let noUnstableFeatures = L10n.tr("Localizable", "UnstableFeaturesView.noUnstableFeatures", fallback: "There are currently no unstable features available.")
/// UnstableFeaturesView

View File

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

View File

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

View File

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

View File

@@ -31,13 +31,11 @@ struct SettingsView: View {
@AppStorage("isDevModeEnabled")
var isDevModeEnabled: Bool = false
@AppStorage("isDebugLoggingEnabled")
var isDebugLoggingEnabled: Bool = false
@State var isShowingConnectAppleIDView = false
@State var isShowingResetPairingFileConfirmation = false
@State var isShowingDevModePrompt = false
@State var isShowingDevModeMenu = false
@State var isShowingResetAdiPbConfirmation = false
@State var externalURLToShow: URL?
@State var quickLookURL: URL?
@@ -104,7 +102,7 @@ struct SettingsView: View {
}
Section {
NavigationLink("Show Refresh Attempts") {
NavigationLink(L10n.SettingsView.showRefreshAttempts) {
RefreshAttemptsView()
}
@@ -163,7 +161,7 @@ struct SettingsView: View {
}
Section {
NavigationLink("Show Error Log") {
NavigationLink(L10n.SettingsView.showErrorLog) {
ErrorLogView()
}
@@ -171,12 +169,6 @@ struct SettingsView: View {
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) })
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)
.foregroundColor(.red)
SwiftUI.Button("Reset Pairing File") {
SwiftUI.Button(L10n.SettingsView.resetPairingFile) {
self.isShowingResetPairingFileConfirmation = true
}
.foregroundColor(.red)
.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: [
.destructive(Text("Delete and Reset"), action: self.resetPairingFile),
ActionSheet(title: Text(L10n.SettingsView.ResetPairingFile.title), message: Text(L10n.SettingsView.ResetPairingFile.description), buttons: [
.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()
])
}
@@ -225,13 +226,11 @@ struct SettingsView: View {
}
Section {
} footer: {
Section {} footer: {
Text("SideStore \(appVersion)")
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
}
}.padding([.bottom], 32)
}
.listStyle(InsetGroupedListStyle())
.navigationTitle(L10n.SettingsView.title)
@@ -253,13 +252,10 @@ struct SettingsView: View {
.enableInjection()
}
// var appleIDSection: some View {
//
// }
func connectAppleID() {
guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else {
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() {
do {
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 {
let path = FileManager.default.documentsDirectory.appendingPathComponent("sidestore.log")
var text = LCManager.shared.currentText

View File

@@ -12,13 +12,20 @@ import SwiftUI
struct UnstableFeaturesView: View {
@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
init(inDevMode: Bool) {
self.inDevMode = inDevMode
self.featureCache = UnstableFeatures.getFeatures(inDevMode)
}
var body: some View {
List {
let features = UnstableFeatures.getFeatures(inDevMode)
let description = L10n.UnstableFeaturesView.description + (features.count <= 0 ? "\n\n" + L10n.UnstableFeaturesView.noUnstableFeatures : "")
let description = L10n.UnstableFeaturesView.description + (featureCache.count <= 0 ? "\n\n" + L10n.UnstableFeaturesView.noUnstableFeatures : "")
Section {} footer: {
if #available(iOS 15.0, *),
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))
if features.count > 0 {
ForEach(features.sorted(by: { _, _ in true }), id: \.key) { feature, _ in
Toggle(isOn: Binding(get: { UnstableFeatures.enabled(feature) }, set: { newValue in UnstableFeatures.set(feature, enabled: newValue) })) {
if featureCache.count > 0 {
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)
// 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))
let link = "https://github.com/SideStore/SideStore/issues/\(feature.rawValue)"
Link(link, destination: URL(string: link)!)

View File

@@ -7,6 +7,7 @@
//
import UIKit
import SwiftUI
import SafariServices
import MessageUI
import Intents
@@ -551,13 +552,9 @@ extension SettingsViewController
self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true)
case .advancedSettings:
// Create the URL that deep links to your app's custom settings.
if let url = URL(string: UIApplication.openSettingsURLString) {
// Ask the system to open that URL.
UIApplication.shared.open(url)
} else {
ELOG("UIApplication.openSettingsURLString invalid")
}
let controller = UIHostingController(rootView: AdvancedSettingsView())
navigationController?.pushViewController(controller, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true)
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.
//
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
enum AvailableUnstableFeature: String, CaseIterable {
// The value will be the GitHub Issue number. For example, "123" would correspond to https://github.com/SideStore/SideStore/issues/123
//
// Unstable features must have a GitHub Issue for tracking progress, PRs and feedback/commenting.
class UnstableFeatures {
fileprivate struct Metadata {
var availableOutsideDevMode = false
var onEnable = {}
var onDisable = {}
}
case jitUrlScheme = "0"
/// Dummy variant to ensure there is always at least one variant. DO NOT USE!
case dummy = "dummy"
func availableOutsideDevMode() -> Bool {
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
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
//
// Unstable features must have a GitHub Issue for tracking progress, PRs and feedback/bug reporting/commenting.
//
// 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`.
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
.filter { feature, _ in
feature != .dummy &&
(inDevMode || feature.availableOutsideDevMode())
}.sorted { a, b in a.key.rawValue > b.key.rawValue } // Convert to array of keys and values
(inDevMode || feature.metadata.availableOutsideDevMode)
}.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() {
@@ -50,25 +68,17 @@ class UnstableFeatures: ObservableObject {
if let rawFeatures = UserDefaults.shared.unstableFeatures,
let rawFeatures = try? JSONDecoder().decode([String: Bool].self, from: 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
} else {
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)
} 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)")
for feature in AvailableUnstableFeature.allCases {
for feature in Feature.allCases {
features[feature] = false
}
save()
@@ -84,14 +94,21 @@ class UnstableFeatures: ObservableObject {
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
// Let's save before running the hooks... they might crash the app or something
save()
// Should be no-op for features with the default hooks (they do nothing)
if enabled {
feature.metadata.onEnable()
} else {
feature.metadata.onDisable()
}
}
#endif
@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
features[feature] ?? false
#else