mirror of
https://github.com/izzy2lost/Super3.git
synced 2026-07-05 15:18:38 -07:00
added option in settings to edit Supermodel.ini easy & raw
This commit is contained in:
@@ -26,6 +26,16 @@
|
||||
android:screenOrientation="fullSensor"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".IniEditorActivity"
|
||||
android:screenOrientation="fullSensor"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".IniSettingsActivity"
|
||||
android:screenOrientation="fullSensor"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".Super3Activity"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.izzy2lost.super3
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import java.io.File
|
||||
|
||||
object IniDocumentStore {
|
||||
fun ensureIniDocument(context: Context, treeUri: Uri): DocumentFile? {
|
||||
val tree = DocumentFile.fromTreeUri(context, treeUri) ?: return null
|
||||
val configDir =
|
||||
tree.findFile("Config")?.takeIf { it.isDirectory } ?: tree.createDirectory("Config") ?: return null
|
||||
val existing = findBestIniDoc(configDir)
|
||||
if (existing != null) return existing
|
||||
|
||||
val created = configDir.createFile("application/octet-stream", "Supermodel.ini") ?: return null
|
||||
seedIniDocument(context, created)
|
||||
return created
|
||||
}
|
||||
|
||||
fun readIniText(context: Context, doc: DocumentFile): String? {
|
||||
return runCatching {
|
||||
val input = context.contentResolver.openInputStream(doc.uri) ?: return@runCatching null
|
||||
input.bufferedReader(Charsets.UTF_8).use { it.readText() }
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun writeIniText(context: Context, doc: DocumentFile, text: String): Boolean {
|
||||
return runCatching {
|
||||
val out = context.contentResolver.openOutputStream(doc.uri) ?: return@runCatching false
|
||||
out.writer(Charsets.UTF_8).use { it.write(text) }
|
||||
true
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
private fun findBestIniDoc(configDir: DocumentFile): DocumentFile? {
|
||||
val exact = configDir.findFile("Supermodel.ini")
|
||||
if (exact != null && exact.isFile) return exact
|
||||
|
||||
val candidates =
|
||||
configDir
|
||||
.listFiles()
|
||||
.filter { it.isFile }
|
||||
.mapNotNull { doc ->
|
||||
val name = doc.name ?: return@mapNotNull null
|
||||
if (!name.startsWith("Supermodel.ini", ignoreCase = true)) return@mapNotNull null
|
||||
doc
|
||||
}
|
||||
if (candidates.isEmpty()) return null
|
||||
|
||||
fun nameScore(doc: DocumentFile): Int {
|
||||
val name = doc.name?.lowercase() ?: ""
|
||||
return when {
|
||||
name == "supermodel.ini" -> 0
|
||||
name == "supermodel.ini.txt" -> 1
|
||||
name.startsWith("supermodel.ini(") -> 2
|
||||
else -> 3
|
||||
}
|
||||
}
|
||||
|
||||
val best =
|
||||
if (candidates.any { it.lastModified() > 0L }) {
|
||||
candidates.maxWithOrNull(
|
||||
compareBy<DocumentFile> { it.lastModified() }
|
||||
.thenBy { -nameScore(it) }
|
||||
.thenBy { -(it.name?.length ?: Int.MAX_VALUE) },
|
||||
)
|
||||
} else {
|
||||
candidates.minWithOrNull(
|
||||
compareBy<DocumentFile> { nameScore(it) }
|
||||
.thenBy { it.name?.length ?: Int.MAX_VALUE },
|
||||
)
|
||||
} ?: return null
|
||||
|
||||
val bestName = best.name ?: return best
|
||||
if (!bestName.equals("Supermodel.ini", ignoreCase = true)) {
|
||||
val renamed = runCatching { best.renameTo("Supermodel.ini") }.getOrDefault(false)
|
||||
if (renamed) {
|
||||
val renamedDoc = configDir.findFile("Supermodel.ini")
|
||||
if (renamedDoc != null && renamedDoc.isFile) return renamedDoc
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
private fun seedIniDocument(context: Context, doc: DocumentFile) {
|
||||
val internal = File(File(context.getExternalFilesDir(null), "super3/Config"), "Supermodel.ini")
|
||||
val input =
|
||||
when {
|
||||
internal.exists() -> runCatching { internal.inputStream() }.getOrNull()
|
||||
else -> runCatching { context.assets.open("Config/Supermodel.ini") }.getOrNull()
|
||||
}
|
||||
if (input == null) return
|
||||
input.use { ins ->
|
||||
context.contentResolver.openOutputStream(doc.uri)?.use { outs ->
|
||||
ins.copyTo(outs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.izzy2lost.super3
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class IniEditorActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
const val EXTRA_TREE_URI = "treeUri"
|
||||
}
|
||||
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
private lateinit var pathText: TextView
|
||||
private lateinit var editor: EditText
|
||||
|
||||
private var iniDoc: DocumentFile? = null
|
||||
private var suppressDirty = false
|
||||
private var dirty = false
|
||||
private var busy = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
applyImmersiveMode()
|
||||
setContentView(R.layout.activity_ini_editor)
|
||||
|
||||
toolbar = findViewById(R.id.ini_toolbar)
|
||||
pathText = findViewById(R.id.ini_path)
|
||||
editor = findViewById(R.id.ini_editor)
|
||||
|
||||
toolbar.inflateMenu(R.menu.menu_ini_editor)
|
||||
toolbar.setNavigationOnClickListener { maybeExit() }
|
||||
toolbar.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.action_save -> {
|
||||
saveIni()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
tintMenuIcons()
|
||||
updateSaveEnabled()
|
||||
|
||||
editor.addTextChangedListener {
|
||||
if (suppressDirty || busy) return@addTextChangedListener
|
||||
if (!dirty) {
|
||||
dirty = true
|
||||
toolbar.subtitle = "Unsaved changes"
|
||||
updateSaveEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
maybeExit()
|
||||
}
|
||||
|
||||
val treeUri = intent.getStringExtra(EXTRA_TREE_URI)?.let(Uri::parse)
|
||||
if (treeUri == null) {
|
||||
Toast.makeText(this, "Data folder not set", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
pathText.text = "Editing: Config/Supermodel.ini\nData folder: $treeUri"
|
||||
loadIni(treeUri)
|
||||
}
|
||||
|
||||
private fun maybeExit() {
|
||||
if (busy) {
|
||||
Toast.makeText(this, "Please wait...", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
if (!dirty) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle("Discard changes?")
|
||||
.setMessage("You have unsaved changes to Supermodel.ini.")
|
||||
.setPositiveButton("Save") { _, _ -> saveIni(finishAfter = true) }
|
||||
.setNegativeButton("Discard") { _, _ -> finish() }
|
||||
.setNeutralButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun loadIni(treeUri: Uri) {
|
||||
setBusy(true, "Loading...")
|
||||
thread(name = "Super3IniLoad") {
|
||||
val doc = IniDocumentStore.ensureIniDocument(this, treeUri)
|
||||
val text = doc?.let { IniDocumentStore.readIniText(this, it) }
|
||||
runOnUiThread {
|
||||
if (doc == null || text == null) {
|
||||
setBusy(false, null)
|
||||
Toast.makeText(this, "Failed to load Supermodel.ini", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return@runOnUiThread
|
||||
}
|
||||
iniDoc = doc
|
||||
suppressDirty = true
|
||||
editor.setText(text)
|
||||
editor.setSelection(0)
|
||||
suppressDirty = false
|
||||
dirty = false
|
||||
toolbar.subtitle = null
|
||||
updateSaveEnabled()
|
||||
setBusy(false, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveIni(finishAfter: Boolean = false) {
|
||||
val doc = iniDoc ?: return
|
||||
val text = editor.text?.toString() ?: ""
|
||||
setBusy(true, "Saving...")
|
||||
thread(name = "Super3IniSave") {
|
||||
val ok = IniDocumentStore.writeIniText(this, doc, text)
|
||||
runOnUiThread {
|
||||
setBusy(false, null)
|
||||
if (!ok) {
|
||||
Toast.makeText(this, "Failed to save Supermodel.ini", Toast.LENGTH_SHORT).show()
|
||||
return@runOnUiThread
|
||||
}
|
||||
dirty = false
|
||||
toolbar.subtitle = "Saved"
|
||||
updateSaveEnabled()
|
||||
if (finishAfter) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBusy(isBusy: Boolean, status: String?) {
|
||||
busy = isBusy
|
||||
editor.isEnabled = !isBusy
|
||||
toolbar.menu.findItem(R.id.action_save)?.isEnabled = !isBusy && dirty
|
||||
if (status != null) {
|
||||
toolbar.subtitle = status
|
||||
} else if (!isBusy) {
|
||||
toolbar.subtitle = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSaveEnabled() {
|
||||
val saveItem = toolbar.menu.findItem(R.id.action_save)
|
||||
saveItem?.isEnabled = dirty && !busy
|
||||
tintMenuIcons()
|
||||
}
|
||||
|
||||
private fun tintMenuIcons() {
|
||||
val enabledColor =
|
||||
MaterialColors.getColor(toolbar, com.google.android.material.R.attr.colorOnPrimary)
|
||||
val disabledColor =
|
||||
MaterialColors.getColor(toolbar, com.google.android.material.R.attr.colorOnSurfaceVariant)
|
||||
val saveItem = toolbar.menu.findItem(R.id.action_save)
|
||||
val tint = if (saveItem?.isEnabled == true) enabledColor else disabledColor
|
||||
saveItem?.icon?.setTint(tint)
|
||||
}
|
||||
|
||||
// IniDocumentStore handles SAF reads/writes + file selection.
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package com.izzy2lost.super3
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class IniSettingsActivity : AppCompatActivity() {
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
private lateinit var btnPpcFrequency: MaterialButton
|
||||
private lateinit var btnMultithreaded: MaterialButton
|
||||
private lateinit var btnGpuMultithreaded: MaterialButton
|
||||
private lateinit var btnVsync: MaterialButton
|
||||
private lateinit var btnEmulateSound: MaterialButton
|
||||
private lateinit var btnSoundVolume: MaterialButton
|
||||
private lateinit var btnMusicVolume: MaterialButton
|
||||
|
||||
private var iniDoc: DocumentFile? = null
|
||||
private var iniLines: MutableList<String> = mutableListOf()
|
||||
private var busy = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
applyImmersiveMode()
|
||||
setContentView(R.layout.activity_ini_settings)
|
||||
|
||||
toolbar = findViewById(R.id.ini_settings_toolbar)
|
||||
btnPpcFrequency = findViewById(R.id.btn_ppc_frequency)
|
||||
btnMultithreaded = findViewById(R.id.btn_multithreaded)
|
||||
btnGpuMultithreaded = findViewById(R.id.btn_gpu_multithreaded)
|
||||
btnVsync = findViewById(R.id.btn_vsync)
|
||||
btnEmulateSound = findViewById(R.id.btn_emulate_sound)
|
||||
btnSoundVolume = findViewById(R.id.btn_sound_volume)
|
||||
btnMusicVolume = findViewById(R.id.btn_music_volume)
|
||||
|
||||
toolbar.setNavigationOnClickListener { finish() }
|
||||
onBackPressedDispatcher.addCallback(this) { finish() }
|
||||
|
||||
val treeUri = intent.getStringExtra(IniEditorActivity.EXTRA_TREE_URI)?.let(Uri::parse)
|
||||
if (treeUri == null) {
|
||||
Toast.makeText(this, "Data folder not set", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
bindActions()
|
||||
loadIni(treeUri)
|
||||
}
|
||||
|
||||
private fun bindActions() {
|
||||
btnPpcFrequency.setOnClickListener {
|
||||
val current = readIniInt("PowerPCFrequency") ?: 50
|
||||
showNumberDialog(
|
||||
title = "PowerPC frequency",
|
||||
key = "PowerPCFrequency",
|
||||
current = current,
|
||||
min = 10,
|
||||
max = 200,
|
||||
)
|
||||
}
|
||||
|
||||
btnMultithreaded.setOnClickListener {
|
||||
val enabled = btnMultithreaded.isChecked
|
||||
applyUpdate(
|
||||
updates = mapOf("MultiThreaded" to if (enabled) "1" else "0"),
|
||||
onApplied = { btnMultithreaded.isChecked = enabled },
|
||||
onFailed = { btnMultithreaded.isChecked = !enabled },
|
||||
)
|
||||
}
|
||||
|
||||
btnGpuMultithreaded.setOnClickListener {
|
||||
val enabled = btnGpuMultithreaded.isChecked
|
||||
applyUpdate(
|
||||
updates = mapOf("GPUMultiThreaded" to if (enabled) "1" else "0"),
|
||||
onApplied = { btnGpuMultithreaded.isChecked = enabled },
|
||||
onFailed = { btnGpuMultithreaded.isChecked = !enabled },
|
||||
)
|
||||
}
|
||||
|
||||
btnVsync.setOnClickListener {
|
||||
val enabled = btnVsync.isChecked
|
||||
applyUpdate(
|
||||
updates = mapOf("VSync" to if (enabled) "1" else "0"),
|
||||
onApplied = { btnVsync.isChecked = enabled },
|
||||
onFailed = { btnVsync.isChecked = !enabled },
|
||||
)
|
||||
}
|
||||
|
||||
btnEmulateSound.setOnClickListener {
|
||||
val enabled = btnEmulateSound.isChecked
|
||||
applyUpdate(
|
||||
updates = mapOf("EmulateSound" to if (enabled) "1" else "0"),
|
||||
onApplied = { btnEmulateSound.isChecked = enabled },
|
||||
onFailed = { btnEmulateSound.isChecked = !enabled },
|
||||
)
|
||||
}
|
||||
|
||||
btnSoundVolume.setOnClickListener {
|
||||
val current = readIniInt("SoundVolume") ?: 100
|
||||
showNumberDialog(
|
||||
title = "Sound volume",
|
||||
key = "SoundVolume",
|
||||
current = current,
|
||||
min = 0,
|
||||
max = 200,
|
||||
)
|
||||
}
|
||||
|
||||
btnMusicVolume.setOnClickListener {
|
||||
val current = readIniInt("MusicVolume") ?: 150
|
||||
showNumberDialog(
|
||||
title = "Music volume",
|
||||
key = "MusicVolume",
|
||||
current = current,
|
||||
min = 0,
|
||||
max = 200,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadIni(treeUri: Uri) {
|
||||
setBusy(true, "Loading...")
|
||||
thread(name = "Super3IniSettingsLoad") {
|
||||
val doc = IniDocumentStore.ensureIniDocument(this, treeUri)
|
||||
val text = doc?.let { IniDocumentStore.readIniText(this, it) }
|
||||
runOnUiThread {
|
||||
if (doc == null || text == null) {
|
||||
setBusy(false, null)
|
||||
Toast.makeText(this, "Failed to load Supermodel.ini", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return@runOnUiThread
|
||||
}
|
||||
iniDoc = doc
|
||||
iniLines = text.split("\n").toMutableList()
|
||||
applyUiFromIni()
|
||||
setBusy(false, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyUiFromIni() {
|
||||
val ppc = readIniInt("PowerPCFrequency") ?: 50
|
||||
btnPpcFrequency.text = "PowerPC frequency: $ppc"
|
||||
|
||||
btnMultithreaded.isChecked = readIniBool("MultiThreaded") ?: true
|
||||
btnGpuMultithreaded.isChecked = readIniBool("GPUMultiThreaded") ?: false
|
||||
btnVsync.isChecked = readIniBool("VSync") ?: true
|
||||
btnEmulateSound.isChecked = readIniBool("EmulateSound") ?: true
|
||||
|
||||
val sound = readIniInt("SoundVolume") ?: 100
|
||||
val music = readIniInt("MusicVolume") ?: 150
|
||||
btnSoundVolume.text = "Sound volume: $sound"
|
||||
btnMusicVolume.text = "Music volume: $music"
|
||||
}
|
||||
|
||||
private fun showNumberDialog(title: String, key: String, current: Int, min: Int, max: Int) {
|
||||
val view = layoutInflater.inflate(R.layout.dialog_number_input, null)
|
||||
val inputLayout = view.findViewById<TextInputLayout>(R.id.number_input_layout)
|
||||
val input = view.findViewById<TextInputEditText>(R.id.number_input)
|
||||
inputLayout.hint = "$title ($min-$max)"
|
||||
input.inputType = InputType.TYPE_CLASS_NUMBER
|
||||
input.setText(current.toString())
|
||||
input.setSelection(input.text?.length ?: 0)
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(title)
|
||||
.setView(view)
|
||||
.setPositiveButton("Save") { _, _ ->
|
||||
val raw = input.text?.toString()?.trim().orEmpty()
|
||||
val value = raw.toIntOrNull()
|
||||
if (value == null) {
|
||||
Toast.makeText(this, "Enter a number between $min and $max", Toast.LENGTH_SHORT).show()
|
||||
return@setPositiveButton
|
||||
}
|
||||
val clamped = value.coerceIn(min, max)
|
||||
if (clamped != value) {
|
||||
Toast.makeText(this, "Clamped to $clamped", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
applyUpdate(
|
||||
updates = mapOf(key to clamped.toString()),
|
||||
onApplied = {
|
||||
when (key) {
|
||||
"PowerPCFrequency" -> btnPpcFrequency.text = "PowerPC frequency: $clamped"
|
||||
"SoundVolume" -> btnSoundVolume.text = "Sound volume: $clamped"
|
||||
"MusicVolume" -> btnMusicVolume.text = "Music volume: $clamped"
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun applyUpdate(
|
||||
updates: Map<String, String>,
|
||||
onApplied: () -> Unit,
|
||||
onFailed: (() -> Unit)? = null,
|
||||
) {
|
||||
if (busy) return
|
||||
val doc = iniDoc ?: return
|
||||
val updated = updateIniSection(iniLines, "global", updates)
|
||||
setBusy(true, "Saving...")
|
||||
thread(name = "Super3IniSettingsSave") {
|
||||
val ok = IniDocumentStore.writeIniText(this, doc, updated.joinToString("\n"))
|
||||
runOnUiThread {
|
||||
setBusy(false, null)
|
||||
if (!ok) {
|
||||
Toast.makeText(this, "Failed to save Supermodel.ini", Toast.LENGTH_SHORT).show()
|
||||
onFailed?.invoke()
|
||||
return@runOnUiThread
|
||||
}
|
||||
iniLines = updated
|
||||
onApplied()
|
||||
Toast.makeText(this, "Saved", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readIniInt(key: String): Int? {
|
||||
return readIniString(key)?.trim()?.toIntOrNull()
|
||||
}
|
||||
|
||||
private fun readIniBool(key: String): Boolean? {
|
||||
val v = readIniString(key)?.trim()?.lowercase() ?: return null
|
||||
return when (v) {
|
||||
"1", "true", "yes", "on" -> true
|
||||
"0", "false", "no", "off" -> false
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun readIniString(key: String): String? {
|
||||
val range = findSectionRange("global") ?: (0 until iniLines.size)
|
||||
for (i in range) {
|
||||
val line = iniLines[i]
|
||||
val trimmed = line.trim()
|
||||
if (trimmed.isBlank() || trimmed.startsWith(";")) continue
|
||||
val rx = Regex("^\\s*${Regex.escape(key)}\\s*=", RegexOption.IGNORE_CASE)
|
||||
if (rx.containsMatchIn(line)) {
|
||||
val idx = line.indexOf("=")
|
||||
return if (idx >= 0) line.substring(idx + 1).trim() else null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun updateIniSection(
|
||||
lines: List<String>,
|
||||
section: String,
|
||||
updates: Map<String, String>,
|
||||
): MutableList<String> {
|
||||
val (start, end) = findSectionBounds(lines, section) ?: run {
|
||||
val out = ArrayList<String>(lines.size + updates.size + 2)
|
||||
out.addAll(lines)
|
||||
if (out.isNotEmpty() && out.last().isNotBlank()) out.add("")
|
||||
out.add("[ Global ]")
|
||||
for ((k, v) in updates) {
|
||||
out.add("$k = $v")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
val out = ArrayList<String>(lines.size + updates.size)
|
||||
out.addAll(lines.take(start + 1))
|
||||
|
||||
val existing = HashSet<String>(updates.size)
|
||||
for (i in (start + 1) until end) {
|
||||
val line = lines[i]
|
||||
val trimmed = line.trim()
|
||||
if (trimmed.startsWith(";") || trimmed.isBlank()) {
|
||||
out.add(line)
|
||||
continue
|
||||
}
|
||||
var replaced = false
|
||||
for ((k, v) in updates) {
|
||||
val rx = Regex("^\\s*${Regex.escape(k)}\\s*=", RegexOption.IGNORE_CASE)
|
||||
if (rx.containsMatchIn(line)) {
|
||||
out.add("$k = $v")
|
||||
existing.add(k.lowercase())
|
||||
replaced = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!replaced) out.add(line)
|
||||
}
|
||||
|
||||
for ((k, v) in updates) {
|
||||
if (existing.contains(k.lowercase())) continue
|
||||
out.add("$k = $v")
|
||||
}
|
||||
|
||||
out.addAll(lines.drop(end))
|
||||
return out
|
||||
}
|
||||
|
||||
private fun findSectionRange(section: String): IntRange? {
|
||||
val (start, end) = findSectionBounds(iniLines, section) ?: return null
|
||||
return (start + 1) until end
|
||||
}
|
||||
|
||||
private fun findSectionBounds(lines: List<String>, section: String): Pair<Int, Int>? {
|
||||
val target = section.lowercase()
|
||||
var start = -1
|
||||
for (i in lines.indices) {
|
||||
val name = sectionName(lines[i]) ?: continue
|
||||
if (name == target) {
|
||||
start = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (start < 0) return null
|
||||
var end = lines.size
|
||||
for (i in (start + 1) until lines.size) {
|
||||
if (sectionName(lines[i]) != null) {
|
||||
end = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return start to end
|
||||
}
|
||||
|
||||
private fun sectionName(line: String): String? {
|
||||
val trimmed = line.trim()
|
||||
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null
|
||||
return trimmed.substring(1, trimmed.length - 1).trim().lowercase()
|
||||
}
|
||||
|
||||
private fun setBusy(isBusy: Boolean, status: String?) {
|
||||
busy = isBusy
|
||||
toolbar.subtitle = status ?: if (busy) toolbar.subtitle else "Easy settings"
|
||||
val enabled = !isBusy
|
||||
btnPpcFrequency.isEnabled = enabled
|
||||
btnMultithreaded.isEnabled = enabled
|
||||
btnGpuMultithreaded.isEnabled = enabled
|
||||
btnVsync.isEnabled = enabled
|
||||
btnEmulateSound.isEnabled = enabled
|
||||
btnSoundVolume.isEnabled = enabled
|
||||
btnMusicVolume.isEnabled = enabled
|
||||
}
|
||||
}
|
||||
@@ -173,6 +173,7 @@ class MainActivity : AppCompatActivity() {
|
||||
val btnShowShifterOverlay: MaterialButton = headerView.findViewById(R.id.btn_show_shifter_overlay)
|
||||
val btnGyroSteering: MaterialButton = headerView.findViewById(R.id.btn_gyro_steering)
|
||||
val btnGyroSensitivity: MaterialButton = headerView.findViewById(R.id.btn_gyro_sensitivity)
|
||||
val btnEditSupermodelIni: MaterialButton = headerView.findViewById(R.id.btn_edit_supermodel_ini)
|
||||
|
||||
gamesAdapter = GamesAdapter { item ->
|
||||
if (!item.launchable) {
|
||||
@@ -197,6 +198,31 @@ class MainActivity : AppCompatActivity() {
|
||||
btnPickUserFolder.setOnClickListener { pickUserFolder.launch(null) }
|
||||
btnRescan.setOnClickListener { refreshUi() }
|
||||
|
||||
btnEditSupermodelIni.setOnClickListener {
|
||||
val tree = userTreeUri
|
||||
if (tree == null) {
|
||||
Toast.makeText(this, "Pick a data folder first", Toast.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
drawerLayout.closeDrawer(GravityCompat.START)
|
||||
val options = arrayOf("Easy settings", "Raw text")
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle("Edit Supermodel.ini")
|
||||
.setItems(options) { _, which ->
|
||||
val target =
|
||||
when (which) {
|
||||
0 -> IniSettingsActivity::class.java
|
||||
else -> IniEditorActivity::class.java
|
||||
}
|
||||
val intent =
|
||||
Intent(this, target).apply {
|
||||
putExtra(IniEditorActivity.EXTRA_TREE_URI, tree.toString())
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
bindVideoSettingsUi()
|
||||
bindTimingUi()
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ object UserDataSync {
|
||||
private fun copyFileToDocFile(resolver: ContentResolver, from: File, toDir: DocumentFile) {
|
||||
val existing = toDir.findFile(from.name)
|
||||
val outDoc = existing ?: toDir.createFile("application/octet-stream", from.name) ?: return
|
||||
resolver.openOutputStream(outDoc.uri, "wt")?.use { output ->
|
||||
resolver.openOutputStream(outDoc.uri)?.use { output ->
|
||||
from.inputStream().use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/ini_toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navigationIcon="@drawable/exit_to_app_24px"
|
||||
app:navigationIconTint="?attr/colorOnPrimary"
|
||||
app:title="Supermodel.ini"
|
||||
app:titleTextColor="?attr/colorOnPrimary"
|
||||
app:subtitleTextColor="?attr/colorOnPrimary" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/ini_path"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:ellipsize="middle"
|
||||
android:maxLines="2"
|
||||
android:text="Editing: Config/Supermodel.ini"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ini_toolbar" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/ini_editor"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/setup_wizard_path_background"
|
||||
android:fontFamily="monospace"
|
||||
android:gravity="top|start"
|
||||
android:fadingEdgeLength="12dp"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:scrollbars="vertical"
|
||||
android:scrollbarStyle="insideInset"
|
||||
android:scrollbarFadeDuration="200"
|
||||
android:scrollbarDefaultDelayBeforeFade="600"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textColorHint="?attr/colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ini_path" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,134 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/ini_settings_toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navigationIcon="@drawable/exit_to_app_24px"
|
||||
app:navigationIconTint="?attr/colorOnPrimary"
|
||||
app:title="Supermodel.ini"
|
||||
app:subtitle="Easy settings"
|
||||
app:titleTextColor="?attr/colorOnPrimary"
|
||||
app:subtitleTextColor="?attr/colorOnPrimary" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/ini_settings_scroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:paddingBottom="24dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ini_settings_toolbar">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ini_settings_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/ini_settings_perf_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Performance"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_ppc_frequency"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="PowerPC frequency: 50"
|
||||
style="?attr/materialButtonElevatedStyle" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_multithreaded"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:checkable="true"
|
||||
android:text="Multi-threaded (CPU)"
|
||||
style="?attr/materialButtonElevatedStyle" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_gpu_multithreaded"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:checkable="true"
|
||||
android:text="Multi-threaded (GPU)"
|
||||
style="?attr/materialButtonElevatedStyle" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/ini_settings_display_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="Display"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_vsync"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:checkable="true"
|
||||
android:text="VSync"
|
||||
style="?attr/materialButtonElevatedStyle" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/ini_settings_audio_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="Audio"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_emulate_sound"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:checkable="true"
|
||||
android:text="Emulate sound"
|
||||
style="?attr/materialButtonElevatedStyle" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_sound_volume"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="Sound volume: 100"
|
||||
style="?attr/materialButtonElevatedStyle" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_music_volume"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="Music volume: 150"
|
||||
style="?attr/materialButtonElevatedStyle" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="20dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/number_input_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/number_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -172,6 +172,24 @@
|
||||
android:text="Gyro sensitivity: Normal"
|
||||
style="?attr/materialButtonElevatedStyle" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/advanced_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Advanced"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
android:textColor="?attr/colorOnPrimary" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_edit_supermodel_ini"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="Edit Supermodel.ini"
|
||||
app:icon="@drawable/hard_drive_24px"
|
||||
style="?attr/materialButtonElevatedStyle" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/games_folder_text"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_save"
|
||||
android:icon="@drawable/save_24px"
|
||||
android:title="Save"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
||||
Reference in New Issue
Block a user