android: shared ui components (SectionCard, ListRow, StatusPill, SnackbarHost)

Replace the duplicate FormSection, ActiveBadge and SnackbarHost call sites.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-10-09 23:39:35 +07:00
1 parent 5f6726f173
commit 65989b3a5a
8 files changed
+220 -36

No files matched your search

@@ -9,6 +9,7 @@
package org.strongswan.android.ui.compose
import org.strongswan.android.ui.compose.components.ShxSectionCard
import android.app.Activity
import android.content.Intent
import android.security.KeyChain
@@ -389,7 +390,7 @@ fun ProfileFormScreen(
{
Text(stringResource(R.string.alert_text_vpn_profile_read_only), color = MaterialTheme.colorScheme.error)
}
FormSection(title = stringResource(R.string.profile_section_basic))
ShxSectionCard(title = stringResource(R.string.profile_section_basic))
{
OutlinedTextField(
value = state.gateway,
@@ -625,26 +626,6 @@ fun ProfileFormScreen(
}
}
@Composable
private fun FormSection(title: String, content: @Composable () -> Unit)
{
Surface(
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.surfaceContainerLow,
modifier = Modifier.fillMaxWidth(),
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp))
{
Text(
text = title.uppercase(),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
content()
}
}
}
@Composable
private fun SwitchRow(
label: String,
@@ -9,6 +9,7 @@
package org.strongswan.android.ui.compose
import org.strongswan.android.ui.compose.components.ShxConnectedPill
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
@@ -117,19 +118,7 @@ fun ShxProfileRow(
@Composable
fun ActiveBadge(modifier: Modifier = Modifier)
{
val status = LocalShxStatusColors.current
Surface(
color = status.connectedContainer,
contentColor = status.connected,
shape = CircleShape,
modifier = modifier,
) {
Text(
text = stringResource(R.string.badge_connected),
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
)
}
ShxConnectedPill(text = stringResource(R.string.badge_connected), modifier = modifier)
}
@Composable
@@ -9,6 +9,7 @@
package org.strongswan.android.ui.compose
import org.strongswan.android.ui.compose.components.ShxSnackbarHost
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -111,7 +112,7 @@ fun ProfilesScreen(
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
snackbarHost = { ShxSnackbarHost(snackbarHostState) },
containerColor = MaterialTheme.colorScheme.background,
) { _ ->
Column(Modifier.fillMaxSize())
@@ -9,6 +9,7 @@
package org.strongswan.android.ui.compose
import org.strongswan.android.ui.compose.components.ShxSnackbarHost
import android.app.Activity
import android.content.Intent
import android.text.format.DateUtils
@@ -122,7 +123,7 @@ fun RoutingScreen()
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
snackbarHost = { ShxSnackbarHost(snackbarHostState) },
containerColor = MaterialTheme.colorScheme.background,
) { _ ->
RoutingContent(snackbarHostState)
@@ -0,0 +1,80 @@
/*
* Copyright (C) 2026 SHX VPN
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*/
package org.strongswan.android.ui.compose.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.strongswan.android.ui.compose.theme.ShxSpacing
/**
* Generic list row: optional leading slot, headline + supporting text, optional trailing slot.
* Minimum height 56dp keeps touch targets above 48dp. Clickable when [onClick] is set.
*/
@Composable
fun ShxListRow(
headline: String,
modifier: Modifier = Modifier,
supporting: String? = null,
enabled: Boolean = true,
leading: (@Composable () -> Unit)? = null,
trailing: (@Composable () -> Unit)? = null,
onClick: (() -> Unit)? = null,
)
{
val clickable = if (onClick != null) Modifier.clickable(enabled = enabled, onClick = onClick) else Modifier
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.then(clickable)
.heightIn(min = 56.dp)
.padding(horizontal = ShxSpacing.lg, vertical = ShxSpacing.md),
)
{
if (leading != null)
{
leading()
}
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = if (leading != null || trailing != null) ShxSpacing.lg else 0.dp),
)
{
Text(
headline,
style = MaterialTheme.typography.bodyLarge,
color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
)
if (supporting != null)
{
Text(
supporting,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
if (trailing != null)
{
trailing()
}
}
}
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2026 SHX VPN
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*/
package org.strongswan.android.ui.compose.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.strongswan.android.ui.compose.theme.ShxSpacing
/** Card with an uppercase caption and a padded content column (form groups, detail blocks). */
@Composable
fun ShxSectionCard(
title: String,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
)
{
Surface(
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.surfaceContainerLow,
modifier = modifier.fillMaxWidth(),
) {
Column(Modifier.padding(ShxSpacing.lg), verticalArrangement = Arrangement.spacedBy(ShxSpacing.md))
{
Text(
text = title.uppercase(),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
content()
}
}
}
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2026 SHX VPN
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*/
package org.strongswan.android.ui.compose.components
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
/**
* The single snackbar host used by every screen scaffold, so error/undo messages
* look the same everywhere (replaces Toast).
*/
@Composable
fun ShxSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier)
{
SnackbarHost(hostState = hostState, modifier = modifier)
}
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2026 SHX VPN
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*/
package org.strongswan.android.ui.compose.components
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import org.strongswan.android.ui.compose.theme.LocalShxStatusColors
/** Small rounded label with explicit colors, e.g. "Connected". */
@Composable
fun ShxStatusPill(
text: String,
containerColor: Color,
contentColor: Color,
modifier: Modifier = Modifier,
shape: Shape = CircleShape,
style: TextStyle = MaterialTheme.typography.labelSmall,
)
{
Surface(
color = containerColor,
contentColor = contentColor,
shape = shape,
modifier = modifier,
) {
Text(
text = text,
style = style,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
)
}
}
/** Pill in the "connected" status palette (independent of dynamic color). */
@Composable
fun ShxConnectedPill(text: String, modifier: Modifier = Modifier)
{
val status = LocalShxStatusColors.current
ShxStatusPill(
text = text,
containerColor = status.connectedContainer,
contentColor = status.connected,
modifier = modifier,
)
}