1
0
forked from MeloNX/MeloNX

Settings screen implemented

This commit is contained in:
Daniil Vinogradov 2025-03-08 21:08:24 +01:00
parent 68ba8868cc
commit b7a338f8e3
8 changed files with 369 additions and 48 deletions

View File

@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import melo.nxmodel.Ryujinx import melo.nxmodel.Ryujinx
@ -73,11 +74,14 @@ open class MainActivity: SDLActivity {
PresentationRootView(ComposeContext()) PresentationRootView(ComposeContext())
SideEffect { saveableStateHolder.removeState(true) } SideEffect { saveableStateHolder.removeState(true) }
} }
} else {
Text(
"FPS: ${fps?.value ?: 0}",
color = Color.White,
fontSize = 24.sp
)
} }
} }
Text(
"FPS: ${fps?.value ?: 0 }"
)
if (startGameConfig?.value != null) { if (startGameConfig?.value != null) {
runSimulator(GameState.shared.startGameConfig!!) runSimulator(GameState.shared.startGameConfig!!)

View File

@ -15,7 +15,7 @@ public enum AspectRatio: String, Codable, CaseIterable {
case fixed32x9 = "Fixed32x9" case fixed32x9 = "Fixed32x9"
case stretched = "Stretched" case stretched = "Stretched"
var displayName: String { public var displayName: String {
switch self { switch self {
case .fixed4x3: return "4:3" case .fixed4x3: return "4:3"
case .fixed16x9: return "16:9 (Default)" case .fixed16x9: return "16:9 (Default)"

View File

@ -27,7 +27,7 @@ public enum SystemLanguage: String, Codable, CaseIterable {
case traditionalChinese = "TraditionalChinese" case traditionalChinese = "TraditionalChinese"
case brazilianPortuguese = "BrazilianPortuguese" case brazilianPortuguese = "BrazilianPortuguese"
var displayName: String { public var displayName: String {
switch self { switch self {
case .japanese: return "Japanese" case .japanese: return "Japanese"
case .americanEnglish: return "American English" case .americanEnglish: return "American English"

View File

@ -16,7 +16,7 @@ public enum SystemRegionCode: String, Codable, CaseIterable {
case korea = "Korea" case korea = "Korea"
case taiwan = "Taiwan" case taiwan = "Taiwan"
var displayName: String { public var displayName: String {
switch self { switch self {
case .japan: return "Japan" case .japan: return "Japan"
case .usa: return "United States" case .usa: return "United States"

View File

@ -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
}
}

View File

@ -18,16 +18,10 @@ struct ContentView: View {
@State var viewModel = ViewModel() @State var viewModel = ViewModel()
@State var appearance = "" @State var appearance = ""
@State private var config: Ryujinx.Configuration @State private var config = Ryujinx.loadSettings()
@State private var game: Game? @State private var game: Game?
@State private var isLoading = true @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 { var body: some View {
if game != nil { if game != nil {
if isLoading { if isLoading {
@ -40,15 +34,13 @@ struct ContentView: View {
} else { } else {
TabView(selection: $tab) { TabView(selection: $tab) {
NavigationStack { NavigationStack {
NavigationStack { GamesView(startemu: $game)
GamesView(startemu: $game)
}
} }
.tabItem { Label("Games", systemImage: "house.fill") } .tabItem { Label("Games", systemImage: "house.fill") }
.tag(ContentTab.games) .tag(ContentTab.games)
NavigationStack { NavigationStack {
SettingsView(appearance: $appearance) SettingsView(appearance: $appearance, config: $config)
.navigationTitle("Settings") .navigationTitle("Settings")
} }
.tabItem { Label("Settings", systemImage: "gearshape.fill") } .tabItem { Label("Settings", systemImage: "gearshape.fill") }

View File

@ -13,13 +13,6 @@ struct GamesView: View {
@State var firmwareversion = "0" @State var firmwareversion = "0"
@State var searchQuery: String = "" @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]> { var allGames: Binding<[Game]> {
Binding( Binding(
get: { Ryujinx.shared.games }, get: { Ryujinx.shared.games },

View File

@ -5,39 +5,327 @@
// Created by Даниил Виноградов on 02.03.2025. // Created by Даниил Виноградов on 02.03.2025.
// //
import SwiftUI
import MeloNXModel import MeloNXModel
import SwiftUI
struct SettingsView: View {
@AppStorage("oldWindowCode") private var windowCode: Bool = false
@AppStorage("portal") private var gamepo = false
struct SettingsView : View {
@Environment(ViewModel.self) var viewModel: ViewModel @Environment(ViewModel.self) var viewModel: ViewModel
@Binding var appearance: String @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 { var body: some View {
@Bindable var viewModel = viewModel @Bindable var viewModel = viewModel
Form { List {
TextField("Name", text: $viewModel.name) // Graphics & Performance
Picker("Appearance", selection: $appearance) { Section {
Text("System").tag("") Picker(selection: $config.aspectRatio) {
Text("Light").tag("light") ForEach(AspectRatio.allCases, id: \.self) { ratio in
Text("Dark").tag("dark") Text(ratio.displayName).tag(ratio)
} }
HStack { } label: {
#if SKIP labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
ComposeView { ctx in // Mix in Compose code!
androidx.compose.material3.Text("💚", modifier: ctx.modifier)
} }
#else
Text(verbatim: "💙")
#endif
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] 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)
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")
Text("Dark").tag("dark")
}
HStack {
#if SKIP
ComposeView { ctx in // Mix in Compose code!
androidx.compose.material3.Text("💚", modifier: ctx.modifier)
}
#else
Text(verbatim: "💙")
#endif
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] 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)
} }
} }