android: shared ui components (SectionCard, ListRow, StatusPill, SnackbarHost)
Replace the duplicate FormSection, ActiveBadge and SnackbarHost call sites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f6726f173
commit
65989b3a5a
8 files changed
+220
-36
No files matched your search
+2
-21
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
package org.strongswan.android.ui.compose
|
package org.strongswan.android.ui.compose
|
||||||
|
|
||||||
|
import org.strongswan.android.ui.compose.components.ShxSectionCard
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.security.KeyChain
|
import android.security.KeyChain
|
||||||
@@ -389,7 +390,7 @@ fun ProfileFormScreen(
|
|||||||
{
|
{
|
||||||
Text(stringResource(R.string.alert_text_vpn_profile_read_only), color = MaterialTheme.colorScheme.error)
|
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(
|
OutlinedTextField(
|
||||||
value = state.gateway,
|
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
|
@Composable
|
||||||
private fun SwitchRow(
|
private fun SwitchRow(
|
||||||
label: String,
|
label: String,
|
||||||
|
|||||||
+2
-13
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
package org.strongswan.android.ui.compose
|
package org.strongswan.android.ui.compose
|
||||||
|
|
||||||
|
import org.strongswan.android.ui.compose.components.ShxConnectedPill
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
@@ -117,19 +118,7 @@ fun ShxProfileRow(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ActiveBadge(modifier: Modifier = Modifier)
|
fun ActiveBadge(modifier: Modifier = Modifier)
|
||||||
{
|
{
|
||||||
val status = LocalShxStatusColors.current
|
ShxConnectedPill(text = stringResource(R.string.badge_connected), modifier = modifier)
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+2
-1
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
package org.strongswan.android.ui.compose
|
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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -111,7 +112,7 @@ fun ProfilesScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { ShxSnackbarHost(snackbarHostState) },
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
) { _ ->
|
) { _ ->
|
||||||
Column(Modifier.fillMaxSize())
|
Column(Modifier.fillMaxSize())
|
||||||
|
|||||||
+2
-1
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
package org.strongswan.android.ui.compose
|
package org.strongswan.android.ui.compose
|
||||||
|
|
||||||
|
import org.strongswan.android.ui.compose.components.ShxSnackbarHost
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
@@ -122,7 +123,7 @@ fun RoutingScreen()
|
|||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { ShxSnackbarHost(snackbarHostState) },
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
) { _ ->
|
) { _ ->
|
||||||
RoutingContent(snackbarHostState)
|
RoutingContent(snackbarHostState)
|
||||||
|
|||||||
+80
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -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)
|
||||||
|
}
|
||||||
+61
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user