@ -31,6 +31,8 @@ import coil.annotation.ExperimentalCoilApi
import coil.compose.AsyncImage
import coil.compose.AsyncImage
import com.tailscale.ipn.R
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.util.AndroidTVUtil
import com.tailscale.ipn.ui.util.conditional
@OptIn ( ExperimentalCoilApi :: class )
@OptIn ( ExperimentalCoilApi :: class )
@Composable
@Composable
@ -43,53 +45,49 @@ fun Avatar(
var isFocused = remember { mutableStateOf ( false ) }
var isFocused = remember { mutableStateOf ( false ) }
val focusManager = LocalFocusManager . current
val focusManager = LocalFocusManager . current
// Outer Box for the larger focusable and clickable area
// Outer Box for the larger focusable and clickable area
Box (
Box (
contentAlignment = Alignment . Center ,
contentAlignment = Alignment . Center ,
modifier = Modifier
modifier =
. padding ( 4. dp )
Modifier . conditional ( AndroidTVUtil . isAndroidTV ( ) , { padding ( 4. dp ) } )
. size ( ( size * 1.5f ) . dp ) // Focusable area is larger than the avatar
. conditional (
. clip ( CircleShape ) // Ensure both the focus and click area are circular
AndroidTVUtil . isAndroidTV ( ) ,
. background (
{
if ( isFocused . value ) MaterialTheme . colorScheme . surface
size ( ( size * 1.5f ) . dp ) // Focusable area is larger than the avatar
else Color . Transparent ,
} )
)
. clip ( CircleShape ) // Ensure both the focus and click area are circular
. onFocusChanged { focusState ->
. background (
isFocused . value = focusState . isFocused
if ( isFocused . value ) MaterialTheme . colorScheme . surface else Color . Transparent ,
}
)
. focusable ( ) // Make this outer Box focusable (after onFocusChanged)
. onFocusChanged { focusState -> isFocused . value = focusState . isFocused }
. clickable (
. focusable ( ) // Make this outer Box focusable (after onFocusChanged)
interactionSource = remember { MutableInteractionSource ( ) } ,
. clickable (
indication = ripple ( bounded = true ) , // Apply ripple effect inside circular bounds
interactionSource = remember { MutableInteractionSource ( ) } ,
onClick = {
indication = ripple ( bounded = true ) , // Apply ripple effect inside circular bounds
action ?. invoke ( )
onClick = {
action ?. invoke ( )
focusManager . clearFocus ( ) // Clear focus after clicking the avatar
focusManager . clearFocus ( ) // Clear focus after clicking the avatar
}
} ) ) {
)
) {
// Inner Box to hold the avatar content (Icon or AsyncImage)
// Inner Box to hold the avatar content (Icon or AsyncImage)
Box (
Box (
contentAlignment = Alignment . Center ,
contentAlignment = Alignment . Center ,
modifier = Modifier
modifier = Modifier . size ( size . dp ) . clip ( CircleShape ) ) {
. size ( size . dp )
// Always display the default icon as a background layer
. clip ( CircleShape )
Icon (
) {
imageVector = Icons . Default . Person ,
// Always display the default icon as a background layer
contentDescription = stringResource ( R . string . settings _title ) ,
Icon (
modifier =
imageVector = Icons . Default . Person ,
Modifier . conditional ( AndroidTVUtil . isAndroidTV ( ) , { size ( ( size * 0.8f ) . dp ) } )
contentDescription = stringResource ( R . string . settings _title ) ,
. clip ( CircleShape ) // Icon size slightly smaller than the Box
modifier =
)
Modifier . size ( ( size * 0.8f ) . dp )
. clip ( CircleShape ) // Icon size slightly smaller than the Box
)
// Overlay the profile picture if available
// Overlay the profile picture if available
profile ?. UserProfile ?. ProfilePicURL ?. let { url ->
profile ?. UserProfile ?. ProfilePicURL ?. let { url ->
AsyncImage (
AsyncImage (
model = url ,
model = url ,
modifier = Modifier . size ( size . dp ) . clip ( CircleShape ) ,
modifier = Modifier . size ( size . dp ) . clip ( CircleShape ) ,
contentDescription = null )
contentDescription = null )
}
}
}
}
}
}
}
}