initial ui changes
This commit is contained in:
parent
89f6dec357
commit
553279ea76
|
@ -44,6 +44,8 @@ android {
|
||||||
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().joinToString(separator = ", ") { "\"$it\"" }} }")
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables { useSupportLibrary = true }
|
vectorDrawables { useSupportLibrary = true }
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,6 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
|
||||||
android:maxSdkVersion="32" />
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
||||||
android:maxSdkVersion="32"
|
|
||||||
tools:ignore="ScopedStorage" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
@ -19,7 +12,8 @@
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
<!--foreground service exempt android 14-->
|
<!--foreground service exempt android 14-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
||||||
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
|
||||||
<!--foreground service permissions-->
|
<!--foreground service permissions-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.os.StrictMode.ThreadPolicy
|
import android.os.StrictMode.ThreadPolicy
|
||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.datastore.LocaleStorage
|
||||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
@ -18,6 +21,10 @@ import javax.inject.Inject
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class WireGuardAutoTunnel : Application() {
|
class WireGuardAutoTunnel : Application() {
|
||||||
|
|
||||||
|
val localeStorage: LocaleStorage by lazy {
|
||||||
|
LocaleStorage(this)
|
||||||
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@ApplicationScope
|
@ApplicationScope
|
||||||
lateinit var applicationScope: CoroutineScope
|
lateinit var applicationScope: CoroutineScope
|
||||||
|
@ -52,6 +59,10 @@ class WireGuardAutoTunnel : Application() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
super.attachBaseContext(LocaleUtil.getLocalizedContext(base, LocaleStorage(base).getPreferredLocale()))
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: WireGuardAutoTunnel
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
private set
|
private set
|
||||||
|
|
|
@ -21,11 +21,12 @@ class DataStoreManager(
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||||
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||||
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
|
val currentSSID = stringPreferencesKey("CURRENT_SSID")
|
||||||
val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED")
|
val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED")
|
||||||
val IS_TUNNEL_STATS_EXPANDED = booleanPreferencesKey("TUNNEL_STATS_EXPANDED")
|
val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED")
|
||||||
|
val theme = stringPreferencesKey("THEME")
|
||||||
}
|
}
|
||||||
|
|
||||||
// preferences
|
// preferences
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.data.datastore
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||||
|
|
||||||
|
class LocaleStorage(context: Context) {
|
||||||
|
private var preferences: SharedPreferences = context.getSharedPreferences("sp", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
fun getPreferredLocale(): String {
|
||||||
|
return preferences.getString("preferred_locale", LocaleUtil.OPTION_PHONE_LANGUAGE)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPreferredLocale(localeCode: String) {
|
||||||
|
preferences.edit().putString("preferred_locale", localeCode).apply()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.data.domain
|
package com.zaneschepke.wireguardautotunnel.data.domain
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||||
|
|
||||||
data class GeneralState(
|
data class GeneralState(
|
||||||
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||||
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||||
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
|
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
|
||||||
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
|
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
|
||||||
|
val theme: Theme = Theme.AUTOMATIC
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
|
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
|
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface AppStateRepository {
|
interface AppStateRepository {
|
||||||
|
@ -24,5 +25,9 @@ interface AppStateRepository {
|
||||||
|
|
||||||
suspend fun setTunnelStatsExpanded(expanded: Boolean)
|
suspend fun setTunnelStatsExpanded(expanded: Boolean)
|
||||||
|
|
||||||
|
suspend fun setTheme(theme: Theme)
|
||||||
|
|
||||||
|
suspend fun getTheme() : Theme
|
||||||
|
|
||||||
val generalStateFlow: Flow<GeneralState>
|
val generalStateFlow: Flow<GeneralState>
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
|
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -11,47 +12,61 @@ class DataStoreAppStateRepository(
|
||||||
) :
|
) :
|
||||||
AppStateRepository {
|
AppStateRepository {
|
||||||
override suspend fun isLocationDisclosureShown(): Boolean {
|
override suspend fun isLocationDisclosureShown(): Boolean {
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
|
return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown)
|
||||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
|
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setLocationDisclosureShown(shown: Boolean) {
|
override suspend fun setLocationDisclosureShown(shown: Boolean) {
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
|
dataStoreManager.saveToDataStore(DataStoreManager.locationDisclosureShown, shown)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun isPinLockEnabled(): Boolean {
|
override suspend fun isPinLockEnabled(): Boolean {
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED)
|
return dataStoreManager.getFromStore(DataStoreManager.pinLockEnabled)
|
||||||
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
|
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setPinLockEnabled(enabled: Boolean) {
|
override suspend fun setPinLockEnabled(enabled: Boolean) {
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled)
|
dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
|
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
|
return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown)
|
||||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
|
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
|
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown)
|
dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getCurrentSsid(): String? {
|
override suspend fun getCurrentSsid(): String? {
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID)
|
return dataStoreManager.getFromStore(DataStoreManager.currentSSID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setCurrentSsid(ssid: String) {
|
override suspend fun setCurrentSsid(ssid: String) {
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
|
dataStoreManager.saveToDataStore(DataStoreManager.currentSSID, ssid)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun isTunnelStatsExpanded(): Boolean {
|
override suspend fun isTunnelStatsExpanded(): Boolean {
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.IS_TUNNEL_STATS_EXPANDED)
|
return dataStoreManager.getFromStore(DataStoreManager.tunnelStatsExpanded)
|
||||||
?: GeneralState.IS_TUNNEL_STATS_EXPANDED
|
?: GeneralState.IS_TUNNEL_STATS_EXPANDED
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setTunnelStatsExpanded(expanded: Boolean) {
|
override suspend fun setTunnelStatsExpanded(expanded: Boolean) {
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.IS_TUNNEL_STATS_EXPANDED, expanded)
|
dataStoreManager.saveToDataStore(DataStoreManager.tunnelStatsExpanded, expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setTheme(theme: Theme) {
|
||||||
|
dataStoreManager.saveToDataStore(DataStoreManager.theme, theme.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getTheme(): Theme {
|
||||||
|
return dataStoreManager.getFromStore(DataStoreManager.theme)?.let {
|
||||||
|
try {
|
||||||
|
Theme.valueOf(it)
|
||||||
|
} catch (_ : IllegalArgumentException) {
|
||||||
|
Theme.AUTOMATIC
|
||||||
|
}
|
||||||
|
} ?: Theme.AUTOMATIC
|
||||||
}
|
}
|
||||||
|
|
||||||
override val generalStateFlow: Flow<GeneralState> =
|
override val generalStateFlow: Flow<GeneralState> =
|
||||||
|
@ -60,15 +75,16 @@ class DataStoreAppStateRepository(
|
||||||
try {
|
try {
|
||||||
GeneralState(
|
GeneralState(
|
||||||
isLocationDisclosureShown =
|
isLocationDisclosureShown =
|
||||||
pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
|
pref[DataStoreManager.locationDisclosureShown]
|
||||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||||
isBatteryOptimizationDisableShown =
|
isBatteryOptimizationDisableShown =
|
||||||
pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
|
pref[DataStoreManager.batteryDisableShown]
|
||||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||||
isPinLockEnabled =
|
isPinLockEnabled =
|
||||||
pref[DataStoreManager.IS_PIN_LOCK_ENABLED]
|
pref[DataStoreManager.pinLockEnabled]
|
||||||
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
|
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
|
||||||
isTunnelStatsExpanded = pref[DataStoreManager.IS_TUNNEL_STATS_EXPANDED] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
|
isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
|
||||||
|
theme = getTheme()
|
||||||
)
|
)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
|
|
|
@ -10,4 +10,6 @@ data class AppUiState(
|
||||||
val tunnels: List<TunnelConfig> = emptyList(),
|
val tunnels: List<TunnelConfig> = emptyList(),
|
||||||
val vpnState: VpnState = VpnState(),
|
val vpnState: VpnState = VpnState(),
|
||||||
val generalState: GeneralState = GeneralState(),
|
val generalState: GeneralState = GeneralState(),
|
||||||
|
val isKernelAvailable: Boolean = false,
|
||||||
|
val isRooted: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,6 +2,8 @@ package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
|
import com.wireguard.android.util.RootShell
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||||
|
@ -18,10 +20,13 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
|
@ -32,19 +37,34 @@ class AppViewModel
|
||||||
constructor(
|
constructor(
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
private val tunnelService: Provider<TunnelService>,
|
private val tunnelService: Provider<TunnelService>,
|
||||||
|
private val rootShell: Provider<RootShell>,
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _appUiState = MutableStateFlow(AppUiState())
|
private val _appUiState = MutableStateFlow(AppUiState())
|
||||||
|
|
||||||
|
val appUiState = _appUiState.onStart {
|
||||||
|
_appUiState.update {
|
||||||
|
it.copy(
|
||||||
|
isRooted = isRooted(),
|
||||||
|
isKernelAvailable = isKernelSupported(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.stateIn(
|
||||||
|
viewModelScope + ioDispatcher,
|
||||||
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
|
AppUiState(),
|
||||||
|
)
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
combine(
|
combine(
|
||||||
appDataRepository.settings.getSettingsFlow(),
|
appDataRepository.settings.getSettingsFlow(),
|
||||||
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
||||||
tunnelService.get().vpnState,
|
tunnelService.get().vpnState,
|
||||||
appDataRepository.appState.generalStateFlow,
|
appDataRepository.appState.generalStateFlow,
|
||||||
) { settings, tunnels, tunnelState, generalState ->
|
appUiState,
|
||||||
AppUiState(
|
) { settings, tunnels, tunnelState, generalState, appUiState ->
|
||||||
|
appUiState.copy(
|
||||||
settings,
|
settings,
|
||||||
tunnels,
|
tunnels,
|
||||||
tunnelState,
|
tunnelState,
|
||||||
|
@ -112,4 +132,21 @@ constructor(
|
||||||
fun onPinLockEnabled() = viewModelScope.launch {
|
fun onPinLockEnabled() = viewModelScope.launch {
|
||||||
appDataRepository.appState.setPinLockEnabled(true)
|
appDataRepository.appState.setPinLockEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun isKernelSupported(): Boolean {
|
||||||
|
return withContext(ioDispatcher) {
|
||||||
|
WgQuickBackend.hasKernelSupport()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun isRooted(): Boolean {
|
||||||
|
return try {
|
||||||
|
withContext(ioDispatcher) {
|
||||||
|
rootShell.get().start()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.SystemBarStyle
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
@ -11,8 +10,12 @@ import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Home
|
import androidx.compose.material.icons.rounded.Home
|
||||||
import androidx.compose.material.icons.rounded.QuestionMark
|
import androidx.compose.material.icons.rounded.QuestionMark
|
||||||
|
@ -29,8 +32,6 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
|
@ -41,6 +42,8 @@ import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.datastore.LocaleStorage
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||||
|
@ -48,8 +51,9 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||||
|
@ -57,10 +61,15 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.scanner.ScannerScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.scanner.ScannerScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
@ -68,6 +77,13 @@ import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private val localeStorage: LocaleStorage by lazy {
|
||||||
|
(application as WireGuardAutoTunnel).localeStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var oldPrefLocaleCode: String
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var appStateRepository: AppStateRepository
|
lateinit var appStateRepository: AppStateRepository
|
||||||
|
|
||||||
|
@ -78,12 +94,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge(
|
|
||||||
navigationBarStyle = SystemBarStyle.auto(
|
|
||||||
lightScrim = Color.Transparent.toArgb(),
|
|
||||||
darkScrim = Color.Transparent.toArgb(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
installSplashScreen().apply {
|
installSplashScreen().apply {
|
||||||
setKeepOnScreenCondition {
|
setKeepOnScreenCondition {
|
||||||
|
@ -113,9 +123,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
CompositionLocalProvider(LocalNavController provides navController) {
|
CompositionLocalProvider(LocalNavController provides navController) {
|
||||||
SnackbarControllerProvider { host ->
|
SnackbarControllerProvider { host ->
|
||||||
WireguardAutoTunnelTheme {
|
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme){
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
contentWindowInsets = WindowInsets(0.dp),
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
SnackbarHost(host) { snackbarData: SnackbarData ->
|
SnackbarHost(host) { snackbarData: SnackbarData ->
|
||||||
CustomSnackBar(
|
CustomSnackBar(
|
||||||
|
@ -160,8 +171,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { padding ->
|
) {
|
||||||
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
|
Box(modifier = Modifier.fillMaxSize().padding(it)) {
|
||||||
NavHost(
|
NavHost(
|
||||||
navController,
|
navController,
|
||||||
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
|
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
|
||||||
|
@ -181,6 +192,20 @@ class MainActivity : AppCompatActivity() {
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable<Route.AutoTunnel> {
|
||||||
|
AutoTunnelScreen(
|
||||||
|
appUiState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable<Route.Appearance> {
|
||||||
|
AppearanceScreen()
|
||||||
|
}
|
||||||
|
composable<Route.Language> {
|
||||||
|
LanguageScreen(localeStorage)
|
||||||
|
}
|
||||||
|
composable<Route.Display> {
|
||||||
|
DisplayScreen(appUiState)
|
||||||
|
}
|
||||||
composable<Route.Support> {
|
composable<Route.Support> {
|
||||||
SupportScreen(
|
SupportScreen(
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
|
@ -222,6 +247,21 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context) {
|
||||||
|
oldPrefLocaleCode = LocaleStorage(newBase).getPreferredLocale()
|
||||||
|
applyOverrideConfiguration(LocaleUtil.getLocalizedConfiguration(oldPrefLocaleCode))
|
||||||
|
super.attachBaseContext(newBase)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
val currentLocaleCode = LocaleStorage(this).getPreferredLocale()
|
||||||
|
if (oldPrefLocaleCode != currentLocaleCode) {
|
||||||
|
recreate() // locale is changed, restart the activity to update
|
||||||
|
oldPrefLocaleCode = currentLocaleCode
|
||||||
|
}
|
||||||
|
super.onResume()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
tunnelService.cancelStatsJob()
|
tunnelService.cancelStatsJob()
|
||||||
|
|
|
@ -9,6 +9,18 @@ sealed class Route {
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Settings : Route()
|
data object Settings : Route()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object AutoTunnel : Route()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Appearance : Route()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Display : Route()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Language : Route()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Main : Route()
|
data object Main : Route()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectedLabel() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.selected),
|
||||||
|
modifier =
|
||||||
|
Modifier.padding(
|
||||||
|
horizontal = 24.dp.scaledWidth(),
|
||||||
|
vertical = 16.dp.scaledHeight(),
|
||||||
|
),
|
||||||
|
color =
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.button
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
import kotlin.let
|
||||||
|
|
||||||
|
@androidx.compose.runtime.Composable
|
||||||
|
fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, leadingIcon: ImageVector? = null, description: String? = null) {
|
||||||
|
val border: BorderStroke? =
|
||||||
|
if (selected) BorderStroke(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
) else null
|
||||||
|
val interactionSource =
|
||||||
|
androidx.compose.runtime.remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
|
||||||
|
androidx.compose.material3.Card(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
|
.clickable(interactionSource = interactionSource, indication = null) {
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp),
|
||||||
|
border = border,
|
||||||
|
colors = androidx.compose.material3.CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = 8.dp.scaledWidth(), vertical = 10.dp.scaledHeight())
|
||||||
|
.padding(end = 16.dp.scaledWidth()).padding(start = 8.dp.scaledWidth())
|
||||||
|
.fillMaxSize(),
|
||||||
|
verticalArrangement = androidx.compose.foundation.layout.Arrangement.Center,
|
||||||
|
horizontalAlignment = androidx.compose.ui.Alignment.Companion.Start,
|
||||||
|
) {
|
||||||
|
androidx.compose.foundation.layout.Row(
|
||||||
|
verticalAlignment = androidx.compose.ui.Alignment.Companion.CenterVertically,
|
||||||
|
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(16.dp.scaledWidth()),
|
||||||
|
) {
|
||||||
|
androidx.compose.foundation.layout.Row(
|
||||||
|
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(
|
||||||
|
16.dp.scaledWidth()
|
||||||
|
),
|
||||||
|
verticalAlignment = androidx.compose.ui.Alignment.Companion.CenterVertically,
|
||||||
|
modifier = Modifier.padding(vertical = if (description == null) 10.dp.scaledHeight() else 0.dp),
|
||||||
|
) {
|
||||||
|
leadingIcon?.let {
|
||||||
|
Icon(
|
||||||
|
leadingIcon,
|
||||||
|
leadingIcon.name,
|
||||||
|
Modifier.Companion.size(iconSize.scaledWidth()),
|
||||||
|
if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
description?.let {
|
||||||
|
Text(
|
||||||
|
description,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.button
|
||||||
|
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScaledSwitch(checked: Boolean, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true) {
|
||||||
|
Switch(
|
||||||
|
checked,
|
||||||
|
{ onClick(it) },
|
||||||
|
Modifier.scale((52.dp.scaledHeight() / 52.dp)),
|
||||||
|
enabled = enabled
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.button
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.ripple
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectionItemButton(
|
||||||
|
leading: (@Composable () -> Unit)? = null,
|
||||||
|
buttonText: String,
|
||||||
|
trailing: (@Composable () -> Unit)? = null,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
ripple: Boolean = true,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier =
|
||||||
|
Modifier.clip(RoundedCornerShape(8.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = if (ripple) ripple() else null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = { onClick() },
|
||||||
|
)
|
||||||
|
.height(56.dp.scaledHeight()),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
leading?.let {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
buttonText,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
trailing?.let {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
|
data class SelectionItem(
|
||||||
|
val leadingIcon: ImageVector? = null,
|
||||||
|
val trailing: (@Composable () -> Unit)? = null,
|
||||||
|
val title: (@Composable () -> Unit),
|
||||||
|
val description: (@Composable () -> Unit)? = null,
|
||||||
|
val onClick: (() -> Unit)? = null,
|
||||||
|
val height: Int = 64,
|
||||||
|
)
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowRight
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||||
|
) {
|
||||||
|
items.mapIndexed { index, it ->
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = (it.onClick?.let {
|
||||||
|
Modifier
|
||||||
|
.clickable {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
} ?: Modifier).fillMaxWidth()
|
||||||
|
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 16.dp.scaledWidth())
|
||||||
|
.weight(4f, false)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
it.leadingIcon?.let { icon ->
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
icon.name,
|
||||||
|
modifier = Modifier.size(iconSize.scaledWidth()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = if (it.leadingIcon != null) 16.dp.scaledWidth() else 0.dp)
|
||||||
|
.padding(vertical = if (it.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()),
|
||||||
|
) {
|
||||||
|
it.title()
|
||||||
|
it.description?.let {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.trailing?.let {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.CenterEnd,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth())
|
||||||
|
.weight(1f),
|
||||||
|
) {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.ui.common.config
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
@ -11,22 +10,19 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigurationToggle(
|
fun ConfigurationToggle(
|
||||||
label: String,
|
label: String,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
padding: Dp,
|
|
||||||
onCheckChanged: (checked: Boolean) -> Unit,
|
onCheckChanged: (checked: Boolean) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(),
|
||||||
.padding(padding),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.common.screen
|
|
||||||
|
|
||||||
import androidx.compose.foundation.focusable
|
|
||||||
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.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LoadingScreen() {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.focusable()
|
|
||||||
.padding(),
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.common.prompt
|
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
|
@ -0,0 +1,110 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.textbox
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
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
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CustomTextField(
|
||||||
|
value: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
textStyle: TextStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||||
|
label: @Composable () -> Unit,
|
||||||
|
containerColor: Color,
|
||||||
|
onValueChange: (value: String) -> Unit = {},
|
||||||
|
singleLine: Boolean = false,
|
||||||
|
placeholder: @Composable (() -> Unit)? = null,
|
||||||
|
keyboardOptions: KeyboardOptions,
|
||||||
|
keyboardActions: KeyboardActions,
|
||||||
|
supportingText: @Composable (() -> Unit)? = null,
|
||||||
|
leading: @Composable (() -> Unit)? = null,
|
||||||
|
trailing: @Composable (() -> Unit)? = null,
|
||||||
|
isError: Boolean = false,
|
||||||
|
readOnly: Boolean = false,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val space = " "
|
||||||
|
BasicTextField(
|
||||||
|
value = value,
|
||||||
|
textStyle = textStyle,
|
||||||
|
onValueChange = {
|
||||||
|
onValueChange(it)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
readOnly = readOnly,
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface),
|
||||||
|
modifier = modifier,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
enabled = enabled,
|
||||||
|
singleLine = singleLine,
|
||||||
|
) {
|
||||||
|
OutlinedTextFieldDefaults.DecorationBox(
|
||||||
|
value = space + value,
|
||||||
|
innerTextField = {
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
if (placeholder != null) {
|
||||||
|
placeholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.invoke()
|
||||||
|
},
|
||||||
|
contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 0.dp, bottom = 0.dp),
|
||||||
|
leadingIcon = leading,
|
||||||
|
trailingIcon = trailing,
|
||||||
|
singleLine = singleLine,
|
||||||
|
supportingText = supportingText,
|
||||||
|
colors = TextFieldDefaults.colors().copy(
|
||||||
|
disabledLabelColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
disabledContainerColor = containerColor,
|
||||||
|
focusedLabelColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
focusedContainerColor = containerColor,
|
||||||
|
unfocusedContainerColor = containerColor,
|
||||||
|
focusedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
cursorColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
enabled = enabled,
|
||||||
|
label = label,
|
||||||
|
visualTransformation = VisualTransformation.None,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
placeholder = placeholder,
|
||||||
|
container = {
|
||||||
|
OutlinedTextFieldDefaults.ContainerBox(
|
||||||
|
enabled,
|
||||||
|
isError = isError,
|
||||||
|
interactionSource,
|
||||||
|
colors = TextFieldDefaults.colors().copy(
|
||||||
|
errorContainerColor = containerColor,
|
||||||
|
disabledLabelColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
disabledContainerColor = containerColor,
|
||||||
|
focusedIndicatorColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
focusedLabelColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
focusedContainerColor = containerColor,
|
||||||
|
unfocusedContainerColor = containerColor,
|
||||||
|
focusedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
cursorColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
focusedBorderThickness = 0.5.dp,
|
||||||
|
unfocusedBorderThickness = 0.5.dp,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,6 +68,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.config.components.Applicat
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -193,7 +194,7 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Column(Modifier.padding(it)) {
|
Column(Modifier.padding(top = 24.dp.scaledHeight()).padding(it)) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
|
@ -235,7 +236,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(id = R.string.show_amnezia_properties),
|
stringResource(id = R.string.show_amnezia_properties),
|
||||||
checked = derivedConfigType.value == ConfigType.AMNEZIA,
|
checked = derivedConfigType.value == ConfigType.AMNEZIA,
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { configType = if (it) ConfigType.AMNEZIA else ConfigType.WIREGUARD },
|
onCheckChanged = { configType = if (it) ConfigType.AMNEZIA else ConfigType.WIREGUARD },
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,10 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
@ -146,7 +149,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
|
||||||
selectedTunnel = null
|
selectedTunnel = null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
}.windowInsetsPadding(WindowInsets.systemBars),
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
ScrollDismissFab({
|
ScrollDismissFab({
|
||||||
|
|
|
@ -163,7 +163,6 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), focusReq
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel(config) },
|
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel(config) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -201,7 +200,6 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), focusReq
|
||||||
stringResource(R.string.mobile_data_tunnel),
|
stringResource(R.string.mobile_data_tunnel),
|
||||||
enabled = true,
|
enabled = true,
|
||||||
checked = config.isMobileDataTunnel,
|
checked = config.isMobileDataTunnel,
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
|
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
|
||||||
)
|
)
|
||||||
Column {
|
Column {
|
||||||
|
@ -273,7 +271,6 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), focusReq
|
||||||
stringResource(R.string.restart_on_ping),
|
stringResource(R.string.restart_on_ping),
|
||||||
enabled = !appUiState.settings.isPingEnabled,
|
enabled = !appUiState.settings.isPingEnabled,
|
||||||
checked = config.isPingEnabled || appUiState.settings.isPingEnabled,
|
checked = config.isPingEnabled || appUiState.settings.isPingEnabled,
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { optionsViewModel.onToggleRestartOnPing(config) },
|
onCheckChanged = { optionsViewModel.onToggleRestartOnPing(config) },
|
||||||
)
|
)
|
||||||
if (config.isPingEnabled || appUiState.settings.isPingEnabled) {
|
if (config.isPingEnabled || appUiState.settings.isPingEnabled) {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,8 +1,7 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.LocationManager
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.location.LocationManagerCompat
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
|
@ -16,6 +15,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
|
|
||||||
|
@ -52,22 +52,6 @@ constructor(
|
||||||
private val settings = appDataRepository.settings.getSettingsFlow()
|
private val settings = appDataRepository.settings.getSettingsFlow()
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, Settings())
|
.stateIn(viewModelScope, SharingStarted.Eagerly, Settings())
|
||||||
|
|
||||||
fun onSaveTrustedSSID(ssid: String) = viewModelScope.launch {
|
|
||||||
val trimmed = ssid.trim()
|
|
||||||
with(settings.value) {
|
|
||||||
if (!trustedNetworkSSIDs.contains(trimmed)) {
|
|
||||||
this.trustedNetworkSSIDs.add(ssid)
|
|
||||||
appDataRepository.settings.save(this)
|
|
||||||
} else {
|
|
||||||
SnackbarController.showMessage(
|
|
||||||
StringValue.StringResource(
|
|
||||||
R.string.error_ssid_exists,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLocationDisclosureShown() = viewModelScope.launch {
|
fun setLocationDisclosureShown() = viewModelScope.launch {
|
||||||
appDataRepository.appState.setLocationDisclosureShown(true)
|
appDataRepository.appState.setLocationDisclosureShown(true)
|
||||||
}
|
}
|
||||||
|
@ -76,34 +60,6 @@ constructor(
|
||||||
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
|
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleTunnelOnMobileData() = viewModelScope.launch {
|
|
||||||
with(settings.value) {
|
|
||||||
appDataRepository.settings.save(
|
|
||||||
copy(
|
|
||||||
isTunnelOnMobileDataEnabled = !this.isTunnelOnMobileDataEnabled,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch {
|
|
||||||
with(settings.value) {
|
|
||||||
appDataRepository.settings.save(
|
|
||||||
copy(
|
|
||||||
trustedNetworkSSIDs = (this.trustedNetworkSSIDs - ssid).toMutableList(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun exportTunnels(files: List<File>) = viewModelScope.launch {
|
|
||||||
fileUtils.saveFilesToZip(files).onSuccess {
|
|
||||||
SnackbarController.showMessage(StringValue.StringResource(R.string.exported_configs_message))
|
|
||||||
}.onFailure {
|
|
||||||
SnackbarController.showMessage(StringValue.StringResource(R.string.export_configs_failed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onToggleAutoTunnel(context: Context) = viewModelScope.launch {
|
fun onToggleAutoTunnel(context: Context) = viewModelScope.launch {
|
||||||
with(settings.value) {
|
with(settings.value) {
|
||||||
var isAutoTunnelPaused = this.isAutoTunnelPaused
|
var isAutoTunnelPaused = this.isAutoTunnelPaused
|
||||||
|
@ -132,24 +88,6 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleTunnelOnEthernet() = viewModelScope.launch {
|
|
||||||
with(settings.value) {
|
|
||||||
appDataRepository.settings.save(
|
|
||||||
copy(
|
|
||||||
isTunnelOnEthernetEnabled = !isTunnelOnEthernetEnabled,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isLocationEnabled(context: Context): Boolean {
|
|
||||||
val locationManager =
|
|
||||||
context.getSystemService(
|
|
||||||
Context.LOCATION_SERVICE,
|
|
||||||
) as LocationManager
|
|
||||||
return LocationManagerCompat.isLocationEnabled(locationManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onToggleShortcutsEnabled() = viewModelScope.launch {
|
fun onToggleShortcutsEnabled() = viewModelScope.launch {
|
||||||
with(settings.value) {
|
with(settings.value) {
|
||||||
appDataRepository.settings.save(
|
appDataRepository.settings.save(
|
||||||
|
@ -170,16 +108,6 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleTunnelOnWifi() = viewModelScope.launch {
|
|
||||||
with(settings.value) {
|
|
||||||
appDataRepository.settings.save(
|
|
||||||
copy(
|
|
||||||
isTunnelOnWifiEnabled = !isTunnelOnWifiEnabled,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onToggleAmnezia() = viewModelScope.launch {
|
fun onToggleAmnezia() = viewModelScope.launch {
|
||||||
with(settings.value) {
|
with(settings.value) {
|
||||||
if (isKernelEnabled) {
|
if (isKernelEnabled) {
|
||||||
|
@ -210,16 +138,6 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleRestartOnPing() = viewModelScope.launch {
|
|
||||||
with(settings.value) {
|
|
||||||
appDataRepository.settings.save(
|
|
||||||
copy(
|
|
||||||
isPingEnabled = !isPingEnabled,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun isKernelSupported(): Boolean {
|
private suspend fun isKernelSupported(): Boolean {
|
||||||
return withContext(ioDispatcher) {
|
return withContext(ioDispatcher) {
|
||||||
WgQuickBackend.hasKernelSupport()
|
WgQuickBackend.hasKernelSupport()
|
||||||
|
@ -262,12 +180,16 @@ constructor(
|
||||||
requestRoot()
|
requestRoot()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportAllConfigs() = viewModelScope.launch {
|
fun exportAllConfigs(context: Context) = viewModelScope.launch {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
|
val shareFile = fileUtils.createNewShareFile("wg-export_${Instant.now().epochSecond}.zip")
|
||||||
val tunnels = appDataRepository.tunnels.getAll()
|
val tunnels = appDataRepository.tunnels.getAll()
|
||||||
val wgFiles = fileUtils.createWgFiles(tunnels)
|
val wgFiles = fileUtils.createWgFiles(tunnels)
|
||||||
val amFiles = fileUtils.createAmFiles(tunnels)
|
val amFiles = fileUtils.createAmFiles(tunnels)
|
||||||
exportTunnels(wgFiles + amFiles)
|
val allFiles = wgFiles + amFiles
|
||||||
|
fileUtils.zipAll(shareFile, allFiles)
|
||||||
|
val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), shareFile)
|
||||||
|
context.launchShareFile(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
||||||
|
import androidx.compose.material.icons.outlined.Contrast
|
||||||
|
import androidx.compose.material.icons.outlined.Translate
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
|
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.LocalNavController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppearanceScreen() {
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.windowInsetsPadding(WindowInsets.systemBars)
|
||||||
|
.padding(top = 24.dp.scaledHeight())
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth()),
|
||||||
|
) {
|
||||||
|
SurfaceSelectionGroupButton(
|
||||||
|
listOf(
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.Translate,
|
||||||
|
title = { Text(stringResource(R.string.language), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||||
|
onClick = { navController.navigate(Route.Language) },
|
||||||
|
trailing = {
|
||||||
|
val icon = Icons.AutoMirrored.Outlined.ArrowForward
|
||||||
|
Icon(icon, icon.name)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
SurfaceSelectionGroupButton(
|
||||||
|
listOf(
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.Contrast,
|
||||||
|
title = { Text(stringResource(R.string.display_theme), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||||
|
onClick = { navController.navigate(Route.Display) },
|
||||||
|
trailing = {
|
||||||
|
val icon = Icons.AutoMirrored.Outlined.ArrowForward
|
||||||
|
Icon(icon, icon.name)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisplayScreen(appUiState: AppUiState, viewModel: DisplayViewModel = hiltViewModel()) {
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.windowInsetsPadding(WindowInsets.systemBars)
|
||||||
|
.padding(top = 24.dp.scaledHeight())
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth()),
|
||||||
|
) {
|
||||||
|
IconSurfaceButton(
|
||||||
|
title = stringResource(R.string.automatic),
|
||||||
|
onClick = {
|
||||||
|
viewModel.onThemeChange(Theme.AUTOMATIC)
|
||||||
|
},
|
||||||
|
selected = appUiState.generalState.theme == Theme.AUTOMATIC,
|
||||||
|
)
|
||||||
|
IconSurfaceButton(
|
||||||
|
title = stringResource(R.string.light),
|
||||||
|
onClick = { viewModel.onThemeChange(Theme.LIGHT) },
|
||||||
|
selected = appUiState.generalState.theme == Theme.LIGHT,
|
||||||
|
)
|
||||||
|
IconSurfaceButton(
|
||||||
|
title = stringResource(R.string.dark),
|
||||||
|
onClick = { viewModel.onThemeChange(Theme.DARK) },
|
||||||
|
selected = appUiState.generalState.theme == Theme.DARK,
|
||||||
|
)
|
||||||
|
IconSurfaceButton(
|
||||||
|
title = stringResource(R.string.dynamic),
|
||||||
|
onClick = { viewModel.onThemeChange(Theme.DYNAMIC) },
|
||||||
|
selected = appUiState.generalState.theme == Theme.DYNAMIC,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class DisplayViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val appStateRepository: AppStateRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
fun onThemeChange(theme: Theme) = viewModelScope.launch {
|
||||||
|
appStateRepository.setTheme(theme)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.datastore.LocaleStorage
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.SelectedLabel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.navigateAndForget
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.text.Collator
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LanguageScreen(localeStorage: LocaleStorage) {
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val collator = Collator.getInstance(Locale.getDefault())
|
||||||
|
|
||||||
|
val currentLocale = remember { mutableStateOf(LocaleUtil.OPTION_PHONE_LANGUAGE) }
|
||||||
|
|
||||||
|
val locales = LocaleUtil.supportedLocales.map {
|
||||||
|
val tag = it.replace("_", "-")
|
||||||
|
Locale.forLanguageTag(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortedLocales =
|
||||||
|
remember(locales) {
|
||||||
|
locales.sortedWith(compareBy(collator) { it.getDisplayName(it) }).toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
currentLocale.value = localeStorage.getPreferredLocale()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onChangeLocale(locale: String) {
|
||||||
|
Timber.d("Setting preferred locale: $locale")
|
||||||
|
localeStorage.setPreferredLocale(locale)
|
||||||
|
LocaleUtil.applyLocalizedContext(context, locale)
|
||||||
|
navController.navigateAndForget(Route.Main)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.windowInsetsPadding(WindowInsets.systemBars)
|
||||||
|
.padding(top = 24.dp.scaledHeight())
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth()).windowInsetsPadding(WindowInsets.navigationBars),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SelectionItemButton(
|
||||||
|
buttonText = stringResource(R.string.automatic),
|
||||||
|
onClick = {
|
||||||
|
onChangeLocale(LocaleUtil.OPTION_PHONE_LANGUAGE)
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
if (currentLocale.value == LocaleUtil.OPTION_PHONE_LANGUAGE) {
|
||||||
|
SelectedLabel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ripple = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(sortedLocales, key = { it }) { locale ->
|
||||||
|
SelectionItemButton(
|
||||||
|
buttonText = locale.getDisplayLanguage(locale).capitalize(locale) +
|
||||||
|
if (locale.toLanguageTag().contains("-")) " (${locale.getDisplayCountry(locale).capitalize(locale)})" else "",
|
||||||
|
onClick = {
|
||||||
|
onChangeLocale(locale.toLanguageTag())
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
if (locale.toLanguageTag() == currentLocale.value) {
|
||||||
|
SelectedLabel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ripple = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,232 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.ime
|
||||||
|
import androidx.compose.foundation.layout.isImeVisible
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.NetworkPing
|
||||||
|
import androidx.compose.material.icons.outlined.Security
|
||||||
|
import androidx.compose.material.icons.outlined.SettingsEthernet
|
||||||
|
import androidx.compose.material.icons.outlined.SignalCellular4Bar
|
||||||
|
import androidx.compose.material.icons.outlined.Wifi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
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 com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.accompanist.permissions.isGranted
|
||||||
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
|
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.TopNavBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isLocationServicesEnabled
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class, ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltViewModel()) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
var currentText by remember { mutableStateOf("") }
|
||||||
|
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||||
|
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||||
|
var showLocationDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(uiState.settings.trustedNetworkSSIDs) {
|
||||||
|
currentText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAutoTunnelWifiChecked() {
|
||||||
|
when (false) {
|
||||||
|
isBackgroundLocationGranted -> showLocationDialog = true
|
||||||
|
fineLocationState.status.isGranted -> showLocationDialog = true
|
||||||
|
context.isLocationServicesEnabled() ->
|
||||||
|
showLocationServicesAlertDialog = true
|
||||||
|
else -> {
|
||||||
|
viewModel.onToggleTunnelOnWifi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopNavBar(stringResource(R.string.auto_tunneling))
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 24.dp.scaledHeight()).padding(it)
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth()),
|
||||||
|
) {
|
||||||
|
SurfaceSelectionGroupButton(
|
||||||
|
mutableListOf(
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.Wifi,
|
||||||
|
title = { Text(stringResource(R.string.tunnel_on_wifi), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||||
|
description = {
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
|
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||||
|
onClick = { checked ->
|
||||||
|
if (!checked || uiState.isRooted) viewModel.onToggleTunnelOnWifi().also { return@ScaledSwitch }
|
||||||
|
onAutoTunnelWifiChecked()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.SignalCellular4Bar,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.tunnel_mobile_data),
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
|
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||||
|
onClick = { viewModel.onToggleTunnelOnMobileData() },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.SettingsEthernet,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.tunnel_on_ethernet),
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||||
|
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||||
|
onClick = { viewModel.onToggleTunnelOnEthernet() },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.NetworkPing,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.restart_on_ping),
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
checked = uiState.settings.isPingEnabled,
|
||||||
|
onClick = { viewModel.onToggleRestartOnPing() },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).apply {
|
||||||
|
if (uiState.settings.isTunnelOnWifiEnabled) {
|
||||||
|
add(1,
|
||||||
|
SelectionItem(
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(4f, false)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
val icon = Icons.Outlined.Security
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
icon.name,
|
||||||
|
modifier = Modifier.size(iconSize.scaledWidth()),
|
||||||
|
)
|
||||||
|
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(
|
||||||
|
"Trusted wifi names",
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
description = {
|
||||||
|
TrustedNetworkTextBox(
|
||||||
|
uiState.settings.trustedNetworkSSIDs, onDelete = viewModel::onDeleteTrustedSSID,
|
||||||
|
currentText = currentText,
|
||||||
|
onSave = viewModel::onSaveTrustedSSID,
|
||||||
|
onValueChange = { currentText = it }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class AutoTunnelViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val appDataRepository: AppDataRepository,
|
||||||
|
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val settings = appDataRepository.settings.getSettingsFlow()
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, Settings())
|
||||||
|
|
||||||
|
fun onToggleTunnelOnWifi() = viewModelScope.launch {
|
||||||
|
with(settings.value) {
|
||||||
|
appDataRepository.settings.save(
|
||||||
|
copy(
|
||||||
|
isTunnelOnWifiEnabled = !isTunnelOnWifiEnabled,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onToggleTunnelOnMobileData() = viewModelScope.launch {
|
||||||
|
with(settings.value) {
|
||||||
|
appDataRepository.settings.save(
|
||||||
|
copy(
|
||||||
|
isTunnelOnMobileDataEnabled = !this.isTunnelOnMobileDataEnabled,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch {
|
||||||
|
with(settings.value) {
|
||||||
|
appDataRepository.settings.save(
|
||||||
|
copy(
|
||||||
|
trustedNetworkSSIDs = (this.trustedNetworkSSIDs - ssid).toMutableList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onToggleTunnelOnEthernet() = viewModelScope.launch {
|
||||||
|
with(settings.value) {
|
||||||
|
appDataRepository.settings.save(
|
||||||
|
copy(
|
||||||
|
isTunnelOnEthernetEnabled = !isTunnelOnEthernetEnabled,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSaveTrustedSSID(ssid: String) = viewModelScope.launch {
|
||||||
|
if (ssid.isEmpty()) return@launch
|
||||||
|
val trimmed = ssid.trim()
|
||||||
|
with(settings.value) {
|
||||||
|
if (!trustedNetworkSSIDs.contains(trimmed)) {
|
||||||
|
this.trustedNetworkSSIDs.add(ssid)
|
||||||
|
appDataRepository.settings.save(this)
|
||||||
|
} else {
|
||||||
|
SnackbarController.showMessage(
|
||||||
|
StringValue.StringResource(
|
||||||
|
R.string.error_ssid_exists,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onToggleRestartOnPing() = viewModelScope.launch {
|
||||||
|
with(settings.value) {
|
||||||
|
appDataRepository.settings.save(
|
||||||
|
copy(
|
||||||
|
isPingEnabled = !isPingEnabled,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
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
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
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 com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun TrustedNetworkTextBox(trustedNetworks: List<String>, onDelete: (ssid: String) -> Unit, currentText: String, onSave : (ssid: String) -> Unit, onValueChange: (network: String) -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())){
|
||||||
|
FlowRow(
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
|
) {
|
||||||
|
trustedNetworks.forEach { ssid ->
|
||||||
|
ClickableIconButton(
|
||||||
|
onClick = {
|
||||||
|
if (context.isRunningOnTv()) {
|
||||||
|
//focusRequester.requestFocus()
|
||||||
|
onDelete(ssid)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onIconClick = {
|
||||||
|
//if (context.isRunningOnTv()) focusRequester.requestFocus()
|
||||||
|
onDelete(ssid)
|
||||||
|
},
|
||||||
|
text = ssid,
|
||||||
|
icon = Icons.Filled.Close,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CustomTextField(
|
||||||
|
value = currentText,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(
|
||||||
|
top = 5.dp,
|
||||||
|
bottom = 10.dp,
|
||||||
|
).fillMaxWidth().padding(end = 16.dp.scaledWidth()),
|
||||||
|
supportingText = { WildcardSupportingLabel { context.openWebUrl(it)} },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { onSave(currentText) }),
|
||||||
|
trailing = {
|
||||||
|
if (currentText != "") {
|
||||||
|
IconButton(onClick = {
|
||||||
|
onSave(currentText)
|
||||||
|
}) {
|
||||||
|
val icon = Icons.Outlined.Add
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
R.string
|
||||||
|
.trusted_ssid_value_description,
|
||||||
|
),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,11 +7,14 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
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.height
|
import androidx.compose.foundation.layout.height
|
||||||
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.systemBars
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
@ -63,6 +66,7 @@ fun SupportScreen(focusRequester: FocusRequester, appUiState: AppUiState) {
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.windowInsetsPadding(WindowInsets.systemBars)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.focusable(),
|
.focusable(),
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -4,8 +4,11 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
|
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.chunked
|
import com.zaneschepke.wireguardautotunnel.util.extensions.chunked
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
|
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
@ -19,7 +20,6 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -29,6 +29,7 @@ class LogsViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val localLogCollector: LogReader,
|
private val localLogCollector: LogReader,
|
||||||
|
private val fileUtils: FileUtils,
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
|
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
@ -51,12 +52,7 @@ constructor(
|
||||||
|
|
||||||
fun shareLogs(context: Context): Job = viewModelScope.launch(ioDispatcher) {
|
fun shareLogs(context: Context): Job = viewModelScope.launch(ioDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val sharePath = File(context.filesDir, "external_files")
|
val file = fileUtils.createNewShareFile("${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.zip")
|
||||||
if (sharePath.exists()) sharePath.delete()
|
|
||||||
sharePath.mkdir()
|
|
||||||
val file = File("${sharePath.path + "/" + Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.zip")
|
|
||||||
if (file.exists()) file.delete()
|
|
||||||
file.createNewFile()
|
|
||||||
localLogCollector.zipLogFiles(file.absolutePath)
|
localLogCollector.zipLogFiles(file.absolutePath)
|
||||||
val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), file)
|
val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), file)
|
||||||
context.launchShareFile(uri)
|
context.launchShareFile(uri)
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
|
||||||
|
val iconSize = 24.dp.scaledHeight()
|
|
@ -36,24 +36,34 @@ private val LightColorScheme =
|
||||||
onSecondaryContainer = ThemeColors.Light.primary,
|
onSecondaryContainer = ThemeColors.Light.primary,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enum class Theme {
|
||||||
|
AUTOMATIC,
|
||||||
|
LIGHT,
|
||||||
|
DARK,
|
||||||
|
DYNAMIC
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WireguardAutoTunnelTheme(
|
fun WireguardAutoTunnelTheme(
|
||||||
// force dark theme
|
theme: Theme = Theme.AUTOMATIC,
|
||||||
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val colorScheme = when {
|
val isDark = isSystemInDarkTheme()
|
||||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> {
|
val autoTheme = if(isDark) DarkColorScheme else LightColorScheme
|
||||||
if (useDarkTheme) {
|
val colorScheme = when(theme) {
|
||||||
dynamicDarkColorScheme(context)
|
Theme.AUTOMATIC -> autoTheme
|
||||||
} else {
|
Theme.DARK -> DarkColorScheme
|
||||||
dynamicLightColorScheme(context)
|
Theme.LIGHT -> LightColorScheme
|
||||||
}
|
Theme.DYNAMIC -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
if (isDark) {
|
||||||
|
dynamicDarkColorScheme(context)
|
||||||
|
} else {
|
||||||
|
dynamicLightColorScheme(context)
|
||||||
|
}
|
||||||
|
} else autoTheme
|
||||||
}
|
}
|
||||||
useDarkTheme -> DarkColorScheme
|
|
||||||
// TODO force dark theme for now until light theme designed
|
|
||||||
else -> DarkColorScheme
|
|
||||||
}
|
}
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
if (!view.isInEditMode) {
|
if (!view.isInEditMode) {
|
||||||
|
@ -62,8 +72,7 @@ fun WireguardAutoTunnelTheme(
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
window.statusBarColor = Color.Transparent.toArgb()
|
window.statusBarColor = Color.Transparent.toArgb()
|
||||||
window.navigationBarColor = Color.Transparent.toArgb()
|
window.navigationBarColor = Color.Transparent.toArgb()
|
||||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
|
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = isDark
|
||||||
!useDarkTheme
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@ import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaled
|
||||||
|
|
||||||
// Set of Material typography styles to start with
|
// Set of Material typography styles to start with
|
||||||
|
|
||||||
|
@ -17,43 +17,45 @@ val inter = FontFamily(
|
||||||
|
|
||||||
val Typography =
|
val Typography =
|
||||||
Typography(
|
Typography(
|
||||||
bodyLarge =
|
bodyLarge = TextStyle(
|
||||||
TextStyle(
|
|
||||||
fontFamily = inter,
|
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp.scaled(),
|
||||||
lineHeight = 24.sp,
|
lineHeight = 24.sp.scaled(),
|
||||||
letterSpacing = 0.5.sp,
|
letterSpacing = 0.5.sp,
|
||||||
),
|
),
|
||||||
bodySmall = TextStyle(
|
bodySmall = TextStyle(
|
||||||
fontFamily = inter,
|
fontFamily = inter,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp.scaled(),
|
||||||
lineHeight = 20.sp,
|
lineHeight = 20.sp.scaled(),
|
||||||
letterSpacing = 1.sp,
|
letterSpacing = 1.sp,
|
||||||
color = LightGrey,
|
color = LightGrey,
|
||||||
),
|
),
|
||||||
|
bodyMedium = TextStyle(
|
||||||
|
fontSize = 14.sp.scaled(),
|
||||||
|
lineHeight = 20.sp.scaled(),
|
||||||
|
fontWeight = FontWeight(400),
|
||||||
|
letterSpacing = 0.25.sp,
|
||||||
|
),
|
||||||
labelLarge = TextStyle(
|
labelLarge = TextStyle(
|
||||||
fontFamily = inter,
|
fontFamily = inter,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp.scaled(),
|
||||||
lineHeight = 18.sp,
|
lineHeight = 18.sp.scaled(),
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
labelMedium = TextStyle(
|
labelMedium = TextStyle(
|
||||||
fontFamily = inter,
|
fontFamily = inter,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp.scaled(),
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp.scaled(),
|
||||||
letterSpacing = 0.5.sp,
|
letterSpacing = 0.5.sp,
|
||||||
),
|
),
|
||||||
titleMedium = TextStyle(
|
titleMedium = TextStyle(
|
||||||
fontFamily = inter,
|
fontFamily = inter,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp.scaled(),
|
||||||
lineHeight = 21.sp,
|
lineHeight = 21.sp.scaled(),
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val iconSize = 15.dp
|
|
||||||
|
|
|
@ -1,19 +1,13 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.util
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.provider.MediaStore.MediaColumns
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStream
|
import java.io.FileOutputStream
|
||||||
import java.time.Instant
|
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
|
@ -22,73 +16,60 @@ class FileUtils(
|
||||||
private val ioDispatcher: CoroutineDispatcher,
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun createWgFiles(tunnels: TunnelConfigs): List<File> {
|
suspend fun createWgFiles(tunnels: TunnelConfigs): List<File> {
|
||||||
return tunnels.map { config ->
|
|
||||||
val file = File(context.cacheDir, "${config.name}-wg.conf")
|
|
||||||
file.outputStream().use {
|
|
||||||
it.write(config.wgQuick.toByteArray())
|
|
||||||
}
|
|
||||||
file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createAmFiles(tunnels: TunnelConfigs): List<File> {
|
|
||||||
return tunnels.filter { it.amQuick != TunnelConfig.AM_QUICK_DEFAULT }.map { config ->
|
|
||||||
val file = File(context.cacheDir, "${config.name}-am.conf")
|
|
||||||
file.outputStream().use {
|
|
||||||
it.write(config.amQuick.toByteArray())
|
|
||||||
}
|
|
||||||
file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun saveFilesToZip(files: List<File>): Result<Unit> {
|
|
||||||
return withContext(ioDispatcher) {
|
return withContext(ioDispatcher) {
|
||||||
try {
|
tunnels.map { config ->
|
||||||
val zipOutputStream =
|
val file = File(context.cacheDir, "${config.name}-wg.conf")
|
||||||
createDownloadsFileOutputStream(
|
file.outputStream().use {
|
||||||
"wg-export_${Instant.now().epochSecond}.zip",
|
it.write(config.wgQuick.toByteArray())
|
||||||
Constants.ZIP_FILE_MIME_TYPE,
|
}
|
||||||
)
|
file
|
||||||
ZipOutputStream(zipOutputStream).use { zos ->
|
}
|
||||||
files.forEach { file ->
|
}
|
||||||
val entry = ZipEntry(file.name)
|
}
|
||||||
zos.putNextEntry(entry)
|
|
||||||
if (file.isFile) {
|
suspend fun createAmFiles(tunnels: TunnelConfigs): List<File> {
|
||||||
file.inputStream().use { fis -> fis.copyTo(zos) }
|
return withContext(ioDispatcher) {
|
||||||
|
tunnels.filter { it.amQuick != TunnelConfig.AM_QUICK_DEFAULT }.map { config ->
|
||||||
|
val file = File(context.cacheDir, "${config.name}-am.conf")
|
||||||
|
file.outputStream().use {
|
||||||
|
it.write(config.amQuick.toByteArray())
|
||||||
|
}
|
||||||
|
file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun zipAll(zipFile: File, files: List<File>) {
|
||||||
|
withContext(ioDispatcher) {
|
||||||
|
ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { zos ->
|
||||||
|
files.forEach { file ->
|
||||||
|
val zipFileName = (
|
||||||
|
file.parentFile?.let { parent ->
|
||||||
|
file.absolutePath.removePrefix(parent.absolutePath)
|
||||||
|
} ?: file.absolutePath
|
||||||
|
).removePrefix("/")
|
||||||
|
val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
|
||||||
|
zos.putNextEntry(entry)
|
||||||
|
if (file.isFile) {
|
||||||
|
file.inputStream().use {
|
||||||
|
it.copyTo(zos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@withContext Result.success(Unit)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
Result.failure(ConfigExportException)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO issue with android 9
|
suspend fun createNewShareFile(name: String): File {
|
||||||
private fun createDownloadsFileOutputStream(fileName: String, mimeType: String = Constants.ALL_FILE_TYPES): OutputStream? {
|
return withContext(ioDispatcher) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
val sharePath = File(context.filesDir, "external_files")
|
||||||
val resolver = context.contentResolver
|
if (sharePath.exists()) sharePath.delete()
|
||||||
val contentValues =
|
sharePath.mkdir()
|
||||||
ContentValues().apply {
|
val file = File("${sharePath.path}/$name")
|
||||||
put(MediaColumns.DISPLAY_NAME, fileName)
|
if (file.exists()) file.delete()
|
||||||
put(MediaColumns.MIME_TYPE, mimeType)
|
file.createNewFile()
|
||||||
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
file
|
||||||
}
|
|
||||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
|
||||||
if (uri != null) {
|
|
||||||
return resolver.openOutputStream(uri)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val target =
|
|
||||||
File(
|
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
fileName,
|
|
||||||
)
|
|
||||||
return target.outputStream()
|
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.LocaleList
|
||||||
|
import androidx.core.os.ConfigurationCompat
|
||||||
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object LocaleUtil {
|
||||||
|
private const val DEFAULT_LANG = "en"
|
||||||
|
val supportedLocales: Array<String> = BuildConfig.LANGUAGES
|
||||||
|
const val OPTION_PHONE_LANGUAGE = "sys_def"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the locale to use depending on the preference value
|
||||||
|
* when preference value = "sys_def" returns the locale of current system
|
||||||
|
* else it returns the locale code e.g. "en", "bn" etc.
|
||||||
|
*/
|
||||||
|
fun getLocaleFromPrefCode(prefCode: String): Locale {
|
||||||
|
val localeCode = if (prefCode != OPTION_PHONE_LANGUAGE) {
|
||||||
|
prefCode
|
||||||
|
} else {
|
||||||
|
val systemLang = ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)?.language ?: DEFAULT_LANG
|
||||||
|
if (systemLang in supportedLocales) {
|
||||||
|
systemLang
|
||||||
|
} else {
|
||||||
|
DEFAULT_LANG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Locale.forLanguageTag(localeCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLocalizedConfiguration(prefLocaleCode: String): Configuration {
|
||||||
|
val locale = getLocaleFromPrefCode(prefLocaleCode)
|
||||||
|
return getLocalizedConfiguration(locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLocalizedConfiguration(locale: Locale): Configuration {
|
||||||
|
val config = Configuration()
|
||||||
|
return config.apply {
|
||||||
|
config.setLayoutDirection(locale)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
config.setLocale(locale)
|
||||||
|
val localeList = LocaleList(locale)
|
||||||
|
LocaleList.setDefault(localeList)
|
||||||
|
config.setLocales(localeList)
|
||||||
|
} else {
|
||||||
|
config.setLocale(locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLocalizedContext(baseContext: Context, prefLocaleCode: String?): Context {
|
||||||
|
if (prefLocaleCode == null) return baseContext
|
||||||
|
val currentLocale = getLocaleFromPrefCode(prefLocaleCode)
|
||||||
|
val baseLocale = getLocaleFromConfiguration(baseContext.resources.configuration)
|
||||||
|
Locale.setDefault(currentLocale)
|
||||||
|
return if (!baseLocale.toString().equals(currentLocale.toString(), ignoreCase = true)) {
|
||||||
|
val config = getLocalizedConfiguration(currentLocale)
|
||||||
|
baseContext.createConfigurationContext(config)
|
||||||
|
baseContext
|
||||||
|
} else {
|
||||||
|
baseContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyLocalizedContext(baseContext: Context, prefLocaleCode: String) {
|
||||||
|
val currentLocale = getLocaleFromPrefCode(prefLocaleCode)
|
||||||
|
val baseLocale = getLocaleFromConfiguration(baseContext.resources.configuration)
|
||||||
|
Locale.setDefault(currentLocale)
|
||||||
|
if (!baseLocale.toString().equals(currentLocale.toString(), ignoreCase = true)) {
|
||||||
|
val config = getLocalizedConfiguration(currentLocale)
|
||||||
|
baseContext.resources.updateConfiguration(config, baseContext.resources.displayMetrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun getLocaleFromConfiguration(configuration: Configuration): Locale {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
configuration.locales.get(0)
|
||||||
|
} else {
|
||||||
|
configuration.locale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLocalizedResources(resources: Resources, prefLocaleCode: String): Resources {
|
||||||
|
val locale = getLocaleFromPrefCode(prefLocaleCode)
|
||||||
|
val config = resources.configuration
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
config.locale = locale
|
||||||
|
config.setLayoutDirection(locale)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
resources.updateConfiguration(config, resources.displayMetrics)
|
||||||
|
return resources
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,16 +4,24 @@ import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.location.LocationManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.core.location.LocationManagerCompat
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.receiver.BackgroundActionReceiver
|
import com.zaneschepke.wireguardautotunnel.receiver.BackgroundActionReceiver
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
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
|
||||||
|
|
||||||
fun Context.openWebUrl(url: String): Result<Unit> {
|
fun Context.openWebUrl(url: String): Result<Unit> {
|
||||||
return kotlin.runCatching {
|
return kotlin.runCatching {
|
||||||
val webpage: Uri = Uri.parse(url)
|
val webpage: Uri = Uri.parse(url)
|
||||||
|
@ -26,6 +34,44 @@ fun Context.openWebUrl(url: String): Result<Unit> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val Context.actionBarSize
|
||||||
|
get() = theme.obtainStyledAttributes(intArrayOf(android.R.attr.actionBarSize))
|
||||||
|
.let { attrs -> attrs.getDimension(0, 0F).toInt().also { attrs.recycle() } }
|
||||||
|
|
||||||
|
fun Context.resizeHeight(dp: Dp): Dp {
|
||||||
|
val displayMetrics = resources.displayMetrics
|
||||||
|
val density = displayMetrics.density
|
||||||
|
val height = displayMetrics.heightPixels - this.actionBarSize
|
||||||
|
val resizeHeightPercentage =
|
||||||
|
(height.toFloat() / BASELINE_HEIGHT) * (BASELINE_DENSITY.toFloat() / density)
|
||||||
|
return dp * resizeHeightPercentage
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.resizeHeight(textUnit: TextUnit): TextUnit {
|
||||||
|
val displayMetrics = resources.displayMetrics
|
||||||
|
val density = displayMetrics.density
|
||||||
|
val height = displayMetrics.heightPixels - actionBarSize
|
||||||
|
val resizeHeightPercentage =
|
||||||
|
(height.toFloat() / BASELINE_HEIGHT) * (BASELINE_DENSITY.toFloat() / density)
|
||||||
|
return textUnit * resizeHeightPercentage * 1.1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.resizeWidth(dp: Dp): Dp {
|
||||||
|
val displayMetrics = resources.displayMetrics
|
||||||
|
val density = displayMetrics.density
|
||||||
|
val width = displayMetrics.widthPixels
|
||||||
|
val resizeWidthPercentage =
|
||||||
|
(width.toFloat() / BASELINE_WIDTH) * (BASELINE_DENSITY.toFloat() / density)
|
||||||
|
return dp * resizeWidthPercentage
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.launchNotificationSettings() {
|
||||||
|
val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
|
||||||
|
this.startActivity(settingsIntent)
|
||||||
|
}
|
||||||
|
|
||||||
fun Context.launchShareFile(file: Uri) {
|
fun Context.launchShareFile(file: Uri) {
|
||||||
val shareIntent = Intent().apply {
|
val shareIntent = Intent().apply {
|
||||||
setAction(Intent.ACTION_SEND)
|
setAction(Intent.ACTION_SEND)
|
||||||
|
@ -36,6 +82,14 @@ fun Context.launchShareFile(file: Uri) {
|
||||||
this.startActivity(Intent.createChooser(shareIntent, ""))
|
this.startActivity(Intent.createChooser(shareIntent, ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Context.isLocationServicesEnabled(): Boolean {
|
||||||
|
val locationManager =
|
||||||
|
getSystemService(
|
||||||
|
Context.LOCATION_SERVICE,
|
||||||
|
) as LocationManager
|
||||||
|
return LocationManagerCompat.isLocationEnabled(locationManager)
|
||||||
|
}
|
||||||
|
|
||||||
fun Context.showToast(resId: Int) {
|
fun Context.showToast(resId: Int) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
this,
|
this,
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
|
|
||||||
fun NavController.navigateAndForget(route: Route) {
|
fun NavController.navigateAndForget(route: Route) {
|
||||||
|
@ -8,3 +11,15 @@ fun NavController.navigateAndForget(route: Route) {
|
||||||
popUpTo(0)
|
popUpTo(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Dp.scaledHeight(): Dp {
|
||||||
|
return WireGuardAutoTunnel.instance.resizeHeight(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Dp.scaledWidth(): Dp {
|
||||||
|
return WireGuardAutoTunnel.instance.resizeWidth(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TextUnit.scaled(): TextUnit {
|
||||||
|
return WireGuardAutoTunnel.instance.resizeHeight(this)
|
||||||
|
}
|
||||||
|
|
|
@ -200,4 +200,15 @@
|
||||||
<string name="sec">sec</string>
|
<string name="sec">sec</string>
|
||||||
<string name="handshake">handshake</string>
|
<string name="handshake">handshake</string>
|
||||||
<string name="logs">Logs</string>
|
<string name="logs">Logs</string>
|
||||||
|
<string name="tunnel_notifications">Tunnel notifications</string>
|
||||||
|
<string name="kill_switch">Kill switch</string>
|
||||||
|
<string name="appearance">Appearance</string>
|
||||||
|
<string name="notifications">Notifications</string>
|
||||||
|
<string name="automatic">Automatic</string>
|
||||||
|
<string name="light">Light</string>
|
||||||
|
<string name="dark">Dark</string>
|
||||||
|
<string name="dynamic">Dynamic</string>
|
||||||
|
<string name="language">Language</string>
|
||||||
|
<string name="display_theme">Display theme</string>
|
||||||
|
<string name="selected">Selected</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -72,6 +72,18 @@ fun Project.getSigningProperty(property: String): String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Project.languageList(): List<String> {
|
||||||
|
return fileTree("../app/src/main/res") { include("**/strings.xml") }
|
||||||
|
.asSequence()
|
||||||
|
.map { stringFile -> stringFile.parentFile.name }
|
||||||
|
.map { valuesFolderName -> valuesFolderName.replace("values-", "") }
|
||||||
|
.filter { valuesFolderName -> valuesFolderName != "values" }
|
||||||
|
.map { languageCode -> languageCode.replace("-r", "_") }
|
||||||
|
.distinct()
|
||||||
|
.sorted()
|
||||||
|
.toList() + "en"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue