commit 03736d574f155f50b6c06e30f69bdb84827d5a6b
parent a249d41887937b40b2d3fb80372ab31c5c60bd31
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Wed, 27 Dec 2023 12:01:29 +0100

perf(app): log viewer

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt | 23++++++++++++++---------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
2 files changed, 90 insertions(+), 46 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt @@ -70,21 +70,26 @@ class LogReader( fun incrementLineCount() { randomAccessFile.seek(randomAccessFile.length()) - startLineIndexes.add(randomAccessFile.filePointer) + startLineIndexes.add(randomAccessFile.filePointer + 1) lineCount++ } private fun queryLineCount(): Int { randomAccessFile.seek(0) - var lines = 0 - var lastIndex: Long - while (true) { - lastIndex = randomAccessFile.filePointer - readLogLine() ?: break - startLineIndexes.add(lastIndex) - lines++ + var lineCount = 0 + var lastPointer: Long + var line: String? + + while (randomAccessFile.also { + lastPointer = it.filePointer + }.readLine().also { line = it } != null) { + if (line?.startsWith('|') == true) { + lineCount++ + startLineIndexes.add(lastPointer + 1) + } } - return lines + + return lineCount } private fun getLine(index: Int): String? { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt @@ -14,7 +14,6 @@ import androidx.compose.material.icons.outlined.BugReport import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Report import androidx.compose.material.icons.outlined.Warning -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -30,16 +29,20 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import me.rhunk.snapenhance.LogReader import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.common.logger.LogChannel import me.rhunk.snapenhance.common.logger.LogLevel +import me.rhunk.snapenhance.ui.util.pullrefresh.PullRefreshIndicator +import me.rhunk.snapenhance.ui.util.pullrefresh.rememberPullRefreshState class HomeSubSection( private val context: RemoteSideContext ) { - private lateinit var logListState: LazyListState + private val logListState by lazy { LazyListState(0) } @Composable fun LogsSection() { @@ -47,27 +50,66 @@ class HomeSubSection( val clipboardManager = LocalClipboardManager.current var lineCount by remember { mutableIntStateOf(0) } var logReader by remember { mutableStateOf<LogReader?>(null) } - logListState = remember { LazyListState(0) } + var isRefreshing by remember { mutableStateOf(false) } - Column( + fun refreshLogs() { + coroutineScope.launch(Dispatchers.IO) { + runCatching { + logReader = context.log.newReader { + lineCount++ + } + lineCount = logReader!!.lineCount + }.onFailure { + context.longToast("Failed to read logs!") + } + delay(300) + isRefreshing = false + withContext(Dispatchers.Main) { + logListState.scrollToItem((logListState.layoutInfo.totalItemsCount - 1).takeIf { it >= 0 } ?: return@withContext) + } + } + } + + val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh = { + refreshLogs() + }) + + LaunchedEffect(Unit) { + isRefreshing = true + refreshLogs() + } + + Box( modifier = Modifier .fillMaxSize() ) { LazyColumn( - modifier = Modifier.background(MaterialTheme.colorScheme.surface ) + modifier = Modifier + .background(MaterialTheme.colorScheme.surface) .horizontalScroll(ScrollState(0)), state = logListState ) { + item { + if (lineCount == 0 && logReader != null) { + Text( + text = "No logs found!", + modifier = Modifier.padding(16.dp), + fontSize = 12.sp, + fontWeight = FontWeight.Light + ) + } + } items(lineCount) { index -> - val line = logReader?.getLogLine(index) ?: return@items + val logLine = remember(index) { logReader?.getLogLine(index) } ?: return@items var expand by remember { mutableStateOf(false) } + Box(modifier = Modifier .fillMaxWidth() .pointerInput(Unit) { detectTapGestures( onLongPress = { coroutineScope.launch { - clipboardManager.setText(AnnotatedString(line.message)) + clipboardManager.setText(AnnotatedString(logLine.message)) } }, onTap = { @@ -85,7 +127,7 @@ class HomeSubSection( ) { if (!expand) { Icon( - imageVector = when (line.logLevel) { + imageVector = when (logLine.logLevel) { LogLevel.DEBUG -> Icons.Outlined.BugReport LogLevel.ERROR, LogLevel.ASSERT -> Icons.Outlined.Report LogLevel.INFO, LogLevel.VERBOSE -> Icons.Outlined.Info @@ -95,21 +137,21 @@ class HomeSubSection( ) Text( - text = LogChannel.fromChannel(line.tag)?.shortName ?: line.tag, + text = LogChannel.fromChannel(logLine.tag)?.shortName ?: logLine.tag, modifier = Modifier.padding(start = 4.dp), fontWeight = FontWeight.Light, fontSize = 10.sp, ) Text( - text = line.dateTime, + text = logLine.dateTime, modifier = Modifier.padding(start = 4.dp, end = 4.dp), fontSize = 10.sp ) } Text( - text = line.message.trimIndent(), + text = logLine.message.trimIndent(), fontSize = 10.sp, maxLines = if (expand) Int.MAX_VALUE else 6, overflow = if (expand) TextOverflow.Visible else TextOverflow.Ellipsis, @@ -120,22 +162,11 @@ class HomeSubSection( } } - if (logReader == null) { - CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally)) - } - - LaunchedEffect(Unit) { - coroutineScope.launch(Dispatchers.IO) { - runCatching { - logReader = context.log.newReader { - lineCount++ - } - lineCount = logReader!!.lineCount - }.onFailure { - context.longToast("Failed to read logs!") - } - } - } + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) } } @@ -145,19 +176,27 @@ class HomeSubSection( Column( verticalArrangement = Arrangement.spacedBy(5.dp), ) { - FilledIconButton(onClick = { - coroutineScope.launch { - logListState.scrollToItem(0) - } - }) { + val firstVisibleItem by remember { derivedStateOf { logListState.firstVisibleItemIndex } } + val layoutInfo by remember { derivedStateOf { logListState.layoutInfo } } + FilledIconButton( + onClick = { + coroutineScope.launch { + logListState.scrollToItem(0) + } + }, + enabled = firstVisibleItem != 0 + ) { Icon(Icons.Filled.KeyboardDoubleArrowUp, contentDescription = null) } - FilledIconButton(onClick = { - coroutineScope.launch { - logListState.scrollToItem((logListState.layoutInfo.totalItemsCount - 1).takeIf { it >= 0 } ?: return@launch) - } - }) { + FilledIconButton( + onClick = { + coroutineScope.launch { + logListState.scrollToItem((logListState.layoutInfo.totalItemsCount - 1).takeIf { it >= 0 } ?: return@launch) + } + }, + enabled = layoutInfo.visibleItemsInfo.lastOrNull()?.index != layoutInfo.totalItemsCount - 1 + ) { Icon(Icons.Filled.KeyboardDoubleArrowDown, contentDescription = null) } }