android: 设置页+任务巡检解析器(sqlite/xml) 阶段6a

This commit is contained in:
ZXCLI
2026-09-06 20:34:21 +08:00
parent 3bca426c99
commit a07a49b437
5 changed files with 309 additions and 10 deletions
@@ -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<TaskItem>,
) {
fun toTree(): Map<String, List<TaskItem>> =
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<Long, String>()
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<TaskItem>()
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<TaskItem>()
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()))
}
@@ -25,14 +25,15 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.mag160c.thermal.R import com.mag160c.thermal.R
import com.mag160c.thermal.ui.live.LiveScreen 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 data class Tab(val label: String, val icon: Int)
private val TABS = listOf( private val TABS = listOf(
Tab("实时", R.drawable.ic_ffc), Tab("实时", R.drawable.ic_ffc),
Tab("相册", R.drawable.ic_gallery), Tab("相册", R.drawable.ic_gallery),
Tab("分析", R.drawable.ic_analysis), Tab("分析", R.drawable.ic_analysis),
Tab("设置", R.drawable.ic_settings), Tab("设置", R.drawable.ic_settings),
) )
@Composable @Composable
@@ -44,9 +45,9 @@ fun AppRoot() {
val content: @Composable () -> Unit = { val content: @Composable () -> Unit = {
when (tab) { when (tab) {
0 -> LiveScreen() 0 -> LiveScreen()
1 -> Placeholder("媒体库(阶段4实现)") 1 -> Placeholder("媒体库(阶段4实现)")
2 -> Placeholder("MDT 离线分析(阶段5实现)") 2 -> Placeholder("MDT 离线分析(阶段5实现)")
else -> Placeholder("设置(阶段6实现)") else -> SettingsScreen()
} }
} }
@@ -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("确定")
}
},
)
}
@@ -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()
}
@@ -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<String?>(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()
}