android: log screen - level filter chips, snackbar instead of toast

This commit is contained in:
Denozordec committed 2026-10-10 00:30:27 +07:00
1 parent 42cd361c58
commit 8bedf432e2
5 files changed
+100 -29

No files matched your search

@@ -46,6 +46,13 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import org.strongswan.android.R
import org.strongswan.android.data.LogContentProvider
import org.strongswan.android.logic.CharonVpnService
@@ -59,13 +66,15 @@ import java.io.FileReader
class LogActivity : AppCompatActivity()
{
private val messages = kotlinx.coroutines.flow.MutableSharedFlow<Int>(extraBufferCapacity = 1)
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ShxTheme {
LogScreen(onBack = { finish() }, onSend = { sendLog() })
LogScreen(onBack = { finish() }, onSend = { sendLog() }, messages = messages)
}
}
}
@@ -80,7 +89,7 @@ class LogActivity : AppCompatActivity()
).filter { it.exists() && it.length() > 0L }
if (files.isEmpty())
{
Toast.makeText(this, R.string.empty_log, Toast.LENGTH_SHORT).show()
messages.tryEmit(R.string.empty_log)
return
}
var version = ""
@@ -165,17 +174,47 @@ class LogViewModel : ViewModel()
}
}
private enum class LogLevelFilter(val labelRes: Int)
{
ALL(R.string.log_filter_all),
WARN(R.string.log_filter_warn),
ERROR(R.string.log_filter_error),
}
private fun String.isErrorLine() = contains(" ERROR ")
private fun String.isWarnLine() = contains(" WARN ") || contains(" WARNING ")
@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
@Composable
private fun LogScreen(onBack: () -> Unit, onSend: () -> Unit, viewModel: LogViewModel = viewModel())
private fun LogScreen(
onBack: () -> Unit,
onSend: () -> Unit,
messages: kotlinx.coroutines.flow.Flow<Int>,
viewModel: LogViewModel = viewModel(),
)
{
val context = LocalContext.current
val lines by viewModel.lines.collectAsStateWithLifecycle()
val allLines by viewModel.lines.collectAsStateWithLifecycle()
val listState = rememberLazyListState()
val snackbarHostState = remember { androidx.compose.material3.SnackbarHostState() }
var filter by rememberSaveable { mutableStateOf(LogLevelFilter.ALL) }
val lines = remember(allLines, filter) {
when (filter)
{
LogLevelFilter.ALL -> allLines
LogLevelFilter.WARN -> allLines.filter { it.isErrorLine() || it.isWarnLine() }
LogLevelFilter.ERROR -> allLines.filter { it.isErrorLine() }
}
}
LaunchedEffect(Unit) {
viewModel.start(File(context.filesDir, CharonVpnService.LOG_FILE))
}
LaunchedEffect(lines.size) {
LaunchedEffect(messages) {
messages.collect { snackbarHostState.showSnackbar(context.getString(it)) }
}
LaunchedEffect(lines.size, filter) {
if (lines.isNotEmpty())
{
listState.scrollToItem(lines.lastIndex)
@@ -190,36 +229,57 @@ private fun LogScreen(onBack: () -> Unit, onSend: () -> Unit, viewModel: LogView
Icon(Icons.Default.Share, contentDescription = stringResource(R.string.send_log))
}
},
snackbarHost = { org.strongswan.android.ui.compose.components.ShxSnackbarHost(snackbarHostState) },
) { padding ->
if (lines.isEmpty())
Column(Modifier.fillMaxSize().padding(padding))
{
Text(
stringResource(R.string.empty_log),
modifier = Modifier.padding(padding).padding(ShxSpacing.xl),
color = MaterialTheme.colorScheme.onSurfaceVariant,
Row(
modifier = Modifier.padding(horizontal = ShxSpacing.lg),
horizontalArrangement = Arrangement.spacedBy(ShxSpacing.sm),
)
}
else
{
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = ShxSpacing.md),
) {
items(lines.size) { index ->
val line = lines[index]
val isError = line.contains(" ERROR ", ignoreCase = false)
Text(
line,
style = MaterialTheme.typography.bodySmall.copy(
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
),
color = if (isError) MaterialTheme.colorScheme.error
else MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(vertical = 2.dp),
{
LogLevelFilter.entries.forEach { level ->
androidx.compose.material3.FilterChip(
selected = filter == level,
onClick = { filter = level },
label = { Text(stringResource(level.labelRes)) },
)
}
}
if (lines.isEmpty())
{
Text(
stringResource(R.string.empty_log),
modifier = Modifier.padding(ShxSpacing.xl),
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
else
{
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize().padding(horizontal = ShxSpacing.md),
) {
items(lines.size) { index ->
val line = lines[index]
val color = when
{
line.isErrorLine() -> MaterialTheme.colorScheme.error
line.isWarnLine() -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.onSurface
}
Text(
line,
style = MaterialTheme.typography.bodySmall.copy(
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
),
color = color,
modifier = Modifier.padding(vertical = 2.dp),
)
}
}
}
}
}
}
@@ -33,10 +33,12 @@ fun SecondaryScaffold(
title: String,
onBack: () -> Unit,
actions: @Composable RowScope.() -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit,
)
{
Scaffold(
snackbarHost = snackbarHost,
topBar = {
TopAppBar(
title = { Text(title, maxLines = 1, overflow = TextOverflow.Ellipsis, fontWeight = FontWeight.SemiBold) },
@@ -506,4 +506,7 @@
<string name="profile_detail_placeholder">Profil auswählen oder hinzufügen</string>
<string name="update_downloading_title">Update %1$s wird heruntergeladen</string>
<string name="log_filter_all">Alle</string>
<string name="log_filter_warn">Warnungen</string>
<string name="log_filter_error">Fehler</string>
</resources>
@@ -503,4 +503,7 @@
<string name="widget_description">Статус защиты и подключение одним касанием</string>
<string name="profile_detail_placeholder">Выберите или добавьте профиль</string>
<string name="update_downloading_title">Загрузка обновления %1$s</string>
<string name="log_filter_all">Все</string>
<string name="log_filter_warn">Предупреждения</string>
<string name="log_filter_error">Ошибки</string>
</resources>
@@ -508,4 +508,7 @@
<string name="widget_description">Protection status and one-tap connect</string>
<string name="profile_detail_placeholder">Select or add a profile</string>
<string name="update_downloading_title">Downloading update %1$s</string>
<string name="log_filter_all">All</string>
<string name="log_filter_warn">Warnings</string>
<string name="log_filter_error">Errors</string>
</resources>