feat: Add avatar selection and display functionality in ProfileEditScreen and ProfileSelectionScreen

This commit is contained in:
tapframe 2026-03-28 20:24:06 +05:30
parent a0559012b9
commit dea3291fec
3 changed files with 108 additions and 9 deletions

View file

@ -28,6 +28,7 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -38,9 +39,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import com.nuvio.app.core.ui.NuvioInputField
import com.nuvio.app.core.ui.NuvioPrimaryButton
import com.nuvio.app.core.ui.NuvioScreen
@ -63,12 +67,19 @@ fun ProfileEditScreen(
var name by rememberSaveable { mutableStateOf(profile?.name ?: "") }
var selectedColor by rememberSaveable { mutableStateOf(profile?.avatarColorHex ?: PROFILE_COLORS.first()) }
var selectedAvatarId by rememberSaveable { mutableStateOf(profile?.avatarId) }
var usesPrimaryAddons by rememberSaveable { mutableStateOf(profile?.usesPrimaryAddons ?: false) }
var isSaving by remember { mutableStateOf(false) }
var showDeleteConfirm by remember { mutableStateOf(false) }
var showPinSetup by remember { mutableStateOf(false) }
var showPinClear by remember { mutableStateOf(false) }
val avatars by AvatarRepository.avatars.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { AvatarRepository.fetchAvatars() }
val selectedAvatarItem = remember(selectedAvatarId, avatars) {
selectedAvatarId?.let { id -> avatars.find { it.id == id } }
}
NuvioScreen(modifier = modifier) {
stickyHeader {
NuvioScreenHeader(
@ -88,11 +99,28 @@ fun ProfileEditScreen(
modifier = Modifier
.size(100.dp)
.clip(CircleShape)
.background(color.copy(alpha = 0.2f))
.border(3.dp, color.copy(alpha = 0.6f), CircleShape),
.background(
if (selectedAvatarItem != null) {
selectedAvatarItem!!.bgColor?.let { parseHexColor(it) } ?: color
} else {
color.copy(alpha = 0.2f)
}
)
.border(
3.dp,
if (selectedAvatarItem != null) Color.Transparent else color.copy(alpha = 0.6f),
CircleShape,
),
contentAlignment = Alignment.Center,
) {
if (name.isNotBlank()) {
if (selectedAvatarItem != null) {
AsyncImage(
model = avatarStorageUrl(selectedAvatarItem!!.storagePath),
contentDescription = selectedAvatarItem!!.displayName,
modifier = Modifier.size(100.dp).clip(CircleShape),
contentScale = ContentScale.Crop,
)
} else if (name.isNotBlank()) {
Text(
text = name.take(1).uppercase(),
style = MaterialTheme.typography.displayLarge,
@ -125,6 +153,46 @@ fun ProfileEditScreen(
}
}
item {
NuvioSectionLabel(text = "AVATAR")
}
item {
NuvioSurfaceCard {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
avatars.forEach { avatar ->
val isSelected = avatar.id == selectedAvatarId
Box(
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(
avatar.bgColor?.let { parseHexColor(it) }
?: MaterialTheme.colorScheme.surfaceVariant
)
.then(
if (isSelected) Modifier.border(3.dp, MaterialTheme.colorScheme.primary, CircleShape)
else Modifier
)
.clickable {
selectedAvatarId = avatar.id
},
contentAlignment = Alignment.Center,
) {
AsyncImage(
model = avatarStorageUrl(avatar.storagePath),
contentDescription = avatar.displayName,
modifier = Modifier.size(56.dp).clip(CircleShape),
contentScale = ContentScale.Crop,
)
}
}
}
}
}
item {
NuvioSectionLabel(text = "COLOR")
}
@ -136,7 +204,7 @@ fun ProfileEditScreen(
) {
PROFILE_COLORS.forEach { hex ->
val color = remember(hex) { parseHexColor(hex) }
val isSelected = hex == selectedColor
val isSelected = hex == selectedColor && selectedAvatarId == null
Box(
modifier = Modifier
.size(44.dp)
@ -146,7 +214,10 @@ fun ProfileEditScreen(
if (isSelected) Modifier.border(3.dp, MaterialTheme.colorScheme.onSurface, CircleShape)
else Modifier
)
.clickable { selectedColor = hex },
.clickable {
selectedColor = hex
selectedAvatarId = null
},
contentAlignment = Alignment.Center,
) {
if (isSelected) {
@ -234,6 +305,7 @@ fun ProfileEditScreen(
ProfileRepository.createProfile(
name = name,
avatarColorHex = selectedColor,
avatarId = selectedAvatarId,
usesPrimaryAddons = usesPrimaryAddons,
)
} else {
@ -241,8 +313,8 @@ fun ProfileEditScreen(
profileIndex = profile!!.profileIndex,
name = name,
avatarColorHex = selectedColor,
avatarId = selectedAvatarId,
usesPrimaryAddons = usesPrimaryAddons,
avatarId = profile.avatarId,
)
}
isSaving = false

View file

@ -70,3 +70,6 @@ val PROFILE_COLORS = listOf(
"#C0CA33", "#D81B60", "#00897B", "#5E35B1",
"#7CB342", "#039BE5", "#FFB300", "#6D4C41",
)
fun avatarStorageUrl(storagePath: String): String =
"${com.nuvio.app.core.network.SupabaseConfig.URL}/storage/v1/object/public/avatars/$storagePath"

View file

@ -43,8 +43,10 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import kotlinx.coroutines.launch
@OptIn(ExperimentalLayoutApi::class)
@ -62,6 +64,7 @@ fun ProfileSelectionScreen(
LaunchedEffect(Unit) {
ProfileRepository.pullProfiles()
AvatarRepository.fetchAvatars()
}
val statusBarTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
@ -155,6 +158,10 @@ private fun ProfileAvatar(
val avatarColor = remember(profile.avatarColorHex) {
parseHexColor(profile.avatarColorHex)
}
val avatars by AvatarRepository.avatars.collectAsStateWithLifecycle()
val avatarItem = remember(profile.avatarId, avatars) {
profile.avatarId?.let { id -> avatars.find { it.id == id } }
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@ -171,11 +178,28 @@ private fun ProfileAvatar(
modifier = Modifier
.size(96.dp)
.clip(CircleShape)
.background(avatarColor.copy(alpha = 0.2f))
.border(2.dp, avatarColor.copy(alpha = 0.5f), CircleShape),
.background(
if (avatarItem != null) {
avatarItem.bgColor?.let { parseHexColor(it) } ?: avatarColor
} else {
avatarColor.copy(alpha = 0.2f)
}
)
.border(
2.dp,
if (avatarItem != null) Color.Transparent else avatarColor.copy(alpha = 0.5f),
CircleShape,
),
contentAlignment = Alignment.Center,
) {
if (profile.name.isNotBlank()) {
if (avatarItem != null) {
AsyncImage(
model = avatarStorageUrl(avatarItem.storagePath),
contentDescription = avatarItem.displayName,
modifier = Modifier.size(96.dp).clip(CircleShape),
contentScale = ContentScale.Crop,
)
} else if (profile.name.isNotBlank()) {
Text(
text = profile.name.take(1).uppercase(),
style = MaterialTheme.typography.headlineLarge,