feat: ui and splash screen improvements

bump deps

allow tunnel stat to stay expanded
closes #265
This commit is contained in:
Zane Schepke 2024-10-12 19:39:56 -04:00
parent 1fb953e2fe
commit ffad6b331f
33 changed files with 410 additions and 418 deletions

View File

@ -66,12 +66,12 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.AppSplashScreen" android:theme="@style/Theme.App.Start"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity <activity
android:name=".ui.SplashActivity" android:name=".ui.MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.AppSplashScreen"> android:theme="@style/Theme.WireguardAutoTunnel">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -83,11 +83,6 @@
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
</activity> </activity>
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.WireguardAutoTunnel">
</activity>
<activity <activity
android:name="com.journeyapps.barcodescanner.CaptureActivity" android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"

View File

@ -25,6 +25,7 @@ class DataStoreManager(
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID") val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED") val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED")
val IS_TUNNEL_STATS_EXPANDED = booleanPreferencesKey("TUNNEL_STATS_EXPANDED")
} }
// preferences // preferences

View File

@ -4,10 +4,12 @@ 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,
) { ) {
companion object { companion object {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false const val PIN_LOCK_ENABLED_DEFAULT = false
const val IS_TUNNEL_STATS_EXPANDED = false
} }
} }

View File

@ -20,5 +20,9 @@ interface AppStateRepository {
suspend fun setCurrentSsid(ssid: String) suspend fun setCurrentSsid(ssid: String)
suspend fun isTunnelStatsExpanded(): Boolean
suspend fun setTunnelStatsExpanded(expanded: Boolean)
val generalStateFlow: Flow<GeneralState> val generalStateFlow: Flow<GeneralState>
} }

View File

@ -45,6 +45,15 @@ class DataStoreAppStateRepository(
dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid) dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
} }
override suspend fun isTunnelStatsExpanded(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.IS_TUNNEL_STATS_EXPANDED)
?: GeneralState.IS_TUNNEL_STATS_EXPANDED
}
override suspend fun setTunnelStatsExpanded(expanded: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.IS_TUNNEL_STATS_EXPANDED, expanded)
}
override val generalStateFlow: Flow<GeneralState> = override val generalStateFlow: Flow<GeneralState> =
dataStoreManager.preferencesFlow.map { prefs -> dataStoreManager.preferencesFlow.map { prefs ->
prefs?.let { pref -> prefs?.let { pref ->
@ -59,6 +68,7 @@ class DataStoreAppStateRepository(
isPinLockEnabled = isPinLockEnabled =
pref[DataStoreManager.IS_PIN_LOCK_ENABLED] pref[DataStoreManager.IS_PIN_LOCK_ENABLED]
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT, ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
isTunnelStatsExpanded = pref[DataStoreManager.IS_TUNNEL_STATS_EXPANDED] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
) )
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Timber.e(e) Timber.e(e)

View File

@ -1,22 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import androidx.navigation.NavHostController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.NavigationService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityRetainedScoped
@Module
@InstallIn(ActivityRetainedComponent::class)
object NavigationModule {
@Provides
@ActivityRetainedScoped
fun provideNestedNavController(@ApplicationContext context: Context): NavHostController {
return NavigationService(context).navController
}
}

View File

@ -9,7 +9,7 @@ import android.content.Intent
import android.graphics.Color import android.graphics.Color
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.SplashActivity import com.zaneschepke.wireguardautotunnel.ui.MainActivity
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
@ -63,7 +63,7 @@ constructor(
} }
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
val pendingIntent: PendingIntent = val pendingIntent: PendingIntent =
Intent(context, SplashActivity::class.java).let { notificationIntent -> Intent(context, MainActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity( PendingIntent.getActivity(
context, context,
0, 0,

View File

@ -2,30 +2,36 @@ package com.zaneschepke.wireguardautotunnel.ui
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController 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
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
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
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
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
@HiltViewModel @HiltViewModel
class AppViewModel class AppViewModel
@Inject @Inject
constructor( constructor(
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
tunnelService: TunnelService, private val tunnelService: Provider<TunnelService>,
val navHostController: NavHostController,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
@ -35,7 +41,7 @@ constructor(
combine( combine(
appDataRepository.settings.getSettingsFlow(), appDataRepository.settings.getSettingsFlow(),
appDataRepository.tunnels.getTunnelConfigsFlow(), appDataRepository.tunnels.getTunnelConfigsFlow(),
tunnelService.vpnState, tunnelService.get().vpnState,
appDataRepository.appState.generalStateFlow, appDataRepository.appState.generalStateFlow,
) { settings, tunnels, tunnelState, generalState -> ) { settings, tunnels, tunnelState, generalState ->
AppUiState( AppUiState(
@ -44,12 +50,50 @@ constructor(
tunnelState, tunnelState,
generalState, generalState,
) )
}.stateIn(
viewModelScope + ioDispatcher,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
_appUiState.value,
)
private val _isAppReady = MutableStateFlow<Boolean>(false)
val isAppReady = _isAppReady.asStateFlow()
init {
viewModelScope.launch {
initPin()
initAutoTunnel()
initTunnel()
appReadyCheck()
} }
.stateIn( }
viewModelScope + ioDispatcher,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), private suspend fun appReadyCheck() {
_appUiState.value, val tunnelCount = appDataRepository.tunnels.count()
) uiState.takeWhile { it.tunnels.size != tunnelCount }.onCompletion {
_isAppReady.emit(true)
}.collect()
}
private suspend fun initTunnel() {
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startStatsJob()
val activeTunnels = appDataRepository.tunnels.getActive()
if (activeTunnels.isNotEmpty() &&
tunnelService.get().getState() == TunnelState.DOWN
) {
tunnelService.get().startTunnel(activeTunnels.first())
}
}
private suspend fun initPin() {
val isPinEnabled = appDataRepository.appState.isPinLockEnabled()
if(isPinEnabled) PinManager.initialize(WireGuardAutoTunnel.instance)
}
private suspend fun initAutoTunnel() {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) ServiceManager.startWatcherService(WireGuardAutoTunnel.instance)
}
fun setTunnels(tunnels: TunnelConfigs) = viewModelScope.launch(ioDispatcher) { fun setTunnels(tunnels: TunnelConfigs) = viewModelScope.launch(ioDispatcher) {
_appUiState.emit( _appUiState.emit(

View File

@ -4,11 +4,13 @@ import android.os.Bundle
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
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
import androidx.compose.animation.fadeIn 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -19,8 +21,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarData import androidx.compose.material3.SnackbarData
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Surface
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -31,11 +33,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb 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.hilt.navigation.compose.hiltViewModel import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
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.data.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
@ -44,6 +47,7 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState 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.isCurrentRoute import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
@ -58,6 +62,7 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -68,11 +73,10 @@ class MainActivity : AppCompatActivity() {
@Inject @Inject
lateinit var tunnelService: TunnelService lateinit var tunnelService: TunnelService
private val viewModel by viewModels<AppViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val isPinLockEnabled = intent.extras?.getBoolean(SplashActivity.IS_PIN_LOCK_ENABLED_KEY)
enableEdgeToEdge( enableEdgeToEdge(
navigationBarStyle = SystemBarStyle.auto( navigationBarStyle = SystemBarStyle.auto(
lightScrim = Color.Transparent.toArgb(), lightScrim = Color.Transparent.toArgb(),
@ -80,10 +84,15 @@ class MainActivity : AppCompatActivity() {
), ),
) )
installSplashScreen().apply {
setKeepOnScreenCondition {
!viewModel.isAppReady.value
}
}
setContent { setContent {
val appViewModel = hiltViewModel<AppViewModel>() val appUiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycle = this.lifecycle)
val appUiState by appViewModel.uiState.collectAsStateWithLifecycle(lifecycle = this.lifecycle) val navController = rememberNavController()
val navController = appViewModel.navHostController
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
LaunchedEffect(appUiState.vpnState.status) { LaunchedEffect(appUiState.vpnState.status) {
@ -95,109 +104,105 @@ class MainActivity : AppCompatActivity() {
context.requestTunnelTileServiceStateUpdate() context.requestTunnelTileServiceStateUpdate()
} }
SnackbarControllerProvider { host -> CompositionLocalProvider(LocalNavController provides navController) {
WireguardAutoTunnelTheme { SnackbarControllerProvider { host ->
val focusRequester = remember { FocusRequester() } WireguardAutoTunnelTheme {
Scaffold( val focusRequester = remember { FocusRequester() }
snackbarHost = { Scaffold(
SnackbarHost(host) { snackbarData: SnackbarData -> snackbarHost = {
CustomSnackBar( SnackbarHost(host) { snackbarData: SnackbarData ->
snackbarData.visuals.message, CustomSnackBar(
isRtl = false, snackbarData.visuals.message,
containerColor = isRtl = false,
MaterialTheme.colorScheme.surfaceColorAtElevation( containerColor =
2.dp, MaterialTheme.colorScheme.surfaceColorAtElevation(
), 2.dp,
) ),
} )
},
containerColor = MaterialTheme.colorScheme.background,
modifier =
Modifier
.focusable()
.focusProperties {
if (navBackStackEntry?.isCurrentRoute(Route.Lock) == true) {
Unit
} else {
up = focusRequester
} }
}, },
bottomBar = { modifier =
BottomNavBar( Modifier
navController, .focusable()
listOf( .focusProperties {
BottomNavItem( if (navBackStackEntry?.isCurrentRoute(Route.Lock) == true) {
name = stringResource(R.string.tunnels), Unit
route = Route.Main, } else {
icon = Icons.Rounded.Home, up = focusRequester
}
},
bottomBar = {
BottomNavBar(
navController,
listOf(
BottomNavItem(
name = stringResource(R.string.tunnels),
route = Route.Main,
icon = Icons.Rounded.Home,
),
BottomNavItem(
name = stringResource(R.string.settings),
route = Route.Settings,
icon = Icons.Rounded.Settings,
),
BottomNavItem(
name = stringResource(R.string.support),
route = Route.Support,
icon = Icons.Rounded.QuestionMark,
),
), ),
BottomNavItem( )
name = stringResource(R.string.settings), },
route = Route.Settings, ) { padding ->
icon = Icons.Rounded.Settings, Box(modifier = Modifier.fillMaxSize().padding(padding)) {
), NavHost(
BottomNavItem( navController,
name = stringResource(R.string.support), enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
route = Route.Support, exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
icon = Icons.Rounded.QuestionMark, startDestination = (if (appUiState.generalState.isPinLockEnabled == true) Route.Lock else Route.Main),
), ) {
), composable<Route.Main> {
) MainScreen(
}, focusRequester = focusRequester,
) { padding -> uiState = appUiState,
Surface(modifier = Modifier.fillMaxSize().padding(padding)) { )
NavHost( }
navController, composable<Route.Settings> {
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) }, SettingsScreen(
exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) }, appViewModel = viewModel,
startDestination = (if (isPinLockEnabled == true) Route.Lock else Route.Main), uiState = appUiState,
) { focusRequester = focusRequester,
composable<Route.Main> { )
MainScreen( }
focusRequester = focusRequester, composable<Route.Support> {
uiState = appUiState, SupportScreen(
navController = navController, focusRequester = focusRequester,
) appUiState = appUiState,
} )
composable<Route.Settings> { }
SettingsScreen( composable<Route.Logs> {
appViewModel = appViewModel, LogsScreen()
uiState = appUiState, }
navController = navController, composable<Route.Config> {
focusRequester = focusRequester, val args = it.toRoute<Route.Config>()
) ConfigScreen(
} focusRequester = focusRequester,
composable<Route.Support> { tunnelId = args.id,
SupportScreen( )
focusRequester = focusRequester, }
navController = navController, composable<Route.Option> {
appUiState = appUiState, val args = it.toRoute<Route.Option>()
) OptionsScreen(
} tunnelId = args.id,
composable<Route.Logs> { focusRequester = focusRequester,
LogsScreen() appUiState = appUiState,
} )
composable<Route.Config> { }
val args = it.toRoute<Route.Config>() composable<Route.Lock> {
ConfigScreen( PinLockScreen(
focusRequester = focusRequester, appViewModel = viewModel,
tunnelId = args.id, )
) }
}
composable<Route.Option> {
val args = it.toRoute<Route.Option>()
OptionsScreen(
navController = navController,
tunnelId = args.id,
focusRequester = focusRequester,
appUiState = appUiState,
)
}
composable<Route.Lock> {
PinLockScreen(
navController = navController,
appViewModel = appViewModel,
)
} }
} }
} }

View File

@ -1,87 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
import javax.inject.Provider
@SuppressLint("CustomSplashScreen")
@AndroidEntryPoint
class SplashActivity : ComponentActivity() {
@Inject
lateinit var appStateRepository: AppStateRepository
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: Provider<TunnelService>
private val appViewModel: AppViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { true }
}
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val pinLockEnabled = async {
appStateRepository.isPinLockEnabled().also {
if (it) PinManager.initialize(WireGuardAutoTunnel.instance)
}
}.await()
async {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) ServiceManager.startWatcherService(application.applicationContext)
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startStatsJob()
val activeTunnels = appDataRepository.tunnels.getActive()
if (activeTunnels.isNotEmpty() &&
tunnelService.get().getState() == TunnelState.DOWN
) {
tunnelService.get().startTunnel(activeTunnels.first())
}
}.await()
async {
val tunnels = appDataRepository.tunnels.getAll()
appViewModel.setTunnels(tunnels)
}.await()
requestAutoTunnelTileServiceUpdate()
val intent =
Intent(this@SplashActivity, MainActivity::class.java).apply {
putExtra(IS_PIN_LOCK_ENABLED_KEY, pinLockEnabled)
}
startActivity(intent)
finish()
}
}
}
companion object {
const val IS_PIN_LOCK_ENABLED_KEY = "is_pin_lock_enabled"
}
}

View File

@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
class NestedScrollListener( val onUp: () -> Unit, val onDown: () -> Unit) : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (available.y < -1) onDown()
if (available.y > 1) onUp()
return Offset.Zero
}
}

View File

@ -10,6 +10,7 @@ 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.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -17,9 +18,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceString import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceString
@ -52,16 +54,17 @@ fun RowListItem(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 15.dp, vertical = 5.dp), .padding(horizontal = 15.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(15.dp),
modifier = Modifier.fillMaxWidth(13 / 20f), modifier = Modifier.fillMaxWidth(13 / 20f),
) { ) {
icon() icon()
Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis) Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelLarge)
} }
rowButton() rowButton()
} }
@ -71,26 +74,32 @@ fun RowListItem(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp), .padding(end = 10.dp, bottom = 10.dp, start = 45.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.spacedBy(30.dp, Alignment.Start),
) { ) {
// TODO change these to string resources
val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis
val peerTx = statistics.peerStats(it)!!.txBytes
val peerRx = statistics.peerStats(it)!!.rxBytes
val peerId = it.toBase64().subSequence(0, 3).toString() + "***" val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
val handshakeSec = val handshakeSec =
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
val handshake = val handshake =
if (handshakeSec == null) "never" else "$handshakeSec secs ago" if (handshakeSec == null) stringResource(R.string.never) else "$handshakeSec " + stringResource(R.string.sec)
val peerTx = statistics.peerStats(it)!!.txBytes
val peerRx = statistics.peerStats(it)!!.rxBytes
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString() val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString()
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString() val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString()
val fontSize = 9.sp Column(
Text("peer: $peerId", fontSize = fontSize) verticalArrangement = Arrangement.spacedBy(10.dp),
Text("handshake: $handshake", fontSize = fontSize) ) {
Text("tx: $peerTxMB MB", fontSize = fontSize) Text(stringResource(R.string.peer).lowercase() + ": $peerId", style = MaterialTheme.typography.bodySmall)
Text("rx: $peerRxMB MB", fontSize = fontSize) Text("tx: $peerTxMB MB", style = MaterialTheme.typography.bodySmall)
}
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(stringResource(R.string.handshake) + ": $handshake", style = MaterialTheme.typography.bodySmall)
Text("rx: $peerRxMB MB", style = MaterialTheme.typography.bodySmall)
}
} }
} }
} }

View File

@ -4,6 +4,7 @@ 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.foundation.layout.padding
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
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -32,6 +33,7 @@ fun ConfigurationToggle(
Text( Text(
label, label,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
style = MaterialTheme.typography.labelLarge,
modifier = modifier =
Modifier Modifier
.weight( .weight(

View File

@ -10,10 +10,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
@ -22,11 +19,9 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
var showBottomBar by rememberSaveable { mutableStateOf(true) } var showBottomBar by rememberSaveable { mutableStateOf(true) }
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
showBottomBar = bottomNavItems.firstOrNull { showBottomBar = bottomNavItems.any {
navBackStackEntry?.destination?.hierarchy?.any { dest -> navBackStackEntry?.isCurrentRoute(it.route) == true
bottomNavItems.map { dest.hasRoute(route = it.route::class) }.contains(true) }
} == true
} != null
if (showBottomBar) { if (showBottomBar) {
NavigationBar( NavigationBar(
@ -53,7 +48,7 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
label = { label = {
Text( Text(
text = item.name, text = item.name,
fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.labelMedium,
) )
}, },
icon = { icon = {

View File

@ -1,10 +1,12 @@
package com.zaneschepke.wireguardautotunnel.ui.common.navigation package com.zaneschepke.wireguardautotunnel.ui.common.navigation
import android.annotation.SuppressLint
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
@SuppressLint("RestrictedApi")
fun NavBackStackEntry?.isCurrentRoute(route: Route): Boolean { fun NavBackStackEntry?.isCurrentRoute(route: Route): Boolean {
return this?.destination?.hierarchy?.any { return this?.destination?.hierarchy?.any {
it.hasRoute(route = route::class) it.hasRoute(route = route::class)

View File

@ -0,0 +1,8 @@
package com.zaneschepke.wireguardautotunnel.ui.common.navigation
import androidx.compose.runtime.compositionLocalOf
import androidx.navigation.NavHostController
val LocalNavController = compositionLocalOf<NavHostController> {
error("NavController was not provided")
}

View File

@ -1,15 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.navigation
import android.content.Context
import androidx.navigation.NavHostController
import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.compose.DialogNavigator
class NavigationService constructor(
context: Context,
) {
val navController = NavHostController(context).apply {
navigatorProvider.addNavigator(ComposeNavigator())
navigatorProvider.addNavigator(DialogNavigator())
}
}

View File

@ -1,22 +1,20 @@
package com.zaneschepke.wireguardautotunnel.ui.common.text package com.zaneschepke.wireguardautotunnel.ui.common.text
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable @Composable
fun SectionTitle(title: String, padding: Dp) { fun SectionTitle(title: String, padding: Dp) {
Text( Text(
title, title,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold), style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp), modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
) )
} }

View File

@ -56,8 +56,10 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
@ -78,12 +80,21 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
val snackbar = SnackbarController.current val snackbar = SnackbarController.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current val clipboardManager: ClipboardManager = LocalClipboardManager.current
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val navController = LocalNavController.current
var showApplicationsDialog by remember { mutableStateOf(false) } var showApplicationsDialog by remember { mutableStateOf(false) }
var showAuthPrompt by remember { mutableStateOf(false) } var showAuthPrompt by remember { mutableStateOf(false) }
var isAuthenticated by remember { mutableStateOf(false) } var isAuthenticated by remember { mutableStateOf(false) }
var configType by remember { mutableStateOf(ConfigType.WIREGUARD) } var configType by remember { mutableStateOf(ConfigType.WIREGUARD) }
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val saved by viewModel.saved.collectAsStateWithLifecycle(null)
LaunchedEffect(saved) {
if (saved == true) {
navController.navigate(Route.Main)
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (!uiState.loading && context.isRunningOnTv()) { if (!uiState.loading && context.isRunningOnTv()) {

View File

@ -6,7 +6,6 @@ import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.wireguard.config.Config import com.wireguard.config.Config
import com.wireguard.config.Interface import com.wireguard.config.Interface
import com.wireguard.config.Peer import com.wireguard.config.Peer
@ -17,7 +16,6 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
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
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
@ -30,8 +28,10 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@ -44,12 +44,14 @@ class ConfigViewModel
@AssistedInject @AssistedInject
constructor( constructor(
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val navController: NavHostController,
@Assisted val id: Int, @Assisted val id: Int,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
private val packageManager = WireGuardAutoTunnel.instance.packageManager private val packageManager = WireGuardAutoTunnel.instance.packageManager
private val _saved = MutableSharedFlow<Boolean>()
val saved = _saved.asSharedFlow()
private val _uiState = MutableStateFlow(ConfigUiState()) private val _uiState = MutableStateFlow(ConfigUiState())
val uiState = _uiState.onStart { val uiState = _uiState.onStart {
appDataRepository.tunnels.getById(id)?.let { appDataRepository.tunnels.getById(id)?.let {
@ -335,7 +337,7 @@ constructor(
SnackbarController.showMessage( SnackbarController.showMessage(
StringValue.StringResource(R.string.config_changes_saved), StringValue.StringResource(R.string.config_changes_saved),
) )
navController.navigate(Route.Main) _saved.emit(true)
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
val message = it.message?.substringAfter(":", missingDelimiterValue = "") val message = it.message?.substringAfter(":", missingDelimiterValue = "")

View File

@ -11,7 +11,6 @@ import androidx.compose.foundation.gestures.detectTapGestures
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.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -46,20 +45,15 @@ import androidx.compose.ui.Alignment
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.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import com.wireguard.android.backend.GoBackend import com.wireguard.android.backend.GoBackend
@ -69,16 +63,19 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.NestedScrollListener
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.theme.corn import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.ui.theme.mint import com.zaneschepke.wireguardautotunnel.ui.theme.Corn
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.extensions.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
@ -86,49 +83,31 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.mapPeerStats
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import timber.log.Timber
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, focusRequester: FocusRequester, navController: NavController) { fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, focusRequester: FocusRequester) {
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current
val snackbar = SnackbarController.current val snackbar = SnackbarController.current
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
var showVpnPermissionDialog by remember { mutableStateOf(false) } var showVpnPermissionDialog by remember { mutableStateOf(false) }
val isVisible = rememberSaveable { mutableStateOf(true) } var isFabVisible by rememberSaveable { mutableStateOf(true) }
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) } var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val nestedScrollConnection = val nestedScrollConnection = remember {
remember { NestedScrollListener({ isFabVisible = false },{ isFabVisible = true })
object : NestedScrollConnection { }
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// Hide FAB
if (available.y < -1) {
isVisible.value = false
}
// Show FAB
if (available.y > 1) {
isVisible.value = true
}
return Offset.Zero
}
}
}
val vpnActivityResultState = val vpnActivityResultState =
rememberLauncherForActivityResult( rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(), ActivityResultContracts.StartActivityForResult(),
onResult = { onResult = {
val accepted = (it.resultCode == RESULT_OK) if(it.resultCode != RESULT_OK) showVpnPermissionDialog = true
if (accepted) {
Timber.d("VPN permission granted")
} else {
showVpnPermissionDialog = true
}
}, },
) )
@ -179,16 +158,11 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
} }
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if (checked) { if (!checked) viewModel.onTunnelStop(tunnel).also { return }
if (uiState.settings.isKernelEnabled) { if (uiState.settings.isKernelEnabled) {
context.startTunnelBackground(tunnel.id) context.startTunnelBackground(tunnel.id)
} else {
viewModel.onTunnelStart(tunnel)
}
} else { } else {
viewModel.onTunnelStop( viewModel.onTunnelStart(tunnel)
tunnel,
)
} }
} }
@ -223,7 +197,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
contentDescription = icon.name, contentDescription = icon.name,
tint = MaterialTheme.colorScheme.onPrimary, tint = MaterialTheme.colorScheme.onPrimary,
) )
}, focusRequester, isVisible = isVisible.value, onClick = { }, focusRequester, isVisible = isFabVisible, onClick = {
showBottomSheet = true showBottomSheet = true
}) })
}, },
@ -282,13 +256,12 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
icon.name, icon.name,
modifier = modifier =
Modifier Modifier
.padding(end = 8.5.dp) .size(iconSize),
.size(25.dp),
tint = tint =
if (uiState.settings.isAutoTunnelPaused) { if (uiState.settings.isAutoTunnelPaused) {
Color.Gray Color.Gray
} else { } else {
mint SilverTree
}, },
) )
}, },
@ -330,6 +303,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
it.id == tunnel.id && it.id == tunnel.id &&
it.isActive it.isActive
} }
val expanded = uiState.generalState.isTunnelStatsExpanded
val leadingIconColor = val leadingIconColor =
( (
if ( if (
@ -339,8 +313,8 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
.map { it.value?.handshakeStatus() } .map { it.value?.handshakeStatus() }
.let { statuses -> .let { statuses ->
when { when {
statuses.all { it == HandshakeStatus.HEALTHY } -> mint statuses.all { it == HandshakeStatus.HEALTHY } -> SilverTree
statuses.any { it == HandshakeStatus.STALE } -> corn statuses.any { it == HandshakeStatus.STALE } -> Corn
statuses.all { it == HandshakeStatus.NOT_STARTED } -> statuses.all { it == HandshakeStatus.NOT_STARTED } ->
Color.Gray Color.Gray
@ -354,7 +328,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
} }
) )
val itemFocusRequester = remember { FocusRequester() } val itemFocusRequester = remember { FocusRequester() }
val expanded = remember { mutableStateOf(false) }
RowListItem( RowListItem(
icon = { icon = {
val circleIcon = Icons.Rounded.Circle val circleIcon = Icons.Rounded.Circle
@ -370,13 +343,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
icon, icon,
icon.name, icon.name,
tint = leadingIconColor, tint = leadingIconColor,
modifier = modifier = Modifier.size(iconSize),
Modifier
.padding(
end = if (icon == circleIcon) 12.5.dp else 10.dp,
start = if (icon == circleIcon) 2.5.dp else 0.dp,
)
.size(if (icon == circleIcon) 15.dp else 20.dp),
) )
}, },
text = tunnel.name, text = tunnel.name,
@ -389,7 +356,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
if ( if (
isActive isActive
) { ) {
expanded.value = !expanded.value viewModel.onExpandedChanged(!expanded)
} }
} else { } else {
selectedTunnel = tunnel selectedTunnel = tunnel
@ -397,7 +364,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
} }
}, },
statistics = uiState.vpnState.statistics, statistics = uiState.vpnState.statistics,
expanded = expanded.value, expanded = expanded && isActive,
focusRequester = focusRequester, focusRequester = focusRequester,
rowButton = { rowButton = {
if ( if (
@ -437,13 +404,11 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
} }
} }
} else { } else {
if (!isActive) expanded.value = false
@Composable @Composable
fun TunnelSwitch() = Switch( fun TunnelSwitch() = Switch(
modifier = Modifier.focusRequester(itemFocusRequester), modifier = Modifier.focusRequester(itemFocusRequester),
checked = isActive, checked = isActive,
onCheckedChange = { checked -> onCheckedChange = { checked ->
if (!checked) expanded.value = false
val intent = if (uiState.settings.isKernelEnabled) null else GoBackend.VpnService.prepare(context) val intent = if (uiState.settings.isKernelEnabled) null else GoBackend.VpnService.prepare(context)
if (intent != null) return@Switch vpnActivityResultState.launch(intent) if (intent != null) return@Switch vpnActivityResultState.launch(intent)
onTunnelToggle(checked, tunnel) onTunnelToggle(checked, tunnel)
@ -474,7 +439,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
uiState.vpnState.status == TunnelState.UP && uiState.vpnState.status == TunnelState.UP &&
(uiState.vpnState.tunnelConfig?.name == tunnel.name) (uiState.vpnState.tunnelConfig?.name == tunnel.name)
) { ) {
expanded.value = !expanded.value viewModel.onExpandedChanged(!expanded)
} else { } else {
snackbar.showMessage( snackbar.showMessage(
context.getString(R.string.turn_on_tunnel), context.getString(R.string.turn_on_tunnel),

View File

@ -63,6 +63,10 @@ constructor(
) )
} }
fun onExpandedChanged(expanded: Boolean) = viewModelScope.launch {
appDataRepository.appState.setTunnelStatsExpanded(expanded)
}
fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch { fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch {
Timber.i("Starting tunnel ${tunnelConfig.name}") Timber.i("Starting tunnel ${tunnelConfig.name}")
tunnelService.startTunnel(tunnelConfig) tunnelService.startTunnel(tunnelConfig)

View File

@ -54,6 +54,7 @@ import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel
@ -68,13 +69,13 @@ import kotlinx.coroutines.delay
@Composable @Composable
fun OptionsScreen( fun OptionsScreen(
optionsViewModel: OptionsViewModel = hiltViewModel(), optionsViewModel: OptionsViewModel = hiltViewModel(),
navController: NavController,
focusRequester: FocusRequester, focusRequester: FocusRequester,
appUiState: AppUiState, appUiState: AppUiState,
tunnelId: Int, tunnelId: Int,
) { ) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current
val config = appUiState.tunnels.first { it.id == tunnelId } val config = appUiState.tunnels.first { it.id == tunnelId }
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }

View File

@ -9,14 +9,16 @@ import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import xyz.teamgravity.pin_lock_compose.PinLock import xyz.teamgravity.pin_lock_compose.PinLock
@Composable @Composable
fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) { fun PinLockScreen(appViewModel: AppViewModel) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current
val snackbar = SnackbarController.current val snackbar = SnackbarController.current
PinLock( PinLock(
title = { pinExists -> title = { pinExists ->

View File

@ -72,6 +72,7 @@ import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
@ -95,12 +96,13 @@ fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(), viewModel: SettingsViewModel = hiltViewModel(),
appViewModel: AppViewModel, appViewModel: AppViewModel,
uiState: AppUiState, uiState: AppUiState,
navController: NavController,
focusRequester: FocusRequester, focusRequester: FocusRequester,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val snackbar = SnackbarController.current val snackbar = SnackbarController.current
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val isRunningOnTv = context.isRunningOnTv() val isRunningOnTv = context.isRunningOnTv()
@ -545,10 +547,12 @@ fun SettingsScreen(
ConfigurationToggle( ConfigurationToggle(
stringResource(R.string.always_on_vpn_support), stringResource(R.string.always_on_vpn_support),
enabled = !( enabled = !(
(uiState.settings.isTunnelOnWifiEnabled || (
uiState.settings.isTunnelOnEthernetEnabled || uiState.settings.isTunnelOnWifiEnabled ||
uiState.settings.isTunnelOnMobileDataEnabled) && uiState.settings.isTunnelOnEthernetEnabled ||
uiState.settings.isAutoTunnelEnabled uiState.settings.isTunnelOnMobileDataEnabled
) &&
uiState.settings.isAutoTunnelEnabled
), ),
checked = uiState.settings.isAlwaysOnVpnEnabled, checked = uiState.settings.isAlwaysOnVpnEnabled,
padding = screenPadding, padding = screenPadding,

View File

@ -47,13 +47,15 @@ import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@Composable @Composable
fun SupportScreen(navController: NavController, focusRequester: FocusRequester, appUiState: AppUiState) { fun SupportScreen(focusRequester: FocusRequester, appUiState: AppUiState) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current
val fillMaxWidth = .85f val fillMaxWidth = .85f
Column( Column(

View File

@ -2,17 +2,37 @@ package com.zaneschepke.wireguardautotunnel.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF) val OffWhite = Color(0xFFE5E1E5)
val PurpleGrey80 = Color(0xFFCCC2DC) val LightGrey = Color(0xFF8D9D9F)
val Pink80 = Color(0xFF492532) val Aqua = Color(0xFF76BEBD)
val virdigris = Color(0xFF5BC0BE) val SilverTree = Color(0xFF6DB58B)
val Plantation = Color(0xFF264A49)
val Shark = Color(0xFF21272A)
val BalticSea = Color(0xFF1C1B1F)
val Brick = Color(0xFFCE4257)
val Corn = Color(0xFFFBEC5D)
val Purple40 = Color(0xFF6650a4) sealed class ThemeColors(
val PurpleGrey40 = Color(0xFF625b71) val background: Color,
val Pink40 = Color(0xFFFFFFFF) val surface: Color,
val primary: Color,
val secondary: Color,
val onSurface: Color,
) {
//TODO fix light theme colors
data object Light : ThemeColors(
background = LightGrey,
surface = OffWhite,
primary = Plantation,
secondary = OffWhite,
onSurface = BalticSea,
)
// status colors data object Dark : ThemeColors(
val brickRed = Color(0xFFCE4257) background = BalticSea,
val corn = Color(0xFFFBEC5D) surface = Shark,
val pinkRed = Color(0xFFEF476F) primary = Aqua,
val mint = Color(0xFF52B788) secondary = Plantation,
onSurface = OffWhite,
)
}

View File

@ -18,30 +18,22 @@ import androidx.core.view.WindowCompat
private val DarkColorScheme = private val DarkColorScheme =
darkColorScheme( darkColorScheme(
// primary = Purple80, primary = ThemeColors.Dark.primary,
primary = virdigris, surface = ThemeColors.Dark.surface,
secondary = PurpleGrey40, background = ThemeColors.Dark.background,
// secondary = PurpleGrey80, secondaryContainer = ThemeColors.Dark.secondary,
tertiary = Pink40, onSurface = ThemeColors.Dark.onSurface,
surfaceTint = Pink80, onSecondaryContainer = ThemeColors.Dark.primary,
// tertiary = Pink80
) )
private val LightColorScheme = private val LightColorScheme =
lightColorScheme( lightColorScheme(
primary = Purple40, primary = ThemeColors.Light.primary,
secondary = PurpleGrey40, surface = ThemeColors.Light.surface,
tertiary = Pink40, background = ThemeColors.Light.background,
surfaceTint = Pink80, secondaryContainer = ThemeColors.Light.secondary,
/* Other default colors to override onSurface = ThemeColors.Light.onSurface,
background = Color(0xFFFFFBFE), onSecondaryContainer = ThemeColors.Light.primary,
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
) )
@Composable @Composable
@ -52,15 +44,16 @@ fun WireguardAutoTunnelTheme(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val colorScheme = when { val colorScheme = when {
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> { (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> {
if (useDarkTheme) { if (useDarkTheme) {
dynamicDarkColorScheme(context) dynamicDarkColorScheme(context)
} else { } else {
dynamicLightColorScheme(context) dynamicLightColorScheme(context)
} }
} }
useDarkTheme -> DarkColorScheme useDarkTheme -> DarkColorScheme
else -> LightColorScheme //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) {

View File

@ -2,35 +2,58 @@ package com.zaneschepke.wireguardautotunnel.ui.theme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
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
// Set of Material typography styles to start with // Set of Material typography styles to start with
val inter = FontFamily(
Font(R.font.inter),
)
val Typography = val Typography =
Typography( Typography(
bodyLarge = bodyLarge =
TextStyle( TextStyle(
fontFamily = FontFamily.Default, fontFamily = inter,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.5.sp, letterSpacing = 0.5.sp,
), ),
/* Other default text styles to override bodySmall = TextStyle(
titleLarge = TextStyle( fontFamily = inter,
fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal,
fontWeight = FontWeight.Normal, fontSize = 13.sp,
fontSize = 22.sp, lineHeight = 20.sp,
lineHeight = 28.sp, letterSpacing = 1.sp,
letterSpacing = 0.sp color = LightGrey,
), ),
labelSmall = TextStyle( labelLarge = TextStyle(
fontFamily = FontFamily.Default, fontFamily = inter,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Normal,
fontSize = 11.sp, fontSize = 15.sp,
lineHeight = 16.sp, lineHeight = 18.sp,
letterSpacing = 0.5.sp letterSpacing = 0.sp,
) ),
*/ labelMedium = TextStyle(
fontFamily = inter,
fontWeight = FontWeight.SemiBold,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp,
),
titleMedium = TextStyle(
fontFamily = inter,
fontWeight = FontWeight.Bold,
fontSize = 17.sp,
lineHeight = 21.sp,
letterSpacing = 0.sp,
),
) )
val iconSize = 15.dp

Binary file not shown.

View File

@ -196,4 +196,7 @@
<string name="wildcard_supported">Learn about supported wildcards.</string> <string name="wildcard_supported">Learn about supported wildcards.</string>
<string name="details">details</string> <string name="details">details</string>
<string name="show_amnezia_properties">Show Amnezia properties</string> <string name="show_amnezia_properties">Show Amnezia properties</string>
<string name="never">never</string>
<string name="sec">sec</string>
<string name="handshake">handshake</string>
</resources> </resources>

View File

@ -5,11 +5,9 @@
<item name="android:windowBackground">@color/black_background</item> <item name="android:windowBackground">@color/black_background</item>
</style> </style>
<style name="Theme.AppSplashScreen" parent="Theme.SplashScreen"> <style name="Theme.App.Start" parent="@style/Theme.SplashScreen">
<!--<item name="windowSplashScreenBackground">@color/white</item>--> <item name="windowSplashScreenBackground">@color/black_background</item>
<!-- icon has to be a circle -->
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item> <item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
<item name="windowSplashScreenAnimationDuration">500</item>
<item name="postSplashScreenTheme">@style/Theme.WireguardAutoTunnel</item> <item name="postSplashScreenTheme">@style/Theme.WireguardAutoTunnel</item>
</style> </style>
</resources> </resources>

View File

@ -16,19 +16,19 @@ junit = "4.13.2"
kotlinx-serialization-json = "1.7.3" kotlinx-serialization-json = "1.7.3"
lifecycle-runtime-compose = "2.8.6" lifecycle-runtime-compose = "2.8.6"
material3 = "1.3.0" material3 = "1.3.0"
navigationCompose = "2.8.1" navigationCompose = "2.8.2"
pinLockCompose = "1.0.3" pinLockCompose = "1.0.3"
roomVersion = "2.6.1" roomVersion = "2.6.1"
timber = "5.0.1" timber = "5.0.1"
tunnel = "1.2.1" tunnel = "1.2.1"
androidGradlePlugin = "8.6.1" androidGradlePlugin = "8.7.0"
kotlin = "2.0.20" kotlin = "2.0.21"
ksp = "2.0.20-1.0.25" ksp = "2.0.20-1.0.25"
composeBom = "2024.09.02" composeBom = "2024.09.03"
compose = "1.7.2" compose = "1.7.3"
zxingAndroidEmbedded = "4.3.0" zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1" coreSplashscreen = "1.0.1"
gradlePlugins-grgit = "5.2.2" gradlePlugins-grgit = "5.3.0"
#plugins #plugins
material = "1.12.0" material = "1.12.0"