feat: add split tunneling and tunnel configuration screen
This adds a tunnel configuration screen where user can configure split application tunneling and change tunnel name. Additional QR code validation added to prevent adding of invalid QR codes to tunnels. Minor UI quality-of-life changes. Closes #3
This commit is contained in:
parent
49bf7fa8b9
commit
8b81831910
|
@ -28,6 +28,7 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
|
||||||
|
|
||||||
<p float="center">
|
<p float="center">
|
||||||
<img label="Main" style="padding-right:25px" src="./asset/main_screen.png" width="200" />
|
<img label="Main" style="padding-right:25px" src="./asset/main_screen.png" width="200" />
|
||||||
|
<img label="Config" style="padding-left:25px" src="./asset/config_screen.png" width="200" />
|
||||||
<img label="Settings" style="padding-left:25px" src="./asset/settings_screen.png" width="200" />
|
<img label="Settings" style="padding-left:25px" src="./asset/settings_screen.png" width="200" />
|
||||||
<img label="Support" style="padding-left:25px" src="./asset/support_screen.png" width="200" />
|
<img label="Support" style="padding-left:25px" src="./asset/support_screen.png" width="200" />
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -15,8 +15,8 @@ android {
|
||||||
namespace = "com.zaneschepke.wireguardautotunnel"
|
namespace = "com.zaneschepke.wireguardautotunnel"
|
||||||
compileSdk = 33
|
compileSdk = 33
|
||||||
|
|
||||||
val versionMajor = 1
|
val versionMajor = 2
|
||||||
val versionMinor = 2
|
val versionMinor = 0
|
||||||
val versionPatch = 0
|
val versionPatch = 0
|
||||||
val versionBuild = 0
|
val versionBuild = 0
|
||||||
|
|
||||||
|
@ -101,6 +101,7 @@ dependencies {
|
||||||
implementation("com.google.accompanist:accompanist-permissions:${rExtra.get("accompanistVersion")}")
|
implementation("com.google.accompanist:accompanist-permissions:${rExtra.get("accompanistVersion")}")
|
||||||
implementation("com.google.accompanist:accompanist-flowlayout:${rExtra.get("accompanistVersion")}")
|
implementation("com.google.accompanist:accompanist-flowlayout:${rExtra.get("accompanistVersion")}")
|
||||||
implementation("com.google.accompanist:accompanist-navigation-animation:${rExtra.get("accompanistVersion")}")
|
implementation("com.google.accompanist:accompanist-navigation-animation:${rExtra.get("accompanistVersion")}")
|
||||||
|
implementation("com.google.accompanist:accompanist-drawablepainter:${rExtra.get("accompanistVersion")}")
|
||||||
|
|
||||||
//db
|
//db
|
||||||
implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}")
|
implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}")
|
||||||
|
|
|
@ -17,6 +17,11 @@
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<!--start service on boot permission-->
|
<!--start service on boot permission-->
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:name=".WireGuardAutoTunnel"
|
android:name=".WireGuardAutoTunnel"
|
||||||
|
|
|
@ -19,11 +19,64 @@ data class TunnelConfig(
|
||||||
var name : String,
|
var name : String,
|
||||||
var wgQuick : String
|
var wgQuick : String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return Json.encodeToString(serializer(), this)
|
return Json.encodeToString(serializer(), this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val INCLUDED_APPLICATIONS = "IncludedApplications = "
|
||||||
|
private const val EXCLUDED_APPLICATIONS = "ExcludedApplications = "
|
||||||
|
private const val INTERFACE = "[Interface]"
|
||||||
|
private const val NEWLINE_CHAR = "\n"
|
||||||
|
private const val APP_CONFIG_SEPARATOR = ", "
|
||||||
|
|
||||||
|
private fun addApplicationsToConfig(appConfig : String, wgQuick : String) : String {
|
||||||
|
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
|
||||||
|
val interfaceIndex = configList.indexOf(INTERFACE)
|
||||||
|
configList.add(interfaceIndex + 1, appConfig)
|
||||||
|
return configList.joinToString(NEWLINE_CHAR)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAllApplicationsFromConfig(wgQuick : String) : String {
|
||||||
|
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
|
||||||
|
val itr = configList.iterator()
|
||||||
|
while (itr.hasNext()) {
|
||||||
|
val next = itr.next()
|
||||||
|
if(next.contains(INCLUDED_APPLICATIONS) || next.contains(EXCLUDED_APPLICATIONS)) {
|
||||||
|
itr.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return configList.joinToString(NEWLINE_CHAR)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun setExcludedApplicationsOnQuick(packages : List<String>, wgQuick: String) : String {
|
||||||
|
if(packages.isEmpty()) {
|
||||||
|
return wgQuick
|
||||||
|
}
|
||||||
|
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
|
||||||
|
val excludeConfig = buildExcludedApplicationsString(packages)
|
||||||
|
return addApplicationsToConfig(excludeConfig, clearedWgQuick)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIncludedApplicationsOnQuick(packages : List<String>, wgQuick: String) : String {
|
||||||
|
if(packages.isEmpty()) {
|
||||||
|
return wgQuick
|
||||||
|
}
|
||||||
|
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
|
||||||
|
val includeConfig = buildIncludedApplicationsString(packages)
|
||||||
|
return addApplicationsToConfig(includeConfig, clearedWgQuick)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildExcludedApplicationsString(packages : List<String>) : String {
|
||||||
|
return EXCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildIncludedApplicationsString(packages : List<String>) : String {
|
||||||
|
return INCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
|
||||||
|
}
|
||||||
fun from(string : String) : TunnelConfig {
|
fun from(string : String) : TunnelConfig {
|
||||||
return Json.decodeFromString<TunnelConfig>(string)
|
return Json.decodeFromString<TunnelConfig>(string)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import com.wireguard.android.backend.GoBackend
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
|
@ -121,11 +122,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
fadeIn(animationSpec = tween(2000))
|
fadeIn(animationSpec = tween(1000))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
MainScreen(padding = padding, snackbarHostState = snackbarHostState)
|
MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController)
|
||||||
}
|
}
|
||||||
composable(Routes.Settings.name, enterTransition = {
|
composable(Routes.Settings.name, enterTransition = {
|
||||||
when (initialState.destination.route) {
|
when (initialState.destination.route) {
|
||||||
|
@ -143,7 +144,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
fadeIn(animationSpec = tween(2000))
|
fadeIn(animationSpec = tween(1000))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) }
|
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) }
|
||||||
|
@ -156,10 +157,13 @@ class MainActivity : AppCompatActivity() {
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
fadeIn(animationSpec = tween(2000))
|
fadeIn(animationSpec = tween(1000))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) { SupportScreen(padding = padding) }
|
}) { SupportScreen(padding = padding) }
|
||||||
|
composable("${Routes.Config.name}/{id}", enterTransition = {
|
||||||
|
fadeIn(animationSpec = tween(1000))
|
||||||
|
}) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"))}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,8 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||||
enum class Routes {
|
enum class Routes {
|
||||||
Main,
|
Main,
|
||||||
Settings,
|
Settings,
|
||||||
Support;
|
Support,
|
||||||
|
Config;
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
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
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Android
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.google.accompanist.drawablepainter.DrawablePainter
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ConfigScreen(
|
||||||
|
viewModel: ConfigViewModel = hiltViewModel(),
|
||||||
|
padding: PaddingValues,
|
||||||
|
navController: NavController,
|
||||||
|
id : String?
|
||||||
|
) {
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null)
|
||||||
|
val tunnelName = viewModel.tunnelName.collectAsStateWithLifecycle()
|
||||||
|
val packages by viewModel.packages.collectAsStateWithLifecycle()
|
||||||
|
val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle()
|
||||||
|
val include by viewModel.include.collectAsStateWithLifecycle()
|
||||||
|
val allApplications by viewModel.allApplications.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.getTunnelById(id)
|
||||||
|
viewModel.emitAllInternetCapablePackages()
|
||||||
|
viewModel.emitCurrentPackageConfigurations(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(tunnel != null) {
|
||||||
|
Column(
|
||||||
|
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),
|
||||||
|
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(
|
||||||
|
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))
|
||||||
|
} else {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
Checkbox(
|
||||||
|
checked = (checkedPackages.contains(pack.packageName)),
|
||||||
|
onCheckedChange = {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}, Modifier.padding(25.dp)) {
|
||||||
|
Text(stringResource(id = R.string.save_changes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ConfigViewModel @Inject constructor(private val application : Application,
|
||||||
|
private val tunnelRepo : Repository<TunnelConfig>) : ViewModel() {
|
||||||
|
|
||||||
|
private val _tunnel = MutableStateFlow<TunnelConfig?>(null)
|
||||||
|
private val _tunnelName = MutableStateFlow("")
|
||||||
|
val tunnelName get() = _tunnelName.asStateFlow()
|
||||||
|
val tunnel get() = _tunnel.asStateFlow()
|
||||||
|
private val _packages = MutableStateFlow(emptyList<PackageInfo>())
|
||||||
|
val packages get() = _packages.asStateFlow()
|
||||||
|
private val packageManager = application.packageManager
|
||||||
|
|
||||||
|
private val _checkedPackages = MutableStateFlow(mutableStateListOf<String>())
|
||||||
|
val checkedPackages get() = _checkedPackages.asStateFlow()
|
||||||
|
private val _include = MutableStateFlow(true)
|
||||||
|
val include get() = _include.asStateFlow()
|
||||||
|
|
||||||
|
private val _allApplications = MutableStateFlow(true)
|
||||||
|
val allApplications get() = _allApplications.asStateFlow()
|
||||||
|
|
||||||
|
suspend fun getTunnelById(id : String?) : TunnelConfig? {
|
||||||
|
return try {
|
||||||
|
if(id != null) {
|
||||||
|
val config = tunnelRepo.getById(id.toLong())
|
||||||
|
if (config != null) {
|
||||||
|
_tunnel.emit(config)
|
||||||
|
_tunnelName.emit(config.name)
|
||||||
|
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (e : Exception) {
|
||||||
|
Timber.e(e.message)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTunnelNameChange(name : String) {
|
||||||
|
_tunnelName.value = name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIncludeChange(include : Boolean) {
|
||||||
|
_include.value = include
|
||||||
|
}
|
||||||
|
fun onAddCheckedPackage(packageName : String) {
|
||||||
|
_checkedPackages.value.add(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAllApplicationsChange(allApplications : Boolean) {
|
||||||
|
_allApplications.value = allApplications
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRemoveCheckedPackage(packageName : String) {
|
||||||
|
_checkedPackages.value.remove(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun emitCurrentPackageConfigurations(id : String?) {
|
||||||
|
val tunnelConfig = getTunnelById(id)
|
||||||
|
if(tunnelConfig != null) {
|
||||||
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
|
val excludedApps = config.`interface`.excludedApplications
|
||||||
|
val includedApps = config.`interface`.includedApplications
|
||||||
|
if(excludedApps.isNullOrEmpty() && includedApps.isNullOrEmpty()) {
|
||||||
|
_allApplications.emit(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(excludedApps.isEmpty()) {
|
||||||
|
_include.emit(true)
|
||||||
|
_checkedPackages.emit(includedApps.toMutableStateList())
|
||||||
|
} else {
|
||||||
|
_include.emit(false)
|
||||||
|
_checkedPackages.emit(excludedApps.toMutableStateList())
|
||||||
|
}
|
||||||
|
_allApplications.emit(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun emitAllInternetCapablePackages() {
|
||||||
|
_packages.emit(getAllInternetCapablePackages())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAllInternetCapablePackages() : List<PackageInfo> {
|
||||||
|
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
packageManager.getPackagesHoldingPermissions(permissions, PackageManager.PackageInfoFlags.of(0L))
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onSaveAllChanges() {
|
||||||
|
var wgQuick = _tunnel.value?.wgQuick
|
||||||
|
if(wgQuick != null) {
|
||||||
|
wgQuick = if(_include.value) {
|
||||||
|
TunnelConfig.setIncludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
|
||||||
|
} else {
|
||||||
|
TunnelConfig.setExcludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
|
||||||
|
}
|
||||||
|
if(_allApplications.value) {
|
||||||
|
wgQuick = TunnelConfig.clearAllApplicationsFromConfig(wgQuick)
|
||||||
|
}
|
||||||
|
_tunnel.value?.copy(
|
||||||
|
name = _tunnelName.value,
|
||||||
|
wgQuick = wgQuick
|
||||||
|
)?.let {
|
||||||
|
tunnelRepo.save(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
@ -22,10 +21,7 @@ import androidx.compose.material.icons.filled.QrCode
|
||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.Edit
|
import androidx.compose.material.icons.rounded.Edit
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.DrawerValue
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
@ -33,17 +29,12 @@ import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.ModalDrawerSheet
|
|
||||||
import androidx.compose.material3.ModalNavigationDrawer
|
|
||||||
import androidx.compose.material3.NavigationDrawerItem
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberDrawerState
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
@ -57,7 +48,6 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.modifier.modifierLocalConsumer
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
@ -65,9 +55,11 @@ import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.NavController
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@ -75,7 +67,7 @@ import kotlinx.coroutines.launch
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValues,
|
fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValues,
|
||||||
snackbarHostState : SnackbarHostState) {
|
snackbarHostState : SnackbarHostState, navController: NavController) {
|
||||||
|
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
@ -85,7 +77,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
||||||
val viewState = viewModel.viewState.collectAsStateWithLifecycle()
|
val viewState = viewModel.viewState.collectAsStateWithLifecycle()
|
||||||
var showAlertDialog by remember { mutableStateOf(false) }
|
|
||||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
|
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
|
||||||
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
|
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
|
||||||
|
@ -131,7 +122,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Add,
|
imageVector = Icons.Rounded.Add,
|
||||||
contentDescription = "Add Tunnel",
|
contentDescription = stringResource(id = R.string.add_tunnel),
|
||||||
tint = Color.DarkGray,
|
tint = Color.DarkGray,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -157,24 +148,30 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu
|
||||||
) {
|
) {
|
||||||
// Sheet content
|
// Sheet content
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().clickable {
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
pickFileLauncher.launch("*/*")
|
pickFileLauncher.launch("*/*")
|
||||||
}.padding(10.dp)
|
}
|
||||||
|
.padding(10.dp)
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.FileOpen, contentDescription = "File Open", modifier = Modifier.padding(10.dp))
|
Icon(Icons.Filled.FileOpen, contentDescription = stringResource(id = R.string.open_file), modifier = Modifier.padding(10.dp))
|
||||||
Text("Add tunnel from files", modifier = Modifier.padding(10.dp))
|
Text(stringResource(id = R.string.add_from_file), modifier = Modifier.padding(10.dp))
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
Row(modifier = Modifier.fillMaxWidth().clickable {
|
Row(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
viewModel.onTunnelQRSelected()
|
viewModel.onTunnelQRSelected()
|
||||||
}
|
}
|
||||||
}.padding(10.dp)
|
}
|
||||||
|
.padding(10.dp)
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.QrCode, contentDescription = "QR Scan", modifier = Modifier.padding(10.dp))
|
Icon(Icons.Filled.QrCode, contentDescription = stringResource(id = R.string.qr_scan), modifier = Modifier.padding(10.dp))
|
||||||
Text("Add tunnel from QR code", modifier = Modifier.padding(10.dp))
|
Text(stringResource(id = R.string.add_from_qr), modifier = Modifier.padding(10.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,12 +198,12 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu
|
||||||
if (tunnel.id == selectedTunnel?.id) {
|
if (tunnel.id == selectedTunnel?.id) {
|
||||||
Row() {
|
Row() {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
showAlertDialog = true
|
navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
|
||||||
}) {
|
}) {
|
||||||
Icon(Icons.Rounded.Edit, "Edit")
|
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||||
}
|
}
|
||||||
IconButton(onClick = { viewModel.onDelete(tunnel) }) {
|
IconButton(onClick = { viewModel.onDelete(tunnel) }) {
|
||||||
Icon(Icons.Rounded.Delete, "Delete")
|
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -220,40 +217,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showAlertDialog && selectedTunnel != null) {
|
|
||||||
AlertDialog(onDismissRequest = {
|
|
||||||
showAlertDialog = false
|
|
||||||
}, confirmButton = {
|
|
||||||
Button(onClick = {
|
|
||||||
if (tunnels.any { it.name == selectedTunnel?.name }) {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.resources.getString(R.string.tunnel_exists),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
)
|
|
||||||
.show()
|
|
||||||
return@Button
|
|
||||||
}
|
|
||||||
viewModel.onEditTunnel(selectedTunnel!!)
|
|
||||||
showAlertDialog = false
|
|
||||||
}) {
|
|
||||||
Text("Save")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = { Text("Tunnel Edit") }, text = {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = selectedTunnel!!.name,
|
|
||||||
onValueChange = {
|
|
||||||
selectedTunnel = selectedTunnel!!.copy(
|
|
||||||
name = it
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = { Text("Tunnel Name") },
|
|
||||||
modifier = Modifier.padding(start = 15.dp, top = 5.dp),
|
|
||||||
maxLines = 1,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,22 +90,6 @@ class MainViewModel @Inject constructor(private val application : Application,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun onEditTunnel(tunnel: TunnelConfig) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
tunnelRepo.save(tunnel)
|
|
||||||
val settings = settingsRepo.getAll()
|
|
||||||
if(!settings.isNullOrEmpty() && settings[0].defaultTunnel != null) {
|
|
||||||
val setting = settings[0]
|
|
||||||
val defaultTunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
|
|
||||||
if(defaultTunnelConfig.id == tunnel.id) {
|
|
||||||
setting.defaultTunnel = tunnel.toString()
|
|
||||||
settingsRepo.save(setting)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch {
|
fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch {
|
||||||
ServiceTracker.actionOnService( Action.START, application, WireGuardTunnelService::class.java,
|
ServiceTracker.actionOnService( Action.START, application, WireGuardTunnelService::class.java,
|
||||||
mapOf(application.resources.getString(R.string.tunnel_extras_key) to tunnelConfig.toString()))
|
mapOf(application.resources.getString(R.string.tunnel_extras_key) to tunnelConfig.toString()))
|
||||||
|
@ -118,8 +102,10 @@ class MainViewModel @Inject constructor(private val application : Application,
|
||||||
suspend fun onTunnelQRSelected() {
|
suspend fun onTunnelQRSelected() {
|
||||||
codeScanner.scan().collect {
|
codeScanner.scan().collect {
|
||||||
Timber.d(it)
|
Timber.d(it)
|
||||||
if(!it.isNullOrEmpty()) {
|
if(!it.isNullOrEmpty() && it.contains(application.resources.getString(R.string.config_validation))) {
|
||||||
tunnelRepo.save(TunnelConfig(name = defaultConfigName(), wgQuick = it))
|
tunnelRepo.save(TunnelConfig(name = defaultConfigName(), wgQuick = it))
|
||||||
|
} else {
|
||||||
|
showSnackBarMessage("Invalid QR code. Try again.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,28 +4,24 @@ import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Done
|
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material.icons.outlined.AddCircleOutline
|
|
||||||
import androidx.compose.material.icons.outlined.Done
|
|
||||||
import androidx.compose.material.icons.rounded.LocationOff
|
import androidx.compose.material.icons.rounded.LocationOff
|
||||||
import androidx.compose.material.icons.rounded.Map
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
@ -51,6 +47,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
@ -68,7 +65,6 @@ import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class,
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class,
|
||||||
|
@ -84,6 +80,9 @@ fun SettingsScreen(
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
val viewState by viewModel.viewState.collectAsStateWithLifecycle()
|
val viewState by viewModel.viewState.collectAsStateWithLifecycle()
|
||||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
||||||
|
@ -122,7 +121,7 @@ fun SettingsScreen(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)) {
|
.padding(padding)) {
|
||||||
Icon(Icons.Rounded.LocationOff, contentDescription = "Map", modifier = Modifier
|
Icon(Icons.Rounded.LocationOff, contentDescription = stringResource(id = R.string.map), modifier = Modifier
|
||||||
.padding(30.dp)
|
.padding(30.dp)
|
||||||
.size(128.dp))
|
.size(128.dp))
|
||||||
Text(stringResource(R.string.prominent_background_location_title), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 20.sp)
|
Text(stringResource(R.string.prominent_background_location_title), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 20.sp)
|
||||||
|
@ -138,7 +137,7 @@ fun SettingsScreen(
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
navController.navigate(Routes.Main.name)
|
navController.navigate(Routes.Main.name)
|
||||||
}) {
|
}) {
|
||||||
Text("No thanks")
|
Text(stringResource(id = R.string.no_thanks))
|
||||||
}
|
}
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -149,7 +148,7 @@ fun SettingsScreen(
|
||||||
context.startActivity(intentSettings)
|
context.startActivity(intentSettings)
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Text("Turn on")
|
Text(stringResource(id = R.string.turn_on))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,6 +178,9 @@ fun SettingsScreen(
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.clickable(indication = null, interactionSource = interactionSource) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
|
@ -37,4 +37,24 @@
|
||||||
<string name="support_text">Thank you for using WG Tunnel! If you are experiencing issues with the app, please reach out on Discord or create an issue on Github. I will try to address the issue as quickly as possible. Thank you!</string>
|
<string name="support_text">Thank you for using WG Tunnel! If you are experiencing issues with the app, please reach out on Discord or create an issue on Github. I will try to address the issue as quickly as possible. Thank you!</string>
|
||||||
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
||||||
<string name="trusted_ssid_value_description">Submit SSID</string>
|
<string name="trusted_ssid_value_description">Submit SSID</string>
|
||||||
|
<string name="config_validation">[Interface]</string>
|
||||||
|
<string name="invalid_qr">Invalid QR code.</string>
|
||||||
|
<string name="add_from_file">Add tunnel from files</string>
|
||||||
|
<string name="open_file">File Open</string>
|
||||||
|
<string name="add_from_qr">Add tunnel from QR code</string>
|
||||||
|
<string name="qr_scan">QR Scan</string>
|
||||||
|
<string name="tunnel_edit">Tunnel Edit</string>
|
||||||
|
<string name="tunnel_name">Tunnel Name</string>
|
||||||
|
<string name="edit">Edit</string>
|
||||||
|
<string name="delete">Delete</string>
|
||||||
|
<string name="add_tunnel">Add Tunnel</string>
|
||||||
|
<string name="exclude">Exclude</string>
|
||||||
|
<string name="include">Include</string>
|
||||||
|
<string name="tunnel_all">Tunnel all applications</string>
|
||||||
|
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||||
|
<string name="save_changes">Save changes</string>
|
||||||
|
<string name="icon">Icon</string>
|
||||||
|
<string name="no_thanks">No thanks</string>
|
||||||
|
<string name="turn_on">Turn on</string>
|
||||||
|
<string name="map">Map</string>
|
||||||
</resources>
|
</resources>
|
Binary file not shown.
After Width: | Height: | Size: 154 KiB |
|
@ -4,12 +4,11 @@ buildscript {
|
||||||
val objectBoxVersion by extra("3.5.1")
|
val objectBoxVersion by extra("3.5.1")
|
||||||
val hiltVersion by extra("2.44")
|
val hiltVersion by extra("2.44")
|
||||||
val accompanistVersion by extra("0.31.2-alpha")
|
val accompanistVersion by extra("0.31.2-alpha")
|
||||||
val cameraVersion by extra("1.3.0-beta01")
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("io.objectbox:objectbox-gradle-plugin:$objectBoxVersion")
|
classpath("io.objectbox:objectbox-gradle-plugin:$objectBoxVersion")
|
||||||
classpath("com.google.gms:google-services:4.3.15")
|
classpath("com.google.gms:google-services:4.3.15")
|
||||||
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.5")
|
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.6")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue