diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ProfileFormScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ProfileFormScreen.kt index 8d0d105d1..69db58d8a 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ProfileFormScreen.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ProfileFormScreen.kt @@ -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, diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ProfileRow.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ProfileRow.kt index a1e3f30bd..9c3cb27c8 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ProfileRow.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ProfileRow.kt @@ -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 diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ProfilesScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ProfilesScreen.kt index 8ad99a545..21fadba7a 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ProfilesScreen.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ProfilesScreen.kt @@ -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()) diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/RoutingScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/RoutingScreen.kt index a8ccfac8f..32a9ad655 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/RoutingScreen.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/RoutingScreen.kt @@ -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) diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxListRow.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxListRow.kt new file mode 100644 index 000000000..33d9f536b --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxListRow.kt @@ -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() + } + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxSectionCard.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxSectionCard.kt new file mode 100644 index 000000000..926b45b79 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxSectionCard.kt @@ -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() + } + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxSnackbarHost.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxSnackbarHost.kt new file mode 100644 index 000000000..1f66c9800 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxSnackbarHost.kt @@ -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) +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxStatusPill.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxStatusPill.kt new file mode 100644 index 000000000..ebb817ad5 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxStatusPill.kt @@ -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, + ) +}