android: log screen - level filter chips, snackbar instead of toast
This commit is contained in:
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
@@ -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>
|
||||||
Reference in new issue
Block a user