android: 媒体库+MDT离线分析查看器(缩放/调色板/备注) 阶段4-5a
This commit is contained in:
@@ -45,14 +45,14 @@ object Mdt {
|
||||
* @param jpg rendered image JPEG (offset 0, used as thumbnail + analysis base)
|
||||
* @param info0 camera info block from command 0x6BB6B66B (0x38B)
|
||||
* @param info1 second cached info block (0x38B) or null
|
||||
* @param rawFrame latest raw USB frame (0x38-byte header + 38400B pixels)
|
||||
* @param framePixels raw measurement frame (38400 bytes, 19200 x uint16 LE) or null
|
||||
* @param text UTF-8 note bytes or null
|
||||
*/
|
||||
fun compose(
|
||||
jpg: ByteArray,
|
||||
info0: ByteArray?,
|
||||
info1: ByteArray?,
|
||||
rawFrame: ByteArray?,
|
||||
framePixels: ByteArray?,
|
||||
text: ByteArray? = null,
|
||||
): ByteArray {
|
||||
val out = ByteArrayOutputStream(align4(jpg.size) + 0x88 + 38400 + 320)
|
||||
@@ -72,9 +72,8 @@ object Mdt {
|
||||
}
|
||||
info0?.let { emit(BLOCK_INFO0, it) }
|
||||
info1?.let { emit(BLOCK_INFO1, it) }
|
||||
val raw = rawFrame
|
||||
if (raw != null && raw.size >= 0x1C + 38400) {
|
||||
emit(BLOCK_FRAME, raw.copyOfRange(0x1C, 0x1C + 38400))
|
||||
if (framePixels != null && framePixels.size == 38400) {
|
||||
emit(BLOCK_FRAME, framePixels)
|
||||
}
|
||||
text?.let { emit(BLOCK_TXT, it) }
|
||||
|
||||
|
||||
@@ -25,9 +25,13 @@ object PhotoSaver {
|
||||
fun encodeJpeg(frame: IntArray, w: Int = 320, h: Int = 240, quality: Int = 92): ByteArray {
|
||||
val bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
bmp.setPixels(frame, 0, w, 0, 0, w, h)
|
||||
val out = ByteArrayOutputStream(w * h / 4)
|
||||
return encodeJpeg(bmp, quality)
|
||||
}
|
||||
|
||||
/** Encode an existing bitmap to JPEG bytes. */
|
||||
fun encodeJpeg(bmp: Bitmap, quality: Int = 92): ByteArray {
|
||||
val out = ByteArrayOutputStream(bmp.width * bmp.height / 4)
|
||||
bmp.compress(Bitmap.CompressFormat.JPEG, quality, out)
|
||||
bmp.recycle()
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.mag160c.thermal.ui.analyze
|
||||
|
||||
import android.app.Application
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.mag160c.thermal.media.Mdt
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Offline MDT analysis state: loaded container, palette re-render, probes.
|
||||
*/
|
||||
class AnalyzeViewModel(
|
||||
app: Application,
|
||||
private val containerBytes: ByteArray,
|
||||
private val fileUri: android.net.Uri,
|
||||
) : AndroidViewModel(app) {
|
||||
val parsed: Mdt.Parsed? = Mdt.parse(containerBytes)
|
||||
|
||||
/** Raw measurement frame (19200 uint16) if present. */
|
||||
val rawFrame: IntArray? by lazy {
|
||||
parsed?.frame?.let { raw ->
|
||||
val out = IntArray(19200)
|
||||
for (i in out.indices) {
|
||||
out[i] = (raw[i * 2].toInt() and 0xFF) or ((raw[i * 2 + 1].toInt() and 0xFF) shl 8)
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
private val _render = MutableStateFlow<Bitmap?>(null)
|
||||
val render: StateFlow<Bitmap?> = _render
|
||||
|
||||
private val _paletteIndex = MutableStateFlow(2)
|
||||
val paletteIndex: StateFlow<Int> = _paletteIndex
|
||||
|
||||
/** Re-render the raw frame with the given palette + auto window. */
|
||||
fun render(paletteIdx: Int) {
|
||||
val raw = rawFrame ?: return
|
||||
_paletteIndex.value = paletteIdx
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
var mn = Int.MAX_VALUE
|
||||
var mx = -1
|
||||
for (v in raw) {
|
||||
if (v < mn) mn = v
|
||||
if (v > mx) mx = v
|
||||
}
|
||||
if (mx <= mn) mx = mn + 1
|
||||
val pal = com.mag160c.thermal.core.Palettes.buildAll()[paletteIdx.coerceIn(0, 11)]
|
||||
val argb = IntArray(19200)
|
||||
val scale = (255 shl 12) / (mx - mn)
|
||||
for (i in argb.indices) {
|
||||
var g = ((raw[i] - mn) * scale) shr 8
|
||||
if (g < 0) g = 0 else if (g > 255) g = 255
|
||||
argb[i] = pal[g]
|
||||
}
|
||||
val bmp = Bitmap.createBitmap(160, 120, Bitmap.Config.ARGB_8888)
|
||||
bmp.setPixels(argb, 0, 160, 0, 0, 160, 120)
|
||||
withContext(Dispatchers.Main) { _render.value = bmp }
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeNote(): String? {
|
||||
val t = parsed?.text ?: return null
|
||||
return String(t, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
/** Probe temperature approximation at a raw pixel (millidegrees C). */
|
||||
fun probeTemp(x: Int, y: Int): Int? {
|
||||
val raw = rawFrame ?: return null
|
||||
if (x < 0 || y < 0 || x >= 160 || y >= 120) return null
|
||||
return com.mag160c.thermal.core.TempMath.countsToTempMc(raw[y * 160 + x])
|
||||
}
|
||||
|
||||
/** Save note: rewrite the container in place (jpg = current render). */
|
||||
fun saveNote(note: String, onDone: (Boolean) -> Unit) {
|
||||
val bmp = _render.value
|
||||
if (bmp == null) {
|
||||
onDone(false)
|
||||
return
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val jpg = com.mag160c.thermal.media.PhotoSaver.encodeJpeg(bmp)
|
||||
val mdt = Mdt.compose(
|
||||
jpg = jpg,
|
||||
info0 = parsed?.info0,
|
||||
info1 = parsed?.info1,
|
||||
framePixels = parsed?.frame,
|
||||
text = note.toByteArray(Charsets.UTF_8),
|
||||
)
|
||||
val ok = runCatching {
|
||||
val ctx = getApplication<Application>()
|
||||
ctx.contentResolver.openOutputStream(fileUri, "w")?.use { it.write(mdt) } != null
|
||||
}.getOrDefault(false)
|
||||
withContext(Dispatchers.Main) { onDone(ok) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.mag160c.thermal.ui.analyze
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mag160c.thermal.core.Palettes
|
||||
import com.mag160c.thermal.ui.gallery.GalleryViewModel
|
||||
|
||||
/**
|
||||
* Single-file MDT analysis viewer: pinch zoom/pan, palette re-render,
|
||||
* text note editing.
|
||||
*/
|
||||
@Composable
|
||||
fun AnalyzeViewer(item: GalleryViewModel.Item, galleryVm: GalleryViewModel) {
|
||||
val context = LocalContext.current
|
||||
val vm = remember(item.name) {
|
||||
val bytes = runCatching {
|
||||
context.contentResolver.openInputStream(item.uri)?.use { it.readBytes() }
|
||||
}.getOrNull() ?: ByteArray(0)
|
||||
AnalyzeViewModel(context.applicationContext as android.app.Application, bytes, item.uri)
|
||||
}
|
||||
val render by vm.render.collectAsState()
|
||||
val paletteIdx by vm.paletteIndex.collectAsState()
|
||||
var zoom by remember { mutableStateOf(1f) }
|
||||
var pan by remember { mutableStateOf(Offset.Zero) }
|
||||
var showNote by remember { mutableStateOf(false) }
|
||||
var note by remember { mutableStateOf(vm.decodeNote() ?: "") }
|
||||
|
||||
LaunchedEffect(Unit) { vm.render(2) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black),
|
||||
) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.aspectRatio(4f / 3f)
|
||||
.align(Alignment.Center)
|
||||
.pointerInput(Unit) {
|
||||
detectTransformGestures { _, gesturePan, gestureZoom, _ ->
|
||||
zoom = (zoom * gestureZoom).coerceIn(1f, 4f)
|
||||
pan += gesturePan
|
||||
if (zoom <= 1.01f) {
|
||||
zoom = 1f
|
||||
pan = Offset.Zero
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
val img = render?.asImageBitmap()
|
||||
if (img != null) {
|
||||
val w = size.width * zoom
|
||||
val h = size.height * zoom
|
||||
val left = (size.width - w) / 2 + pan.x
|
||||
val top = (size.height - h) / 2 + pan.y
|
||||
drawImage(
|
||||
image = img,
|
||||
dstOffset = androidx.compose.ui.unit.IntOffset(left.toInt(), top.toInt()),
|
||||
dstSize = IntSize(w.toInt(), h.toInt()),
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.background(Color(0x66000000))
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
Palettes.NAMES.forEachIndexed { idx, name ->
|
||||
Text(
|
||||
name,
|
||||
color = if (paletteIdx == idx) MaterialTheme.colorScheme.primary else Color.White,
|
||||
modifier = Modifier
|
||||
.clickable { vm.render(idx) }
|
||||
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = { showNote = true },
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Text("备注")
|
||||
}
|
||||
if (showNote) {
|
||||
NoteEditor(
|
||||
initial = note,
|
||||
onSave = {
|
||||
note = it
|
||||
vm.saveNote(it) { }
|
||||
showNote = false
|
||||
},
|
||||
onDismiss = { showNote = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun IntOffsetCompat(x: Float, y: Float): Offset = Offset(x, y)
|
||||
|
||||
@Composable
|
||||
private fun NoteEditor(initial: String, onSave: (String) -> Unit, onDismiss: () -> Unit) {
|
||||
var text by remember { mutableStateOf(initial) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("文字备注") },
|
||||
text = {
|
||||
OutlinedTextField(value = text, onValueChange = { text = it })
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onSave(text) }) { Text("保存") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text("取消") }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.mag160c.thermal.ui.gallery
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.mag160c.thermal.ui.analyze.AnalyzeViewer
|
||||
|
||||
@Composable
|
||||
fun GalleryScreen(vm: GalleryViewModel = viewModel()) {
|
||||
val items by vm.items.collectAsState()
|
||||
val context = LocalContext.current
|
||||
var showViewer by remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text("媒体库 (${items.size})", style = MaterialTheme.typography.titleMedium)
|
||||
Button(onClick = { vm.refresh() }) { Text("刷新") }
|
||||
}
|
||||
LazyVerticalGrid(columns = GridCells.Fixed(if (isLandscape()) 4 else 3)) {
|
||||
items(items.size) { idx ->
|
||||
val item = items[idx]
|
||||
var bmp by remember(item.name) { mutableStateOf<android.graphics.Bitmap?>(null) }
|
||||
LaunchedEffect(item.name) {
|
||||
vm.thumbnail(item) { b -> bmp = b }
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.aspectRatio(4f / 3f)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.clickable {
|
||||
vm.select(item)
|
||||
showViewer = true
|
||||
},
|
||||
) {
|
||||
val b = bmp
|
||||
if (b != null) {
|
||||
Image(
|
||||
bitmap = b.asImageBitmap(),
|
||||
contentDescription = item.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
item.name.removePrefix("MAG160C_"),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(4.dp)
|
||||
.background(Color(0x88000000)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showViewer) {
|
||||
val sel = vm.selected.value
|
||||
if (sel != null) {
|
||||
AnalyzeViewer(item = sel, galleryVm = vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun isLandscape(): Boolean =
|
||||
androidx.compose.ui.platform.LocalConfiguration.current.orientation ==
|
||||
android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.mag160c.thermal.ui.gallery
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ContentUris
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.provider.MediaStore
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.mag160c.thermal.media.Mdt
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Media library: lists MDT thermal files under DCIM/MAG160C via MediaStore
|
||||
* with async embedded-JPEG thumbnails.
|
||||
*/
|
||||
class GalleryViewModel(app: Application) : AndroidViewModel(app) {
|
||||
data class Item(
|
||||
val id: Long,
|
||||
val uri: android.net.Uri,
|
||||
val name: String,
|
||||
val size: Long,
|
||||
val dateMs: Long,
|
||||
)
|
||||
|
||||
private val _items = MutableStateFlow<List<Item>>(emptyList())
|
||||
val items: StateFlow<List<Item>> = _items
|
||||
|
||||
private val _selected = MutableStateFlow<Item?>(null)
|
||||
val selected: StateFlow<Item?> = _selected
|
||||
|
||||
private val thumbs = ConcurrentHashMap<String, Bitmap>()
|
||||
|
||||
init {
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val ctx = getApplication<Application>()
|
||||
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
val proj = arrayOf(
|
||||
MediaStore.MediaColumns._ID,
|
||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||
MediaStore.MediaColumns.SIZE,
|
||||
)
|
||||
val list = ArrayList<Item>()
|
||||
ctx.contentResolver.query(
|
||||
uri, proj,
|
||||
"${MediaStore.MediaColumns.RELATIVE_PATH} LIKE ?",
|
||||
arrayOf("%DCIM/MAG160C%"),
|
||||
"${MediaStore.MediaColumns.DATE_MODIFIED} DESC",
|
||||
)?.use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val id = c.getLong(0)
|
||||
val name = c.getString(1) ?: ""
|
||||
val date = c.getLong(2) * 1000
|
||||
val size = c.getLong(3)
|
||||
if (size < 152 + 136) continue
|
||||
list.add(Item(id, ContentUris.withAppendedId(uri, id), name, size, date))
|
||||
}
|
||||
}
|
||||
// validate MDT containers
|
||||
val valid = list.filter { isMdt(it) }
|
||||
withContext(Dispatchers.Main) { _items.value = valid }
|
||||
}
|
||||
}
|
||||
|
||||
private fun isMdt(item: Item): Boolean = runCatching {
|
||||
val ctx = getApplication<Application>()
|
||||
ctx.contentResolver.openInputStream(item.uri)?.use { s ->
|
||||
val buf = ByteArray(152)
|
||||
if (s.skip(item.size - 152) != item.size - 152) return false
|
||||
var off = 0
|
||||
while (off < 152) {
|
||||
val n = s.read(buf, off, 152 - off)
|
||||
if (n <= 0) return false
|
||||
off += n
|
||||
}
|
||||
Mdt.u32(buf, 0) == Mdt.SECTION_TAIL
|
||||
} ?: false
|
||||
}.getOrDefault(false)
|
||||
|
||||
/** Embedded-JPEG thumbnail (async). */
|
||||
fun thumbnail(item: Item, onReady: (Bitmap) -> Unit) {
|
||||
thumbs[item.name]?.let { onReady(it); return }
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val ctx = getApplication<Application>()
|
||||
val bmp = runCatching {
|
||||
ctx.contentResolver.openInputStream(item.uri)?.use { s ->
|
||||
val all = s.readBytes()
|
||||
val parsed = Mdt.parse(all)
|
||||
if (parsed != null) {
|
||||
BitmapFactory.decodeByteArray(parsed.jpg, 0, parsed.jpg.size)
|
||||
} else null
|
||||
}
|
||||
}.getOrNull()
|
||||
if (bmp != null) {
|
||||
thumbs[item.name] = bmp
|
||||
withContext(Dispatchers.Main) { onReady(bmp) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun select(item: Item?) {
|
||||
_selected.value = item
|
||||
}
|
||||
|
||||
fun delete(item: Item) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val ctx = getApplication<Application>()
|
||||
runCatching { ctx.contentResolver.delete(item.uri, null, null) }
|
||||
thumbs.remove(item.name)
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,11 +100,15 @@ class LiveViewModel(app: Application) : AndroidViewModel(app) {
|
||||
val frame = latestFrame ?: return
|
||||
val s = session
|
||||
val jpg = com.mag160c.thermal.media.PhotoSaver.encodeJpeg(frame)
|
||||
val rawFrame = s.lastRawFrame
|
||||
val pixels = if (rawFrame != null && rawFrame.size >= 0x1C + 38400) {
|
||||
rawFrame.copyOfRange(0x1C, 0x1C + 38400)
|
||||
} else null
|
||||
val mdt = com.mag160c.thermal.media.Mdt.compose(
|
||||
jpg = jpg,
|
||||
info0 = s.lastInfo0,
|
||||
info1 = s.lastInfo1,
|
||||
rawFrame = s.lastRawFrame,
|
||||
framePixels = pixels,
|
||||
)
|
||||
val saved = com.mag160c.thermal.media.PhotoSaver.saveMdt(
|
||||
context, mdt, com.mag160c.thermal.media.PhotoSaver.fileName(),
|
||||
|
||||
Reference in New Issue
Block a user