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 }