@ -10,21 +10,23 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Column
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItem
@ -32,7 +34,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.SearchBarDefaults
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
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.collectAsState
@ -68,17 +69,23 @@ data class MainViewNavigation(
@Composable
@Composable
fun MainView ( navigation : MainViewNavigation , viewModel : MainViewModel = viewModel ( ) ) {
fun MainView ( navigation : MainViewNavigation , viewModel : MainViewModel = viewModel ( ) ) {
Scaffold { _ ->
Scaffold ( contentWindowInsets = WindowInsets . Companion . statusBars ) { paddingInsets ->
Column ( modifier = Modifier . fillMaxWidth ( ) , verticalArrangement = Arrangement . Center ) {
Column (
modifier = Modifier . fillMaxWidth ( ) . padding ( paddingInsets ) ,
verticalArrangement = Arrangement . Center ) {
val state = viewModel . ipnState . collectAsState ( initial = Ipn . State . NoState )
val state = viewModel . ipnState . collectAsState ( initial = Ipn . State . NoState )
val user = viewModel . loggedInUser . collectAsState ( initial = null )
val user = viewModel . loggedInUser . collectAsState ( initial = null )
Row (
Row (
modifier = Modifier . fillMaxWidth ( ) . padding ( horizontal = 8. dp ) ,
modifier =
Modifier . fillMaxWidth ( )
. background ( MaterialTheme . colorScheme . secondaryContainer )
. padding ( horizontal = 8. dp )
. padding ( top = 10. dp ) ,
verticalAlignment = Alignment . CenterVertically ) {
verticalAlignment = Alignment . CenterVertically ) {
val isOn = viewModel . vpnToggleState . collectAsState ( initial = false )
val isOn = viewModel . vpnToggleState . collectAsState ( initial = false )
if ( state . value != Ipn . State . NeedsLogin && state . value != Ipn . State . NoState ) {
if ( state . value != Ipn . State . NeedsLogin && state . value != Ipn . State . NoState ) {
Switch ( onCheckedChange = { viewModel . toggleVpn ( ) } , checked = isOn . value )
Tinted Switch( onCheckedChange = { viewModel . toggleVpn ( ) } , checked = isOn . value )
Spacer ( Modifier . size ( 3. dp ) )
Spacer ( Modifier . size ( 3. dp ) )
}
}
@ -98,7 +105,13 @@ fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewMode
Ipn . State . Running -> {
Ipn . State . Running -> {
val selfPeerId = viewModel . selfPeerId . collectAsState ( initial = " " )
val selfPeerId = viewModel . selfPeerId . collectAsState ( initial = " " )
ExitNodeStatus ( navAction = navigation . onNavigateToExitNodes , viewModel = viewModel )
Row (
modifier =
Modifier . background ( MaterialTheme . colorScheme . secondaryContainer )
. padding ( top = 10. dp , bottom = 20. dp ) ) {
ExitNodeStatus (
navAction = navigation . onNavigateToExitNodes , viewModel = viewModel )
}
PeerList (
PeerList (
searchTerm = viewModel . searchTerm ,
searchTerm = viewModel . searchTerm ,
state = viewModel . ipnState ,
state = viewModel . ipnState ,
@ -135,16 +148,17 @@ fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) {
Modifier . clickable { navAction ( ) }
Modifier . clickable { navAction ( ) }
. padding ( horizontal = 8. dp )
. padding ( horizontal = 8. dp )
. clip ( shape = RoundedCornerShape ( 10. dp , 10. dp , 10. dp , 10. dp ) )
. clip ( shape = RoundedCornerShape ( 10. dp , 10. dp , 10. dp , 10. dp ) )
. background ( MaterialTheme . colorScheme . secondaryContainer )
. background ( MaterialTheme . colorScheme . background )
. fillMaxWidth ( ) ) {
. fillMaxWidth ( ) ) {
Column ( modifier = Modifier . padding ( 6 .dp ) ) {
Column ( modifier = Modifier . padding ( vertical = 15. dp , horizontal = 18 .dp ) ) {
Text (
Text (
text = stringResource ( id = R . string . exit _node ) ,
text = stringResource ( id = R . string . exit _node ) ,
style = MaterialTheme . typography . titleMedium )
color = MaterialTheme . colorScheme . secondary ,
style = MaterialTheme . typography . titleSmall )
Row ( verticalAlignment = Alignment . CenterVertically ) {
Row ( verticalAlignment = Alignment . CenterVertically ) {
Text (
Text (
text = exitNode ?: stringResource ( id = R . string . none ) ,
text = exitNode ?: stringResource ( id = R . string . none ) ,
style = MaterialTheme . typography . body Medium )
style = MaterialTheme . typography . body Large )
Icon (
Icon (
Icons . Outlined . ArrowDropDown ,
Icons . Outlined . ArrowDropDown ,
null ,
null ,
@ -208,12 +222,13 @@ fun StartingView() {
fun ConnectView ( user : IpnLocal . LoginProfile ? , connectAction : ( ) -> Unit , loginAction : ( ) -> Unit ) {
fun ConnectView ( user : IpnLocal . LoginProfile ? , connectAction : ( ) -> Unit , loginAction : ( ) -> Unit ) {
Row ( horizontalArrangement = Arrangement . Center , modifier = Modifier . fillMaxWidth ( ) ) {
Row ( horizontalArrangement = Arrangement . Center , modifier = Modifier . fillMaxWidth ( ) ) {
Column (
Column (
horizontalAlignment = Alignment . CenterHorizontally ,
modifier =
modifier =
Modifier . background ( MaterialTheme . colorScheme . secondaryContainer )
Modifier . background ( MaterialTheme . colorScheme . secondaryContainer ) . fillMaxWidth ( ) ) {
. padding ( 8. dp )
Column (
. fillMaxWidth ( 0.7f )
modifier = Modifier . padding ( 8. dp ) . fillMaxWidth ( 0.7f ) . fillMaxHeight ( ) ,
. fillMaxHeight ( ) ,
verticalArrangement =
verticalArrangement = Arrangement . spacedBy ( 8. dp , alignment = Alignment . CenterVertically ) ,
Arrangement . spacedBy ( 8. dp , alignment = Alignment . CenterVertically ) ,
horizontalAlignment = Alignment . CenterHorizontally ,
horizontalAlignment = Alignment . CenterHorizontally ,
) {
) {
if ( user != null && ! user . isEmpty ( ) ) {
if ( user != null && ! user . isEmpty ( ) ) {
@ -266,6 +281,7 @@ fun ConnectView(user: IpnLocal.LoginProfile?, connectAction: () -> Unit, loginAc
}
}
}
}
}
}
}
@Composable
@Composable
fun ClearButton ( onClick : ( ) -> Unit ) {
fun ClearButton ( onClick : ( ) -> Unit ) {
@ -308,9 +324,11 @@ fun PeerList(
trailingIcon = {
trailingIcon = {
if ( searchTermStr . isNotEmpty ( ) ) ClearButton ( { onSearch ( " " ) } ) else CloseButton ( )
if ( searchTermStr . isNotEmpty ( ) ) ClearButton ( { onSearch ( " " ) } ) else CloseButton ( )
} ,
} ,
tonalElevation = 2. dp ,
tonalElevation = 0. dp ,
shadowElevation = 2. dp ,
shadowElevation = 0. dp ,
colors = SearchBarDefaults . colors ( ) ,
colors =
SearchBarDefaults . colors (
containerColor = Color . Transparent , dividerColor = Color . Transparent ) ,
modifier = Modifier . fillMaxWidth ( ) ) {
modifier = Modifier . fillMaxWidth ( ) ) {
LazyColumn (
LazyColumn (
modifier =
modifier =
@ -323,7 +341,8 @@ fun PeerList(
Text (
Text (
text =
text =
peerSet . user ?. DisplayName ?: stringResource ( id = R . string . unknown _user ) ,
peerSet . user ?. DisplayName ?: stringResource ( id = R . string . unknown _user ) ,
style = MaterialTheme . typography . titleLarge )
style = MaterialTheme . typography . titleLarge ,
fontWeight = FontWeight . SemiBold )
} )
} )
}
}
peerSet . peers . forEach { peer ->
peerSet . peers . forEach { peer ->
@ -344,19 +363,20 @@ fun PeerList(
}
}
Box (
Box (
modifier =
modifier =
Modifier . size ( 8 .dp )
Modifier . size ( 10 .dp )
. background (
. background (
color = color , shape = RoundedCornerShape ( percent = 50 ) ) ) { }
color = color , shape = RoundedCornerShape ( percent = 50 ) ) ) { }
Spacer ( modifier = Modifier . size ( 8 .dp ) )
Spacer ( modifier = Modifier . size ( 6 .dp ) )
Text ( text = peer . ComputedName , style = MaterialTheme . typography . titleMedium )
Text ( text = peer . ComputedName , style = MaterialTheme . typography . titleMedium )
}
}
} ,
} ,
supportingContent = {
supportingContent = {
Text (
Text (
text = peer . Addresses ?. first ( ) ?. split ( " / " ) ?. first ( ) ?: " " ,
text = peer . Addresses ?. first ( ) ?. split ( " / " ) ?. first ( ) ?: " " ,
style = MaterialTheme . typography . bodyMedium )
style = MaterialTheme . typography . bodyMedium ,
} ,
color = MaterialTheme . colorScheme . secondary )
trailingContent = { Icon ( Icons . AutoMirrored . Filled . KeyboardArrowRight , null ) } )
} )
HorizontalDivider ( color = MaterialTheme . colorScheme . secondaryContainer )
}
}
}
}
}
}