android: 设置页+任务巡检解析器(sqlite/xml) 阶段6a
This commit is contained in:
@@ -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 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
|
||||
@@ -44,9 +45,9 @@ fun AppRoot() {
|
||||
val content: @Composable () -> Unit = {
|
||||
when (tab) {
|
||||
0 -> LiveScreen()
|
||||
1 -> Placeholder("媒体库(阶段4实现)")
|
||||
2 -> Placeholder("MDT 离线分析(阶段5实现)")
|
||||
else -> Placeholder("设置(阶段6实现)")
|
||||
1 -> Placeholder("媒体库(阶段4实现)")
|
||||
2 -> Placeholder("MDT 离线分析(阶段5实现)")
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user