Compare commits

..

10 Commits

Author SHA1 Message Date
GitHub Actions 63d46c1817 Automated build update 2024-11-23 02:13:49 +00:00
Zane Schepke f9ef308f8a feat: user logger toggle control
Allow users to toggle on and off local logger. This will hopefully help address #437
Allow shortcuts activation on AndroidTV
closes #451
2024-11-22 21:06:58 -05:00
GitHub Actions b2938c3ac8 Automated build update 2024-11-18 03:39:50 +00:00
Zane Schepke 17b575b7f2 chore: update readme 2024-11-17 01:34:46 -05:00
Zane Schepke 777a948244 fix: add back per tunnel ping settings 2024-11-17 01:30:13 -05:00
Zane Schepke 72bf0a1979 fix: androidtv ui bugs and crash
closes #443
2024-11-16 23:22:25 -05:00
GitHub Actions c7a45845d6 Automated build update 2024-11-14 02:53:17 +00:00
Zane Schepke c2725bf066 chore: changelog to releases 2024-11-13 21:48:12 -05:00
Zane Schepke 2993f1db22 fix: delete tunnel bug
closes #435
2024-11-12 22:47:03 -05:00
Zane Schepke e9ae48f96c add: donation selection to support 2024-11-12 22:46:06 -05:00
24 changed files with 702 additions and 477 deletions
+29
View File
@@ -94,6 +94,32 @@ jobs:
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
# update latest tag
- name: Set latest tag
uses: rickstaa/action-create-tag@v1
id: tag_creation
with:
tag: "latest" # or any tag name you wish to use
message: "Automated tag for HEAD commit"
force_push_tag: true
tag_exists_error: false
- name: Get latest release
id: latest_release
uses: kaliber5/action-get-release@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
latest: true
- name: Generate Changelog
id: changelog
uses: requarks/changelog-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
toTag: ${{ github.event_name == 'schedule' && 'nightly' || steps.latest_release.outputs.tag_name }}
fromTag: "latest"
writeToFile: false # we won't write to file, just output
# create keystore path for gradle to read
- name: Create keystore path env var
run: |
@@ -209,6 +235,9 @@ jobs:
SHA256 fingerprint:
```${{ steps.checksum.outputs.checksum }}```
### Changelog
${{ steps.changelog.outputs.changes }}
tag_name: ${{ env.TAG_NAME }}
name: ${{ env.TAG_NAME }}
draft: false
-1
View File
@@ -5,7 +5,6 @@ WG Tunnel
<div align="center">
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/rbRRNh6H7V)
[![X Community](https://img.shields.io/badge/X-000000?style=for-the-badge&logo=x&logoColor=white)](https://twitter.com/i/communities/1780655267685736818)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/wgtunnel)
</div>
@@ -6,6 +6,7 @@ import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.data.datastore.LocaleStorage
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
@@ -32,6 +33,9 @@ class WireGuardAutoTunnel : Application() {
@Inject
lateinit var logReader: LogReader
@Inject
lateinit var appStateRepository: AppStateRepository
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@@ -54,7 +58,10 @@ class WireGuardAutoTunnel : Application() {
}
if (!isRunningOnTv()) {
applicationScope.launch(ioDispatcher) {
logReader.start()
if (appStateRepository.isLocalLogsEnabled()) {
Timber.d("Starting logger")
logReader.start()
}
}
}
}
@@ -26,6 +26,7 @@ class DataStoreManager(
val currentSSID = stringPreferencesKey("CURRENT_SSID")
val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED")
val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED")
val isLocalLogsEnabled = booleanPreferencesKey("LOCAL_LOGS_ENABLED")
val theme = stringPreferencesKey("THEME")
}
@@ -7,6 +7,7 @@ data class GeneralState(
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT,
val theme: Theme = Theme.AUTOMATIC,
) {
companion object {
@@ -14,5 +15,6 @@ data class GeneralState(
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false
const val IS_TUNNEL_STATS_EXPANDED = false
const val IS_LOGS_ENABLED_DEFAULT = false
}
}
@@ -29,5 +29,9 @@ interface AppStateRepository {
suspend fun getTheme(): Theme
suspend fun isLocalLogsEnabled(): Boolean
suspend fun setLocalLogsEnabled(enabled: Boolean)
val generalStateFlow: Flow<GeneralState>
}
@@ -69,6 +69,14 @@ class DataStoreAppStateRepository(
} ?: Theme.AUTOMATIC
}
override suspend fun isLocalLogsEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.isLocalLogsEnabled) ?: GeneralState.IS_LOGS_ENABLED_DEFAULT
}
override suspend fun setLocalLogsEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.isLocalLogsEnabled, enabled)
}
override val generalStateFlow: Flow<GeneralState> =
dataStoreManager.preferencesFlow.map { prefs ->
prefs?.let { pref ->
@@ -84,6 +92,7 @@ class DataStoreAppStateRepository(
pref[DataStoreManager.pinLockEnabled]
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
isLocalLogsEnabled = pref[DataStoreManager.isLocalLogsEnabled] ?: GeneralState.IS_LOGS_ENABLED_DEFAULT,
theme = getTheme(),
)
} catch (e: IllegalArgumentException) {
@@ -49,7 +49,7 @@ constructor(
vpnState.copy(
tunnelConfig = tunnels.firstOrNull { it.id == vpnState.tunnelConfig?.id },
)
}.stateIn(applicationScope, SharingStarted.Lazily, VpnState())
}.stateIn(applicationScope, SharingStarted.Eagerly, VpnState())
private var statsJob: Job? = null
@@ -2,13 +2,20 @@ package com.zaneschepke.wireguardautotunnel.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
@@ -19,8 +26,10 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
import javax.inject.Provider
@@ -32,7 +41,9 @@ constructor(
private val appDataRepository: AppDataRepository,
private val tunnelService: Provider<TunnelService>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@AppShell private val rootShell: Provider<RootShell>,
private val serviceManager: ServiceManager,
private val logReader: LogReader,
) : ViewModel() {
val uiState =
@@ -56,11 +67,14 @@ constructor(
AppUiState(),
)
private val _isAppReady = MutableStateFlow<Boolean>(false)
private val _isAppReady = MutableStateFlow(false)
val isAppReady = _isAppReady.asStateFlow()
private val _configurationChange = MutableStateFlow(false)
val configurationChange = _configurationChange.asStateFlow()
init {
viewModelScope.launch {
viewModelScope.launch(ioDispatcher) {
initPin()
initAutoTunnel()
initTunnel()
@@ -76,7 +90,6 @@ constructor(
}
private suspend fun initTunnel() {
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startStatsJob()
val activeTunnels = appDataRepository.tunnels.getActive()
if (activeTunnels.isNotEmpty() &&
tunnelService.get().getState() == TunnelState.DOWN
@@ -107,4 +120,95 @@ constructor(
fun setLocationDisclosureShown() = viewModelScope.launch {
appDataRepository.appState.setLocationDisclosureShown(true)
}
fun onToggleLocalLogging() = viewModelScope.launch(ioDispatcher) {
with(uiState.value.generalState) {
val toggledOn = !isLocalLogsEnabled
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
if (!toggledOn) onLoggerStop()
_configurationChange.update {
true
}
}
}
private suspend fun onLoggerStop() {
logReader.stop()
logReader.deleteAndClearLogs()
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
copy(
isAlwaysOnVpnEnabled = !isAlwaysOnVpnEnabled,
),
)
}
}
fun onToggleRestartAtBoot() = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
copy(
isRestoreOnBootEnabled = !isRestoreOnBootEnabled,
),
)
}
}
fun onToggleShortcutsEnabled() = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
this.copy(
isShortcutsEnabled = !isShortcutsEnabled,
),
)
}
}
private fun saveKernelMode(enabled: Boolean) = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
this.copy(
isKernelEnabled = enabled,
),
)
}
}
fun onToggleKernelMode() = viewModelScope.launch {
with(uiState.value.settings) {
if (!isKernelEnabled) {
requestRoot().onSuccess {
if (!isKernelSupported()) return@onSuccess SnackbarController.showMessage(StringValue.StringResource(R.string.kernel_not_supported))
appDataRepository.settings.save(
copy(
isKernelEnabled = true,
isAmneziaEnabled = false,
),
)
}
} else {
saveKernelMode(enabled = false)
}
}
}
private suspend fun isKernelSupported(): Boolean {
return withContext(ioDispatcher) {
WgQuickBackend.hasKernelSupport()
}
}
private suspend fun requestRoot(): Result<Unit> {
return withContext(ioDispatcher) {
kotlin.runCatching {
rootShell.get().start()
SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
}.onFailure {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
}
}
}
}
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
@@ -24,9 +25,7 @@ import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -42,7 +41,6 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusRequester
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
@@ -65,8 +63,8 @@ import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import javax.inject.Inject
import kotlin.system.exitProcess
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@@ -83,11 +81,11 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var tunnelService: TunnelService
private val viewModel by viewModels<AppViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel by viewModels<AppViewModel>()
installSplashScreen().apply {
setKeepOnScreenCondition {
!viewModel.isAppReady.value
@@ -95,15 +93,23 @@ class MainActivity : AppCompatActivity() {
}
setContent {
val appUiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycle = this.lifecycle)
val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
val configurationChange by viewModel.configurationChange.collectAsStateWithLifecycle()
val navController = rememberNavController()
val rootItemFocusRequester = remember { FocusRequester() }
LaunchedEffect(appUiState.tunnels) {
Timber.d("Updating launched")
requestTunnelTileServiceStateUpdate()
}
LaunchedEffect(configurationChange) {
if (configurationChange) {
Intent(this@MainActivity, MainActivity::class.java).also {
startActivity(it)
exitProcess(0)
}
}
}
LaunchedEffect(appUiState.autoTunnelActive) {
requestAutoTunnelTileServiceUpdate()
}
@@ -114,109 +120,107 @@ class MainActivity : AppCompatActivity() {
}
}
CompositionLocalProvider(LocalFocusRequester provides rootItemFocusRequester) {
CompositionLocalProvider(LocalNavController provides navController) {
SnackbarControllerProvider { host ->
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
Scaffold(
contentWindowInsets = WindowInsets(0.dp),
snackbarHost = {
SnackbarHost(host) { snackbarData: SnackbarData ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp,
),
)
}
},
bottomBar = {
BottomNavBar(
navController,
listOf(
BottomNavItem(
name = stringResource(R.string.tunnels),
route = Route.Main,
icon = Icons.Rounded.Home,
),
BottomNavItem(
name = stringResource(R.string.settings),
route = Route.Settings,
icon = Icons.Rounded.Settings,
),
BottomNavItem(
name = stringResource(R.string.support),
route = Route.Support,
icon = Icons.Rounded.QuestionMark,
),
CompositionLocalProvider(LocalNavController provides navController) {
SnackbarControllerProvider { host ->
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
Scaffold(
contentWindowInsets = WindowInsets(0.dp),
snackbarHost = {
SnackbarHost(host) { snackbarData: SnackbarData ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp,
),
)
},
) {
Box(modifier = Modifier.fillMaxSize().padding(it)) {
NavHost(
navController,
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
startDestination = (if (appUiState.generalState.isPinLockEnabled == true) Route.Lock else Route.Main),
) {
composable<Route.Main> {
MainScreen(
uiState = appUiState,
)
}
composable<Route.Settings> {
SettingsScreen(
appViewModel = viewModel,
uiState = appUiState,
)
}
composable<Route.LocationDisclosure> {
LocationDisclosureScreen(viewModel, appUiState)
}
composable<Route.AutoTunnel> {
AutoTunnelScreen(
appUiState,
)
}
composable<Route.Appearance> {
AppearanceScreen()
}
composable<Route.Language> {
LanguageScreen(localeStorage)
}
composable<Route.Display> {
DisplayScreen(appUiState)
}
composable<Route.Support> {
SupportScreen()
}
composable<Route.Logs> {
LogsScreen()
}
composable<Route.Config> {
val args = it.toRoute<Route.Config>()
ConfigScreen(
tunnelId = args.id,
)
}
composable<Route.Option> {
val args = it.toRoute<Route.Option>()
OptionsScreen(
tunnelId = args.id,
appUiState = appUiState,
)
}
composable<Route.Lock> {
PinLockScreen(
appViewModel = viewModel,
)
}
composable<Route.Scanner> {
ScannerScreen()
}
}
},
bottomBar = {
BottomNavBar(
navController,
listOf(
BottomNavItem(
name = stringResource(R.string.tunnels),
route = Route.Main,
icon = Icons.Rounded.Home,
),
BottomNavItem(
name = stringResource(R.string.settings),
route = Route.Settings,
icon = Icons.Rounded.Settings,
),
BottomNavItem(
name = stringResource(R.string.support),
route = Route.Support,
icon = Icons.Rounded.QuestionMark,
),
),
)
},
) {
Box(modifier = Modifier.fillMaxSize().padding(it)) {
NavHost(
navController,
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
startDestination = (if (appUiState.generalState.isPinLockEnabled == true) Route.Lock else Route.Main),
) {
composable<Route.Main> {
MainScreen(
uiState = appUiState,
)
}
composable<Route.Settings> {
SettingsScreen(
appViewModel = viewModel,
uiState = appUiState,
)
}
composable<Route.LocationDisclosure> {
LocationDisclosureScreen(viewModel, appUiState)
}
composable<Route.AutoTunnel> {
AutoTunnelScreen(
appUiState,
)
}
composable<Route.Appearance> {
AppearanceScreen()
}
composable<Route.Language> {
LanguageScreen(localeStorage)
}
composable<Route.Display> {
DisplayScreen(appUiState)
}
composable<Route.Support> {
SupportScreen(appUiState, viewModel)
}
composable<Route.Logs> {
LogsScreen()
}
composable<Route.Config> {
val args = it.toRoute<Route.Config>()
ConfigScreen(
tunnelId = args.id,
)
}
composable<Route.Option> {
val args = it.toRoute<Route.Option>()
OptionsScreen(
tunnelId = args.id,
appUiState = appUiState,
)
}
composable<Route.Lock> {
PinLockScreen(
appViewModel = viewModel,
)
}
composable<Route.Scanner> {
ScannerScreen()
}
}
}
@@ -241,9 +245,4 @@ class MainActivity : AppCompatActivity() {
}
super.onResume()
}
override fun onDestroy() {
super.onDestroy()
tunnelService.cancelStatsJob()
}
}
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
@@ -10,7 +11,6 @@ import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -20,10 +20,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
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 com.zaneschepke.wireguardautotunnel.R
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun SubmitConfigurationTextBox(
@@ -44,17 +45,22 @@ fun SubmitConfigurationTextBox(
var stateValue by remember { mutableStateOf(value ?: "") }
OutlinedTextField(
CustomTextField(
isError = isErrorValue(stateValue),
modifier = Modifier
.fillMaxWidth(),
textStyle = MaterialTheme.typography.bodySmall,
value = stateValue,
singleLine = true,
interactionSource = interactionSource,
onValueChange = { stateValue = it },
interactionSource = interactionSource,
label = { Text(label) },
maxLines = 1,
placeholder = { Text(hint) },
containerColor = MaterialTheme.colorScheme.surface,
placeholder = { Text(hint, style = MaterialTheme.typography.bodySmall) },
modifier =
Modifier
.padding(
top = 5.dp,
bottom = 10.dp,
).fillMaxWidth().padding(end = 16.dp.scaledWidth()),
singleLine = true,
keyboardOptions = keyboardOptions,
keyboardActions = KeyboardActions(
onDone = {
@@ -62,16 +68,17 @@ fun SubmitConfigurationTextBox(
keyboardController?.hide()
},
),
trailingIcon = {
trailing = {
if (!isErrorValue(stateValue) && isFocused) {
IconButton(onClick = {
onSubmit(stateValue)
keyboardController?.hide()
focusManager.clearFocus()
}) {
val icon = Icons.Outlined.Save
Icon(
imageVector = Icons.Outlined.Save,
contentDescription = stringResource(R.string.save_changes),
imageVector = icon,
contentDescription = icon.name,
tint = MaterialTheme.colorScheme.primary,
)
}
@@ -1,11 +1,8 @@
package com.zaneschepke.wireguardautotunnel.ui.common.navigation
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.focus.FocusRequester
import androidx.navigation.NavHostController
val LocalNavController = compositionLocalOf<NavHostController> {
error("NavController was not provided")
}
val LocalFocusRequester = compositionLocalOf<FocusRequester> { error("FocusRequester is not provided") }
@@ -10,7 +10,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
@@ -37,8 +36,8 @@ fun CustomTextField(
isError: Boolean = false,
readOnly: Boolean = false,
enabled: Boolean = true,
interactionSource: MutableInteractionSource,
) {
val interactionSource = remember { MutableInteractionSource() }
val space = " "
BasicTextField(
value = value,
@@ -116,7 +116,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
InfoDialog(
onDismiss = { showDeleteTunnelAlertDialog = false },
onAttest = {
selectedTunnel?.let { viewModel::onDelete }
selectedTunnel?.let { viewModel.onDelete(it) }
showDeleteTunnelAlertDialog = false
selectedTunnel = null
},
@@ -5,10 +5,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -19,12 +17,10 @@ import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@Composable
fun AutoTunnelRowItem(appUiState: AppUiState, onToggle: () -> Unit) {
val context = LocalContext.current
val itemFocusRequester = remember { FocusRequester() }
ExpandingRowListItem(
leading = {
val icon = Icons.Rounded.Bolt
@@ -33,7 +29,7 @@ fun AutoTunnelRowItem(appUiState: AppUiState, onToggle: () -> Unit) {
icon.name,
modifier =
Modifier
.size(16.dp.scaledHeight()).scale(1.5f),
.size(16.dp).scale(1.5f),
tint =
if (!appUiState.autoTunnelActive) {
Color.Gray
@@ -53,7 +49,7 @@ fun AutoTunnelRowItem(appUiState: AppUiState, onToggle: () -> Unit) {
},
onClick = {
if (context.isRunningOnTv()) {
itemFocusRequester.requestFocus()
onToggle()
}
},
isExpanded = false,
@@ -33,7 +33,6 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavControll
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@Composable
fun TunnelRowItem(
@@ -56,20 +55,19 @@ fun TunnelRowItem(
val itemFocusRequester = remember { FocusRequester() }
ExpandingRowListItem(
leading = {
val circleIcon = Icons.Rounded.Circle
val icon =
if (tunnel.isPrimaryTunnel) {
Icons.Rounded.Star
} else if (tunnel.isMobileDataTunnel) {
Icons.Rounded.Smartphone
} else {
circleIcon
Icons.Rounded.Circle
}
Icon(
icon,
icon.name,
tint = leadingIconColor,
modifier = Modifier.size(16.dp.scaledHeight()),
modifier = Modifier.size(16.dp),
)
},
text = tunnel.name,
@@ -2,12 +2,14 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.options
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
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.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.NetworkPing
@@ -28,6 +30,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
@@ -36,16 +40,18 @@ import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiState: AppUiState, tunnelId: Int) {
val navController = LocalNavController.current
@@ -80,117 +86,181 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
Modifier
.fillMaxSize()
.padding(it)
.verticalScroll(rememberScrollState())
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
GroupLabel(stringResource(R.string.auto_tunneling))
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Star,
title = {
Text(
stringResource(R.string.primary_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.set_primary_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
config.isPrimaryTunnel,
buildList {
addAll(
listOf(
SelectionItem(
Icons.Outlined.Star,
title = {
Text(
stringResource(R.string.primary_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.set_primary_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
config.isPrimaryTunnel,
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
)
},
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
)
},
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
),
SelectionItem(
Icons.Outlined.PhoneAndroid,
title = { Text(stringResource(R.string.mobile_tunnel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
description = {
Text(
stringResource(R.string.mobile_data_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
config.isMobileDataTunnel,
),
SelectionItem(
Icons.Outlined.PhoneAndroid,
title = {
Text(
stringResource(R.string.mobile_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.mobile_data_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
config.isMobileDataTunnel,
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
)
},
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
)
},
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = config.isPingEnabled,
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = config.isPingEnabled,
onClick = { optionsViewModel.onToggleRestartOnPing(config) },
)
},
onClick = { optionsViewModel.onToggleRestartOnPing(config) },
)
},
onClick = { optionsViewModel.onToggleRestartOnPing(config) },
),
SelectionItem(
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
) {
),
),
)
if (config.isPingEnabled || appUiState.settings.isPingEnabled) {
add(
SelectionItem(
title = {},
description = {
SubmitConfigurationTextBox(
config.pingIp,
stringResource(R.string.set_custom_ping_ip),
stringResource(R.string.default_ping_ip),
isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingIp = it.ifBlank { null }),
)
},
)
fun isSecondsError(seconds: String?): Boolean {
return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 } ?: false
}
SubmitConfigurationTextBox(
config.pingInterval?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_internal),
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
),
isErrorValue = ::isSecondsError,
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
SubmitConfigurationTextBox(
config.pingCooldown?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_cooldown),
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
isErrorValue = ::isSecondsError,
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
},
),
)
}
add(
SelectionItem(
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.weight(4f, false)
.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
) {
val icon = Icons.Outlined.Security
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize),
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp.scaledWidth())
.padding(vertical = 6.dp.scaledHeight()),
.weight(4f, false)
.fillMaxWidth(),
) {
Text(
stringResource(R.string.use_tunnel_on_wifi_name),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
val icon = Icons.Outlined.Security
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize),
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp.scaledWidth())
.padding(vertical = 6.dp.scaledHeight()),
) {
Text(
stringResource(R.string.use_tunnel_on_wifi_name),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
}
}
}
}
},
description = {
TrustedNetworkTextBox(
config.tunnelNetworks,
onDelete = { optionsViewModel.onDeleteRunSSID(it, config) },
currentText = currentText,
onSave = { optionsViewModel.onSaveRunSSID(it, config) },
onValueChange = { currentText = it },
supporting = {
if (appUiState.settings.isWildcardsEnabled) {
WildcardsLabel()
}
},
)
},
),
),
},
description = {
TrustedNetworkTextBox(
config.tunnelNetworks,
onDelete = { optionsViewModel.onDeleteRunSSID(it, config) },
currentText = currentText,
onSave = { optionsViewModel.onSaveRunSSID(it, config) },
onValueChange = { currentText = it },
supporting = {
if (appUiState.settings.isWildcardsEnabled) {
WildcardsLabel()
}
},
)
},
),
)
},
)
}
}
@@ -5,7 +5,6 @@ import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
@@ -30,13 +29,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
@@ -45,7 +42,6 @@ import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusRequester
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
@@ -59,17 +55,12 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import xyz.teamgravity.pin_lock_compose.PinManager
@OptIn(
ExperimentalPermissionsApi::class,
ExperimentalLayoutApi::class,
)
@Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, uiState: AppUiState) {
val context = LocalContext.current
val navController = LocalNavController.current
val focusManager = LocalFocusManager.current
val snackbar = SnackbarController.current
val rootFocusRequester = LocalFocusRequester.current
val isRunningOnTv = remember { context.isRunningOnTv() }
val interactionSource = remember { MutableInteractionSource() }
@@ -135,32 +126,34 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
navController.navigate(Route.AutoTunnel)
},
trailing = {
ForwardButton(Modifier.focusable().focusRequester(rootFocusRequester)) { navController.navigate(Route.AutoTunnel) }
ForwardButton(Modifier.focusable()) { navController.navigate(Route.AutoTunnel) }
},
),
),
)
SurfaceSelectionGroupButton(
buildList {
add(
SelectionItem(
Icons.Filled.AppShortcut,
{
ScaledSwitch(
uiState.settings.isShortcutsEnabled,
onClick = { appViewModel.onToggleShortcutsEnabled() },
)
},
title = {
Text(
stringResource(R.string.enabled_app_shortcuts),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { appViewModel.onToggleShortcutsEnabled() },
),
)
if (!isRunningOnTv) {
addAll(
listOf(
SelectionItem(
Icons.Filled.AppShortcut,
{
ScaledSwitch(
uiState.settings.isShortcutsEnabled,
onClick = { viewModel.onToggleShortcutsEnabled() },
)
},
title = {
Text(
stringResource(R.string.enabled_app_shortcuts),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { viewModel.onToggleShortcutsEnabled() },
),
SelectionItem(
Icons.Outlined.VpnLock,
{
@@ -173,7 +166,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
) &&
uiState.settings.isAutoTunnelEnabled
),
onClick = { viewModel.onToggleAlwaysOnVPN() },
onClick = { appViewModel.onToggleAlwaysOnVPN() },
checked = uiState.settings.isAlwaysOnVpnEnabled,
)
},
@@ -183,7 +176,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { viewModel.onToggleAlwaysOnVPN() },
onClick = { appViewModel.onToggleAlwaysOnVPN() },
),
SelectionItem(
Icons.Outlined.AdminPanelSettings,
@@ -209,7 +202,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
{
ScaledSwitch(
uiState.settings.isRestoreOnBootEnabled,
onClick = { viewModel.onToggleRestartAtBoot() },
onClick = { appViewModel.onToggleRestartAtBoot() },
)
},
title = {
@@ -218,7 +211,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { viewModel.onToggleRestartAtBoot() },
onClick = { appViewModel.onToggleRestartAtBoot() },
),
)
},
@@ -294,7 +287,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
trailing = {
ScaledSwitch(
uiState.settings.isKernelEnabled,
onClick = { viewModel.onToggleKernelMode() },
onClick = { appViewModel.onToggleKernelMode() },
enabled = !(
uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled ||
@@ -303,7 +296,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
)
},
onClick = {
viewModel.onToggleKernelMode()
appViewModel.onToggleKernelMode()
},
),
),
@@ -4,119 +4,23 @@ import android.content.Context
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.Instant
import javax.inject.Inject
import javax.inject.Provider
@HiltViewModel
class SettingsViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
@AppShell private val rootShell: Provider<RootShell>,
private val fileUtils: FileUtils,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
private val settings = appDataRepository.settings.getSettingsFlow()
.stateIn(viewModelScope, SharingStarted.Eagerly, Settings())
fun setLocationDisclosureShown() = viewModelScope.launch {
appDataRepository.appState.setLocationDisclosureShown(true)
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isAlwaysOnVpnEnabled = !isAlwaysOnVpnEnabled,
),
)
}
}
fun onToggleShortcutsEnabled() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
this.copy(
isShortcutsEnabled = !isShortcutsEnabled,
),
)
}
}
private fun saveKernelMode(enabled: Boolean) = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
this.copy(
isKernelEnabled = enabled,
),
)
}
}
fun onToggleKernelMode() = viewModelScope.launch {
with(settings.value) {
if (!isKernelEnabled) {
requestRoot().onSuccess {
if (!isKernelSupported()) return@onSuccess SnackbarController.showMessage(StringValue.StringResource(R.string.kernel_not_supported))
appDataRepository.settings.save(
copy(
isKernelEnabled = true,
isAmneziaEnabled = false,
),
)
}
} else {
saveKernelMode(enabled = false)
}
}
}
private suspend fun isKernelSupported(): Boolean {
return withContext(ioDispatcher) {
WgQuickBackend.hasKernelSupport()
}
}
fun onToggleRestartAtBoot() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isRestoreOnBootEnabled = !isRestoreOnBootEnabled,
),
)
}
}
private suspend fun requestRoot(): Result<Unit> {
return withContext(ioDispatcher) {
kotlin.runCatching {
rootShell.get().start()
SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
}.onFailure {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
}
}
}
fun exportAllConfigs(context: Context) = viewModelScope.launch {
kotlin.runCatching {
val shareFile = fileUtils.createNewShareFile("wg-export_${Instant.now().epochSecond}.zip")
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
@@ -16,6 +17,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -66,6 +68,7 @@ fun TrustedNetworkTextBox(
textStyle = MaterialTheme.typography.bodySmall,
value = currentText,
onValueChange = onValueChange,
interactionSource = remember { MutableInteractionSource() },
label = { Text(stringResource(R.string.add_wifi_name)) },
containerColor = MaterialTheme.colorScheme.surface,
supportingText = supporting,
@@ -4,14 +4,22 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AttachMoney
import androidx.compose.material.icons.filled.Book
import androidx.compose.material.icons.filled.LineStyle
import androidx.compose.material.icons.filled.Mail
import androidx.compose.material.icons.filled.Policy
import androidx.compose.material.icons.filled.ViewTimeline
import androidx.compose.material.icons.outlined.ViewHeadline
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@@ -19,25 +27,44 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun SupportScreen() {
fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
val context = LocalContext.current
val navController = LocalNavController.current
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
InfoDialog(onAttest = {
showDialog = false
appViewModel.onToggleLocalLogging()
}, onDismiss = {
showDialog = false
}, title = {
Text(stringResource(R.string.configuration_change))
}, body = { Text(stringResource(R.string.requires_app_relaunch)) }, confirmText = { Text(stringResource(R.string.yes)) })
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
@@ -45,110 +72,178 @@ fun SupportScreen() {
Modifier
.fillMaxSize()
.padding(top = topPadding)
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp.scaledWidth()),
) {
GroupLabel(stringResource(R.string.thank_you))
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Filled.Book,
title = {
Text(
stringResource(R.string.docs_description),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
buildList {
add(
SelectionItem(
Icons.Filled.Book,
title = {
Text(
stringResource(R.string.docs_description),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton { context.openWebUrl(context.getString(R.string.docs_url)) }
},
onClick = {
context.openWebUrl(context.getString(R.string.docs_url))
},
),
)
if (!context.isRunningOnTv()) {
add(
SelectionItem(
Icons.Outlined.ViewHeadline,
title = {
Text(
stringResource(R.string.local_logging),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.enable_local_logging),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
appUiState.generalState.isLocalLogsEnabled,
onClick = {
showDialog = true
},
)
},
onClick = {
showDialog = true
},
),
)
if (appUiState.generalState.isLocalLogsEnabled) {
add(
SelectionItem(
Icons.Filled.ViewTimeline,
title = {
Text(
stringResource(R.string.read_logs),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
navController.navigate(Route.Logs)
}
},
onClick = {
navController.navigate(Route.Logs)
},
),
)
},
trailing = {
ForwardButton { context.openWebUrl(context.getString(R.string.docs_url)) }
},
onClick = {
context.openWebUrl(context.getString(R.string.docs_url))
},
),
SelectionItem(
Icons.Filled.LineStyle,
title = {
Text(
stringResource(R.string.read_logs),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
navController.navigate(Route.Logs)
}
},
onClick = {
navController.navigate(Route.Logs)
},
),
SelectionItem(
Icons.Filled.Policy,
title = {
Text(
stringResource(R.string.privacy_policy),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton { context.openWebUrl(context.getString(R.string.privacy_policy_url)) }
},
onClick = {
context.openWebUrl(context.getString(R.string.privacy_policy_url))
},
),
),
}
}
add(
SelectionItem(
Icons.Filled.Policy,
title = {
Text(
stringResource(R.string.privacy_policy),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton { context.openWebUrl(context.getString(R.string.privacy_policy_url)) }
},
onClick = {
context.openWebUrl(context.getString(R.string.privacy_policy_url))
},
),
)
},
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
ImageVector.vectorResource(R.drawable.telegram),
title = {
Text(
stringResource(R.string.chat_description),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
context.openWebUrl(context.getString(R.string.telegram_url))
}
},
onClick = {
context.openWebUrl(context.getString(R.string.telegram_url))
},
),
SelectionItem(
ImageVector.vectorResource(R.drawable.github),
title = { Text(stringResource(R.string.open_issue), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
trailing = {
ForwardButton {
context.openWebUrl(context.getString(R.string.github_url))
}
},
onClick = {
context.openWebUrl(context.getString(R.string.github_url))
},
),
SelectionItem(
Icons.Filled.Mail,
title = {
Text(
stringResource(R.string.email_description),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
context.launchSupportEmail()
}
},
onClick = {
context.launchSupportEmail()
},
),
),
buildList {
addAll(
listOf(
SelectionItem(
ImageVector.vectorResource(R.drawable.telegram),
title = {
Text(
stringResource(R.string.chat_description),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
context.openWebUrl(context.getString(R.string.telegram_url))
}
},
onClick = {
context.openWebUrl(context.getString(R.string.telegram_url))
},
),
SelectionItem(
ImageVector.vectorResource(R.drawable.github),
title = {
Text(
stringResource(R.string.open_issue),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
context.openWebUrl(context.getString(R.string.github_url))
}
},
onClick = {
context.openWebUrl(context.getString(R.string.github_url))
},
),
SelectionItem(
Icons.Filled.Mail,
title = {
Text(
stringResource(R.string.email_description),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
context.launchSupportEmail()
}
},
onClick = {
context.launchSupportEmail()
},
),
),
)
if (BuildConfig.FLAVOR == "fdroid") {
add(
SelectionItem(
Icons.Filled.AttachMoney,
title = {
Text(
stringResource(R.string.donate),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
context.openWebUrl(context.getString(R.string.donate_url))
}
},
onClick = {
context.openWebUrl(context.getString(R.string.donate_url))
},
),
)
}
},
)
VersionLabel()
}
@@ -22,6 +22,7 @@ import com.zaneschepke.wireguardautotunnel.util.Constants
private const val BASELINE_HEIGHT = 2201
private const val BASELINE_WIDTH = 1080
private const val BASELINE_DENSITY = 2.625
private const val ANDROID_TV_SIZE_MULTIPLIER = 1.5f
fun Context.openWebUrl(url: String): Result<Unit> {
return kotlin.runCatching {
@@ -47,7 +48,8 @@ val Context.actionBarSize
fun Context.resizeHeight(dp: Dp): Dp {
val displayMetrics = resources.displayMetrics
val density = displayMetrics.density
val height = displayMetrics.heightPixels - this.actionBarSize
val height = (displayMetrics.heightPixels - this.actionBarSize) *
(if (isRunningOnTv()) ANDROID_TV_SIZE_MULTIPLIER else 1f)
val resizeHeightPercentage =
(height.toFloat() / BASELINE_HEIGHT) * (BASELINE_DENSITY.toFloat() / density)
return dp * resizeHeightPercentage
@@ -56,7 +58,8 @@ fun Context.resizeHeight(dp: Dp): Dp {
fun Context.resizeHeight(textUnit: TextUnit): TextUnit {
val displayMetrics = resources.displayMetrics
val density = displayMetrics.density
val height = displayMetrics.heightPixels - actionBarSize
val height = (displayMetrics.heightPixels - actionBarSize) *
(if (isRunningOnTv()) ANDROID_TV_SIZE_MULTIPLIER else 1f)
val resizeHeightPercentage =
(height.toFloat() / BASELINE_HEIGHT) * (BASELINE_DENSITY.toFloat() / density)
return textUnit * resizeHeightPercentage * 1.1
+6
View File
@@ -9,6 +9,7 @@
<string name="docs_features" translatable="false">https://zaneschepke.com/wgtunnel-docs/features.html</string>
<string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
<string name="docs_wildcards" translatable="false" >https://zaneschepke.com/wgtunnel-docs/features.html#wildcard-wi-fi-name-support</string>
<string name="donate_url" translatable="false">https://zaneschepke.com/donate/</string>
<string name="error_file_extension">File is not a .conf or .zip</string>
<string name="turn_off_tunnel">Action requires tunnel off</string>
<string name="no_tunnels">No tunnels added yet!</string>
@@ -227,4 +228,9 @@
<string name="stop_auto">Stop auto-tunnel</string>
<string name="tunnel_running">Tunnel running</string>
<string name="monitoring_state_changes">Monitoring state changes</string>
<string name="donate">Donate to project</string>
<string name="local_logging">Local logging</string>
<string name="enable_local_logging">Enable local logging</string>
<string name="configuration_change">Configuration change</string>
<string name="requires_app_relaunch">This change requires an app relaunch. Would you like to proceed?</string>
</resources>
+1 -1
View File
@@ -1 +1 @@
4
7