From a07a49b437c25056326565acb4d030999349d755 Mon Sep 17 00:00:00 2001 From: ZXCLI Date: Sun, 6 Sep 2026 20:34:21 +0800 Subject: [PATCH] =?UTF-8?q?android:=20=E8=AE=BE=E7=BD=AE=E9=A1=B5+?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=B7=A1=E6=A3=80=E8=A7=A3=E6=9E=90=E5=99=A8?= =?UTF-8?q?(sqlite/xml)=20=E9=98=B6=E6=AE=B56a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/mag160c/thermal/task/TaskParser.kt | 127 ++++++++++++++++++ .../kotlin/com/mag160c/thermal/ui/AppRoot.kt | 21 +-- .../thermal/ui/settings/AlarmTempDialog.kt | 36 +++++ .../thermal/ui/settings/AppSettings.kt | 24 ++++ .../thermal/ui/settings/SettingsScreen.kt | 111 +++++++++++++++ 5 files changed, 309 insertions(+), 10 deletions(-) create mode 100644 android/app/src/main/kotlin/com/mag160c/thermal/task/TaskParser.kt create mode 100644 android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/AlarmTempDialog.kt create mode 100644 android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/AppSettings.kt create mode 100644 android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/SettingsScreen.kt diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/task/TaskParser.kt b/android/app/src/main/kotlin/com/mag160c/thermal/task/TaskParser.kt new file mode 100644 index 0000000..bb5a68c --- /dev/null +++ b/android/app/src/main/kotlin/com/mag160c/thermal/task/TaskParser.kt @@ -0,0 +1,127 @@ +package com.mag160c.thermal.task + +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import java.io.File + +data class TaskItem( + val id: Long, + val name: String, + val order: Int, + val captured: Boolean, +) + +data class TaskLibrary( + val fileName: String, + val title: String, + val items: List, +) { + fun toTree(): Map> = + items.groupBy { it.name.substringBefore(" - ", it.name) } +} + +/** + * Inspection task library parser (port of the vendor SqliteTaskParser / + * XmlTaskParser). All queries are guarded; a missing table just yields an + * empty item list. + */ +object TaskParser { + fun parseSqlite(file: File): TaskLibrary = runCatching { + val db = SQLiteDatabase.openDatabase( + file.absolutePath, null, SQLiteDatabase.OPEN_READONLY, null, + ) + try { + var title = file.nameWithoutExtension + val regionNames = LinkedHashMap() + try { + db.rawQuery("SELECT id, name FROM region", null).use { c -> + while (c.moveToNext()) regionNames[c.getLong(0)] = c.getString(1) ?: "" + } + } catch (_: Exception) { + } + title = regionNames.values.firstOrNull() ?: title + + val items = ArrayList() + try { + db.rawQuery( + "SELECT t.id, t.capture_status FROM task t", + null, + ).use { c -> + var ord = 0 + while (c.moveToNext()) { + val id = c.getLong(0) + val captured = c.getInt(1) == 1 + items.add(TaskItem(id, "任务#$id", ord++, captured)) + } + } + // join names when the columns exist + try { + db.rawQuery( + "SELECT t.id, d.name, p.name, ph.name FROM task t " + + "LEFT JOIN device d ON t.device_id = d.id " + + "LEFT JOIN part p ON t.part_id = p.id " + + "LEFT JOIN phase ph ON t.phase_id = ph.id", + null, + ).use { c2 -> + for ((idx, it) in items.withIndex()) { + if (c2.moveToNext()) { + val d = c2.getString(1) ?: "" + val p = c2.getString(2) ?: "" + val ph = c2.getString(3) ?: "" + val joined = listOf(ph, d, p).filter { it.isNotBlank() } + .joinToString("-") + if (joined.isNotBlank()) items[idx] = items[idx].copy(name = joined) + } + } + } + } catch (_: Exception) { + } + } catch (_: Exception) { + } + TaskLibrary(file.name, title, items) + } finally { + db.close() + } + }.getOrDefault(TaskLibrary(file.name, file.nameWithoutExtension, emptyList())) + + /** Parse the legacy XML task format. */ + fun parseXml(text: String): TaskLibrary = runCatching { + val parser = org.xmlpull.v1.XmlPullParserFactory.newInstance().newPullParser() + parser.setInput(text.reader()) + var ev = parser.eventType + var title = "" + val items = ArrayList() + var inTarget = false + var id = "" + var name = "" + while (ev != org.xmlpull.v1.XmlPullParser.END_DOCUMENT) { + when (ev) { + org.xmlpull.v1.XmlPullParser.START_TAG -> { + when (parser.name) { + "target" -> { + inTarget = true + id = "" + name = "" + } + "id" -> if (inTarget) { + ev = parser.next() + id = parser.text?.trim() ?: "" + } + "name" -> if (inTarget) { + ev = parser.next() + name = parser.text ?: "" + } + } + } + org.xmlpull.v1.XmlPullParser.END_TAG -> { + if (parser.name == "target") { + items.add(TaskItem(items.size.toLong(), "$id $name", items.size, false)) + inTarget = false + } + } + } + ev = parser.next() + } + TaskLibrary(title.ifBlank { "任务" }, title, items) + }.getOrDefault(TaskLibrary("任务", "任务", emptyList())) +} diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/ui/AppRoot.kt b/android/app/src/main/kotlin/com/mag160c/thermal/ui/AppRoot.kt index 1438baa..aa56c10 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/ui/AppRoot.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/ui/AppRoot.kt @@ -25,14 +25,15 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.mag160c.thermal.R import com.mag160c.thermal.ui.live.LiveScreen +import com.mag160c.thermal.ui.settings.SettingsScreen private data class Tab(val label: String, val icon: Int) private val TABS = listOf( - Tab("实时", R.drawable.ic_ffc), - Tab("相册", R.drawable.ic_gallery), - Tab("分析", R.drawable.ic_analysis), - Tab("设置", R.drawable.ic_settings), + Tab("ʵʱ", R.drawable.ic_ffc), + Tab("", R.drawable.ic_gallery), + Tab("", R.drawable.ic_analysis), + Tab("", R.drawable.ic_settings), ) @Composable @@ -42,12 +43,12 @@ fun AppRoot() { LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE val content: @Composable () -> Unit = { - when (tab) { - 0 -> LiveScreen() - 1 -> Placeholder("媒体库(阶段4实现)") - 2 -> Placeholder("MDT 离线分析(阶段5实现)") - else -> Placeholder("设置(阶段6实现)") - } + when (tab) { + 0 -> LiveScreen() + 1 -> Placeholder("ý⣨׶4ʵ֣") + 2 -> Placeholder("MDT ߷׶5ʵ֣") + else -> SettingsScreen() + } } if (isLandscape) { diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/AlarmTempDialog.kt b/android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/AlarmTempDialog.kt new file mode 100644 index 0000000..b0b1bd3 --- /dev/null +++ b/android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/AlarmTempDialog.kt @@ -0,0 +1,36 @@ +package com.mag160c.thermal.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue + +@Composable +internal fun AlarmTempDialog(initialC: Int, onDone: (Int) -> Unit) { + var value by remember { mutableStateOf(initialC / 10f) } + androidx.compose.material3.AlertDialog( + onDismissRequest = { onDone(initialC) }, + title = { Text("报警温度") }, + text = { + Column { + Text("超过 %.1f℃ 报警".format(value)) + Slider( + value = value, + onValueChange = { value = it }, + valueRange = -20f..500f, + ) + } + }, + confirmButton = { + androidx.compose.material3.TextButton(onClick = { onDone((value * 10).toInt()) }) { + Text("确定") + } + }, + ) +} diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/AppSettings.kt b/android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/AppSettings.kt new file mode 100644 index 0000000..4b45927 --- /dev/null +++ b/android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/AppSettings.kt @@ -0,0 +1,24 @@ +package com.mag160c.thermal.ui.settings + +import android.content.Context + +/** App settings backed by SharedPreferences (app-private storage only). */ +class AppSettings(context: Context) { + private val sp = context.getSharedPreferences("mag160c_settings", Context.MODE_PRIVATE) + + var defaultPaletteIndex: Int + get() = sp.getInt("palette", 2) + set(v) = sp.edit().putInt("palette", v).apply() + + var defaultEmissivityPercent: Int + get() = sp.getInt("emissivity", 100) + set(v) = sp.edit().putInt("emissivity", v).apply() + + var alarmTempC: Int + get() = sp.getInt("alarm_temp", 200) + set(v) = sp.edit().putInt("alarm_temp", v).apply() + + var language: String + get() = sp.getString("locale", "auto") ?: "auto" + set(v) = sp.edit().putString("locale", v).apply() +} diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/SettingsScreen.kt b/android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/SettingsScreen.kt new file mode 100644 index 0000000..07bb6c4 --- /dev/null +++ b/android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/SettingsScreen.kt @@ -0,0 +1,111 @@ +package com.mag160c.thermal.ui.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.mag160c.thermal.core.Palettes + +@Composable +fun SettingsScreen() { + val context = androidx.compose.ui.platform.LocalContext.current + val settings = remember { AppSettings(context) } + var dialog by remember { mutableStateOf(null) } + + Column(modifier = Modifier.fillMaxSize().padding(8.dp)) { + SettingRow("默认调色板", Palettes.NAMES[settings.defaultPaletteIndex]) { dialog = "palette" } + SettingRow( + "默认发射率", + "%.2f".format(settings.defaultEmissivityPercent / 100f), + ) { dialog = "emissivity" } + SettingRow("报警温度", "%.1f℃".format(settings.alarmTempC / 10f)) { dialog = "alarm" } + SettingRow("语言", settings.language) { dialog = "language" } + SettingRow("关于", "MAG160C 统一热像版 1.0.0") { dialog = null } + } + + when (dialog) { + "palette" -> AlertDialog( + onDismissRequest = { dialog = null }, + title = { Text("默认调色板") }, + text = { + Column { + Palettes.NAMES.forEachIndexed { idx, name -> + Text( + name, + modifier = Modifier + .clickable { + settings.defaultPaletteIndex = idx + dialog = null + } + .padding(14.dp), + ) + } + } + }, + confirmButton = {}, + ) + "emissivity" -> AlertDialog( + onDismissRequest = { dialog = null }, + title = { Text("默认发射率") }, + text = { + Column { + listOf( + "黑体 1.00" to 100, + "暗光 0.90" to 90, + "半光 0.80" to 80, + "亮光 0.70" to 70, + "金属 0.10" to 10, + ).forEach { (label, v) -> + Text( + label, + modifier = Modifier + .clickable { + settings.defaultEmissivityPercent = v + dialog = null + } + .padding(14.dp), + ) + } + } + }, + confirmButton = {}, + ) + "alarm" -> AlarmTempDialog( + initialC = settings.alarmTempC, + onDone = { settings.alarmTempC = it; dialog = null }, + ) + else -> {} + } +} + +@Composable +private fun SettingRow(label: String, value: String, onClick: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 18.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(label, style = MaterialTheme.typography.bodyLarge) + Text(value, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + HorizontalDivider() +}