Rewrite Magisk log tab with itemized parsed entries

Parse logcat-format lines into structured MagiskLogEntry objects with
timestamp, level, tag, PID/TID, and message. Display each entry as a
card with a colored log level badge, tag, timestamp, and expandable
message text instead of dumping raw text.

Made-with: Cursor
This commit is contained in:
LoveSy
2026-03-03 14:45:53 +08:00
parent 83b5f8757b
commit b9d60d3de1
3 changed files with 172 additions and 18 deletions

View File

@@ -1,6 +1,7 @@
package com.topjohnwu.magisk.ui.log
import androidx.compose.foundation.horizontalScroll
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.PaddingValues
@@ -11,23 +12,30 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.topjohnwu.magisk.core.ktx.timeDateFormat
@@ -88,7 +96,7 @@ fun LogScreen(viewModel: LogViewModel) {
nestedScrollConnection = scrollBehavior.nestedScrollConnection
)
1 -> MagiskLogTab(
log = uiState.magiskLog,
entries = uiState.magiskLogEntries,
onSave = { viewModel.saveMagiskLog() },
onClear = { viewModel.clearMagiskLog() },
nestedScrollConnection = scrollBehavior.nestedScrollConnection
@@ -202,9 +210,14 @@ private fun SuLogCard(log: SuLog) {
}
@Composable
private fun MagiskLogTab(log: String, onSave: () -> Unit, onClear: () -> Unit, nestedScrollConnection: NestedScrollConnection) {
private fun MagiskLogTab(
entries: List<MagiskLogEntry>,
onSave: () -> Unit,
onClear: () -> Unit,
nestedScrollConnection: NestedScrollConnection
) {
Column(modifier = Modifier.fillMaxSize()) {
if (log.isBlank()) {
if (entries.isEmpty()) {
Box(
modifier = Modifier
.weight(1f)
@@ -218,22 +231,21 @@ private fun MagiskLogTab(log: String, onSave: () -> Unit, onClear: () -> Unit, n
)
}
} else {
Box(
val listState = rememberLazyListState(initialFirstVisibleItemIndex = entries.size - 1)
LazyColumn(
state = listState,
modifier = Modifier
.weight(1f)
.nestedScroll(nestedScrollConnection)
.horizontalScroll(rememberScrollState())
.verticalScroll(rememberScrollState())
.padding(12.dp)
.padding(bottom = 76.dp)
.padding(horizontal = 12.dp),
contentPadding = PaddingValues(bottom = 88.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = log,
fontFamily = FontFamily.Monospace,
fontSize = 12.sp,
lineHeight = 16.sp,
color = MiuixTheme.colorScheme.onSurface
)
item { Spacer(Modifier.height(4.dp)) }
items(entries.size, key = { it }) { index ->
MagiskLogCard(entries[index])
}
item { Spacer(Modifier.height(4.dp)) }
}
}
@@ -254,3 +266,86 @@ private fun MagiskLogTab(log: String, onSave: () -> Unit, onClear: () -> Unit, n
}
}
}
@Composable
private fun MagiskLogCard(entry: MagiskLogEntry) {
var expanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { expanded = !expanded }
) {
Column(modifier = Modifier.padding(12.dp)) {
if (entry.isParsed) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.weight(1f)
) {
LogLevelBadge(entry.level)
Text(
text = entry.tag,
style = MiuixTheme.textStyles.body1,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Spacer(Modifier.width(8.dp))
Text(
text = entry.timestamp,
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
maxLines = 1,
)
}
Spacer(Modifier.height(4.dp))
}
Text(
text = entry.message,
fontFamily = FontFamily.Monospace,
fontSize = 12.sp,
lineHeight = 16.sp,
color = MiuixTheme.colorScheme.onSurface,
maxLines = if (expanded) Int.MAX_VALUE else 3,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
private fun LogLevelBadge(level: Char) {
val (bg, fg) = when (level) {
'V' -> Color(0xFF9E9E9E) to Color.White
'D' -> Color(0xFF2196F3) to Color.White
'I' -> Color(0xFF4CAF50) to Color.White
'W' -> Color(0xFFFFC107) to Color.Black
'E' -> Color(0xFFF44336) to Color.White
'F' -> Color(0xFF9C27B0) to Color.White
else -> Color(0xFF757575) to Color.White
}
Box(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.background(bg)
.padding(horizontal = 5.dp, vertical = 1.dp),
contentAlignment = Alignment.Center
) {
Text(
text = level.toString(),
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
color = fg,
)
}
}

View File

@@ -29,6 +29,7 @@ class LogViewModel(
data class UiState(
val loading: Boolean = true,
val magiskLog: String = "",
val magiskLogEntries: List<MagiskLogEntry> = emptyList(),
val suLogs: List<SuLog> = emptyList(),
)
@@ -42,9 +43,11 @@ class LogViewModel(
withContext(Dispatchers.Default) {
magiskLogRaw = repo.fetchMagiskLogs()
val suLogs = repo.fetchSuLogs()
val entries = MagiskLogParser.parse(magiskLogRaw)
_uiState.update { it.copy(
loading = false,
magiskLog = magiskLogRaw,
magiskLogEntries = entries,
suLogs = suLogs,
) }
}

View File

@@ -0,0 +1,56 @@
package com.topjohnwu.magisk.ui.log
data class MagiskLogEntry(
val timestamp: String = "",
val pid: Int = 0,
val tid: Int = 0,
val level: Char = 'I',
val tag: String = "",
val message: String = "",
val isParsed: Boolean = false,
)
object MagiskLogParser {
// Logcat format: "MM-DD HH:MM:SS.mmm PID TID LEVEL TAG : message"
private val logcatRegex = Regex(
"""(\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})\s+(\d+)\s+(\d+)\s+([VDIWEF])\s+(.+?)\s*:\s+(.*)"""
)
fun parse(raw: String): List<MagiskLogEntry> {
if (raw.isBlank()) return emptyList()
val lines = raw.lines()
val result = mutableListOf<MagiskLogEntry>()
for (line in lines) {
if (line.isBlank()) continue
val match = logcatRegex.find(line)
if (match != null) {
result.add(
MagiskLogEntry(
timestamp = match.groupValues[1],
pid = match.groupValues[2].toIntOrNull() ?: 0,
tid = match.groupValues[3].toIntOrNull() ?: 0,
level = match.groupValues[4].firstOrNull() ?: 'I',
tag = match.groupValues[5].trim(),
message = match.groupValues[6],
isParsed = true,
)
)
} else if (result.isNotEmpty() && result.last().isParsed) {
// Continuation line — append to previous entry
val prev = result.last()
result[result.lastIndex] = prev.copy(
message = prev.message + "\n" + line.trimEnd()
)
} else {
result.add(
MagiskLogEntry(message = line.trimEnd())
)
}
}
return result
}
}