diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fff0e4c..a597205 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -26,6 +26,16 @@
android:screenOrientation="fullSensor"
android:exported="false" />
+
+
+
+
+ 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 { it.lastModified() }
+ .thenBy { -nameScore(it) }
+ .thenBy { -(it.name?.length ?: Int.MAX_VALUE) },
+ )
+ } else {
+ candidates.minWithOrNull(
+ compareBy { 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)
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/izzy2lost/super3/IniEditorActivity.kt b/android/app/src/main/java/com/izzy2lost/super3/IniEditorActivity.kt
new file mode 100644
index 0000000..da84881
--- /dev/null
+++ b/android/app/src/main/java/com/izzy2lost/super3/IniEditorActivity.kt
@@ -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.
+}
diff --git a/android/app/src/main/java/com/izzy2lost/super3/IniSettingsActivity.kt b/android/app/src/main/java/com/izzy2lost/super3/IniSettingsActivity.kt
new file mode 100644
index 0000000..e450b72
--- /dev/null
+++ b/android/app/src/main/java/com/izzy2lost/super3/IniSettingsActivity.kt
@@ -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 = 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(R.id.number_input_layout)
+ val input = view.findViewById(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,
+ 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,
+ section: String,
+ updates: Map,
+ ): MutableList {
+ val (start, end) = findSectionBounds(lines, section) ?: run {
+ val out = ArrayList(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(lines.size + updates.size)
+ out.addAll(lines.take(start + 1))
+
+ val existing = HashSet(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, section: String): Pair? {
+ 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
+ }
+}
diff --git a/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt b/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt
index 64cec3f..bd5821c 100644
--- a/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt
+++ b/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt
@@ -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()
diff --git a/android/app/src/main/java/com/izzy2lost/super3/UserDataSync.kt b/android/app/src/main/java/com/izzy2lost/super3/UserDataSync.kt
index 6221cb3..a07bef8 100644
--- a/android/app/src/main/java/com/izzy2lost/super3/UserDataSync.kt
+++ b/android/app/src/main/java/com/izzy2lost/super3/UserDataSync.kt
@@ -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)
}
diff --git a/android/app/src/main/res/layout/activity_ini_editor.xml b/android/app/src/main/res/layout/activity_ini_editor.xml
new file mode 100644
index 0000000..d466a6c
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_ini_editor.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/activity_ini_settings.xml b/android/app/src/main/res/layout/activity_ini_settings.xml
new file mode 100644
index 0000000..af8bac7
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_ini_settings.xml
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/dialog_number_input.xml b/android/app/src/main/res/layout/dialog_number_input.xml
new file mode 100644
index 0000000..284ecce
--- /dev/null
+++ b/android/app/src/main/res/layout/dialog_number_input.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/nav_header.xml b/android/app/src/main/res/layout/nav_header.xml
index 6f0d014..a3c17a6 100644
--- a/android/app/src/main/res/layout/nav_header.xml
+++ b/android/app/src/main/res/layout/nav_header.xml
@@ -172,6 +172,24 @@
android:text="Gyro sensitivity: Normal"
style="?attr/materialButtonElevatedStyle" />
+
+
+
+
+