mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-03-12 21:23:02 -07:00
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:
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
) }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user