mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 23:42:04 +00:00
feat: add support for custom avatar url
This commit is contained in:
parent
81babba3ed
commit
2fa918fe44
7 changed files with 134 additions and 35 deletions
|
|
@ -1007,9 +1007,14 @@
|
||||||
<string name="pin_locked_try_again">Locked. Try again in %1$ds</string>
|
<string name="pin_locked_try_again">Locked. Try again in %1$ds</string>
|
||||||
<string name="profile_avatar_options_pending">Avatar options will appear here when the catalog loads.</string>
|
<string name="profile_avatar_options_pending">Avatar options will appear here when the catalog loads.</string>
|
||||||
<string name="profile_avatar_selected">Avatar: %1$s</string>
|
<string name="profile_avatar_selected">Avatar: %1$s</string>
|
||||||
|
<string name="profile_avatar_url_invalid">Enter a valid http:// or https:// image URL.</string>
|
||||||
<string name="profile_choose_avatar">Choose an avatar</string>
|
<string name="profile_choose_avatar">Choose an avatar</string>
|
||||||
<string name="profile_choose_avatar_below">Choose an avatar below.</string>
|
<string name="profile_choose_avatar_below">Choose an avatar below.</string>
|
||||||
<string name="profile_create_profile">Create Profile</string>
|
<string name="profile_create_profile">Create Profile</string>
|
||||||
|
<string name="profile_custom_avatar_selected">Custom avatar URL selected.</string>
|
||||||
|
<string name="profile_custom_avatar_url">Custom avatar URL</string>
|
||||||
|
<string name="profile_custom_avatar_url_description">Paste an image link, or leave this empty to use the built-in avatar catalog.</string>
|
||||||
|
<string name="profile_custom_avatar_url_placeholder">https://example.com/avatar.png</string>
|
||||||
<string name="profile_delete_confirm_message">All data for "%1$s" will be permanently deleted.</string>
|
<string name="profile_delete_confirm_message">All data for "%1$s" will be permanently deleted.</string>
|
||||||
<string name="profile_delete_title">Delete Profile</string>
|
<string name="profile_delete_title">Delete Profile</string>
|
||||||
<string name="profile_edit_add_title">Add Profile</string>
|
<string name="profile_edit_add_title">Add Profile</string>
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ import com.nuvio.app.features.profiles.ProfileEditScreen
|
||||||
import com.nuvio.app.features.profiles.ProfileRepository
|
import com.nuvio.app.features.profiles.ProfileRepository
|
||||||
import com.nuvio.app.features.profiles.ProfileSelectionScreen
|
import com.nuvio.app.features.profiles.ProfileSelectionScreen
|
||||||
import com.nuvio.app.features.profiles.ProfileSwitcherTab
|
import com.nuvio.app.features.profiles.ProfileSwitcherTab
|
||||||
import com.nuvio.app.features.profiles.avatarStorageUrl
|
import com.nuvio.app.features.profiles.profileAvatarImageUrl
|
||||||
import com.nuvio.app.features.search.SearchScreen
|
import com.nuvio.app.features.search.SearchScreen
|
||||||
import com.nuvio.app.features.settings.SettingsScreen
|
import com.nuvio.app.features.settings.SettingsScreen
|
||||||
import com.nuvio.app.features.settings.HomescreenSettingsScreen
|
import com.nuvio.app.features.settings.HomescreenSettingsScreen
|
||||||
|
|
@ -331,6 +331,7 @@ fun App() {
|
||||||
profileState.activeProfile?.name,
|
profileState.activeProfile?.name,
|
||||||
profileState.activeProfile?.avatarColorHex,
|
profileState.activeProfile?.avatarColorHex,
|
||||||
profileState.activeProfile?.avatarId,
|
profileState.activeProfile?.avatarId,
|
||||||
|
profileState.activeProfile?.avatarUrl,
|
||||||
profileAvatars,
|
profileAvatars,
|
||||||
) {
|
) {
|
||||||
val activeProfile = profileState.activeProfile
|
val activeProfile = profileState.activeProfile
|
||||||
|
|
@ -340,10 +341,7 @@ fun App() {
|
||||||
NativeTabBridge.publishProfileTabIcon(
|
NativeTabBridge.publishProfileTabIcon(
|
||||||
name = activeProfile?.name,
|
name = activeProfile?.name,
|
||||||
avatarColorHex = activeProfile?.avatarColorHex,
|
avatarColorHex = activeProfile?.avatarColorHex,
|
||||||
avatarImageUrl = avatarItem
|
avatarImageUrl = activeProfile?.let { profileAvatarImageUrl(it, avatarItem) },
|
||||||
?.storagePath
|
|
||||||
?.takeIf { it.isNotBlank() }
|
|
||||||
?.let(::avatarStorageUrl),
|
|
||||||
avatarBackgroundColorHex = avatarItem?.bgColor,
|
avatarBackgroundColorHex = avatarItem?.bgColor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ fun ProfileEditScreen(
|
||||||
|
|
||||||
var name by rememberSaveable { mutableStateOf(currentProfile?.name ?: "") }
|
var name by rememberSaveable { mutableStateOf(currentProfile?.name ?: "") }
|
||||||
var selectedAvatarId by rememberSaveable { mutableStateOf(currentProfile?.avatarId) }
|
var selectedAvatarId by rememberSaveable { mutableStateOf(currentProfile?.avatarId) }
|
||||||
|
var avatarUrl by rememberSaveable { mutableStateOf(currentProfile?.avatarUrl.orEmpty()) }
|
||||||
var usesPrimaryAddons by rememberSaveable { mutableStateOf(currentProfile?.usesPrimaryAddons ?: false) }
|
var usesPrimaryAddons by rememberSaveable { mutableStateOf(currentProfile?.usesPrimaryAddons ?: false) }
|
||||||
var isSaving by remember { mutableStateOf(false) }
|
var isSaving by remember { mutableStateOf(false) }
|
||||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||||
|
|
@ -90,17 +91,20 @@ fun ProfileEditScreen(
|
||||||
AvatarRepository.fetchAvatars()
|
AvatarRepository.fetchAvatars()
|
||||||
AvatarRepository.refreshAvatars()
|
AvatarRepository.refreshAvatars()
|
||||||
}
|
}
|
||||||
LaunchedEffect(isNew, avatars, selectedAvatarId) {
|
LaunchedEffect(isNew, avatars, selectedAvatarId, avatarUrl) {
|
||||||
if (isNew && selectedAvatarId == null && avatars.isNotEmpty()) {
|
if (isNew && avatarUrl.isBlank() && selectedAvatarId == null && avatars.isNotEmpty()) {
|
||||||
selectedAvatarId = avatars.first().id
|
selectedAvatarId = avatars.first().id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val customAvatarUrl = remember(avatarUrl) { normalizedAvatarUrl(avatarUrl) }
|
||||||
|
val avatarUrlIsInvalid = avatarUrl.isNotBlank() && customAvatarUrl == null
|
||||||
val selectedAvatarItem = remember(selectedAvatarId, avatars) {
|
val selectedAvatarItem = remember(selectedAvatarId, avatars) {
|
||||||
selectedAvatarId?.let { id -> avatars.find { it.id == id } }
|
selectedAvatarId?.let { id -> avatars.find { it.id == id } }
|
||||||
}
|
}
|
||||||
val previewAccent = remember(selectedAvatarItem, fallbackColorHex) {
|
val visibleAvatarItem = if (customAvatarUrl == null) selectedAvatarItem else null
|
||||||
parseHexColor(selectedAvatarItem?.bgColor ?: fallbackColorHex)
|
val previewAccent = remember(visibleAvatarItem, fallbackColorHex) {
|
||||||
|
parseHexColor(visibleAvatarItem?.bgColor ?: fallbackColorHex)
|
||||||
}
|
}
|
||||||
|
|
||||||
NuvioScreen(modifier = modifier) {
|
NuvioScreen(modifier = modifier) {
|
||||||
|
|
@ -123,12 +127,47 @@ fun ProfileEditScreen(
|
||||||
usesPrimaryAddons = usesPrimaryAddons,
|
usesPrimaryAddons = usesPrimaryAddons,
|
||||||
onNameChange = { name = it },
|
onNameChange = { name = it },
|
||||||
onUsesPrimaryAddonsChange = { usesPrimaryAddons = it },
|
onUsesPrimaryAddonsChange = { usesPrimaryAddons = it },
|
||||||
selectedAvatar = selectedAvatarItem,
|
selectedAvatar = visibleAvatarItem,
|
||||||
|
customAvatarUrl = customAvatarUrl,
|
||||||
accentColor = previewAccent,
|
accentColor = previewAccent,
|
||||||
hasAvatarChoices = avatars.isNotEmpty(),
|
hasAvatarChoices = avatars.isNotEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
NuvioSurfaceCard {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.profile_custom_avatar_url),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.profile_custom_avatar_url_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
NuvioInputField(
|
||||||
|
value = avatarUrl,
|
||||||
|
onValueChange = { value ->
|
||||||
|
avatarUrl = value
|
||||||
|
if (value.isNotBlank()) {
|
||||||
|
selectedAvatarId = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
placeholder = stringResource(Res.string.profile_custom_avatar_url_placeholder),
|
||||||
|
)
|
||||||
|
if (avatarUrlIsInvalid) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.profile_avatar_url_invalid),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
NuvioSurfaceCard {
|
NuvioSurfaceCard {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
|
|
@ -165,8 +204,11 @@ fun ProfileEditScreen(
|
||||||
AvatarChoiceItem(
|
AvatarChoiceItem(
|
||||||
avatar = avatar,
|
avatar = avatar,
|
||||||
size = avatarSize,
|
size = avatarSize,
|
||||||
isSelected = avatar.id == selectedAvatarId,
|
isSelected = customAvatarUrl == null && avatar.id == selectedAvatarId,
|
||||||
onClick = { selectedAvatarId = avatar.id },
|
onClick = {
|
||||||
|
avatarUrl = ""
|
||||||
|
selectedAvatarId = avatar.id
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -220,16 +262,17 @@ fun ProfileEditScreen(
|
||||||
} else {
|
} else {
|
||||||
stringResource(Res.string.collections_editor_save_changes)
|
stringResource(Res.string.collections_editor_save_changes)
|
||||||
},
|
},
|
||||||
enabled = name.isNotBlank() && !isSaving,
|
enabled = name.isNotBlank() && !avatarUrlIsInvalid && !isSaving,
|
||||||
onClick = {
|
onClick = {
|
||||||
isSaving = true
|
isSaving = true
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val avatarColorHex = selectedAvatarItem?.bgColor ?: fallbackColorHex
|
val avatarColorHex = visibleAvatarItem?.bgColor ?: fallbackColorHex
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
ProfileRepository.createProfile(
|
ProfileRepository.createProfile(
|
||||||
name = name,
|
name = name,
|
||||||
avatarColorHex = avatarColorHex,
|
avatarColorHex = avatarColorHex,
|
||||||
avatarId = selectedAvatarId,
|
avatarId = if (customAvatarUrl == null) selectedAvatarId else null,
|
||||||
|
avatarUrl = customAvatarUrl,
|
||||||
usesPrimaryAddons = usesPrimaryAddons,
|
usesPrimaryAddons = usesPrimaryAddons,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -237,7 +280,8 @@ fun ProfileEditScreen(
|
||||||
profileIndex = currentProfile!!.profileIndex,
|
profileIndex = currentProfile!!.profileIndex,
|
||||||
name = name,
|
name = name,
|
||||||
avatarColorHex = avatarColorHex,
|
avatarColorHex = avatarColorHex,
|
||||||
avatarId = selectedAvatarId,
|
avatarId = if (customAvatarUrl == null) selectedAvatarId else null,
|
||||||
|
avatarUrl = customAvatarUrl,
|
||||||
usesPrimaryAddons = usesPrimaryAddons,
|
usesPrimaryAddons = usesPrimaryAddons,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -330,6 +374,7 @@ private fun ProfileIdentityCard(
|
||||||
onNameChange: (String) -> Unit,
|
onNameChange: (String) -> Unit,
|
||||||
onUsesPrimaryAddonsChange: (Boolean) -> Unit,
|
onUsesPrimaryAddonsChange: (Boolean) -> Unit,
|
||||||
selectedAvatar: AvatarCatalogItem?,
|
selectedAvatar: AvatarCatalogItem?,
|
||||||
|
customAvatarUrl: String?,
|
||||||
accentColor: Color,
|
accentColor: Color,
|
||||||
hasAvatarChoices: Boolean,
|
hasAvatarChoices: Boolean,
|
||||||
) {
|
) {
|
||||||
|
|
@ -345,16 +390,31 @@ private fun ProfileIdentityCard(
|
||||||
.size(88.dp)
|
.size(88.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
if (selectedAvatar != null) accentColor else accentColor.copy(alpha = 0.18f),
|
if (selectedAvatar != null || customAvatarUrl != null) {
|
||||||
|
accentColor
|
||||||
|
} else {
|
||||||
|
accentColor.copy(alpha = 0.18f)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.border(
|
.border(
|
||||||
width = 2.dp,
|
width = 2.dp,
|
||||||
color = if (selectedAvatar == null) accentColor.copy(alpha = 0.35f) else Color.Transparent,
|
color = if (selectedAvatar == null && customAvatarUrl == null) {
|
||||||
|
accentColor.copy(alpha = 0.35f)
|
||||||
|
} else {
|
||||||
|
Color.Transparent
|
||||||
|
},
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
if (selectedAvatar != null) {
|
if (customAvatarUrl != null) {
|
||||||
|
AsyncImage(
|
||||||
|
model = customAvatarUrl,
|
||||||
|
contentDescription = name,
|
||||||
|
modifier = Modifier.size(88.dp).clip(CircleShape),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
)
|
||||||
|
} else if (selectedAvatar != null) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = avatarStorageUrl(selectedAvatar.storagePath),
|
model = avatarStorageUrl(selectedAvatar.storagePath),
|
||||||
contentDescription = selectedAvatar.displayName,
|
contentDescription = selectedAvatar.displayName,
|
||||||
|
|
@ -410,6 +470,7 @@ private fun ProfileIdentityCard(
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = when {
|
text = when {
|
||||||
|
customAvatarUrl != null -> stringResource(Res.string.profile_custom_avatar_selected)
|
||||||
selectedAvatar != null -> stringResource(
|
selectedAvatar != null -> stringResource(
|
||||||
Res.string.profile_avatar_selected,
|
Res.string.profile_avatar_selected,
|
||||||
selectedAvatar.displayName,
|
selectedAvatar.displayName,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ data class NuvioProfile(
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
@SerialName("avatar_color_hex") val avatarColorHex: String = "#1E88E5",
|
@SerialName("avatar_color_hex") val avatarColorHex: String = "#1E88E5",
|
||||||
@SerialName("avatar_id") val avatarId: String? = null,
|
@SerialName("avatar_id") val avatarId: String? = null,
|
||||||
|
@SerialName("avatar_url") val avatarUrl: String? = null,
|
||||||
@SerialName("uses_primary_addons") val usesPrimaryAddons: Boolean = false,
|
@SerialName("uses_primary_addons") val usesPrimaryAddons: Boolean = false,
|
||||||
@SerialName("uses_primary_plugins") val usesPrimaryPlugins: Boolean = false,
|
@SerialName("uses_primary_plugins") val usesPrimaryPlugins: Boolean = false,
|
||||||
@SerialName("pin_enabled") val pinEnabled: Boolean = false,
|
@SerialName("pin_enabled") val pinEnabled: Boolean = false,
|
||||||
|
|
@ -28,6 +29,7 @@ data class ProfilePushPayload(
|
||||||
@SerialName("uses_primary_addons") val usesPrimaryAddons: Boolean = false,
|
@SerialName("uses_primary_addons") val usesPrimaryAddons: Boolean = false,
|
||||||
@SerialName("uses_primary_plugins") val usesPrimaryPlugins: Boolean = false,
|
@SerialName("uses_primary_plugins") val usesPrimaryPlugins: Boolean = false,
|
||||||
@SerialName("avatar_id") val avatarId: String? = null,
|
@SerialName("avatar_id") val avatarId: String? = null,
|
||||||
|
@SerialName("avatar_url") val avatarUrl: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -74,3 +76,20 @@ val PROFILE_COLORS = listOf(
|
||||||
|
|
||||||
fun avatarStorageUrl(storagePath: String): String =
|
fun avatarStorageUrl(storagePath: String): String =
|
||||||
"${com.nuvio.app.core.network.SupabaseConfig.URL}/storage/v1/object/public/avatars/$storagePath"
|
"${com.nuvio.app.core.network.SupabaseConfig.URL}/storage/v1/object/public/avatars/$storagePath"
|
||||||
|
|
||||||
|
fun normalizedAvatarUrl(url: String?): String? =
|
||||||
|
url?.trim()?.takeIf { it.isValidAvatarUrl() }
|
||||||
|
|
||||||
|
fun String.isValidAvatarUrl(): Boolean {
|
||||||
|
val value = trim()
|
||||||
|
return value.length <= 2048 &&
|
||||||
|
!value.any { it.isWhitespace() } &&
|
||||||
|
(value.startsWith("https://") || value.startsWith("http://"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun profileAvatarImageUrl(profile: NuvioProfile, avatar: AvatarCatalogItem?): String? =
|
||||||
|
normalizedAvatarUrl(profile.avatarUrl)
|
||||||
|
?: avatar
|
||||||
|
?.storagePath
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.let(::avatarStorageUrl)
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ object ProfileRepository {
|
||||||
name: String,
|
name: String,
|
||||||
avatarColorHex: String,
|
avatarColorHex: String,
|
||||||
avatarId: String? = null,
|
avatarId: String? = null,
|
||||||
|
avatarUrl: String? = null,
|
||||||
usesPrimaryAddons: Boolean = false,
|
usesPrimaryAddons: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val existing = _state.value.profiles
|
val existing = _state.value.profiles
|
||||||
|
|
@ -192,6 +193,7 @@ object ProfileRepository {
|
||||||
usesPrimaryAddons = profile.usesPrimaryAddons,
|
usesPrimaryAddons = profile.usesPrimaryAddons,
|
||||||
usesPrimaryPlugins = profile.usesPrimaryPlugins,
|
usesPrimaryPlugins = profile.usesPrimaryPlugins,
|
||||||
avatarId = profile.avatarId,
|
avatarId = profile.avatarId,
|
||||||
|
avatarUrl = profile.avatarUrl,
|
||||||
)
|
)
|
||||||
} + ProfilePushPayload(
|
} + ProfilePushPayload(
|
||||||
profileIndex = nextIndex,
|
profileIndex = nextIndex,
|
||||||
|
|
@ -199,6 +201,7 @@ object ProfileRepository {
|
||||||
avatarColorHex = avatarColorHex,
|
avatarColorHex = avatarColorHex,
|
||||||
usesPrimaryAddons = usesPrimaryAddons,
|
usesPrimaryAddons = usesPrimaryAddons,
|
||||||
avatarId = avatarId,
|
avatarId = avatarId,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
)
|
)
|
||||||
|
|
||||||
pushProfiles(allPayloads)
|
pushProfiles(allPayloads)
|
||||||
|
|
@ -209,6 +212,7 @@ object ProfileRepository {
|
||||||
name: String,
|
name: String,
|
||||||
avatarColorHex: String,
|
avatarColorHex: String,
|
||||||
avatarId: String? = null,
|
avatarId: String? = null,
|
||||||
|
avatarUrl: String? = null,
|
||||||
usesPrimaryAddons: Boolean = false,
|
usesPrimaryAddons: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val allPayloads = _state.value.profiles.map { profile ->
|
val allPayloads = _state.value.profiles.map { profile ->
|
||||||
|
|
@ -218,7 +222,8 @@ object ProfileRepository {
|
||||||
name = name,
|
name = name,
|
||||||
avatarColorHex = avatarColorHex,
|
avatarColorHex = avatarColorHex,
|
||||||
usesPrimaryAddons = usesPrimaryAddons,
|
usesPrimaryAddons = usesPrimaryAddons,
|
||||||
avatarId = avatarId ?: profile.avatarId,
|
avatarId = avatarId,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ProfilePushPayload(
|
ProfilePushPayload(
|
||||||
|
|
@ -228,6 +233,7 @@ object ProfileRepository {
|
||||||
usesPrimaryAddons = profile.usesPrimaryAddons,
|
usesPrimaryAddons = profile.usesPrimaryAddons,
|
||||||
usesPrimaryPlugins = profile.usesPrimaryPlugins,
|
usesPrimaryPlugins = profile.usesPrimaryPlugins,
|
||||||
avatarId = profile.avatarId,
|
avatarId = profile.avatarId,
|
||||||
|
avatarUrl = profile.avatarUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -357,6 +363,7 @@ object ProfileRepository {
|
||||||
name = p.name,
|
name = p.name,
|
||||||
avatarColorHex = p.avatarColorHex,
|
avatarColorHex = p.avatarColorHex,
|
||||||
avatarId = p.avatarId,
|
avatarId = p.avatarId,
|
||||||
|
avatarUrl = p.avatarUrl,
|
||||||
usesPrimaryAddons = p.usesPrimaryAddons,
|
usesPrimaryAddons = p.usesPrimaryAddons,
|
||||||
usesPrimaryPlugins = p.usesPrimaryPlugins,
|
usesPrimaryPlugins = p.usesPrimaryPlugins,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,9 @@ private fun ProfileAvatarCard(
|
||||||
val avatarItem = remember(profile.avatarId, avatars) {
|
val avatarItem = remember(profile.avatarId, avatars) {
|
||||||
profile.avatarId?.let { id -> avatars.find { it.id == id } }
|
profile.avatarId?.let { id -> avatars.find { it.id == id } }
|
||||||
}
|
}
|
||||||
|
val avatarImageUrl = remember(profile.avatarUrl, avatarItem) {
|
||||||
|
profileAvatarImageUrl(profile, avatarItem)
|
||||||
|
}
|
||||||
|
|
||||||
val animAlpha = remember { Animatable(0f) }
|
val animAlpha = remember { Animatable(0f) }
|
||||||
val animScale = remember { Animatable(0.85f) }
|
val animScale = remember { Animatable(0.85f) }
|
||||||
|
|
@ -342,8 +345,8 @@ private fun ProfileAvatarCard(
|
||||||
modifier = Modifier.size(110.dp),
|
modifier = Modifier.size(110.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
if (avatarItem != null) {
|
if (avatarImageUrl != null) {
|
||||||
val bgColor = avatarItem.bgColor?.let { parseHexColor(it) } ?: avatarColor
|
val bgColor = avatarItem?.bgColor?.let { parseHexColor(it) } ?: avatarColor
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(110.dp)
|
.size(110.dp)
|
||||||
|
|
@ -364,15 +367,15 @@ private fun ProfileAvatarCard(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
if (avatarItem == null) Modifier.border(2.dp, avatarColor.copy(alpha = 0.4f), CircleShape)
|
if (avatarImageUrl == null) Modifier.border(2.dp, avatarColor.copy(alpha = 0.4f), CircleShape)
|
||||||
else Modifier,
|
else Modifier,
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
if (avatarItem != null) {
|
if (avatarImageUrl != null) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = avatarStorageUrl(avatarItem.storagePath),
|
model = avatarImageUrl,
|
||||||
contentDescription = avatarItem.displayName,
|
contentDescription = avatarItem?.displayName ?: profile.name,
|
||||||
modifier = Modifier.size(100.dp).clip(CircleShape),
|
modifier = Modifier.size(100.dp).clip(CircleShape),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -341,6 +341,9 @@ private fun PopupProfileBubble(
|
||||||
val avatarItem = remember(profile.avatarId, avatars) {
|
val avatarItem = remember(profile.avatarId, avatars) {
|
||||||
profile.avatarId?.let { id -> avatars.find { it.id == id } }
|
profile.avatarId?.let { id -> avatars.find { it.id == id } }
|
||||||
}
|
}
|
||||||
|
val avatarImageUrl = remember(profile.avatarUrl, avatarItem) {
|
||||||
|
profileAvatarImageUrl(profile, avatarItem)
|
||||||
|
}
|
||||||
|
|
||||||
// Per-item entrance animation
|
// Per-item entrance animation
|
||||||
val itemAlpha = remember { Animatable(0f) }
|
val itemAlpha = remember { Animatable(0f) }
|
||||||
|
|
@ -393,8 +396,8 @@ private fun PopupProfileBubble(
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
if (avatarItem != null) {
|
if (avatarImageUrl != null) {
|
||||||
avatarItem.bgColor?.let { parseHexColor(it) } ?: avatarColor
|
avatarItem?.bgColor?.let { parseHexColor(it) } ?: avatarColor
|
||||||
} else {
|
} else {
|
||||||
avatarColor.copy(alpha = 0.15f)
|
avatarColor.copy(alpha = 0.15f)
|
||||||
},
|
},
|
||||||
|
|
@ -411,7 +414,7 @@ private fun PopupProfileBubble(
|
||||||
avatarColor.copy(alpha = 0.6f),
|
avatarColor.copy(alpha = 0.6f),
|
||||||
CircleShape,
|
CircleShape,
|
||||||
)
|
)
|
||||||
avatarItem == null -> Modifier.border(
|
avatarImageUrl == null -> Modifier.border(
|
||||||
1.5.dp,
|
1.5.dp,
|
||||||
avatarColor.copy(alpha = 0.3f),
|
avatarColor.copy(alpha = 0.3f),
|
||||||
CircleShape,
|
CircleShape,
|
||||||
|
|
@ -421,9 +424,9 @@ private fun PopupProfileBubble(
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
if (avatarItem != null) {
|
if (avatarImageUrl != null) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = avatarStorageUrl(avatarItem.storagePath),
|
model = avatarImageUrl,
|
||||||
contentDescription = profile.name,
|
contentDescription = profile.name,
|
||||||
modifier = Modifier.size(48.dp).clip(CircleShape),
|
modifier = Modifier.size(48.dp).clip(CircleShape),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
|
|
@ -700,6 +703,9 @@ fun ActiveProfileMiniAvatar(
|
||||||
val avatarItem = remember(profile.avatarId, avatars) {
|
val avatarItem = remember(profile.avatarId, avatars) {
|
||||||
profile.avatarId?.let { id -> avatars.find { it.id == id } }
|
profile.avatarId?.let { id -> avatars.find { it.id == id } }
|
||||||
}
|
}
|
||||||
|
val avatarImageUrl = remember(profile.avatarUrl, avatarItem) {
|
||||||
|
profileAvatarImageUrl(profile, avatarItem)
|
||||||
|
}
|
||||||
|
|
||||||
val borderColor = if (selected) {
|
val borderColor = if (selected) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
|
|
@ -712,8 +718,8 @@ fun ActiveProfileMiniAvatar(
|
||||||
.size(size.dp)
|
.size(size.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
if (avatarItem != null) {
|
if (avatarImageUrl != null) {
|
||||||
avatarItem.bgColor?.let { parseHexColor(it) } ?: avatarColor
|
avatarItem?.bgColor?.let { parseHexColor(it) } ?: avatarColor
|
||||||
} else {
|
} else {
|
||||||
avatarColor.copy(alpha = 0.15f)
|
avatarColor.copy(alpha = 0.15f)
|
||||||
},
|
},
|
||||||
|
|
@ -721,9 +727,9 @@ fun ActiveProfileMiniAvatar(
|
||||||
.border(1.5.dp, borderColor, CircleShape),
|
.border(1.5.dp, borderColor, CircleShape),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
if (avatarItem != null) {
|
if (avatarImageUrl != null) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = avatarStorageUrl(avatarItem.storagePath),
|
model = avatarImageUrl,
|
||||||
contentDescription = profile.name,
|
contentDescription = profile.name,
|
||||||
modifier = Modifier.size(size.dp).clip(CircleShape),
|
modifier = Modifier.size(size.dp).clip(CircleShape),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue