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.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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.R
import org.strongswan.android.data.LogContentProvider import org.strongswan.android.data.LogContentProvider
import org.strongswan.android.logic.CharonVpnService import org.strongswan.android.logic.CharonVpnService
@@ -59,13 +66,15 @@ import java.io.FileReader
class LogActivity : AppCompatActivity() class LogActivity : AppCompatActivity()
{ {
private val messages = kotlinx.coroutines.flow.MutableSharedFlow<Int>(extraBufferCapacity = 1)
override fun onCreate(savedInstanceState: Bundle?) override fun onCreate(savedInstanceState: Bundle?)
{ {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
ShxTheme { 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 } ).filter { it.exists() && it.length() > 0L }
if (files.isEmpty()) if (files.isEmpty())
{ {
Toast.makeText(this, R.string.empty_log, Toast.LENGTH_SHORT).show() messages.tryEmit(R.string.empty_log)
return return
} }
var version = "" 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 @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 context = LocalContext.current
val lines by viewModel.lines.collectAsStateWithLifecycle() val allLines by viewModel.lines.collectAsStateWithLifecycle()
val listState = rememberLazyListState() 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) { LaunchedEffect(Unit) {
viewModel.start(File(context.filesDir, CharonVpnService.LOG_FILE)) 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()) if (lines.isNotEmpty())
{ {
listState.scrollToItem(lines.lastIndex) 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)) Icon(Icons.Default.Share, contentDescription = stringResource(R.string.send_log))
} }
}, },
snackbarHost = { org.strongswan.android.ui.compose.components.ShxSnackbarHost(snackbarHostState) },
) { padding -> ) { padding ->
if (lines.isEmpty()) Column(Modifier.fillMaxSize().padding(padding))
{ {
Text( Row(
stringResource(R.string.empty_log), modifier = Modifier.padding(horizontal = ShxSpacing.lg),
modifier = Modifier.padding(padding).padding(ShxSpacing.xl), horizontalArrangement = Arrangement.spacedBy(ShxSpacing.sm),
color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} {
else LogLevelFilter.entries.forEach { level ->
{ androidx.compose.material3.FilterChip(
LazyColumn( selected = filter == level,
state = listState, onClick = { filter = level },
modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = ShxSpacing.md), label = { Text(stringResource(level.labelRes)) },
) {
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),
) )
} }
} }
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, title: String,
onBack: () -> Unit, onBack: () -> Unit,
actions: @Composable RowScope.() -> Unit = {}, actions: @Composable RowScope.() -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit, content: @Composable (PaddingValues) -> Unit,
) )
{ {
Scaffold( Scaffold(
snackbarHost = snackbarHost,
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(title, maxLines = 1, overflow = TextOverflow.Ellipsis, fontWeight = FontWeight.SemiBold) }, 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="profile_detail_placeholder">Profil auswählen oder hinzufügen</string>
<string name="update_downloading_title">Update %1$s wird heruntergeladen</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> </resources>
@@ -503,4 +503,7 @@
<string name="widget_description">Статус защиты и подключение одним касанием</string> <string name="widget_description">Статус защиты и подключение одним касанием</string>
<string name="profile_detail_placeholder">Выберите или добавьте профиль</string> <string name="profile_detail_placeholder">Выберите или добавьте профиль</string>
<string name="update_downloading_title">Загрузка обновления %1$s</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> </resources>
@@ -508,4 +508,7 @@
<string name="widget_description">Protection status and one-tap connect</string> <string name="widget_description">Protection status and one-tap connect</string>
<string name="profile_detail_placeholder">Select or add a profile</string> <string name="profile_detail_placeholder">Select or add a profile</string>
<string name="update_downloading_title">Downloading update %1$s</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> </resources>