@ -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,49 +69,61 @@ 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 (
val state = viewModel . ipnState . collectAsState ( initial = Ipn . State . NoState )
modifier = Modifier . fillMaxWidth ( ) . padding ( paddingInsets ) ,
val user = viewModel . loggedInUser . collectAsState ( initial = null )
verticalArrangement = Arrangement . Center ) {
val state = viewModel . ipnState . collectAsState ( initial = Ipn . State . NoState )
val user = viewModel . loggedInUser . collectAsState ( initial = null )
Row (
Row (
modifier = Modifier . fillMaxWidth ( ) . padding ( horizontal = 8. dp ) ,
modifier =
verticalAlignment = Alignment . CenterVertically ) {
Modifier . fillMaxWidth ( )
val isOn = viewModel . vpnToggleState . collectAsState ( initial = false )
. background ( MaterialTheme . colorScheme . secondaryContainer )
if ( state . value != Ipn . State . NeedsLogin && state . value != Ipn . State . NoState ) {
. padding ( horizontal = 8. dp )
Switch ( onCheckedChange = { viewModel . toggleVpn ( ) } , checked = isOn . value )
. padding ( top = 10. dp ) ,
Spacer ( Modifier . size ( 3. dp ) )
verticalAlignment = Alignment . CenterVertically ) {
}
val isOn = viewModel . vpnToggleState . collectAsState ( initial = false )
if ( state . value != Ipn . State . NeedsLogin && state . value != Ipn . State . NoState ) {
TintedSwitch ( onCheckedChange = { viewModel . toggleVpn ( ) } , checked = isOn . value )
Spacer ( Modifier . size ( 3. dp ) )
}
StateDisplay ( viewModel . stateRes , viewModel . userName )
StateDisplay ( viewModel . stateRes , viewModel . userName )
Box (
Box (
modifier = Modifier . weight ( 1f ) . clickable { navigation . onNavigateToSettings ( ) } ,
modifier = Modifier . weight ( 1f ) . clickable { navigation . onNavigateToSettings ( ) } ,
contentAlignment = Alignment . CenterEnd ) {
contentAlignment = Alignment . CenterEnd ) {
when ( user . value ) {
when ( user . value ) {
null -> SettingsButton ( user . value ) { navigation . onNavigateToSettings ( ) }
null -> SettingsButton ( user . value ) { navigation . onNavigateToSettings ( ) }
else -> Avatar ( profile = user . value , size = 36 )
else -> Avatar ( profile = user . value , size = 36 )
}
}
}
}
}
}
when ( state . value ) {
when ( state . value ) {
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 (
PeerList (
modifier =
searchTerm = viewModel . searchTerm ,
Modifier . background ( MaterialTheme . colorScheme . secondaryContainer )
state = viewModel . ipnState ,
. padding ( top = 10. dp , bottom = 20. dp ) ) {
peers = viewModel . peers ,
ExitNodeStatus (
selfPeer = selfPeerId . value ,
navAction = navigation . onNavigateToExitNodes , viewModel = viewModel )
onNavigateToPeerDetails = navigation . onNavigateToPeerDetails ,
}
onSearch = { viewModel . searchPeers ( it ) } )
PeerList (
searchTerm = viewModel . searchTerm ,
state = viewModel . ipnState ,
peers = viewModel . peers ,
selfPeer = selfPeerId . value ,
onNavigateToPeerDetails = navigation . onNavigateToPeerDetails ,
onSearch = { viewModel . searchPeers ( it ) } )
}
Ipn . State . Starting -> StartingView ( )
else -> ConnectView ( user . value , { viewModel . toggleVpn ( ) } , { viewModel . login { } } )
}
}
}
Ipn . State . Starting -> StartingView ( )
else -> ConnectView ( user . value , { viewModel . toggleVpn ( ) } , { viewModel . login { } } )
}
}
}
}
}
}
@ -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,62 +222,64 @@ 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 (
modifier =
Modifier . background ( MaterialTheme . colorScheme . secondaryContainer )
. padding ( 8. dp )
. fillMaxWidth ( 0.7f )
. fillMaxHeight ( ) ,
verticalArrangement = Arrangement . spacedBy ( 8. dp , alignment = Alignment . CenterVertically ) ,
horizontalAlignment = Alignment . CenterHorizontally ,
horizontalAlignment = Alignment . CenterHorizontally ,
) {
modifier =
if ( user != null && ! user . isEmpty ( ) ) {
Modifier . background ( MaterialTheme . colorScheme . secondaryContainer ) . fillMaxWidth ( ) ) {
Icon (
Column (
painter = painterResource ( id = R . drawable . power ) ,
modifier = Modifier . padding ( 8. dp ) . fillMaxWidth ( 0.7f ) . fillMaxHeight ( ) ,
contentDescription = null ,
verticalArrangement =
modifier = Modifier . size ( 48. dp ) ,
Arrangement . spacedBy ( 8. dp , alignment = Alignment . CenterVertically ) ,
tint = MaterialTheme . colorScheme . secondary )
horizontalAlignment = Alignment . CenterHorizontally ,
Text (
) {
text = stringResource ( id = R . string . not _connected ) ,
if ( user != null && ! user . isEmpty ( ) ) {
fontSize = MaterialTheme . typography . titleMedium . fontSize ,
Icon (
fontWeight = FontWeight . SemiBold ,
painter = painterResource ( id = R . drawable . power ) ,
color = MaterialTheme . colorScheme . primary ,
contentDescription = null ,
textAlign = TextAlign . Center ,
modifier = Modifier . size ( 48. dp ) ,
fontFamily = MaterialTheme . typography . titleMedium . fontFamily )
tint = MaterialTheme . colorScheme . secondary )
val tailnetName = user . NetworkProfile ?. DomainName ?: " "
Text (
Text (
text = stringResource ( id = R . string . not _connected ) ,
stringResource ( id = R . string . connect _to _tailnet , tailnetName ) ,
fontSize = MaterialTheme . typography . titleMedium . fontSize ,
fontSize = MaterialTheme . typography . titleMedium . fontSize ,
fontWeight = FontWeight . SemiBold ,
fontWeight = FontWeight . Normal ,
color = MaterialTheme . colorScheme . primary ,
color = MaterialTheme . colorScheme . secondary ,
textAlign = TextAlign . Center ,
textAlign = TextAlign . Center ,
fontFamily = MaterialTheme . typography . titleMedium . fontFamily )
)
val tailnetName = user . NetworkProfile ?. DomainName ?: " "
Spacer ( modifier = Modifier . size ( 1. dp ) )
Text (
PrimaryActionButton ( onClick = connectAction ) {
stringResource ( id = R . string . connect _to _tailnet , tailnetName ) ,
Text (
fontSize = MaterialTheme . typography . titleMedium . fontSize ,
text = stringResource ( id = R . string . connect ) ,
fontWeight = FontWeight . Normal ,
fontSize = MaterialTheme . typography . titleMedium . fontSize )
color = MaterialTheme . colorScheme . secondary ,
}
textAlign = TextAlign . Center ,
} else {
)
TailscaleLogoView ( Modifier . size ( 50. dp ) )
Spacer ( modifier = Modifier . size ( 1. dp ) )
Spacer ( modifier = Modifier . size ( 1. dp ) )
PrimaryActionButton ( onClick = connectAction ) {
Text (
Text (
text = stringResource ( id = R . string . welcome _to _tailscale ) ,
text = stringResource ( id = R . string . connect ) ,
style = MaterialTheme . typography . titleMedium ,
fontSize = MaterialTheme . typography . titleMedium . fontSize )
color = MaterialTheme . colorScheme . primary ,
}
textAlign = TextAlign . Center )
} else {
Text (
TailscaleLogoView ( Modifier . size ( 50. dp ) )
stringResource ( R . string . login _to _join _your _tailnet ) ,
Spacer ( modifier = Modifier . size ( 1. dp ) )
style = MaterialTheme . typography . titleSmall ,
Text (
color = MaterialTheme . colorScheme . secondary ,
text = stringResource ( id = R . string . welcome _to _tailscale ) ,
textAlign = TextAlign . Center )
style = MaterialTheme . typography . titleMedium ,
Spacer ( modifier = Modifier . size ( 1. dp ) )
color = MaterialTheme . colorScheme . primary ,
PrimaryActionButton ( onClick = loginAction ) {
textAlign = TextAlign . Center )
Text (
Text (
text = stringResource ( id = R . string . log _in ) ,
stringResource ( R . string . login _to _join _your _tailnet ) ,
fontSize = MaterialTheme . typography . titleMedium . fontSize )
style = MaterialTheme . typography . titleSmall ,
color = MaterialTheme . colorScheme . secondary ,
textAlign = TextAlign . Center )
Spacer ( modifier = Modifier . size ( 1. dp ) )
PrimaryActionButton ( onClick = loginAction ) {
Text (
text = stringResource ( id = R . string . log _in ) ,
fontSize = MaterialTheme . typography . titleMedium . fontSize )
}
}
}
}
}
}
}
}
}
}
}
@ -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 )
}
}
}
}
}
}