forked from MeloNX/MeloNX
Settings screen implemented
This commit is contained in:
parent
68ba8868cc
commit
b7a338f8e3
@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.app.ActivityCompat
|
||||
import melo.nxmodel.Ryujinx
|
||||
@ -73,11 +74,14 @@ open class MainActivity: SDLActivity {
|
||||
PresentationRootView(ComposeContext())
|
||||
SideEffect { saveableStateHolder.removeState(true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
"FPS: ${fps?.value ?: 0 }"
|
||||
"FPS: ${fps?.value ?: 0}",
|
||||
color = Color.White,
|
||||
fontSize = 24.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (startGameConfig?.value != null) {
|
||||
runSimulator(GameState.shared.startGameConfig!!)
|
||||
|
@ -15,7 +15,7 @@ public enum AspectRatio: String, Codable, CaseIterable {
|
||||
case fixed32x9 = "Fixed32x9"
|
||||
case stretched = "Stretched"
|
||||
|
||||
var displayName: String {
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .fixed4x3: return "4:3"
|
||||
case .fixed16x9: return "16:9 (Default)"
|
||||
|
@ -27,7 +27,7 @@ public enum SystemLanguage: String, Codable, CaseIterable {
|
||||
case traditionalChinese = "TraditionalChinese"
|
||||
case brazilianPortuguese = "BrazilianPortuguese"
|
||||
|
||||
var displayName: String {
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .japanese: return "Japanese"
|
||||
case .americanEnglish: return "American English"
|
||||
|
@ -16,7 +16,7 @@ public enum SystemRegionCode: String, Codable, CaseIterable {
|
||||
case korea = "Korea"
|
||||
case taiwan = "Taiwan"
|
||||
|
||||
var displayName: String {
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .japan: return "Japan"
|
||||
case .usa: return "United States"
|
||||
|
@ -387,3 +387,47 @@ public extension Ryujinx {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Ryujinx.Configuration {
|
||||
func saveSettings() {
|
||||
#if targetEnvironment(simulator)
|
||||
|
||||
print("Saving Settings")
|
||||
#else
|
||||
do {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
let data = try encoder.encode(self)
|
||||
let jsonString = String(data: data, encoding: .utf8)
|
||||
UserDefaults.standard.set(jsonString, forKey: "config")
|
||||
} catch {
|
||||
print("Failed to save settings: \(error)")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public extension Ryujinx {
|
||||
// Original loadSettings function assumed to exist
|
||||
static func loadSettings() -> Ryujinx.Configuration {
|
||||
#if targetEnvironment(simulator)
|
||||
print("Running on Simulator")
|
||||
|
||||
return Ryujinx.Configuration(gamepath: "")
|
||||
#else
|
||||
guard let jsonString = UserDefaults.standard.string(forKey: "config"),
|
||||
let data = jsonString.data(using: .utf8)
|
||||
else {
|
||||
return Ryujinx.Configuration(gamepath: "")
|
||||
}
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
let configs = try decoder.decode(Ryujinx.Configuration.self, from: data)
|
||||
return configs
|
||||
} catch {
|
||||
print("Failed to load settings: \(error)")
|
||||
return Ryujinx.Configuration(gamepath: "")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
@ -18,16 +18,10 @@ struct ContentView: View {
|
||||
@State var viewModel = ViewModel()
|
||||
@State var appearance = ""
|
||||
|
||||
@State private var config: Ryujinx.Configuration
|
||||
@State private var config = Ryujinx.loadSettings()
|
||||
@State private var game: Game?
|
||||
@State private var isLoading = true
|
||||
|
||||
init() {
|
||||
// let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
|
||||
let defaultConfig = Ryujinx.Configuration(gamepath: "")
|
||||
_config = State(initialValue: defaultConfig)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if game != nil {
|
||||
if isLoading {
|
||||
@ -39,16 +33,14 @@ struct ContentView: View {
|
||||
}
|
||||
} else {
|
||||
TabView(selection: $tab) {
|
||||
NavigationStack {
|
||||
NavigationStack {
|
||||
GamesView(startemu: $game)
|
||||
}
|
||||
}
|
||||
.tabItem { Label("Games", systemImage: "house.fill") }
|
||||
.tag(ContentTab.games)
|
||||
|
||||
NavigationStack {
|
||||
SettingsView(appearance: $appearance)
|
||||
SettingsView(appearance: $appearance, config: $config)
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
|
||||
|
@ -13,13 +13,6 @@ struct GamesView: View {
|
||||
@State var firmwareversion = "0"
|
||||
@State var searchQuery: String = ""
|
||||
|
||||
// let allGames: [Game] = [
|
||||
// .init(title: "\(DemoLib.instance.demo_number())", developer: "Pesesda"),
|
||||
// .init(title: "\(Ryujinx.shared.testMessage)", developer: "Bethesda"),
|
||||
// .init(title: "Skyrim", developer: "Bethesda"),
|
||||
// .init(title: "Naruto", developer: "Anime"),
|
||||
// .init(title: "Burnour", developer: "Paradise")
|
||||
// ]
|
||||
var allGames: Binding<[Game]> {
|
||||
Binding(
|
||||
get: { Ryujinx.shared.games },
|
||||
|
@ -5,17 +5,286 @@
|
||||
// Created by Даниил Виноградов on 02.03.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MeloNXModel
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@AppStorage("oldWindowCode") private var windowCode: Bool = false
|
||||
@AppStorage("portal") private var gamepo = false
|
||||
|
||||
@Environment(ViewModel.self) var viewModel: ViewModel
|
||||
@Binding var appearance: String
|
||||
@Binding var config: Ryujinx.Configuration
|
||||
|
||||
@State private var showResolutionInfo = false
|
||||
@State private var showAnisotropicInfo = false
|
||||
@State private var showControllerInfo = false
|
||||
@State private var searchQuery = ""
|
||||
|
||||
private var memoryManagerModes = [
|
||||
("HostMapped", "Host (fast)"),
|
||||
("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
|
||||
("SoftwarePageTable", "Software (slow)"),
|
||||
]
|
||||
|
||||
init(appearance: Binding<String>, config: Binding<Ryujinx.Configuration>) {
|
||||
_appearance = appearance
|
||||
_config = config
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@Bindable var viewModel = viewModel
|
||||
Form {
|
||||
TextField("Name", text: $viewModel.name)
|
||||
List {
|
||||
// Graphics & Performance
|
||||
Section {
|
||||
Picker(selection: $config.aspectRatio) {
|
||||
ForEach(AspectRatio.allCases, id: \.self) { ratio in
|
||||
Text(ratio.displayName).tag(ratio)
|
||||
}
|
||||
} label: {
|
||||
labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
|
||||
}
|
||||
|
||||
Toggle(isOn: $config.disableShaderCache) {
|
||||
labelWithIcon("Shader Cache", iconName: "memorychip")
|
||||
}
|
||||
|
||||
Toggle(isOn: $config.disablevsync) {
|
||||
labelWithIcon("Disable VSync", iconName: "arrow.triangle.2.circlepath")
|
||||
}
|
||||
|
||||
Toggle(isOn: $config.enableTextureRecompression) {
|
||||
labelWithIcon("Texture Recompression", iconName: "rectangle.compress.vertical")
|
||||
}
|
||||
|
||||
Toggle(isOn: $config.disableDockedMode) {
|
||||
labelWithIcon("Docked Mode", iconName: "dock.rectangle")
|
||||
}
|
||||
|
||||
Toggle(isOn: $config.macroHLE) {
|
||||
labelWithIcon("Macro HLE", iconName: "gearshape")
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
labelWithIcon("Resolution Scale", iconName: "magnifyingglass")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Button {
|
||||
showResolutionInfo = !showResolutionInfo
|
||||
} label: {
|
||||
Image(systemName: "info.circle")
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// .help("Learn more about Resolution Scale")
|
||||
.alert("Resolution Scale", isPresented: $showResolutionInfo, presenting: "Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance.", actions: { _ in
|
||||
Button(role: .cancel) {} label: {
|
||||
Text("OK")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Slider(value: $config.resscale, in: 0.1 ... 3.0, step: 0.102) {
|
||||
Text("Resolution Scale")
|
||||
}
|
||||
// minimumValueLabel: {
|
||||
// Text("0.1x")
|
||||
// .font(.footnote)
|
||||
// .foregroundColor(.secondary)
|
||||
// } maximumValueLabel: {
|
||||
// Text("3.0x")
|
||||
// .font(.footnote)
|
||||
// .foregroundColor(.secondary)
|
||||
// }
|
||||
Text("\(config.resscale, specifier: "%.2f")x")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
// .padding(.vertical, 8)
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
labelWithIcon("Max Anisotropic Scale", iconName: "magnifyingglass")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Button {
|
||||
showAnisotropicInfo = !showAnisotropicInfo
|
||||
} label: {
|
||||
Image(systemName: "info.circle")
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// .help("Learn more about Max Anisotropic Scale")
|
||||
.alert("Max Anisotripic Scale", isPresented: $showAnisotropicInfo, presenting: "Adjust the internal Anisotropic resolution. Higher values improve visuals but may reduce performance. Default at 0 lets game decide.", actions: { _ in
|
||||
Button(role: .cancel) {} label: {
|
||||
Text("OK")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Slider(value: $config.maxAnisotropy, in: 0.0 ... 16.0, step: 0.1) {
|
||||
Text("Resolution Scale")
|
||||
}
|
||||
// minimumValueLabel: {
|
||||
// Text("0x")
|
||||
// .font(.footnote)
|
||||
// .foregroundColor(.secondary)
|
||||
// } maximumValueLabel: {
|
||||
// Text("16.0x")
|
||||
// .font(.footnote)
|
||||
// .foregroundColor(.secondary)
|
||||
// }
|
||||
Text("\(config.maxAnisotropy, specifier: "%.2f")x")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
// .padding(.vertical, 8)
|
||||
|
||||
// Toggle(isOn: $performacehud) {
|
||||
// labelWithIcon("Performance Overlay", iconName: "speedometer")
|
||||
// }
|
||||
//
|
||||
} header: {
|
||||
Text("Graphics & Performance")
|
||||
.font(.title3.weight(.semibold))
|
||||
.textCase(nil)
|
||||
// .headerProminence(.increased)
|
||||
} footer: {
|
||||
Text("Fine-tune graphics and performance to suit your device and preferences.")
|
||||
}
|
||||
|
||||
// Language and Region Settings
|
||||
Section {
|
||||
Picker(selection: $config.language) {
|
||||
ForEach(SystemLanguage.allCases, id: \.self) { ratio in
|
||||
Text(ratio.displayName).tag(ratio)
|
||||
}
|
||||
} label: {
|
||||
labelWithIcon("Language", iconName: "character.bubble")
|
||||
}
|
||||
|
||||
Picker(selection: $config.regioncode) {
|
||||
ForEach(SystemRegionCode.allCases, id: \.self) { ratio in
|
||||
Text(ratio.displayName).tag(ratio)
|
||||
}
|
||||
} label: {
|
||||
labelWithIcon("Region", iconName: "globe")
|
||||
}
|
||||
|
||||
// globe
|
||||
} header: {
|
||||
Text("Language and Region Settings")
|
||||
.font(.title3.weight(.semibold))
|
||||
.textCase(nil)
|
||||
// .headerProminence(.increased)
|
||||
} footer: {
|
||||
Text("Configure the System Language and the Region.")
|
||||
}
|
||||
|
||||
// CPU Mode
|
||||
Section {
|
||||
// if filteredMemoryModes.isEmpty {
|
||||
// Text("No matches for \"\(searchText)\"")
|
||||
// .foregroundColor(.secondary)
|
||||
// } else {
|
||||
Picker(selection: $config.memoryManagerMode) {
|
||||
ForEach(memoryManagerModes, id: \.0) { key, displayName in
|
||||
Text(displayName).tag(key)
|
||||
}
|
||||
} label: {
|
||||
labelWithIcon("Memory Manager Mode", iconName: "gearshape")
|
||||
}
|
||||
// }
|
||||
|
||||
Toggle(isOn: $config.disablePTC) {
|
||||
labelWithIcon("Disable PTC", iconName: "cpu")
|
||||
}.tint(.blue)
|
||||
} header: {
|
||||
Text("CPU")
|
||||
.font(.title3.weight(.semibold))
|
||||
.textCase(nil)
|
||||
// .headerProminence(.increased)
|
||||
} footer: {
|
||||
Text("Select how memory is managed. 'Host (fast)' is best for most users.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(isOn: $config.expandRam) {
|
||||
labelWithIcon("Expand Guest Ram (6GB)", iconName: "exclamationmark.bubble")
|
||||
}
|
||||
.tint(.red)
|
||||
|
||||
Toggle(isOn: $config.ignoreMissingServices) {
|
||||
labelWithIcon("Ignore Missing Services", iconName: "waveform.path")
|
||||
}
|
||||
.tint(.red)
|
||||
} header: {
|
||||
Text("Hacks")
|
||||
.font(.title3.weight(.semibold))
|
||||
.textCase(nil)
|
||||
// .headerProminence(.increased)
|
||||
}
|
||||
|
||||
// Advanced
|
||||
Section {
|
||||
Toggle(isOn: $windowCode) {
|
||||
labelWithIcon("SDL Window", iconName: "macwindow.on.rectangle")
|
||||
}
|
||||
.tint(.blue)
|
||||
|
||||
// DisclosureGroup {
|
||||
// Toggle(isOn: $mVKPreFillBuffer) {
|
||||
// labelWithIcon("MVK: Pre-Fill Metal Command Buffers", iconName: "gearshape")
|
||||
// }.tint(.blue)
|
||||
|
||||
Toggle(isOn: $config.dfsIntegrityChecks) {
|
||||
labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield")
|
||||
}.tint(.blue)
|
||||
|
||||
// HStack {
|
||||
// labelWithIcon("Page Size", iconName: "textformat.size")
|
||||
// Spacer()
|
||||
// Text("\(String(Int(getpagesize())))")
|
||||
// .foregroundColor(.secondary)
|
||||
// }
|
||||
|
||||
TextField("Additional Arguments", text: Binding(
|
||||
get: {
|
||||
config.additionalArgs.joined(separator: " ")
|
||||
},
|
||||
set: { newValue in
|
||||
config.additionalArgs = newValue
|
||||
.split(separator: ",")
|
||||
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
}
|
||||
))
|
||||
.textInputAutocapitalization(nil)
|
||||
.autocorrectionDisabled()
|
||||
|
||||
Button {
|
||||
// Ryujinx.shared.removeFirmware()
|
||||
} label: {
|
||||
Text("Remove Firmware")
|
||||
.font(.body)
|
||||
}
|
||||
|
||||
// } label: {
|
||||
// Text("Advanced Options")
|
||||
// }
|
||||
} header: {
|
||||
Text("Advanced")
|
||||
.font(.title3.weight(.semibold))
|
||||
.textCase(nil)
|
||||
// .headerProminence(.increased)
|
||||
} footer: {
|
||||
Text("For advanced users. See page size or add custom arguments for experimental features. (Please don't touch this if you don't know what you're doing). \n \n\(gamepo ? "the cake is a lie" : "")")
|
||||
}
|
||||
|
||||
Section {
|
||||
// TextField("Name", text: $viewModel.name)
|
||||
Picker("Appearance", selection: $appearance) {
|
||||
Text("System").tag("")
|
||||
Text("Light").tag("light")
|
||||
@ -30,14 +299,33 @@ struct SettingsView : View {
|
||||
Text(verbatim: "💙")
|
||||
#endif
|
||||
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
|
||||
let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
||||
let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
|
||||
{
|
||||
Text("Version \(version) (\(buildNumber))")
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
Text("Powered by [Skip](https://skip.tools)")
|
||||
}
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
}
|
||||
.onChange(of: config) { _, newState in
|
||||
newState.saveSettings()
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
// .searchable(text: $searchQuery)
|
||||
}
|
||||
|
||||
}
|
||||
@ViewBuilder
|
||||
private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
if !iconName.isEmpty {
|
||||
Image(systemName: iconName)
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
Text(text)
|
||||
}
|
||||
.font(.body)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user