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:
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)
}
}