HomeLogs.kt (11125B) - raw


      1 package me.rhunk.snapenhance.ui.manager.pages.home
      2 
      3 import android.net.Uri
      4 import androidx.compose.foundation.ScrollState
      5 import androidx.compose.foundation.background
      6 import androidx.compose.foundation.gestures.detectTapGestures
      7 import androidx.compose.foundation.horizontalScroll
      8 import androidx.compose.foundation.layout.*
      9 import androidx.compose.foundation.lazy.LazyColumn
     10 import androidx.compose.foundation.lazy.LazyListState
     11 import androidx.compose.material.icons.Icons
     12 import androidx.compose.material.icons.filled.KeyboardDoubleArrowDown
     13 import androidx.compose.material.icons.filled.KeyboardDoubleArrowUp
     14 import androidx.compose.material.icons.filled.MoreVert
     15 import androidx.compose.material.icons.outlined.BugReport
     16 import androidx.compose.material.icons.outlined.Info
     17 import androidx.compose.material.icons.outlined.Report
     18 import androidx.compose.material.icons.outlined.Warning
     19 import androidx.compose.material3.*
     20 import androidx.compose.runtime.*
     21 import androidx.compose.ui.Alignment
     22 import androidx.compose.ui.Modifier
     23 import androidx.compose.ui.input.pointer.pointerInput
     24 import androidx.compose.ui.platform.LocalClipboardManager
     25 import androidx.compose.ui.text.AnnotatedString
     26 import androidx.compose.ui.text.font.FontWeight
     27 import androidx.compose.ui.text.style.TextOverflow
     28 import androidx.compose.ui.unit.dp
     29 import androidx.compose.ui.unit.sp
     30 import androidx.navigation.NavBackStackEntry
     31 import kotlinx.coroutines.Dispatchers
     32 import kotlinx.coroutines.delay
     33 import kotlinx.coroutines.launch
     34 import kotlinx.coroutines.runBlocking
     35 import kotlinx.coroutines.withContext
     36 import me.rhunk.snapenhance.LogReader
     37 import me.rhunk.snapenhance.common.logger.LogChannel
     38 import me.rhunk.snapenhance.common.logger.LogLevel
     39 import me.rhunk.snapenhance.ui.manager.Routes
     40 import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
     41 import me.rhunk.snapenhance.ui.util.pullrefresh.PullRefreshIndicator
     42 import me.rhunk.snapenhance.ui.util.pullrefresh.rememberPullRefreshState
     43 import me.rhunk.snapenhance.ui.util.saveFile
     44 
     45 class HomeLogs : Routes.Route() {
     46     private val logListState by lazy { LazyListState(0) }
     47     private lateinit var activityLauncherHelper: ActivityLauncherHelper
     48 
     49     override val init: () -> Unit = {
     50         activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
     51     }
     52 
     53     override val topBarActions: @Composable (RowScope.() -> Unit) = {
     54         var showDropDown by remember { mutableStateOf(false) }
     55 
     56         IconButton(onClick = {
     57             showDropDown = true
     58         }) {
     59             Icon(Icons.Filled.MoreVert, contentDescription = null)
     60         }
     61 
     62         DropdownMenu(
     63             expanded = showDropDown,
     64             onDismissRequest = { showDropDown = false },
     65             modifier = Modifier.align(Alignment.CenterVertically)
     66         ) {
     67             DropdownMenuItem(onClick = {
     68                 context.coroutineScope.launch {
     69                     context.log.clearLogs()
     70                 }
     71                 navigateReload()
     72                 showDropDown = false
     73             }, text = {
     74                 Text(translation["clear_logs_button"])
     75             })
     76 
     77             DropdownMenuItem(onClick = {
     78                 activityLauncherHelper.saveFile("snapenhance-logs-${System.currentTimeMillis()}.zip", "application/zip") { uri ->
     79                     context.coroutineScope.launch {
     80                         context.shortToast(translation["saving_logs_toast"])
     81                         context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use {
     82                             runCatching {
     83                                 context.log.exportLogsToZip(it)
     84                                 context.longToast(translation["saved_logs_success_toast"])
     85                             }.onFailure {
     86                                 context.longToast(translation["saved_logs_failure_toast"])
     87                                 context.log.error("Failed to save logs to $uri!", it)
     88                             }
     89                         }
     90                     }
     91                 }
     92                 showDropDown = false
     93             }, text = {
     94                 Text(translation["export_logs_button"])
     95             })
     96         }
     97     }
     98 
     99     override val content: @Composable (NavBackStackEntry) -> Unit = {
    100         val coroutineScope = rememberCoroutineScope()
    101         val clipboardManager = LocalClipboardManager.current
    102         var lineCount by remember { mutableIntStateOf(0) }
    103         var logReader by remember { mutableStateOf<LogReader?>(null) }
    104         var isRefreshing by remember { mutableStateOf(false) }
    105 
    106         fun refreshLogs() {
    107             coroutineScope.launch(Dispatchers.IO) {
    108                 runCatching {
    109                     logReader = context.log.newReader {
    110                         lineCount++
    111                     }
    112                     lineCount = logReader!!.lineCount
    113                 }.onFailure {
    114                     context.longToast("Failed to read logs!")
    115                 }
    116                 delay(300)
    117                 isRefreshing = false
    118                 withContext(Dispatchers.Main) {
    119                     logListState.scrollToItem((logListState.layoutInfo.totalItemsCount - 1).takeIf { it >= 0 } ?: return@withContext)
    120                 }
    121             }
    122         }
    123 
    124         val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh = {
    125             refreshLogs()
    126         })
    127 
    128         LaunchedEffect(Unit) {
    129             isRefreshing = true
    130             refreshLogs()
    131         }
    132 
    133         Box(
    134             modifier = Modifier
    135                 .fillMaxSize()
    136         ) {
    137             LazyColumn(
    138                 modifier = Modifier
    139                     .background(MaterialTheme.colorScheme.surface)
    140                     .horizontalScroll(ScrollState(0)),
    141                 state = logListState
    142             ) {
    143                 item {
    144                     if (lineCount == 0 && logReader != null) {
    145                         Text(
    146                             text = translation["no_logs_hint"],
    147                             modifier = Modifier.padding(16.dp),
    148                             fontSize = 12.sp,
    149                             fontWeight = FontWeight.Light
    150                         )
    151                     }
    152                 }
    153                 items(lineCount) { index ->
    154                     val logLine by remember(index) {
    155                         mutableStateOf(runBlocking(Dispatchers.IO) {
    156                             logReader?.getLogLine(index)
    157                         })
    158                     }
    159                     logLine?.let { line ->
    160                         Box(modifier = Modifier
    161                             .fillMaxWidth()
    162                             .pointerInput(Unit) {
    163                                 detectTapGestures(
    164                                     onLongPress = {
    165                                         coroutineScope.launch {
    166                                             clipboardManager.setText(
    167                                                 AnnotatedString(
    168                                                     line.message
    169                                                 )
    170                                             )
    171                                         }
    172                                     }
    173                                 )
    174                             }) {
    175                             Column(
    176                                 modifier = Modifier
    177                                     .padding(4.dp)
    178                                     .fillMaxWidth()
    179                                     .defaultMinSize(minHeight = 30.dp),
    180                             ) {
    181                                 Row(
    182                                     verticalAlignment = Alignment.CenterVertically,
    183                                 ) {
    184                                     Icon(
    185                                         imageVector = when (line.logLevel) {
    186                                             LogLevel.DEBUG -> Icons.Outlined.BugReport
    187                                             LogLevel.ERROR, LogLevel.ASSERT -> Icons.Outlined.Report
    188                                             LogLevel.INFO, LogLevel.VERBOSE -> Icons.Outlined.Info
    189                                             LogLevel.WARN -> Icons.Outlined.Warning
    190                                             else -> Icons.Outlined.Info
    191                                         },
    192                                         modifier = Modifier.size(16.dp),
    193                                         contentDescription = null,
    194                                     )
    195 
    196                                     Text(
    197                                         text = LogChannel.fromChannel(line.tag)?.shortName ?: line.tag,
    198                                         modifier = Modifier.padding(start = 4.dp),
    199                                         fontWeight = FontWeight.Bold,
    200                                         fontSize = 12.sp,
    201                                     )
    202 
    203                                     Text(
    204                                         text = line.dateTime,
    205                                         modifier = Modifier.padding(start = 4.dp, end = 4.dp),
    206                                         fontSize = 10.sp
    207                                     )
    208                                 }
    209 
    210                                 Text(
    211                                     text = line.message.trimIndent(),
    212                                     lineHeight = 10.sp,
    213                                     fontSize = 9.sp,
    214                                     maxLines = Int.MAX_VALUE,
    215                                 )
    216                             }
    217                         }
    218                     }
    219                 }
    220             }
    221 
    222             PullRefreshIndicator(
    223                 refreshing = isRefreshing,
    224                 state = pullRefreshState,
    225                 modifier = Modifier.align(Alignment.TopCenter)
    226             )
    227         }
    228     }
    229 
    230     override val floatingActionButton: @Composable () -> Unit = {
    231         val coroutineScope = rememberCoroutineScope()
    232         Column(
    233             verticalArrangement = Arrangement.spacedBy(5.dp),
    234         ) {
    235             val firstVisibleItem by remember { derivedStateOf { logListState.firstVisibleItemIndex } }
    236             val layoutInfo by remember { derivedStateOf { logListState.layoutInfo } }
    237             FilledIconButton(
    238                 onClick = {
    239                     coroutineScope.launch {
    240                         logListState.scrollToItem(0)
    241                     }
    242                 },
    243                 enabled = firstVisibleItem != 0
    244             ) {
    245                 Icon(Icons.Filled.KeyboardDoubleArrowUp, contentDescription = null)
    246             }
    247 
    248             FilledIconButton(
    249                 onClick = {
    250                     coroutineScope.launch {
    251                         logListState.scrollToItem((logListState.layoutInfo.totalItemsCount - 1).takeIf { it >= 0 } ?: return@launch)
    252                     }
    253                 },
    254                 enabled = layoutInfo.visibleItemsInfo.lastOrNull()?.index != layoutInfo.totalItemsCount - 1
    255             ) {
    256                 Icon(Icons.Filled.KeyboardDoubleArrowDown, contentDescription = null)
    257             }
    258         }
    259     }
    260 }