mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-20 12:13:26 +01:00
feat: view to enable/disable Unstable Features
This commit is contained in:
@@ -88,6 +88,7 @@
|
|||||||
4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; };
|
4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; };
|
||||||
990D2AE22A1910CD0055D93C /* UnstableFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 990D2AE12A1910CD0055D93C /* UnstableFeatures.swift */; };
|
990D2AE22A1910CD0055D93C /* UnstableFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 990D2AE12A1910CD0055D93C /* UnstableFeatures.swift */; };
|
||||||
990D2AF02A192E060055D93C /* UIApplication+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 990D2AEF2A192E060055D93C /* UIApplication+Alert.swift */; };
|
990D2AF02A192E060055D93C /* UIApplication+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 990D2AEF2A192E060055D93C /* UIApplication+Alert.swift */; };
|
||||||
|
990D2B002A19593F0055D93C /* UnstableFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 990D2AFF2A19593F0055D93C /* UnstableFeaturesView.swift */; };
|
||||||
9922FFEC29B501C50020F868 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 9922FFEB29B501C50020F868 /* Starscream */; };
|
9922FFEC29B501C50020F868 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 9922FFEB29B501C50020F868 /* Starscream */; };
|
||||||
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 */; };
|
||||||
@@ -655,6 +656,7 @@
|
|||||||
1FFEF103298552DB0098374C /* AppVersionHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionHistoryView.swift; sourceTree = "<group>"; };
|
1FFEF103298552DB0098374C /* AppVersionHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionHistoryView.swift; sourceTree = "<group>"; };
|
||||||
990D2AE12A1910CD0055D93C /* UnstableFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnstableFeatures.swift; sourceTree = "<group>"; };
|
990D2AE12A1910CD0055D93C /* UnstableFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnstableFeatures.swift; sourceTree = "<group>"; };
|
||||||
990D2AEF2A192E060055D93C /* UIApplication+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Alert.swift"; sourceTree = "<group>"; };
|
990D2AEF2A192E060055D93C /* UIApplication+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Alert.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; };
|
||||||
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>"; };
|
||||||
@@ -1180,7 +1182,6 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
1FAFC5B52927E06300B8D837 /* RootView.swift */,
|
1FAFC5B52927E06300B8D837 /* RootView.swift */,
|
||||||
1FAFC5B12927E02E00B8D837 /* Authentication */,
|
|
||||||
1F981B0F29AA0F9B0014950E /* Onboarding */,
|
1F981B0F29AA0F9B0014950E /* Onboarding */,
|
||||||
1FAFC5B22927E03300B8D837 /* News */,
|
1FAFC5B22927E03300B8D837 /* News */,
|
||||||
1FAFC5B32927E03D00B8D837 /* Browse */,
|
1FAFC5B32927E03D00B8D837 /* Browse */,
|
||||||
@@ -1191,13 +1192,6 @@
|
|||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
1FAFC5B12927E02E00B8D837 /* Authentication */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
);
|
|
||||||
path = Authentication;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
1FAFC5B22927E03300B8D837 /* News */ = {
|
1FAFC5B22927E03300B8D837 /* News */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1263,6 +1257,7 @@
|
|||||||
99E59E1C299BFE5D00FAF33D /* AppIconsView.swift */,
|
99E59E1C299BFE5D00FAF33D /* AppIconsView.swift */,
|
||||||
99D87A5F299F1B1100ED09A9 /* DevModeView.swift */,
|
99D87A5F299F1B1100ED09A9 /* DevModeView.swift */,
|
||||||
99BCB7DE29A2AC050041D1A7 /* AdvancedSettingsView.swift */,
|
99BCB7DE29A2AC050041D1A7 /* AdvancedSettingsView.swift */,
|
||||||
|
990D2AFF2A19593F0055D93C /* UnstableFeaturesView.swift */,
|
||||||
);
|
);
|
||||||
path = Settings;
|
path = Settings;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -2962,6 +2957,7 @@
|
|||||||
1F0DD8452936B3FE007608A4 /* FilledButtonStyle.swift in Sources */,
|
1F0DD8452936B3FE007608A4 /* FilledButtonStyle.swift in Sources */,
|
||||||
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */,
|
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */,
|
||||||
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
|
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
|
||||||
|
990D2B002A19593F0055D93C /* UnstableFeaturesView.swift in Sources */,
|
||||||
BFB39B5C252BC10E00D1BE50 /* Managed.swift in Sources */,
|
BFB39B5C252BC10E00D1BE50 /* Managed.swift in Sources */,
|
||||||
BF770E5822BC3D0F002A40FE /* RefreshGroup.swift in Sources */,
|
BF770E5822BC3D0F002A40FE /* RefreshGroup.swift in Sources */,
|
||||||
19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */,
|
19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */,
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
// Register default settings before doing anything else.
|
// Register default settings before doing anything else.
|
||||||
UserDefaults.registerDefaults()
|
UserDefaults.registerDefaults()
|
||||||
|
|
||||||
|
#if UNSTABLE
|
||||||
UnstableFeatures.load()
|
UnstableFeatures.load()
|
||||||
|
#endif
|
||||||
|
|
||||||
LCManager.shared.isVisible = UserDefaults.standard.isConsoleEnabled
|
LCManager.shared.isVisible = UserDefaults.standard.isConsoleEnabled
|
||||||
LCManager.shared.isCharacterLimitDisabled = true // we want all logs exported
|
LCManager.shared.isCharacterLimitDisabled = true // we want all logs exported
|
||||||
|
|||||||
@@ -209,3 +209,6 @@ You should only enable Developer Mode if you meet one of the following requireme
|
|||||||
"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.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.usePreferred" = "Use preferred servers";
|
||||||
"AdvancedSettingsView.DangerZone.anisetteURL" = "Anisette URL";
|
"AdvancedSettingsView.DangerZone.anisetteURL" = "Anisette URL";
|
||||||
|
|
||||||
|
/* UnstableFeaturesView */
|
||||||
|
"UnstableFeaturesView.title" = "Unstable Features";
|
||||||
|
|||||||
@@ -405,6 +405,10 @@ internal enum L10n {
|
|||||||
/// Trusted Sources
|
/// Trusted Sources
|
||||||
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 {
|
||||||
|
/// UnstableFeaturesView
|
||||||
|
internal static let title = L10n.tr("Localizable", "UnstableFeaturesView.title", fallback: "Unstable Features")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
|
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
|
||||||
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
|
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||||
|
|||||||
@@ -136,6 +136,15 @@ struct DevModeMenu: View {
|
|||||||
LCManager.shared.isVisible = value
|
LCManager.shared.isVisible = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigationLink(L10n.UnstableFeaturesView.title) {
|
||||||
|
#if UNSTABLE
|
||||||
|
UnstableFeaturesView(allowDevModeOnlyFeatures: true)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#if !UNSTABLE
|
||||||
|
.disabled(true)
|
||||||
|
#endif
|
||||||
|
|
||||||
NavigationLink(L10n.DevModeView.dataExplorer) {
|
NavigationLink(L10n.DevModeView.dataExplorer) {
|
||||||
FileExplorer.normal(url: FileManager.default.altstoreSharedDirectory)
|
FileExplorer.normal(url: FileManager.default.altstoreSharedDirectory)
|
||||||
.navigationTitle(L10n.DevModeView.dataExplorer)
|
.navigationTitle(L10n.DevModeView.dataExplorer)
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ struct SettingsView: View {
|
|||||||
SiriShortcutSetupView(shortcut: shortcut)
|
SiriShortcutSetupView(shortcut: shortcut)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigationLink("Show Refresh Attempts") {
|
||||||
|
RefreshAttemptsView()
|
||||||
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text(L10n.SettingsView.refreshingApps)
|
Text(L10n.SettingsView.refreshingApps)
|
||||||
} footer: {
|
} footer: {
|
||||||
@@ -163,14 +167,16 @@ struct SettingsView: View {
|
|||||||
ErrorLogView()
|
ErrorLogView()
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationLink("Show Refresh Attempts") {
|
|
||||||
RefreshAttemptsView()
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink(L10n.AdvancedSettingsView.title) {
|
NavigationLink(L10n.AdvancedSettingsView.title) {
|
||||||
AdvancedSettingsView()
|
AdvancedSettingsView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if UNSTABLE
|
||||||
|
NavigationLink(L10n.UnstableFeaturesView.title) {
|
||||||
|
UnstableFeaturesView(allowDevModeOnlyFeatures: false)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
Toggle(L10n.SettingsView.debugLogging, isOn: self.$isDebugLoggingEnabled)
|
Toggle(L10n.SettingsView.debugLogging, isOn: self.$isDebugLoggingEnabled)
|
||||||
.onChange(of: self.isDebugLoggingEnabled) { value in
|
.onChange(of: self.isDebugLoggingEnabled) { value in
|
||||||
UserDefaults.shared.isDebugLoggingEnabled = value
|
UserDefaults.shared.isDebugLoggingEnabled = value
|
||||||
|
|||||||
35
AltStore/SwiftUI/Views/Settings/UnstableFeaturesView.swift
Normal file
35
AltStore/SwiftUI/Views/Settings/UnstableFeaturesView.swift
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// UnstableFeaturesView.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by naturecodevoid on 5/20/23.
|
||||||
|
// Copyright © 2023 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#if UNSTABLE
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct UnstableFeaturesView: View {
|
||||||
|
@ObservedObject private var shared = UnstableFeatures.shared
|
||||||
|
|
||||||
|
var allowDevModeOnlyFeatures: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
ForEach(shared.features.filter { feature, _ in feature != AvailableUnstableFeature.dummy && allowDevModeOnlyFeatures ? true : 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)!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.navigationTitle(L10n.UnstableFeaturesView.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UnstableFeaturesView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
UnstableFeaturesView(allowDevModeOnlyFeatures: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
// Copyright © 2023 SideStore. All rights reserved.
|
// Copyright © 2023 SideStore. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
// I prefixed it with Available to make UnstableFeatures come up first in autocomplete, feel free to rename it if you know a better name
|
// 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 {
|
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
|
// The value will be the GitHub Issue number. For example, "123" would correspond to https://github.com/SideStore/SideStore/issues/123
|
||||||
@@ -29,71 +31,58 @@ enum AvailableUnstableFeature: String, CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnstableFeatures {
|
|
||||||
|
class UnstableFeatures: ObservableObject {
|
||||||
#if UNSTABLE
|
#if UNSTABLE
|
||||||
private static var features: [AvailableUnstableFeature: Bool] = [:]
|
static let shared = UnstableFeatures()
|
||||||
#endif
|
@Published var features: [AvailableUnstableFeature: Bool] = [:]
|
||||||
|
|
||||||
static func load() {
|
static func load() {
|
||||||
#if UNSTABLE
|
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,
|
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 = AvailableUnstableFeature.allCases.first(where: { feature in String(describing: feature) == rawFeature.key }) {
|
||||||
features[feature] = rawFeature.value
|
shared.features[feature] = rawFeature.value
|
||||||
} else {
|
} else {
|
||||||
print("Unknown unstable feature: \(rawFeature.key) = \(rawFeature.value)")
|
print("Unknown unstable feature: \(rawFeature.key) = \(rawFeature.value)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for feature in AvailableUnstableFeature.allCases {
|
||||||
|
if shared.features[feature] == nil {
|
||||||
|
shared.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 AvailableUnstableFeature.allCases {
|
||||||
features[feature] = false
|
shared.features[feature] = false
|
||||||
}
|
}
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
|
||||||
print("Unstable features are not available on this build")
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func save(load: Bool = false) {
|
private static func save(load: Bool = false) {
|
||||||
#if UNSTABLE
|
|
||||||
|
|
||||||
var rawFeatures: [String: Bool] = [:]
|
var rawFeatures: [String: Bool] = [:]
|
||||||
for feature in features {
|
for feature in shared.features {
|
||||||
rawFeatures[String(describing: feature.key)] = feature.value
|
rawFeatures[String(describing: feature.key)] = feature.value
|
||||||
}
|
}
|
||||||
UserDefaults.shared.unstableFeatures = try! JSONEncoder().encode(rawFeatures)
|
UserDefaults.shared.unstableFeatures = try! JSONEncoder().encode(rawFeatures)
|
||||||
print("\(load ? "Loaded" : "Saved") unstable features: \(String(describing: rawFeatures))")
|
print("\(load ? "Loaded" : "Saved") unstable features: \(String(describing: rawFeatures))")
|
||||||
|
|
||||||
#else
|
|
||||||
// we want this to crash, this function should never be triggered on non-unstable builds
|
|
||||||
fatalError("Tried to save unstable features on non-unstable build!")
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func set(_ feature: AvailableUnstableFeature, enabled: Bool) {
|
static func set(_ feature: AvailableUnstableFeature, enabled: Bool) {
|
||||||
#if UNSTABLE
|
shared.features[feature] = enabled
|
||||||
|
|
||||||
features[feature] = enabled
|
|
||||||
save()
|
save()
|
||||||
|
|
||||||
#else
|
|
||||||
// we want this to crash, this function should never be triggered on non-unstable builds
|
|
||||||
fatalError("Tried to set unstable feature \(String(describing: feature)) to \(enabled) on non-unstable build!")
|
|
||||||
#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: AvailableUnstableFeature) -> Bool {
|
||||||
#if UNSTABLE
|
#if UNSTABLE
|
||||||
features[feature] ?? false
|
shared.features[feature] ?? false
|
||||||
#else
|
#else
|
||||||
false
|
false
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user