diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsActivity.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsActivity.kt index e6be919fc..1f6b252e0 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsActivity.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsActivity.kt @@ -7,6 +7,8 @@ * option) any later version. */ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + package org.strongswan.android.ui import android.Manifest @@ -25,7 +27,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Checkbox -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.ListItem import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -162,7 +165,7 @@ private fun SelectedAppsScreen( ) if (loading) { - CircularProgressIndicator(Modifier.padding(24.dp).size(32.dp)) + LoadingIndicator(Modifier.padding(24.dp).size(32.dp)) } else { diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/HomeScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/HomeScreen.kt index 6d4b75b18..19c62cb81 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/HomeScreen.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/HomeScreen.kt @@ -7,6 +7,8 @@ * option) any later version. */ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + package org.strongswan.android.ui.compose import androidx.compose.animation.AnimatedContent @@ -72,7 +74,8 @@ import androidx.compose.material.icons.rounded.Wifi import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon @@ -622,7 +625,7 @@ private fun ConnectionCard( content = MaterialTheme.colorScheme.onErrorContainer, ) { Text(stringResource(R.string.disconnect), fontSize = 16.sp) } State.CONNECTING, State.DISCONNECTING -> HeroButton(onClick = onCancel) { - CircularProgressIndicator(Modifier.size(18.dp), strokeWidth = 2.dp) + LoadingIndicator(Modifier.size(18.dp)) Spacer(Modifier.size(12.dp)) Text(stringResource(android.R.string.cancel), fontSize = 16.sp) } @@ -885,7 +888,7 @@ private fun AvailabilityCard( ) if (probing) { - CircularProgressIndicator(Modifier.size(14.dp), strokeWidth = 2.dp) + LoadingIndicator(Modifier.size(14.dp)) Spacer(Modifier.size(12.dp)) } IconButton(onClick = onRefresh, enabled = !probing) { @@ -906,7 +909,7 @@ private fun AvailabilityCard( modifier = Modifier.padding(top = 4.dp), ) { - CircularProgressIndicator(Modifier.size(14.dp), strokeWidth = 2.dp) + LoadingIndicator(Modifier.size(14.dp)) Text( text = stringResource(R.string.availability_measuring), style = MaterialTheme.typography.bodySmall, @@ -1159,7 +1162,7 @@ private fun ProbeRow(result: ProbeResult, measuring: Boolean, history: List(extraBufferCapacity = 4) + val messages: SharedFlow = _messages + private val _ui = MutableStateFlow(HomeUiState()) val ui: StateFlow = _ui @@ -379,7 +385,7 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V withContext(Dispatchers.Main) { if (uri == null) { - Toast.makeText(context, R.string.share_profile_failed, Toast.LENGTH_LONG).show() + _messages.tryEmit(R.string.share_profile_failed) return@withContext } val intent = Intent(Intent.ACTION_SEND) @@ -391,7 +397,7 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V } catch (_: ActivityNotFoundException) { - Toast.makeText(context, R.string.share_profile_failed, Toast.LENGTH_LONG).show() + _messages.tryEmit(R.string.share_profile_failed) } } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ImportScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ImportScreen.kt index 519851afb..286bdb4da 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ImportScreen.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ImportScreen.kt @@ -7,6 +7,8 @@ * option) any later version. */ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + package org.strongswan.android.ui.compose import androidx.activity.ComponentActivity @@ -29,7 +31,8 @@ import androidx.compose.material.icons.rounded.Dns import androidx.compose.material.icons.rounded.FileDownload import androidx.compose.material.icons.rounded.Language import androidx.compose.material.icons.rounded.VpnKey -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -138,7 +141,7 @@ fun ImportScreen( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { - CircularProgressIndicator() + LoadingIndicator() } return } @@ -240,7 +243,7 @@ fun ImportScreen( } if (model.userCertLoading) { - CircularProgressIndicator(Modifier.size(18.dp), strokeWidth = 2.dp) + LoadingIndicator(Modifier.size(18.dp)) } } if (model.showImportUserCert) 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 32a9ad655..883347bd8 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 @@ -7,6 +7,8 @@ * option) any later version. */ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + package org.strongswan.android.ui.compose import org.strongswan.android.ui.compose.components.ShxSnackbarHost @@ -53,11 +55,12 @@ import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Route import androidx.compose.material.icons.rounded.Rule import androidx.compose.material3.AlertDialog -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.LinearWavyProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold @@ -328,7 +331,7 @@ private fun RoutingContent(snackbarHostState: SnackbarHostState) ) if (ruState.status == RuIpListStatus.UPDATING) { - LinearProgressIndicator( + LinearWavyProgressIndicator( Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 4.dp), @@ -586,7 +589,7 @@ private fun RuToggleRow( } if (updating) { - CircularProgressIndicator(Modifier.size(14.dp), strokeWidth = 2.dp) + LoadingIndicator(Modifier.size(14.dp)) Spacer(Modifier.width(12.dp)) } else diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxApp.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxApp.kt index cf2ed8835..9fd3b9070 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxApp.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxApp.kt @@ -11,14 +11,14 @@ package org.strongswan.android.ui.compose import android.content.Intent import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.scaleIn import androidx.compose.animation.togetherWith import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -44,7 +44,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.NavigationRailItemDefaults +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.foundation.layout.Box +import org.strongswan.android.ui.compose.components.ShxSnackbarHost import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors @@ -105,6 +109,12 @@ fun ShxApp(viewModel: HomeViewModel = viewModel()) var pendingShare by remember { mutableStateOf(null) } val shareProfile: (VpnProfile) -> Unit = { pendingShare = it } + val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(viewModel) + { + viewModel.messages.collect { snackbarHostState.showSnackbar(context.getString(it)) } + } + /* the home list keeps its scroll state across tab switches */ val homeListState = rememberLazyListState() @@ -112,6 +122,13 @@ fun ShxApp(viewModel: HomeViewModel = viewModel()) * and on tablets (Stage 3 of ui-ux-plan). Item colors are built here in a * composable context - the items lambda itself does not compose. */ val navItemColors = shxNavigationItemColors() + val fadeSpec = MaterialTheme.motionScheme.defaultEffectsSpec() + val scaleSpec = MaterialTheme.motionScheme.defaultSpatialSpec() + + /* from any other tab "back" returns to Connect instead of leaving the app */ + BackHandler(enabled = state.tab != ShxTab.HOME) { + viewModel.setTab(ShxTab.HOME) + } NavigationSuiteScaffold( navigationSuiteItems = { ShxNavigationSuiteItems(selected = state.tab, onSelect = viewModel::setTab, colors = navItemColors) @@ -122,13 +139,16 @@ fun ShxApp(viewModel: HomeViewModel = viewModel()) modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background, ) { + Box(Modifier.fillMaxSize()) + { AnimatedContent( targetState = state.tab, transitionSpec = { - val forward = targetState.ordinal >= initialState.ordinal - val direction = if (forward) 1 else -1 - (slideInHorizontally(tween(240)) { it / 6 * direction } + fadeIn(tween(240))) togetherWith - (slideOutHorizontally(tween(180)) { -it / 6 * direction } + fadeOut(tween(160))) + /* tabs are not spatially ordered: fade-through (outgoing fades out + * first, incoming fades in with a slight scale), driven by the + * expressive motion scheme */ + (fadeIn(fadeSpec) + scaleIn(scaleSpec, initialScale = 0.96f)) togetherWith + fadeOut(tween(90)) }, label = "tabContent", ) { tab -> @@ -159,6 +179,11 @@ fun ShxApp(viewModel: HomeViewModel = viewModel()) ShxTab.ROUTING -> RoutingScreen() ShxTab.SETTINGS -> SettingsScreen(profiles = state.profiles) } + } + ShxSnackbarHost( + hostState = snackbarHostState, + modifier = Modifier.align(Alignment.BottomCenter), + ) } pendingShare?.let { profile -> ShareProfileDialog(