Rework UI with drawer menu, hero disks, and sound toggle

This commit is contained in:
ml
2026-06-13 14:28:58 +02:00
parent 5a390aa733
commit 392bbeb996
4 changed files with 379 additions and 145 deletions
@@ -11,9 +11,15 @@ import android.util.Log
import android.widget.Button
import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.drawerlayout.widget.DrawerLayout
import fi.iki.elonen.NanoHTTPD
import java.io.File
import java.text.SimpleDateFormat
@@ -23,9 +29,13 @@ import java.util.Locale
private const val PORT = 8888
private const val TAG = "SuM"
private data class DiskSlot(val episode: Int, val part: Char, val btnId: Int) {
val fileName: String get() = "SCHWUM${episode}${part}.D64"
}
private data class DiskSlot(
val episode: Int,
val part: Char,
val btnId: Int,
val label: String,
val fileName: String = "SCHWUM${episode}${part}.D64"
)
class MainActivity : AppCompatActivity() {
@@ -39,6 +49,8 @@ class MainActivity : AppCompatActivity() {
private val mainHandler = Handler(Looper.getMainLooper())
private val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
private lateinit var drawerLayout: DrawerLayout
private var server: CompanionServer? = null
private val engine = C64Engine()
private var emuThread: Thread? = null
@@ -48,12 +60,20 @@ class MainActivity : AppCompatActivity() {
// ---- disk slots ---------------------------------------------------------
private val diskSlots = listOf(
DiskSlot(1, 'A', R.id.btn_ep_1a), DiskSlot(1, 'B', R.id.btn_ep_1b),
DiskSlot(2, 'A', R.id.btn_ep_2a), DiskSlot(2, 'B', R.id.btn_ep_2b),
DiskSlot(3, 'A', R.id.btn_ep_3a), DiskSlot(3, 'B', R.id.btn_ep_3b),
DiskSlot(4, 'A', R.id.btn_ep_4a), DiskSlot(4, 'B', R.id.btn_ep_4b),
DiskSlot(1, 'A', R.id.btn_ep_1a, "Folge 1: Das geheimnisvolle Kraut"),
DiskSlot(1, 'B', R.id.btn_ep_1b, "Folge 2: Der unheimliche Tempel"),
DiskSlot(2, 'A', R.id.btn_ep_2a, "Folge 3: Das Piratenhaus"),
DiskSlot(2, 'B', R.id.btn_ep_2b, "Folge 4: Die Burg des Magiers"),
DiskSlot(3, 'A', R.id.btn_ep_3a, "Folge 5: Das Haus des Vampirs"),
DiskSlot(3, 'B', R.id.btn_ep_3b, "Folge 6: Der Turm des Todes"),
DiskSlot(4, 'A', R.id.btn_ep_4a, "Folge 7: Unter Wasser"),
DiskSlot(4, 'B', R.id.btn_ep_4b, "Folge 8: Insel der Wunder"),
DiskSlot(0, ' ', R.id.btn_hero_1, "Held 1", "HELD1.D64"),
DiskSlot(0, ' ', R.id.btn_hero_2, "Held 2", "HELD2.D64"),
DiskSlot(0, ' ', R.id.btn_hero_3, "Held 3", "HELD3.D64"),
)
private val episodeButtons = mutableMapOf<DiskSlot, Button>()
private val loadedADisks = mutableSetOf<Int>()
// ---- Disk import (multi-select) -----------------------------------------
@@ -83,6 +103,18 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
drawerLayout = findViewById(R.id.drawer_layout)
// On Android 15+ the app draws behind the status bar (edge-to-edge enforced).
// Shift the main content down by the actual status bar height so the
// hamburger button isn't hidden underneath it.
val mainContent = findViewById<android.view.View>(R.id.main_content)
ViewCompat.setOnApplyWindowInsetsListener(mainContent) { view, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val pad8 = (8 * resources.displayMetrics.density).toInt()
view.setPadding(pad8, bars.top + pad8, pad8, pad8)
insets
}
tvStatus = findViewById(R.id.tv_status)
tvTime = findViewById(R.id.tv_time)
tvLog = findViewById(R.id.tv_log)
@@ -90,6 +122,13 @@ class MainActivity : AppCompatActivity() {
display = findViewById(R.id.c64_display)
keyboard = findViewById(R.id.c64_keyboard)
findViewById<Button>(R.id.btn_menu).setOnClickListener {
if (drawerLayout.isDrawerOpen(GravityCompat.START))
drawerLayout.closeDrawer(GravityCompat.START)
else
drawerLayout.openDrawer(GravityCompat.START)
}
findViewById<Button>(R.id.btn_import_disks).setOnClickListener {
launchPicker(diskPickerLauncher, "Select SCHWUM disk images", multiSelect = true)
}
@@ -107,6 +146,15 @@ class MainActivity : AppCompatActivity() {
btn.setOnClickListener { loadDiskSlot(slot) }
}
val neuButtons = mapOf(
R.id.btn_hero_1_new to diskSlots.first { it.fileName == "HELD1.D64" },
R.id.btn_hero_2_new to diskSlots.first { it.fileName == "HELD2.D64" },
R.id.btn_hero_3_new to diskSlots.first { it.fileName == "HELD3.D64" },
)
for ((btnId, slot) in neuButtons) {
findViewById<Button>(btnId).setOnClickListener { confirmCreateHeroDisk(slot) }
}
initEmulator()
refreshDiskButtons()
startHttpServer()
@@ -201,12 +249,21 @@ class MainActivity : AppCompatActivity() {
}
private fun loadDiskSlot(slot: DiskSlot) {
if (slot.part == 'B' && slot.episode !in loadedADisks) {
val aLabel = diskSlots.first { it.episode == slot.episode && it.part == 'A' }.label
val msg = "Load $aLabel first — ${slot.label} continues from where it left off"
Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
appendLog(msg)
return
}
val file = diskFile(slot) ?: run {
appendLog("Disk not found: ${slot.fileName}")
return
}
val ok = engine.loadDisk(file.absolutePath)
appendLog(if (ok) "Loaded: ${slot.fileName}" else "Load failed: ${slot.fileName}")
if (ok && slot.part == 'A') loadedADisks.add(slot.episode)
appendLog(if (ok) "Loaded: ${slot.label}" else "Load failed: ${slot.fileName}")
if (ok) drawerLayout.closeDrawer(GravityCompat.START)
}
// ---- file import helpers ------------------------------------------------
@@ -238,6 +295,99 @@ class MainActivity : AppCompatActivity() {
contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)
?.use { cursor -> if (cursor.moveToFirst()) cursor.getString(0) else null }
// ---- hero disk creation -------------------------------------------------
private fun confirmCreateHeroDisk(slot: DiskSlot) {
val assetDir = getExternalFilesDir(null) ?: filesDir
val dest = File(assetDir, slot.fileName)
if (dest.exists()) {
AlertDialog.Builder(this)
.setTitle("Diskette ersetzen?")
.setMessage("${slot.label} existiert bereits. Alle gespeicherten Daten gehen verloren.")
.setPositiveButton("Ersetzen") { _, _ -> writeHeroDisk(slot) }
.setNegativeButton("Abbrechen", null)
.show()
} else {
writeHeroDisk(slot)
}
}
private fun writeHeroDisk(slot: DiskSlot) {
val assetDir = getExternalFilesDir(null) ?: filesDir
val dest = File(assetDir, slot.fileName)
try {
dest.writeBytes(createBlankD64(slot.label))
refreshDiskButtons()
appendLog("Erstellt: ${slot.label}")
} catch (e: Exception) {
appendLog("Fehler beim Erstellen: ${slot.label}")
Log.e(TAG, "createHeroDisk failed", e)
}
}
/** Returns a 174 848-byte blank formatted 1541 D64 image with [diskLabel] as the disk name. */
private fun createBlankD64(diskLabel: String): ByteArray {
// Sectors per track for a standard 35-track 1541 disk
val sectorsPerTrack = intArrayOf(
0, // index 0 unused
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, // 1-17
19, // 18 (directory)
19, 19, 19, 19, 19, 19, // 19-24
18, 18, 18, 18, 18, 18, // 25-30
17, 17, 17, 17, 17 // 31-35
)
val data = ByteArray(174848)
fun offset(track: Int, sector: Int): Int {
var off = 0
for (t in 1 until track) off += sectorsPerTrack[t] * 256
return off + sector * 256
}
// BAM sector — track 18, sector 0
val bam = offset(18, 0)
data[bam + 0] = 18 // first directory track
data[bam + 1] = 1 // first directory sector
data[bam + 2] = 0x41 // DOS version 'A'
data[bam + 3] = 0x00
for (t in 1..35) {
val entry = bam + 4 + (t - 1) * 4
val sectors = sectorsPerTrack[t]
// All sectors free; track 18 has sectors 0 (BAM) and 1 (dir) pre-allocated
var mask = (1 shl sectors) - 1
var free = sectors
if (t == 18) {
mask = mask and (1 shl 0).inv() and (1 shl 1).inv()
free -= 2
}
data[entry + 0] = free.toByte()
data[entry + 1] = (mask and 0xFF).toByte()
data[entry + 2] = ((mask shr 8) and 0xFF).toByte()
data[entry + 3] = ((mask shr 16) and 0xFF).toByte()
}
// Disk name — 16 PETSCII bytes padded with 0xA0
val name = diskLabel.uppercase().toByteArray(Charsets.ISO_8859_1)
for (i in 0..15) data[bam + 144 + i] = if (i < name.size) name[i] else 0xA0.toByte()
data[bam + 160] = 0xA0.toByte()
data[bam + 161] = 0xA0.toByte()
data[bam + 162] = 'H'.code.toByte() // disk ID byte 1
data[bam + 163] = 'D'.code.toByte() // disk ID byte 2
data[bam + 164] = 0xA0.toByte()
data[bam + 165] = '2'.code.toByte() // DOS type "2A"
data[bam + 166] = 'A'.code.toByte()
for (i in 167..255) data[bam + i] = 0xA0.toByte()
// First directory sector — track 18, sector 1
val dir = offset(18, 1)
data[dir + 0] = 0x00 // no next track (end of chain)
data[dir + 1] = 0xFF.toByte()
return data
}
// ---- helpers ------------------------------------------------------------
private fun appendLog(entry: String) {
@@ -246,6 +396,13 @@ class MainActivity : AppCompatActivity() {
scrollLog.post { scrollLog.scrollTo(0, 0) }
}
override fun onBackPressed() {
if (drawerLayout.isDrawerOpen(GravityCompat.START))
drawerLayout.closeDrawer(GravityCompat.START)
else
super.onBackPressed()
}
override fun onDestroy() {
emuRunning = false
emuThread?.join(500)
@@ -1,5 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- ── Main content ─────────────────────────────────────────────────── -->
<LinearLayout
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@@ -10,13 +18,25 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:orientation="horizontal"
android:gravity="center_vertical">
<Button
android:id="@+id/btn_menu"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="36dp"
android:layout_height="36dp"
android:minWidth="0dp"
android:padding="0dp"
android:text="☰"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Initialising…"
android:textColor="#AAFFAA"
android:textSize="12sp" />
@@ -30,91 +50,7 @@
android:textSize="12sp" />
</LinearLayout>
<!-- Import disk images + sound toggle -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="horizontal">
<Button
android:id="@+id/btn_import_disks"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="2dp"
android:text="Import Disks"
android:textSize="11sp"
style="?attr/materialButtonOutlinedStyle" />
<Button
android:id="@+id/btn_sound"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginStart="2dp"
android:text="Sound: OFF"
android:textSize="11sp"
style="?attr/materialButtonOutlinedStyle" />
</LinearLayout>
<!-- Episode selector — row A (parts 1A4A), row B (parts 1B4B) -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Load episode:"
android:textColor="#888888"
android:textSize="11sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button android:id="@+id/btn_ep_1a" style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
android:layout_marginEnd="1dp" android:text="1A" android:textSize="11sp"
android:enabled="false" />
<Button android:id="@+id/btn_ep_2a" style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:text="2A" android:textSize="11sp"
android:enabled="false" />
<Button android:id="@+id/btn_ep_3a" style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:text="3A" android:textSize="11sp"
android:enabled="false" />
<Button android:id="@+id/btn_ep_4a" style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
android:layout_marginStart="1dp" android:text="4A" android:textSize="11sp"
android:enabled="false" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="2dp">
<Button android:id="@+id/btn_ep_1b" style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
android:layout_marginEnd="1dp" android:text="1B" android:textSize="11sp"
android:enabled="false" />
<Button android:id="@+id/btn_ep_2b" style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:text="2B" android:textSize="11sp"
android:enabled="false" />
<Button android:id="@+id/btn_ep_3b" style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:text="3B" android:textSize="11sp"
android:enabled="false" />
<Button android:id="@+id/btn_ep_4b" style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
android:layout_marginStart="1dp" android:text="4B" android:textSize="11sp"
android:enabled="false" />
</LinearLayout>
<!-- C64 display (320×200, scales to fill width, capped at 40 % of screen height) -->
<!-- C64 display (320×200, scaled with correct aspect ratio) -->
<de.ladkau.schwertundmagieonpebblecompanionapp.C64DisplayView
android:id="@+id/c64_display"
android:layout_width="match_parent"
@@ -156,4 +92,145 @@
android:textSize="11sp" />
</ScrollView>
</LinearLayout>
</LinearLayout>
<!-- ── Drawer ────────────────────────────────────────────────────────── -->
<LinearLayout
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:orientation="vertical"
android:background="#1A1A2E"
android:padding="12dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Schwert und Magie"
android:textColor="#AAFFAA"
android:textSize="14sp"
android:textStyle="bold"
android:paddingBottom="8dp" />
<Button android:id="@+id/btn_ep_1a" style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_marginBottom="2dp" android:gravity="center_vertical|start"
android:text="Folge 1: Das geheimnisvolle Kraut" android:textSize="12sp"
android:enabled="false" />
<Button android:id="@+id/btn_ep_1b" style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_marginBottom="2dp" android:gravity="center_vertical|start"
android:text="Folge 2: Der unheimliche Tempel" android:textSize="12sp"
android:enabled="false" />
<Button android:id="@+id/btn_ep_2a" style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_marginBottom="2dp" android:gravity="center_vertical|start"
android:text="Folge 3: Das Piratenhaus" android:textSize="12sp"
android:enabled="false" />
<Button android:id="@+id/btn_ep_2b" style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_marginBottom="2dp" android:gravity="center_vertical|start"
android:text="Folge 4: Die Burg des Magiers" android:textSize="12sp"
android:enabled="false" />
<Button android:id="@+id/btn_ep_3a" style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_marginBottom="2dp" android:gravity="center_vertical|start"
android:text="Folge 5: Das Haus des Vampirs" android:textSize="12sp"
android:enabled="false" />
<Button android:id="@+id/btn_ep_3b" style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_marginBottom="2dp" android:gravity="center_vertical|start"
android:text="Folge 6: Der Turm des Todes" android:textSize="12sp"
android:enabled="false" />
<Button android:id="@+id/btn_ep_4a" style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_marginBottom="2dp" android:gravity="center_vertical|start"
android:text="Folge 7: Unter Wasser" android:textSize="12sp"
android:enabled="false" />
<Button android:id="@+id/btn_ep_4b" style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:gravity="center_vertical|start"
android:text="Folge 8: Insel der Wunder" android:textSize="12sp"
android:enabled="false" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#333355"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Helden-Disketten"
android:textColor="#AAFFAA"
android:textSize="12sp"
android:textStyle="bold"
android:paddingBottom="4dp" />
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="horizontal" android:layout_marginBottom="2dp">
<Button android:id="@+id/btn_hero_1" style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp" android:layout_height="wrap_content"
android:layout_weight="1" android:gravity="center_vertical|start"
android:text="Held 1" android:textSize="12sp" android:enabled="false" />
<Button android:id="@+id/btn_hero_1_new" style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:text="Neu" android:textSize="11sp" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="horizontal" android:layout_marginBottom="2dp">
<Button android:id="@+id/btn_hero_2" style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp" android:layout_height="wrap_content"
android:layout_weight="1" android:gravity="center_vertical|start"
android:text="Held 2" android:textSize="12sp" android:enabled="false" />
<Button android:id="@+id/btn_hero_2_new" style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:text="Neu" android:textSize="11sp" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="horizontal" android:layout_marginBottom="8dp">
<Button android:id="@+id/btn_hero_3" style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp" android:layout_height="wrap_content"
android:layout_weight="1" android:gravity="center_vertical|start"
android:text="Held 3" android:textSize="12sp" android:enabled="false" />
<Button android:id="@+id/btn_hero_3_new" style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:text="Neu" android:textSize="11sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#333355"
android:layout_marginBottom="8dp" />
<Button
android:id="@+id/btn_import_disks"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="Import Disks"
android:textSize="12sp" />
<Button
android:id="@+id/btn_sound"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sound: OFF"
android:textSize="12sp" />
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout>
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB