mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-05 01:39:08 +00:00
feat: Enhance avatar fetching and improve animations in ProfileSelectionScreen
This commit is contained in:
parent
dea3291fec
commit
c5cf82d54b
3 changed files with 251 additions and 78 deletions
|
|
@ -1,6 +1,11 @@
|
||||||
package com.nuvio.app
|
package com.nuvio.app
|
||||||
|
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
|
@ -42,6 +47,7 @@ import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.compose.setSingletonImageLoaderFactory
|
import coil3.compose.setSingletonImageLoaderFactory
|
||||||
|
import coil3.request.CachePolicy
|
||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
import com.nuvio.app.core.auth.AuthRepository
|
import com.nuvio.app.core.auth.AuthRepository
|
||||||
import com.nuvio.app.core.auth.AuthState
|
import com.nuvio.app.core.auth.AuthState
|
||||||
|
|
@ -133,6 +139,8 @@ fun App() {
|
||||||
setSingletonImageLoaderFactory { context ->
|
setSingletonImageLoaderFactory { context ->
|
||||||
ImageLoader.Builder(context)
|
ImageLoader.Builder(context)
|
||||||
.crossfade(true)
|
.crossfade(true)
|
||||||
|
.diskCachePolicy(CachePolicy.ENABLED)
|
||||||
|
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
NuvioTheme {
|
NuvioTheme {
|
||||||
|
|
@ -157,9 +165,13 @@ fun App() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Crossfade(
|
AnimatedContent(
|
||||||
targetState = gateScreen,
|
targetState = gateScreen,
|
||||||
label = "app_gate",
|
label = "app_gate",
|
||||||
|
transitionSpec = {
|
||||||
|
(fadeIn(tween(400)) + scaleIn(tween(400), initialScale = 0.94f))
|
||||||
|
.togetherWith(fadeOut(tween(250)))
|
||||||
|
},
|
||||||
) { currentGate ->
|
) { currentGate ->
|
||||||
when (currentGate) {
|
when (currentGate) {
|
||||||
AppGateScreen.Loading.name -> {
|
AppGateScreen.Loading.name -> {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,14 @@ object AvatarRepository {
|
||||||
|
|
||||||
suspend fun fetchAvatars() {
|
suspend fun fetchAvatars() {
|
||||||
if (loaded && _avatars.value.isNotEmpty()) return
|
if (loaded && _avatars.value.isNotEmpty()) return
|
||||||
|
doFetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun refreshAvatars() {
|
||||||
|
doFetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun doFetch() {
|
||||||
runCatching {
|
runCatching {
|
||||||
val result = SupabaseProvider.client.postgrest.rpc("get_avatar_catalog")
|
val result = SupabaseProvider.client.postgrest.rpc("get_avatar_catalog")
|
||||||
val items = result.decodeList<AvatarCatalogItem>()
|
val items = result.decodeList<AvatarCatalogItem>()
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,27 @@
|
||||||
package com.nuvio.app.features.profiles
|
package com.nuvio.app.features.profiles
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
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.layout.statusBars
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
|
@ -26,7 +32,6 @@ import androidx.compose.material.icons.rounded.Edit
|
||||||
import androidx.compose.material.icons.rounded.Lock
|
import androidx.compose.material.icons.rounded.Lock
|
||||||
import androidx.compose.material.icons.rounded.Person
|
import androidx.compose.material.icons.rounded.Person
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -39,17 +44,20 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileSelectionScreen(
|
fun ProfileSelectionScreen(
|
||||||
onProfileSelected: (NuvioProfile) -> Unit,
|
onProfileSelected: (NuvioProfile) -> Unit,
|
||||||
|
|
@ -62,9 +70,21 @@ fun ProfileSelectionScreen(
|
||||||
var pinDialogProfile by remember { mutableStateOf<NuvioProfile?>(null) }
|
var pinDialogProfile by remember { mutableStateOf<NuvioProfile?>(null) }
|
||||||
var isEditMode by remember { mutableStateOf(false) }
|
var isEditMode by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val titleAlpha = remember { Animatable(0f) }
|
||||||
|
val titleOffset = remember { Animatable(20f) }
|
||||||
|
val manageAlpha = remember { Animatable(0f) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
ProfileRepository.pullProfiles()
|
ProfileRepository.pullProfiles()
|
||||||
AvatarRepository.fetchAvatars()
|
AvatarRepository.fetchAvatars()
|
||||||
|
AvatarRepository.refreshAvatars()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
launch { titleAlpha.animateTo(1f, tween(600, easing = FastOutSlowInEasing)) }
|
||||||
|
launch { titleOffset.animateTo(0f, tween(600, easing = FastOutSlowInEasing)) }
|
||||||
|
delay(300)
|
||||||
|
manageAlpha.animateTo(1f, tween(500))
|
||||||
}
|
}
|
||||||
|
|
||||||
val statusBarTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
val statusBarTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||||
|
|
@ -72,63 +92,120 @@ fun ProfileSelectionScreen(
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background)
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
MaterialTheme.colorScheme.background,
|
||||||
|
MaterialTheme.colorScheme.background,
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.15f),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
.padding(top = statusBarTop),
|
.padding(top = statusBarTop),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(horizontal = 32.dp, vertical = 48.dp),
|
.padding(horizontal = 24.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(60.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Who's watching?",
|
text = "Who's watching?",
|
||||||
style = MaterialTheme.typography.headlineLarge,
|
style = MaterialTheme.typography.headlineLarge.copy(
|
||||||
|
fontSize = 30.sp,
|
||||||
|
letterSpacing = (-0.5).sp,
|
||||||
|
),
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.graphicsLayer {
|
||||||
|
alpha = titleAlpha.value
|
||||||
|
translationY = titleOffset.value
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(40.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
||||||
FlowRow(
|
val profiles = profileState.profiles
|
||||||
horizontalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterHorizontally),
|
val items = profiles.size + if (profiles.size < 4) 1 else 0
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
profileState.profiles.forEach { profile ->
|
var index = 0
|
||||||
ProfileAvatar(
|
while (index < items) {
|
||||||
profile = profile,
|
Row(
|
||||||
isEditMode = isEditMode,
|
horizontalArrangement = Arrangement.spacedBy(20.dp, Alignment.CenterHorizontally),
|
||||||
onClick = {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
if (isEditMode) {
|
) {
|
||||||
onEditProfile(profile)
|
for (col in 0..1) {
|
||||||
} else if (profile.pinEnabled) {
|
if (index < items) {
|
||||||
pinDialogProfile = profile
|
val currentIndex = index
|
||||||
|
if (currentIndex < profiles.size) {
|
||||||
|
val profile = profiles[currentIndex]
|
||||||
|
ProfileAvatarCard(
|
||||||
|
profile = profile,
|
||||||
|
isEditMode = isEditMode,
|
||||||
|
animDelay = currentIndex * 80,
|
||||||
|
onClick = {
|
||||||
|
if (isEditMode) {
|
||||||
|
onEditProfile(profile)
|
||||||
|
} else if (profile.pinEnabled) {
|
||||||
|
pinDialogProfile = profile
|
||||||
|
} else {
|
||||||
|
ProfileRepository.selectProfile(profile.profileIndex)
|
||||||
|
onProfileSelected(profile)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AddProfileCard(
|
||||||
|
animDelay = currentIndex * 80,
|
||||||
|
onClick = onAddProfile,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
index++
|
||||||
} else {
|
} else {
|
||||||
ProfileRepository.selectProfile(profile.profileIndex)
|
Spacer(modifier = Modifier.width(150.dp))
|
||||||
onProfileSelected(profile)
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (profileState.profiles.size < 4) {
|
|
||||||
AddProfileButton(onClick = onAddProfile)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(40.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
||||||
Text(
|
Box(
|
||||||
text = if (isEditMode) "Done" else "Manage Profiles",
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.graphicsLayer { alpha = manageAlpha.value }
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
.background(
|
||||||
|
if (isEditMode) MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
|
||||||
|
else Color.Transparent,
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = if (isEditMode) MaterialTheme.colorScheme.primary.copy(alpha = 0.4f)
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f),
|
||||||
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
)
|
||||||
.clickable { isEditMode = !isEditMode }
|
.clickable { isEditMode = !isEditMode }
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 24.dp, vertical = 10.dp),
|
||||||
)
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (isEditMode) "Done" else "Manage Profiles",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = if (isEditMode) MaterialTheme.colorScheme.primary
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,9 +227,10 @@ fun ProfileSelectionScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProfileAvatar(
|
private fun ProfileAvatarCard(
|
||||||
profile: NuvioProfile,
|
profile: NuvioProfile,
|
||||||
isEditMode: Boolean,
|
isEditMode: Boolean,
|
||||||
|
animDelay: Int,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val avatarColor = remember(profile.avatarColorHex) {
|
val avatarColor = remember(profile.avatarColorHex) {
|
||||||
|
|
@ -163,32 +241,67 @@ private fun ProfileAvatar(
|
||||||
profile.avatarId?.let { id -> avatars.find { it.id == id } }
|
profile.avatarId?.let { id -> avatars.find { it.id == id } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val animAlpha = remember { Animatable(0f) }
|
||||||
|
val animScale = remember { Animatable(0.85f) }
|
||||||
|
val animOffset = remember { Animatable(30f) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(animDelay.toLong() + 150)
|
||||||
|
launch { animAlpha.animateTo(1f, tween(450, easing = FastOutSlowInEasing)) }
|
||||||
|
launch { animScale.animateTo(1f, tween(500, easing = FastOutSlowInEasing)) }
|
||||||
|
launch { animOffset.animateTo(0f, tween(500, easing = FastOutSlowInEasing)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
|
val pressScale = if (isPressed) 0.95f else 1f
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.width(150.dp)
|
||||||
.clickable(onClick = onClick)
|
.graphicsLayer {
|
||||||
|
alpha = animAlpha.value
|
||||||
|
scaleX = animScale.value * pressScale
|
||||||
|
scaleY = animScale.value * pressScale
|
||||||
|
translationY = animOffset.value
|
||||||
|
}
|
||||||
|
.clip(RoundedCornerShape(20.dp))
|
||||||
|
.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null,
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.size(96.dp),
|
modifier = Modifier.size(110.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
|
if (avatarItem != null) {
|
||||||
|
val bgColor = avatarItem.bgColor?.let { parseHexColor(it) } ?: avatarColor
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(110.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(bgColor.copy(alpha = 0.2f)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(96.dp)
|
.size(100.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
if (avatarItem != null) {
|
if (avatarItem != null) {
|
||||||
avatarItem.bgColor?.let { parseHexColor(it) } ?: avatarColor
|
avatarItem.bgColor?.let { parseHexColor(it) } ?: avatarColor
|
||||||
} else {
|
} else {
|
||||||
avatarColor.copy(alpha = 0.2f)
|
avatarColor.copy(alpha = 0.15f)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
.border(
|
.then(
|
||||||
2.dp,
|
if (avatarItem == null) Modifier.border(2.dp, avatarColor.copy(alpha = 0.4f), CircleShape)
|
||||||
if (avatarItem != null) Color.Transparent else avatarColor.copy(alpha = 0.5f),
|
else Modifier,
|
||||||
CircleShape,
|
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
|
|
@ -196,13 +309,13 @@ private fun ProfileAvatar(
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = avatarStorageUrl(avatarItem.storagePath),
|
model = avatarStorageUrl(avatarItem.storagePath),
|
||||||
contentDescription = avatarItem.displayName,
|
contentDescription = avatarItem.displayName,
|
||||||
modifier = Modifier.size(96.dp).clip(CircleShape),
|
modifier = Modifier.size(100.dp).clip(CircleShape),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
)
|
)
|
||||||
} else if (profile.name.isNotBlank()) {
|
} else if (profile.name.isNotBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = profile.name.take(1).uppercase(),
|
text = profile.name.take(1).uppercase(),
|
||||||
style = MaterialTheme.typography.headlineLarge,
|
style = MaterialTheme.typography.headlineLarge.copy(fontSize = 38.sp),
|
||||||
color = avatarColor,
|
color = avatarColor,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
|
|
@ -211,7 +324,7 @@ private fun ProfileAvatar(
|
||||||
imageVector = Icons.Rounded.Person,
|
imageVector = Icons.Rounded.Person,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = avatarColor,
|
tint = avatarColor,
|
||||||
modifier = Modifier.size(42.dp),
|
modifier = Modifier.size(46.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -220,9 +333,10 @@ private fun ProfileAvatar(
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.size(32.dp)
|
.size(34.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(MaterialTheme.colorScheme.primary),
|
.background(MaterialTheme.colorScheme.primary)
|
||||||
|
.border(2.dp, MaterialTheme.colorScheme.background, CircleShape),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
@ -238,9 +352,10 @@ private fun ProfileAvatar(
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.size(28.dp)
|
.size(30.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
.border(2.dp, MaterialTheme.colorScheme.background, CircleShape),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
@ -253,13 +368,13 @@ private fun ProfileAvatar(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = profile.name.ifBlank { "Profile ${profile.profileIndex}" },
|
text = profile.name.ifBlank { "Profile ${profile.profileIndex}" },
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge.copy(fontSize = 16.sp),
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.SemiBold,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
|
@ -268,37 +383,75 @@ private fun ProfileAvatar(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AddProfileButton(onClick: () -> Unit) {
|
private fun AddProfileCard(
|
||||||
|
animDelay: Int,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val animAlpha = remember { Animatable(0f) }
|
||||||
|
val animScale = remember { Animatable(0.85f) }
|
||||||
|
val animOffset = remember { Animatable(30f) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(animDelay.toLong() + 150)
|
||||||
|
launch { animAlpha.animateTo(1f, tween(450, easing = FastOutSlowInEasing)) }
|
||||||
|
launch { animScale.animateTo(1f, tween(500, easing = FastOutSlowInEasing)) }
|
||||||
|
launch { animOffset.animateTo(0f, tween(500, easing = FastOutSlowInEasing)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
|
val pressScale = if (isPressed) 0.95f else 1f
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.width(150.dp)
|
||||||
.clickable(onClick = onClick)
|
.graphicsLayer {
|
||||||
|
alpha = animAlpha.value
|
||||||
|
scaleX = animScale.value * pressScale
|
||||||
|
scaleY = animScale.value * pressScale
|
||||||
|
translationY = animOffset.value
|
||||||
|
}
|
||||||
|
.clip(RoundedCornerShape(20.dp))
|
||||||
|
.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null,
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.size(110.dp),
|
||||||
.size(96.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
|
||||||
.border(2.dp, MaterialTheme.colorScheme.outline, CircleShape),
|
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Box(
|
||||||
imageVector = Icons.Rounded.Add,
|
modifier = Modifier
|
||||||
contentDescription = null,
|
.size(100.dp)
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
.clip(CircleShape)
|
||||||
modifier = Modifier.size(36.dp),
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
)
|
.border(
|
||||||
|
2.dp,
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f),
|
||||||
|
CircleShape,
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
|
modifier = Modifier.size(40.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Add Profile",
|
text = "Add Profile",
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge.copy(fontSize = 16.sp),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.SemiBold,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue