From dea3291fecef6a4c5e0184591777f2c1c97b8320 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:24:06 +0530 Subject: [PATCH] feat: Add avatar selection and display functionality in ProfileEditScreen and ProfileSelectionScreen --- .../features/profiles/ProfileEditScreen.kt | 84 +++++++++++++++++-- .../app/features/profiles/ProfileModels.kt | 3 + .../profiles/ProfileSelectionScreen.kt | 30 ++++++- 3 files changed, 108 insertions(+), 9 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt index e1ae2a84f..2435b52e2 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileModels.kt index 9af6652a9..f57a8445b 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileModels.kt @@ -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" diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSelectionScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSelectionScreen.kt index dbbad6ffa..3fbeecbf2 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSelectionScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSelectionScreen.kt @@ -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,