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(