mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-13 03:17:48 -04:00
Fix/debug panel - remove redundant saveOne (#2737)
This commit is contained in:
@@ -250,12 +250,6 @@ private fun DebugItemHeader(log: UiMeshLog, searchText: String, isSelected: Bool
|
||||
}
|
||||
}
|
||||
CopyIconButton(valueToCopy = fullLogText, modifier = Modifier.padding(start = 8.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FileDownload,
|
||||
contentDescription = stringResource(id = R.string.logs),
|
||||
tint = Color.Gray.copy(alpha = 0.6f),
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
val dateAnnotatedString = rememberAnnotatedString(text = log.formattedReceivedDate, searchText = searchText)
|
||||
Text(
|
||||
text = dateAnnotatedString,
|
||||
@@ -315,6 +309,131 @@ private fun rememberAnnotatedLogMessage(log: UiMeshLog, searchText: String): Ann
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DebugMenuActions(viewModel: DebugViewModel = hiltViewModel(), modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val logs by viewModel.meshLog.collectAsStateWithLifecycle()
|
||||
var showDeleteLogsDialog by remember { mutableStateOf(false) }
|
||||
|
||||
IconButton(onClick = { scope.launch { exportAllLogs(context, logs) } }, modifier = modifier.padding(4.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FileDownload,
|
||||
contentDescription = stringResource(id = R.string.debug_logs_export),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { showDeleteLogsDialog = true }, modifier = modifier.padding(4.dp)) {
|
||||
Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(id = R.string.debug_clear))
|
||||
}
|
||||
if (showDeleteLogsDialog) {
|
||||
SimpleAlertDialog(
|
||||
title = R.string.debug_clear,
|
||||
text = R.string.debug_clear_logs_confirm,
|
||||
onConfirm = {
|
||||
showDeleteLogsDialog = false
|
||||
viewModel.deleteAllLogs()
|
||||
},
|
||||
onDismiss = { showDeleteLogsDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun exportAllLogs(context: Context, logs: List<UiMeshLog>) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val fileName = "meshtastic_debug_$timestamp.txt"
|
||||
|
||||
// Get the Downloads directory
|
||||
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val logFile = File(downloadsDir, fileName)
|
||||
|
||||
// Create the file and write logs
|
||||
OutputStreamWriter(FileOutputStream(logFile), StandardCharsets.UTF_8).use { writer ->
|
||||
logs.forEach { log ->
|
||||
writer.write("${log.formattedReceivedDate} [${log.messageType}]\n")
|
||||
writer.write(log.logMessage)
|
||||
if (!log.decodedPayload.isNullOrBlank()) {
|
||||
writer.write("\n\nDecoded Payload:\n{")
|
||||
writer.write("\n")
|
||||
writer.write(log.decodedPayload)
|
||||
writer.write("\n}")
|
||||
}
|
||||
writer.write("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Notify user of success
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "Logs exported to ${logFile.absolutePath}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "Permission denied: Cannot write to Downloads folder", Toast.LENGTH_LONG).show()
|
||||
warn("Error:SecurityException: " + e.toString())
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "Failed to write log file: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
warn("Error:IOException: " + e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DecodedPayloadBlock(
|
||||
decodedPayload: String,
|
||||
isSelected: Boolean,
|
||||
colorScheme: ColorScheme,
|
||||
searchText: String = "",
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val commonTextStyle =
|
||||
TextStyle(fontSize = if (isSelected) 10.sp else 8.sp, fontWeight = FontWeight.Bold, color = colorScheme.primary)
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.debug_decoded_payload),
|
||||
style = commonTextStyle,
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
|
||||
)
|
||||
Text(text = "{", style = commonTextStyle, modifier = Modifier.padding(start = 8.dp, bottom = 2.dp))
|
||||
val annotatedPayload = rememberAnnotatedDecodedPayload(decodedPayload, searchText, colorScheme)
|
||||
Text(
|
||||
text = annotatedPayload,
|
||||
softWrap = true,
|
||||
style =
|
||||
TextStyle(
|
||||
fontSize = if (isSelected) 10.sp else 8.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = colorScheme.onSurface.copy(alpha = 0.8f),
|
||||
),
|
||||
modifier = Modifier.padding(start = 16.dp, bottom = 0.dp),
|
||||
)
|
||||
Text(text = "}", style = commonTextStyle, modifier = Modifier.padding(start = 8.dp, bottom = 4.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberAnnotatedDecodedPayload(
|
||||
decodedPayload: String,
|
||||
searchText: String,
|
||||
colorScheme: ColorScheme,
|
||||
): AnnotatedString {
|
||||
val highlightStyle = SpanStyle(background = colorScheme.primary.copy(alpha = 0.3f), color = colorScheme.onSurface)
|
||||
return remember(decodedPayload, searchText) {
|
||||
buildAnnotatedString {
|
||||
append(decodedPayload)
|
||||
if (searchText.isNotEmpty()) {
|
||||
searchText.split(" ").forEach { term ->
|
||||
Regex(Regex.escape(term), RegexOption.IGNORE_CASE).findAll(decodedPayload).forEach { match ->
|
||||
addStyle(style = highlightStyle, start = match.range.first, end = match.range.last + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun DebugPacketPreview() {
|
||||
@@ -592,128 +711,3 @@ private fun DebugScreenWithSampleDataPreview() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DebugMenuActions(viewModel: DebugViewModel = hiltViewModel(), modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val logs by viewModel.meshLog.collectAsStateWithLifecycle()
|
||||
var showDeleteLogsDialog by remember { mutableStateOf(false) }
|
||||
|
||||
IconButton(onClick = { scope.launch { exportAllLogs(context, logs) } }, modifier = modifier.padding(4.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FileDownload,
|
||||
contentDescription = stringResource(id = R.string.debug_logs_export),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { showDeleteLogsDialog = true }, modifier = modifier.padding(4.dp)) {
|
||||
Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(id = R.string.debug_clear))
|
||||
}
|
||||
if (showDeleteLogsDialog) {
|
||||
SimpleAlertDialog(
|
||||
title = R.string.debug_clear,
|
||||
text = R.string.debug_clear_logs_confirm,
|
||||
onConfirm = {
|
||||
showDeleteLogsDialog = false
|
||||
viewModel.deleteAllLogs()
|
||||
},
|
||||
onDismiss = { showDeleteLogsDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun exportAllLogs(context: Context, logs: List<UiMeshLog>) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val fileName = "meshtastic_debug_$timestamp.txt"
|
||||
|
||||
// Get the Downloads directory
|
||||
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val logFile = File(downloadsDir, fileName)
|
||||
|
||||
// Create the file and write logs
|
||||
OutputStreamWriter(FileOutputStream(logFile), StandardCharsets.UTF_8).use { writer ->
|
||||
logs.forEach { log ->
|
||||
writer.write("${log.formattedReceivedDate} [${log.messageType}]\n")
|
||||
writer.write(log.logMessage)
|
||||
if (!log.decodedPayload.isNullOrBlank()) {
|
||||
writer.write("\n\nDecoded Payload:\n{")
|
||||
writer.write("\n")
|
||||
writer.write(log.decodedPayload)
|
||||
writer.write("\n}")
|
||||
}
|
||||
writer.write("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Notify user of success
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "Logs exported to ${logFile.absolutePath}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "Permission denied: Cannot write to Downloads folder", Toast.LENGTH_LONG).show()
|
||||
warn("Error:SecurityException: " + e.toString())
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "Failed to write log file: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
warn("Error:IOException: " + e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DecodedPayloadBlock(
|
||||
decodedPayload: String,
|
||||
isSelected: Boolean,
|
||||
colorScheme: ColorScheme,
|
||||
searchText: String = "",
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val commonTextStyle =
|
||||
TextStyle(fontSize = if (isSelected) 10.sp else 8.sp, fontWeight = FontWeight.Bold, color = colorScheme.primary)
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.debug_decoded_payload),
|
||||
style = commonTextStyle,
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
|
||||
)
|
||||
Text(text = "{", style = commonTextStyle, modifier = Modifier.padding(start = 8.dp, bottom = 2.dp))
|
||||
val annotatedPayload = rememberAnnotatedDecodedPayload(decodedPayload, searchText, colorScheme)
|
||||
Text(
|
||||
text = annotatedPayload,
|
||||
softWrap = true,
|
||||
style =
|
||||
TextStyle(
|
||||
fontSize = if (isSelected) 10.sp else 8.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = colorScheme.onSurface.copy(alpha = 0.8f),
|
||||
),
|
||||
modifier = Modifier.padding(start = 16.dp, bottom = 0.dp),
|
||||
)
|
||||
Text(text = "}", style = commonTextStyle, modifier = Modifier.padding(start = 8.dp, bottom = 4.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberAnnotatedDecodedPayload(
|
||||
decodedPayload: String,
|
||||
searchText: String,
|
||||
colorScheme: ColorScheme,
|
||||
): AnnotatedString {
|
||||
val highlightStyle = SpanStyle(background = colorScheme.primary.copy(alpha = 0.3f), color = colorScheme.onSurface)
|
||||
return remember(decodedPayload, searchText) {
|
||||
buildAnnotatedString {
|
||||
append(decodedPayload)
|
||||
if (searchText.isNotEmpty()) {
|
||||
searchText.split(" ").forEach { term ->
|
||||
Regex(Regex.escape(term), RegexOption.IGNORE_CASE).findAll(decodedPayload).forEach { match ->
|
||||
addStyle(style = highlightStyle, start = match.range.first, end = match.range.last + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user