diff --git a/AltStore/Resources/en.lproj/Localizable.strings b/AltStore/Resources/en.lproj/Localizable.strings index c8b0f81a..933c9d56 100644 --- a/AltStore/Resources/en.lproj/Localizable.strings +++ b/AltStore/Resources/en.lproj/Localizable.strings @@ -198,17 +198,25 @@ 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"; /* AsyncFallibleButton */ "AsyncFallibleButton.error" = "An error occurred"; /* AdvancedSettingsView */ "AdvancedSettingsView.title" = "Advanced Settings"; -"AdvancedSettingsView.anisette" = "Anisette Server"; +"AdvancedSettingsView.anisetteSettings" = "Anisette"; +"AdvancedSettingsView.AnisetteSettings.server" = "Anisette Server"; +"AdvancedSettingsView.AnisetteSettings.usePreferred" = "Use preferred servers"; +"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.dangerZoneInfo" = "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.usePreferred" = "Use preferred servers"; -"AdvancedSettingsView.DangerZone.anisetteURL" = "Anisette URL"; /* 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. + +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."; diff --git a/AltStore/SwiftUI/Generated/Localizations.swift b/AltStore/SwiftUI/Generated/Localizations.swift index cd6ef1c9..cf18385a 100644 --- a/AltStore/SwiftUI/Generated/Localizations.swift +++ b/AltStore/SwiftUI/Generated/Localizations.swift @@ -37,19 +37,21 @@ internal enum L10n { internal static let title = L10n.tr("Localizable", "AddSourceView.title", fallback: "Add Source") } internal enum AdvancedSettingsView { - /// Anisette Server - internal static let anisette = L10n.tr("Localizable", "AdvancedSettingsView.anisette", fallback: "Anisette Server") + /// Anisette + internal static let anisetteSettings = L10n.tr("Localizable", "AdvancedSettingsView.anisetteSettings", fallback: "Anisette") /// Danger Zone internal static let dangerZone = L10n.tr("Localizable", "AdvancedSettingsView.dangerZone", fallback: "Danger Zone") - /// 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". - internal static let dangerZoneInfo = L10n.tr("Localizable", "AdvancedSettingsView.dangerZoneInfo", fallback: "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 internal static let title = L10n.tr("Localizable", "AdvancedSettingsView.title", fallback: "Advanced Settings") - internal enum DangerZone { + internal enum AnisetteSettings { /// Anisette URL - internal static let anisetteURL = L10n.tr("Localizable", "AdvancedSettingsView.DangerZone.anisetteURL", fallback: "Anisette URL") + internal static let anisetteURL = L10n.tr("Localizable", "AdvancedSettingsView.AnisetteSettings.anisetteURL", fallback: "Anisette URL") + /// 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". + internal static let footer = L10n.tr("Localizable", "AdvancedSettingsView.AnisetteSettings.footer", fallback: "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\".") + /// Anisette Server + internal static let server = L10n.tr("Localizable", "AdvancedSettingsView.AnisetteSettings.server", fallback: "Anisette Server") /// Use preferred servers - internal static let usePreferred = L10n.tr("Localizable", "AdvancedSettingsView.DangerZone.usePreferred", fallback: "Use preferred servers") + internal static let usePreferred = L10n.tr("Localizable", "AdvancedSettingsView.AnisetteSettings.usePreferred", fallback: "Use preferred servers") } } internal enum AppAction { @@ -281,6 +283,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") 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)") @@ -406,6 +410,14 @@ 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. + /// + /// 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.**") + /// 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 internal static let title = L10n.tr("Localizable", "UnstableFeaturesView.title", fallback: "Unstable Features") } diff --git a/AltStore/SwiftUI/Views/Settings/AdvancedSettingsView.swift b/AltStore/SwiftUI/Views/Settings/AdvancedSettingsView.swift index a212b8a7..7e4f7a29 100644 --- a/AltStore/SwiftUI/Views/Settings/AdvancedSettingsView.swift +++ b/AltStore/SwiftUI/Views/Settings/AdvancedSettingsView.swift @@ -40,27 +40,38 @@ struct AdvancedSettingsView: View { var body: some View { List { Section { - Picker(L10n.AdvancedSettingsView.anisette, selection: $selectedAnisetteServer) { + Picker(L10n.AdvancedSettingsView.AnisetteSettings.server, selection: $selectedAnisetteServer) { ForEach(anisetteServers) { server in Text(server.display) } } - } - - Section { - Toggle(L10n.AdvancedSettingsView.DangerZone.usePreferred, isOn: $usePreferred) + + Toggle(L10n.AdvancedSettingsView.AnisetteSettings.usePreferred, isOn: $usePreferred) HStack { - Text(L10n.AdvancedSettingsView.DangerZone.anisetteURL) + Text(L10n.AdvancedSettingsView.AnisetteSettings.anisetteURL) TextField("", text: $anisetteURL) .autocapitalization(.none) .autocorrectionDisabled(true) } } header: { - Text(L10n.AdvancedSettingsView.dangerZone) + Text(L10n.AdvancedSettingsView.anisetteSettings) } footer: { - Text(L10n.AdvancedSettingsView.dangerZoneInfo) + Text(L10n.AdvancedSettingsView.AnisetteSettings.footer) } + + #if UNSTABLE // TODO: remove this once we have more settings for the danger zone. + Section { + #if UNSTABLE + NavigationLink(L10n.UnstableFeaturesView.title) { + UnstableFeaturesView(inDevMode: false) + } + .foregroundColor(.red) + #endif + } header: { + Text(L10n.AdvancedSettingsView.dangerZone) + } + #endif } .navigationTitle(L10n.AdvancedSettingsView.title) .enableInjection() diff --git a/AltStore/SwiftUI/Views/Settings/DevModeView.swift b/AltStore/SwiftUI/Views/Settings/DevModeView.swift index c5d585ee..90099d32 100644 --- a/AltStore/SwiftUI/Views/Settings/DevModeView.swift +++ b/AltStore/SwiftUI/Views/Settings/DevModeView.swift @@ -128,6 +128,10 @@ struct DevModeMenu: View { @AppStorage("isConsoleEnabled") var isConsoleEnabled: Bool = false + #if !UNSTABLE + @State var isUnstableAlertShowing = false + #endif + var body: some View { List { Section { @@ -143,6 +147,10 @@ struct DevModeMenu: View { } #if !UNSTABLE .disabled(true) + .alert(isPresented: $isUnstableAlertShowing) { + Alert(title: Text(L10n.DevModeView.unstableFeaturesNightlyOnly)) + } + .onTapGesture { isUnstableAlertShowing = true } #endif NavigationLink(L10n.DevModeView.dataExplorer) { diff --git a/AltStore/SwiftUI/Views/Settings/SettingsView.swift b/AltStore/SwiftUI/Views/Settings/SettingsView.swift index 0a11dedb..13cfbe51 100644 --- a/AltStore/SwiftUI/Views/Settings/SettingsView.swift +++ b/AltStore/SwiftUI/Views/Settings/SettingsView.swift @@ -104,6 +104,10 @@ struct SettingsView: View { } Section { + NavigationLink("Show Refresh Attempts") { + RefreshAttemptsView() + } + Toggle(isOn: self.$isBackgroundRefreshEnabled, label: { Text(L10n.SettingsView.backgroundRefresh) }) @@ -113,10 +117,6 @@ struct SettingsView: View { SiriShortcutSetupView(shortcut: shortcut) } } - - NavigationLink("Show Refresh Attempts") { - RefreshAttemptsView() - } } header: { Text(L10n.SettingsView.refreshingApps) } footer: { @@ -171,12 +171,6 @@ struct SettingsView: View { AdvancedSettingsView() } - #if UNSTABLE - NavigationLink(L10n.UnstableFeaturesView.title) { - UnstableFeaturesView(inDevMode: false) - } - #endif - Toggle(L10n.SettingsView.debugLogging, isOn: self.$isDebugLoggingEnabled) .onChange(of: self.isDebugLoggingEnabled) { value in UserDefaults.shared.isDebugLoggingEnabled = value diff --git a/AltStore/SwiftUI/Views/Settings/UnstableFeaturesView.swift b/AltStore/SwiftUI/Views/Settings/UnstableFeaturesView.swift index 0f51d22c..712a2c19 100644 --- a/AltStore/SwiftUI/Views/Settings/UnstableFeaturesView.swift +++ b/AltStore/SwiftUI/Views/Settings/UnstableFeaturesView.swift @@ -10,20 +10,36 @@ import SwiftUI struct UnstableFeaturesView: View { - @ObservedObject private var shared = UnstableFeatures.shared + @ObservedObject private var iO = Inject.observer var inDevMode: Bool var body: some View { List { - ForEach(shared.features.filter { feature, _ in feature != .dummy && (inDevMode || feature.availableOutsideDevMode()) }.sorted(by: { _, _ in true }), id: \.key) { feature, _ in - Toggle(isOn: Binding(get: { UnstableFeatures.enabled(feature) }, set: { newValue in UnstableFeatures.set(feature, enabled: newValue) })) { - Text(String(describing: feature)) - let link = "https://github.com/SideStore/SideStore/issues/\(feature.rawValue)" - Link(link, destination: URL(string: link)!) + let features = UnstableFeatures.getFeatures(inDevMode) + + let description = L10n.UnstableFeaturesView.description + (features.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)) { + Text(string).font(.callout).foregroundColor(.primary) + } else { + Text(description).font(.callout).foregroundColor(.primary) + } + }.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) })) { + Text(String(describing: feature)) + let link = "https://github.com/SideStore/SideStore/issues/\(feature.rawValue)" + Link(link, destination: URL(string: link)!) + } } } - }.navigationTitle(L10n.UnstableFeaturesView.title) + } + .navigationTitle(L10n.UnstableFeaturesView.title) + .enableInjection() } } diff --git a/AltStore/Unstable Features/UnstableFeatures.swift b/AltStore/Unstable Features/UnstableFeatures.swift index 3c9d6dff..4cffd932 100644 --- a/AltStore/Unstable Features/UnstableFeatures.swift +++ b/AltStore/Unstable Features/UnstableFeatures.swift @@ -22,7 +22,7 @@ enum AvailableUnstableFeature: String, CaseIterable { 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 Settings (outside of dev mode). To do so, add this: + // 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 @@ -34,31 +34,42 @@ enum AvailableUnstableFeature: String, CaseIterable { class UnstableFeatures: ObservableObject { #if UNSTABLE - static let shared = UnstableFeatures() - @Published var features: [AvailableUnstableFeature: Bool] = [:] + private static var features: [AvailableUnstableFeature: Bool] = [:] + + static func getFeatures(_ inDevMode: Bool) -> [(key: AvailableUnstableFeature, value: Bool)] { + 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 + } static func load() { - if shared.features.count > 0 { return print("It seems unstable features have already been loaded, skipping") } + if features.count > 0 { return print("It seems unstable features have already been loaded, skipping") } 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 }) { - shared.features[feature] = rawFeature.value + 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 shared.features[feature] == nil { - shared.features[feature] = false + 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 { - shared.features[feature] = false + features[feature] = false } save() } @@ -66,7 +77,7 @@ class UnstableFeatures: ObservableObject { private static func save(load: Bool = false) { var rawFeatures: [String: Bool] = [:] - for feature in shared.features { + for feature in features { rawFeatures[String(describing: feature.key)] = feature.value } UserDefaults.shared.unstableFeatures = try! JSONEncoder().encode(rawFeatures) @@ -74,7 +85,7 @@ class UnstableFeatures: ObservableObject { } static func set(_ feature: AvailableUnstableFeature, enabled: Bool) { - shared.features[feature] = enabled + features[feature] = enabled save() } #endif @@ -82,7 +93,7 @@ class UnstableFeatures: ObservableObject { @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 { #if UNSTABLE - shared.features[feature] ?? false + features[feature] ?? false #else false #endif