From 7ca5de1836712e42b072c47a5abc30c6a29631c3 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Wed, 19 Jul 2023 09:02:31 -0400 Subject: [PATCH] fix: screens not scrollable in landscape Fixes not being able to scroll when in landscape mode if content is off screen. Fixes FAB being in the way of controlling tunnels by making it disappear on scroll down. Bump targetSdk to 34 --- app/build.gradle.kts | 8 +- .../ui/screens/config/ConfigScreen.kt | 225 ++++++++++-------- .../ui/screens/detail/DetailScreen.kt | 4 + .../ui/screens/main/MainScreen.kt | 62 +++-- .../ui/screens/settings/SettingsScreen.kt | 5 + .../ui/screens/support/SupportScreen.kt | 3 + 6 files changed, 192 insertions(+), 115 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index eab4a92..a0e4195 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,25 +5,25 @@ plugins { id("org.jetbrains.kotlin.android") kotlin("kapt") id("com.google.dagger.hilt.android") - id("io.objectbox") id("com.google.gms.google-services") id("com.google.firebase.crashlytics") id("org.jetbrains.kotlin.plugin.serialization") + id("io.objectbox") } android { namespace = "com.zaneschepke.wireguardautotunnel" - compileSdk = 33 + compileSdk = 34 val versionMajor = 2 val versionMinor = 1 - val versionPatch = 2 + val versionPatch = 3 val versionBuild = 0 defaultConfig { applicationId = "com.zaneschepke.wireguardautotunnel" minSdk = 29 - targetSdk = 33 + targetSdk = 34 versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild versionName = "${versionMajor}.${versionMinor}.${versionPatch}" diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index 53e3cae..f22727c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -3,10 +3,8 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.config import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -72,127 +70,162 @@ fun ConfigScreen( } if(tunnel != null) { - Column( + LazyColumn( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top, modifier = Modifier .fillMaxSize() .padding(padding) ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - OutlinedTextField( - value = tunnelName.value, - onValueChange = { - viewModel.onTunnelNameChange(it) - }, - label = { Text(stringResource(id = R.string.tunnel_name)) }, - maxLines = 1, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions( - onDone = { - focusManager.clearFocus() - keyboardController?.hide() - viewModel.onTunnelNameChange(tunnelName.value) - } - ), - ) - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text(stringResource(id = R.string.tunnel_all)) - Switch( - checked = allApplications, - onCheckedChange = { - viewModel.onAllApplicationsChange(!allApplications) - } - ) - } - if(!allApplications) { - Row(modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 7.dp), + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween) { - Row(verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween){ - Text(stringResource(id = R.string.include)) - Checkbox( - checked = include, - onCheckedChange = { - viewModel.onIncludeChange(!include) + horizontalArrangement = Arrangement.SpaceBetween + ) { + OutlinedTextField( + value = tunnelName.value, + onValueChange = { + viewModel.onTunnelNameChange(it) + }, + label = { Text(stringResource(id = R.string.tunnel_name)) }, + maxLines = 1, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + keyboardController?.hide() + viewModel.onTunnelNameChange(tunnelName.value) } - ) - } - Row(verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween){ - Text(stringResource(id = R.string.exclude)) - Checkbox( - checked = !include, - onCheckedChange = { - viewModel.onIncludeChange(!include) - } - ) - } + ), + ) } - LazyColumn(modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(.75f) - .padding(horizontal = 14.dp, vertical = 7.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start) { + } + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(id = R.string.tunnel_all)) + Switch( + checked = allApplications, + onCheckedChange = { + viewModel.onAllApplicationsChange(!allApplications) + } + ) + } + } + if (!allApplications) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(id = R.string.include)) + Checkbox( + checked = include, + onCheckedChange = { + viewModel.onIncludeChange(!include) + } + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(id = R.string.exclude)) + Checkbox( + checked = !include, + onCheckedChange = { + viewModel.onIncludeChange(!include) + } + ) + } + } + } +// LazyColumn( +// modifier = Modifier +// .fillMaxWidth() +// .fillMaxHeight(.75f) +// .padding(horizontal = 14.dp, vertical = 7.dp), +// verticalArrangement = Arrangement.Center, +// horizontalAlignment = Alignment.Start +// ) { items(packages) { pack -> - Row(verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(5.dp) ) { - val drawable = pack.applicationInfo?.loadIcon(context.packageManager) - if(drawable != null) { - Image(painter = DrawablePainter(drawable), stringResource(id = R.string.icon), modifier = Modifier.size(50.dp, 50.dp)) + val drawable = + pack.applicationInfo?.loadIcon(context.packageManager) + if (drawable != null) { + Image( + painter = DrawablePainter(drawable), + stringResource(id = R.string.icon), + modifier = Modifier.size(50.dp, 50.dp) + ) } else { - Icon(Icons.Rounded.Android, stringResource(id = R.string.edit), modifier = Modifier.size(50.dp, 50.dp)) + Icon( + Icons.Rounded.Android, + stringResource(id = R.string.edit), + modifier = Modifier.size(50.dp, 50.dp) + ) } - Text(pack.applicationInfo.loadLabel(context.packageManager).toString(), modifier = Modifier.padding(5.dp)) + Text( + pack.applicationInfo.loadLabel(context.packageManager) + .toString(), modifier = Modifier.padding(5.dp) + ) } Checkbox( checked = (checkedPackages.contains(pack.packageName)), onCheckedChange = { - if(it) viewModel.onAddCheckedPackage(pack.packageName) else viewModel.onRemoveCheckedPackage(pack.packageName) + if (it) viewModel.onAddCheckedPackage(pack.packageName) else viewModel.onRemoveCheckedPackage( + pack.packageName + ) } ) } } } - } - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Button(onClick = { - scope.launch { - viewModel.onSaveAllChanges() - Toast.makeText(context, context.resources.getString(R.string.config_changes_saved), Toast.LENGTH_LONG).show() - navController.navigate(Routes.Main.name) + item { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Button(onClick = { + scope.launch { + viewModel.onSaveAllChanges() + Toast.makeText( + context, + context.resources.getString(R.string.config_changes_saved), + Toast.LENGTH_LONG + ).show() + navController.navigate(Routes.Main.name) + } + }, Modifier.padding(25.dp)) { + Text(stringResource(id = R.string.save_changes)) } - }, Modifier.padding(25.dp)) { - Text(stringResource(id = R.string.save_changes)) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt index ca8bc06..c663ebd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt @@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -43,6 +45,7 @@ fun DetailScreen( val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle() val lastHandshake by viewModel.lastHandshake.collectAsStateWithLifecycle(emptyMap()) + LaunchedEffect(Unit) { viewModel.getTunnelById(id) } @@ -58,6 +61,7 @@ fun DetailScreen( verticalArrangement = Arrangement.Top, modifier = Modifier .fillMaxSize() + .verticalScroll(rememberScrollState()) .padding(padding) ) { Row( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index 7a31bea..ebdb8d5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -3,6 +3,9 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main import android.annotation.SuppressLint import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement @@ -43,11 +46,16 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback @@ -65,7 +73,6 @@ import com.zaneschepke.wireguardautotunnel.ui.Routes import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed import com.zaneschepke.wireguardautotunnel.ui.theme.mint -import com.zaneschepke.wireguardautotunnel.ui.theme.pinkRed import kotlinx.coroutines.launch @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @@ -78,6 +85,7 @@ fun MainScreen( val haptic = LocalHapticFeedback.current val context = LocalContext.current + val isVisible = rememberSaveable { mutableStateOf(true) } val scope = rememberCoroutineScope() val sheetState = rememberModalBottomSheetState() @@ -89,6 +97,23 @@ fun MainScreen( val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN) val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("") + // Nested scroll for control FAB + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + // Hide FAB + if (available.y < -1) { + isVisible.value = false + } + // Show FAB + if (available.y > 1) { + isVisible.value = true + } + return Offset.Zero + } + } + } + LaunchedEffect(viewState.value) { if (viewState.value.showSnackbarMessage) { val result = snackbarHostState.showSnackbar( @@ -118,20 +143,26 @@ fun MainScreen( }) }, floatingActionButtonPosition = FabPosition.End, - floatingActionButton = { - FloatingActionButton( - modifier = Modifier.padding(bottom = 90.dp), - onClick = { - showBottomSheet = true - }, - containerColor = MaterialTheme.colorScheme.secondary, - shape = RoundedCornerShape(16.dp), + floatingActionButton = { + AnimatedVisibility( + visible = isVisible.value, + enter = slideInVertically(initialOffsetY = { it * 2 }), + exit = slideOutVertically(targetOffsetY = { it * 2 }), ) { - Icon( - imageVector = Icons.Rounded.Add, - contentDescription = stringResource(id = R.string.add_tunnel), - tint = Color.DarkGray, - ) + FloatingActionButton( + modifier = Modifier.padding(bottom = 90.dp), + onClick = { + showBottomSheet = true + }, + containerColor = MaterialTheme.colorScheme.secondary, + shape = RoundedCornerShape(16.dp), + ) { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = stringResource(id = R.string.add_tunnel), + tint = Color.DarkGray, + ) + } } } ) { @@ -204,7 +235,8 @@ fun MainScreen( .padding(padding) ) { - LazyColumn(modifier = Modifier.fillMaxSize()) { + LazyColumn(modifier = Modifier.fillMaxSize() + .nestedScroll(nestedScrollConnection),) { items(tunnels.toList()) { tunnel -> RowListItem(leadingIcon = Icons.Rounded.Circle, leadingIconColor = when (handshakeStatus) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index 1b80a18..b02deee 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -16,8 +16,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.outlined.Add @@ -91,6 +93,7 @@ fun SettingsScreen( val backgroundLocationState = rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION) var currentText by remember { mutableStateOf("") } + val scrollState = rememberScrollState() LaunchedEffect(viewState) { if (viewState.showSnackbarMessage) { @@ -120,6 +123,7 @@ fun SettingsScreen( verticalArrangement = Arrangement.Top, modifier = Modifier .fillMaxSize() + .verticalScroll(scrollState) .padding(padding)) { Icon(Icons.Rounded.LocationOff, contentDescription = stringResource(id = R.string.map), modifier = Modifier .padding(30.dp) @@ -178,6 +182,7 @@ fun SettingsScreen( verticalArrangement = Arrangement.Top, modifier = Modifier .fillMaxSize() + .verticalScroll(scrollState) .clickable(indication = null, interactionSource = interactionSource) { focusManager.clearFocus() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt index 7237e05..93147e4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text @@ -43,6 +45,7 @@ fun SupportScreen(padding : PaddingValues) { verticalArrangement = Arrangement.Top, modifier = Modifier .fillMaxSize() + .verticalScroll(rememberScrollState()) .padding(padding)) { Text(stringResource(R.string.support_text), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp) Row(