package com.suno.android.ui.screens.home.profile import android.content.Intent import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.FilledIconButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SheetState import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberModalBottomSheetState 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 import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue 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.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.suno.android.common_core_utils.Id import com.suno.android.common_core_utils.environment.ThemeMode import com.suno.android.common_core_utils.model.Url import com.suno.android.common_core_utils.model.UserHandle import com.suno.android.common_data.mappers.clips.SongListData import com.suno.android.common_networking.remote.entities.PlaylistSchema import com.suno.android.common_ui.components.images.ProfileAvatarImage import com.suno.android.common_ui.components.infos.empty_states.NoPublicSongsEmptyState import com.suno.android.common_ui.components.list_items.SongListItem import com.suno.android.common_ui.components.loading.SunoSpinner import com.suno.android.common_ui.components.omni.CircleIconButton import com.suno.android.common_ui.components.preview.SunoPreview import com.suno.android.common_ui.extensions.xAsFormattedCount import com.suno.android.common_ui.theme.ExtendedTheme import com.suno.android.common_ui.theme.SunoTheme import com.suno.android.extensions.xAsUiModelBadge import com.suno.android.preview.FakeSongListData import com.suno.android.ui.bottom_sheets.song_actions.SongActionBottomSheetConsumerEvent import com.suno.android.ui.bottom_sheets.song_actions.SongActionsBottomSheet import com.suno.android.ui.screens.create.CreatePromptForm import com.suno.android.ui.screens.create.audio.CreateAudioState import com.suno.android.ui.screens.home.mediaplayer.omniPlayer import com.suno.android.ui.screens.home.profile.user_hooks.UserHooksSection import com.suno.android.ui.screens.home.profile.user_hooks.UserHooksState import com.suno.android.utils.mvi.MviEffectHandler import kotlinx.coroutines.launch import com.suno.android.common_res.R as CommonResR import com.suno.android.common_ui.R as CommonUIR @Composable fun ProfileScreen( vm: ProfileScreenVM = hiltViewModel(), isTopLevel: Boolean = false, onSettingsClicked: () -> Unit, onBackPressed: () -> Unit, onUpgradeClicked: () -> Unit, onEditProfileClicked: () -> Unit, onPlaylistClicked: (playlistId: String) -> Unit, onReusePromptClicked: (CreatePromptForm) -> Unit, navigateToAssetEdit: (songId: Id) -> Unit, navigateToRemix: (songId: Id, task: CreateAudioState.AudioCreateTask) -> Unit, navigateToFollowers: () -> Unit, navigateToFollowing: () -> Unit, navigateToUserHooksFeed: (userHandle: UserHandle, startIndex: Int) -> Unit, ) { val viewState = vm.state.collectAsStateWithLifecycle().value val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } val songActionsContainerHostState: SheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true, // Allow partial expansion, ) var showSongActionContainerBottomSheet: Boolean by remember { mutableStateOf(false) } LaunchedEffect(viewState.snackBarMessage) { if (viewState.snackBarMessage.isNotEmpty()) { snackbarHostState.showSnackbar(viewState.snackBarMessage) vm.sendEvent(ProfileScreenEvent.OnResetSnackBar) } } // We need this launched effect for the initial setting of the modal sheet state so it'll animate properly for FIRST launch LaunchedEffect(showSongActionContainerBottomSheet) { if (showSongActionContainerBottomSheet) { // Sheet is now in composition, animate it songActionsContainerHostState.show() } } // Bidirectional sync for sheet state so it'll animate LaunchedEffect(songActionsContainerHostState.isVisible) { showSongActionContainerBottomSheet = songActionsContainerHostState.isVisible } MviEffectHandler(effects = vm.effects) { effect -> when (effect) { ProfileScreenEffect.OnEditProfileClicked -> { onEditProfileClicked.invoke() } is ProfileScreenEffect.ShowSnackbar -> { snackbarHostState.showSnackbar(effect.message) } ProfileScreenEffect.NavigateToFollowers -> { navigateToFollowers.invoke() } ProfileScreenEffect.NavigateToFollowing -> { navigateToFollowing.invoke() } is ProfileScreenEffect.NavigateToHooks -> { navigateToUserHooksFeed(viewState.userHandle, effect.hookIndex) } } } ProfileScreen( viewState = viewState, snackbarHostState = snackbarHostState, songActionsContainerHostState = songActionsContainerHostState, onEvent = { event -> vm.sendEvent(event) }, isTopLevel = isTopLevel, showSongActionContainerBottomSheet = showSongActionContainerBottomSheet, onShowSongActionContainerBottomSheet = { show -> showSongActionContainerBottomSheet = show }, onSettingsClicked = onSettingsClicked, onBackPressed = onBackPressed, onUpgradeClicked = onUpgradeClicked, onEditProfileClicked = onEditProfileClicked, onShareProfileClicked = { val handle: String = viewState.userHandle.handle val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND type = "text/plain" putExtra( Intent.EXTRA_TEXT, "https://suno.com/@$handle", ) putExtra( Intent.EXTRA_TITLE, context.getString(CommonResR.string.share_profile_title), ) } val shareIntent = Intent.createChooser(sendIntent, null) context.startActivity(shareIntent) }, onPlaylistClicked = onPlaylistClicked, onReusePromptClicked = onReusePromptClicked, navigateToAssetEdit = navigateToAssetEdit, isPlayerVisible = { vm.isPlayerVisible() }, navigateToRemix = navigateToRemix, onFollowersClicked = { vm.sendEvent(ProfileScreenEvent.OnFollowersClicked) }, onFollowingClicked = { vm.sendEvent(ProfileScreenEvent.OnFollowingClicked) }, ) } @Composable private fun ProfileScreen( viewState: ProfileScreenState, snackbarHostState: SnackbarHostState, songActionsContainerHostState: SheetState, onEvent: (ProfileScreenEvent) -> Unit, isTopLevel: Boolean, showSongActionContainerBottomSheet: Boolean, onShowSongActionContainerBottomSheet: (Boolean) -> Unit, onSettingsClicked: () -> Unit, onBackPressed: () -> Unit, onUpgradeClicked: () -> Unit, onEditProfileClicked: () -> Unit, onShareProfileClicked: () -> Unit, onPlaylistClicked: (playlistId: String) -> Unit, onReusePromptClicked: (CreatePromptForm) -> Unit, navigateToAssetEdit: (songId: Id) -> Unit, topAppBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(), isPlayerVisible: () -> Boolean = { false }, navigateToRemix: (songId: Id, task: CreateAudioState.AudioCreateTask) -> Unit, onFollowersClicked: () -> Unit, onFollowingClicked: () -> Unit, ) { Column { ProfileTopAppBar( nestedScrollConnection = topAppBarScrollBehavior.nestedScrollConnection, isTopLevel = isTopLevel, onBackPressed = onBackPressed, isSelf = viewState.isSelf, onSettingsClicked = onSettingsClicked, ) Box( modifier = Modifier.fillMaxSize(), ) { if (viewState.isLoading) { SunoSpinner( modifier = Modifier .align(Alignment.Center), ) } else { LazyColumn( contentPadding = WindowInsets.omniPlayer.asPaddingValues(), modifier = Modifier .fillMaxSize() .consumeWindowInsets(WindowInsets.omniPlayer), ) { item { ProfileHeader( avatarImageUrl = viewState.avatarImageUrl, userName = viewState.userName, userHandle = viewState.userHandle.handle, followerCount = viewState.followerCount, followingCount = viewState.followingCount, onFollowersClicked = { onFollowersClicked.invoke() }, onFollowingClicked = { onFollowingClicked.invoke() }, ) } item { if (viewState.isSelf) { SelfUserActionButtons( onEditProfileClicked = onEditProfileClicked, onShareProfileClicked = { onShareProfileClicked() }, ) } else { OtherUserActionButtons( isFollowing = viewState.isFollowing, hasClips = viewState.clips.isNotEmpty(), onFollowClicked = { onEvent(ProfileScreenEvent.OnFollowClicked) }, onShareProfileClicked = { onShareProfileClicked() }, onPlayClicked = { onEvent(ProfileScreenEvent.OnPlayClicked) }, ) } } if (viewState.userHooksState is UserHooksState.Enabled) { item { SunoTheme(themeMode = ThemeMode.DARK) { UserHooksSection( state = viewState.userHooksState, onHookClick = { hook, index -> onEvent( ProfileScreenEvent.OnHookCardTapped( userId = viewState.userId, hook = hook, hookIndex = index, ), ) }, contentPadding = PaddingValues( horizontal = 16.dp, ), ) } } } item { Text( modifier = Modifier.padding( top = 32.dp, bottom = 16.dp, start = 16.dp, end = 16.dp, ), text = stringResource(CommonResR.string.public_songs), style = ExtendedTheme.typography.introCopy, ) } if (viewState.isSelf && viewState.clips.isEmpty()) { item { NoPublicSongsEmptyState() } } else { items(viewState.clips) { theClip: SongListData -> SongListItem( clipImageUrl = theClip.displayImageUrl, songTitle = theClip.title?.takeIf { it.isNotEmpty() } ?: stringResource(id = CommonResR.string.untitled), playCount = theClip.playCount?.xAsFormattedCount(), songTags = theClip.tags ?: stringResource(CommonResR.string.no_style), upvoteCount = theClip.upvoteCount?.xAsFormattedCount(), modelBadge = theClip.modelBadge?.xAsUiModelBadge(), onClick = { onEvent(ProfileScreenEvent.OnSongClicked(theClip)) }, onOverflowClick = { onEvent(ProfileScreenEvent.OnSongOverflowOpened(theClip.id)) onShowSongActionContainerBottomSheet(true) }, onUpgradeClicked = onUpgradeClicked, isFreeUser = viewState.isFreeUser == true, isPreviewClip = theClip.isPreview, ) HorizontalDivider( thickness = 1.dp, color = ExtendedTheme.colors.borderPrimary, ) } } if (viewState.playlists.isNotEmpty()) { item { HorizontalDivider( thickness = 1.dp, color = ExtendedTheme.colors.borderPrimary, ) Text( modifier = Modifier.padding( top = 32.dp, bottom = 16.dp, start = 16.dp, end = 16.dp, ), text = stringResource(CommonResR.string.public_playlists), style = ExtendedTheme.typography.categoryHeadline, ) } item { PublicPlaylistsSection( playlists = viewState.playlists, onPlaylistClicked = onPlaylistClicked, ) } } } } SnackbarHost( modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = if (isPlayerVisible()) 160.dp else 90.dp), hostState = snackbarHostState, ) { Snackbar( containerColor = ExtendedTheme.colors.backgroundInvert, ) { Text( text = viewState.snackBarMessage, color = ExtendedTheme.colors.backgroundPrimary, ) } } } } val scope = rememberCoroutineScope() if (showSongActionContainerBottomSheet) { val currentlySelectedSong = viewState.clips.find { it.id == viewState.songIdToOperate } currentlySelectedSong?.let { SongActionsBottomSheet( song = currentlySelectedSong, canRemoveFromPlaylist = false, // Can't remove from profile isShowShareVideoGateEnabled = viewState.isShowShareVideoGateEnabled, songActionBottomSheetConsumerEvents = { event -> when (event) { SongActionBottomSheetConsumerEvent.DismissSongActionsContainerHost -> { scope.launch { songActionsContainerHostState.hide() } } is SongActionBottomSheetConsumerEvent.NavigateToRemix -> { scope.launch { songActionsContainerHostState.hide() } navigateToRemix(event.songId, event.task) } is SongActionBottomSheetConsumerEvent.NavigateToShareVideo -> { navigateToAssetEdit(event.songId) } is SongActionBottomSheetConsumerEvent.OnRemixClicked -> { Unit } is SongActionBottomSheetConsumerEvent.OnReusePromptClicked -> { onReusePromptClicked(event.formData) } is SongActionBottomSheetConsumerEvent.OnShowSnackbar -> { onEvent(ProfileScreenEvent.OnShowSnackbar(event.message)) } is SongActionBottomSheetConsumerEvent.OnSongDeleted -> { onEvent(ProfileScreenEvent.OnSongDeleted(event.song)) } is SongActionBottomSheetConsumerEvent.OnSongRenamed -> { onEvent(ProfileScreenEvent.OnSongRenamed(event.song)) } is SongActionBottomSheetConsumerEvent.OnSongReported -> { scope.launch { snackbarHostState.showSnackbar( message = event.message, ) } } is SongActionBottomSheetConsumerEvent.OnSongUndoDeleted -> { Unit } is SongActionBottomSheetConsumerEvent.OnSongUndoReported -> { Unit } SongActionBottomSheetConsumerEvent.OnUpgradeClicked -> { onUpgradeClicked.invoke() } is SongActionBottomSheetConsumerEvent.OnRemoveFromPlaylistTapped -> TODO() } }, ) } } } @Composable private fun ProfileTopAppBar( nestedScrollConnection: NestedScrollConnection, isTopLevel: Boolean, onBackPressed: () -> Unit, isSelf: Boolean, onSettingsClicked: () -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() .padding( start = 12.dp, top = 8.dp, end = 12.dp, bottom = 12.dp, ) .nestedScroll(nestedScrollConnection), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { if (isTopLevel) { Spacer(modifier = Modifier.size(48.dp)) } else { CircleIconButton( vectorDrawableResId = CommonUIR.drawable.arrow_left, contentDescription = stringResource(CommonResR.string.back), onClick = { onBackPressed.invoke() }, ) } Row( verticalAlignment = Alignment.CenterVertically, ) { if (isSelf) { CircleIconButton( vectorDrawableResId = CommonUIR.drawable.cog, contentDescription = stringResource(CommonResR.string.settings), onClick = { onSettingsClicked.invoke() }, ) } } } } @Composable private fun ProfileHeader( avatarImageUrl: Url?, userName: String, userHandle: String, followerCount: Int?, followingCount: Int?, onFollowersClicked: () -> Unit, onFollowingClicked: () -> Unit, ) { Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { ProfileAvatarImage( modifier = Modifier .padding(vertical = 8.dp) .size(140.dp), avatarUrl = avatarImageUrl, displayName = userName, ) Spacer(modifier = Modifier.width(24.dp)) FlowRow( modifier = Modifier .weight(1f) .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp), maxItemsInEachRow = 2, ) { CounterWithLabel( count = followerCount, label = stringResource(CommonResR.string.followers), onClick = { onFollowersClicked.invoke() }, ) CounterWithLabel( count = followingCount, label = stringResource(CommonResR.string.following), onClick = { onFollowingClicked.invoke() }, ) } } Text( modifier = Modifier.padding(top = 8.dp, bottom = 4.dp), text = userName, style = ExtendedTheme.typography.mediumHeadline, ) Text( text = "@$userHandle", style = ExtendedTheme.typography.body, ) } } @Composable private fun SelfUserActionButtons( onEditProfileClicked: () -> Unit, onShareProfileClicked: () -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() .padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly, ) { Row( modifier = Modifier .weight(1f) .padding(end = 8.dp) .height(56.dp) .fillMaxWidth() .clip(RoundedCornerShape(8.dp)) .background(ExtendedTheme.colors.backgroundTertiary) .clickable { onEditProfileClicked() }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, ) { Icon( modifier = Modifier.padding(horizontal = 4.dp), imageVector = ImageVector.vectorResource( id = CommonUIR.drawable.pencil, ), tint = ExtendedTheme.colors.textPrimary, contentDescription = stringResource(CommonResR.string.edit_profile), ) Text( modifier = Modifier.padding(horizontal = 4.dp), text = stringResource(CommonResR.string.edit_profile), color = ExtendedTheme.colors.textPrimary, style = ExtendedTheme.typography.body, ) } Row( modifier = Modifier .weight(1f) .height(56.dp) .fillMaxWidth() .clip(RoundedCornerShape(8.dp)) .background(ExtendedTheme.colors.backgroundTertiary) .clickable { onShareProfileClicked() }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, ) { Icon( modifier = Modifier.padding(horizontal = 4.dp), imageVector = ImageVector.vectorResource( id = CommonUIR.drawable.share_android, ), tint = ExtendedTheme.colors.textPrimary, contentDescription = stringResource(CommonResR.string.share_profile_title), ) Text( modifier = Modifier.padding(horizontal = 4.dp), text = stringResource(CommonResR.string.share_profile_title), color = ExtendedTheme.colors.textPrimary, style = ExtendedTheme.typography.body, ) } } } @Composable private fun OtherUserActionButtons( isFollowing: Boolean, hasClips: Boolean, onFollowClicked: () -> Unit, onShareProfileClicked: () -> Unit, onPlayClicked: () -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) .height(96.dp), verticalAlignment = Alignment.CenterVertically, ) { val followButtonColorTint = if (isFollowing) { ExtendedTheme.colors.backgroundInvert } else { ExtendedTheme.colors.backgroundTertiary } val followButtonTextTint = if (isFollowing) { ExtendedTheme.colors.backgroundPrimary } else { ExtendedTheme.colors.textPrimary } val followButtonIcon = if (isFollowing) { CommonUIR.drawable.check } else { CommonUIR.drawable.user_plus } val followButtonTextId = if (isFollowing) { CommonResR.string.following } else { CommonResR.string.follow } Row( modifier = Modifier .weight(1f) .padding(vertical = 8.dp) .height(56.dp) .fillMaxWidth() .clip(RoundedCornerShape(8.dp)) .background(followButtonColorTint) .clickable { onFollowClicked() }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, ) { Icon( modifier = Modifier.padding(horizontal = 4.dp), imageVector = ImageVector.vectorResource( id = followButtonIcon, ), tint = followButtonTextTint, contentDescription = stringResource(id = followButtonTextId), ) Text( modifier = Modifier.padding(horizontal = 4.dp), text = stringResource(id = followButtonTextId), color = followButtonTextTint, style = ExtendedTheme.typography.playlistCards, ) } FilledIconButton( modifier = Modifier .padding(vertical = 16.dp, horizontal = 8.dp) .size(56.dp), colors = IconButtonDefaults.iconButtonColors( containerColor = ExtendedTheme.colors.backgroundTertiary, ), onClick = { onShareProfileClicked() }, shape = MaterialTheme.shapes.small, ) { Icon( imageVector = ImageVector.vectorResource( id = CommonUIR.drawable.share_android, ), tint = ExtendedTheme.colors.textPrimary, contentDescription = stringResource(CommonResR.string.share_profile_title), ) } val playButtonBg = if (hasClips) { ExtendedTheme.colors.backgroundInvert } else { ExtendedTheme.colors.textTertiary } FilledIconButton( modifier = Modifier .padding(vertical = 16.dp) .size(56.dp), onClick = { onPlayClicked() }, enabled = hasClips, colors = IconButtonDefaults.iconButtonColors( containerColor = playButtonBg, ), shape = MaterialTheme.shapes.small, ) { Icon( imageVector = ImageVector.vectorResource( id = CommonUIR.drawable.play, ), tint = ExtendedTheme.colors.backgroundPrimary, contentDescription = stringResource(CommonResR.string.play), ) } } } @Composable private fun PublicPlaylistsSection( playlists: List, onPlaylistClicked: (playlistId: String) -> Unit, ) { LazyRow( modifier = Modifier .fillMaxWidth() .height(300.dp), contentPadding = PaddingValues(8.dp), ) { items(playlists) { playlist -> Column( modifier = Modifier .clip(RoundedCornerShape(8.dp)) .clickable { onPlaylistClicked.invoke( playlist.id, ) } .padding(8.dp), ) { Box( modifier = Modifier .size(230.dp) .clip(RoundedCornerShape(8.dp)), ) { AsyncImage( modifier = Modifier.fillMaxSize(), model = playlist.imageUrl ?: playlist.playlistClips.firstOrNull()?.clip?.imageLargeUrl, fallback = painterResource( id = CommonUIR.drawable.ic_launcher_background, ), contentDescription = stringResource( CommonResR.string.profile_picture, ), contentScale = ContentScale.Crop, ) Row( modifier = Modifier .padding(4.dp) .align(Alignment.BottomStart) .background( color = Color(0x55000000), shape = RoundedCornerShape(4.dp), ) .padding(4.dp), verticalAlignment = Alignment.CenterVertically, ) { Icon( modifier = Modifier.size(20.dp), imageVector = ImageVector.vectorResource( CommonUIR.drawable.music, ), contentDescription = stringResource( CommonResR.string.songs_in_playlist, ), tint = Color.White, ) Text( modifier = Modifier.padding(start = 4.dp), text = "${playlist.numTotalResults}", color = Color.White, style = ExtendedTheme.typography.smallBody, ) } } Text( modifier = Modifier.padding(top = 16.dp), text = playlist.name ?: stringResource(CommonResR.string.untitled), color = ExtendedTheme.colors.textPrimary, style = ExtendedTheme.typography.body, ) } } } } @Composable private fun CounterWithLabel( count: Int? = null, label: String? = null, onClick: () -> Unit, ) { Column( modifier = Modifier.clickable { onClick.invoke() }, horizontalAlignment = Alignment.CenterHorizontally, ) { if (count != null) { Text( text = count.xAsFormattedCount(), style = ExtendedTheme.typography.cardMeta.copy(fontSize = 16.sp), ) Text( text = label ?: "", style = ExtendedTheme.typography.body, ) } } } @PreviewLightDark @Composable private fun ProfileScreen_SelfUser_Preview() { SunoPreview { ProfileScreen( viewState = ProfileScreenState( isSelf = true, isLoading = false, userHandle = UserHandle("myuser"), userName = "My Profile", avatarImageUrl = Url("https://cdn1.suno.ai/b1288b3f.webp"), followerCount = 42, followingCount = 123, clips = emptyList(), // No public songs yet playlists = emptyList(), userHooksState = UserHooksState.Loading, ), snackbarHostState = remember { SnackbarHostState() }, songActionsContainerHostState = rememberModalBottomSheetState(), onEvent = {}, isTopLevel = true, showSongActionContainerBottomSheet = false, onShowSongActionContainerBottomSheet = {}, onSettingsClicked = {}, onBackPressed = {}, onUpgradeClicked = {}, onEditProfileClicked = {}, onShareProfileClicked = {}, onPlaylistClicked = {}, onReusePromptClicked = {}, navigateToAssetEdit = {}, navigateToRemix = { _, _ -> }, onFollowersClicked = {}, onFollowingClicked = {}, ) } } @PreviewLightDark @Composable private fun ProfileScreen_PopularUser_Preview() { SunoPreview { ProfileScreen( viewState = ProfileScreenState( isSelf = false, isLoading = false, isFollowing = false, userHandle = UserHandle("felinemusic"), userName = "Feline Music", avatarImageUrl = Url("https://cdn1.suno.ai/b1288b3f.webp"), followerCount = 125000, followingCount = 89, clips = listOf( FakeSongListData.cat, FakeSongListData.cat.copy( id = Id("2"), title = "Meow Symphony", playCount = 1500000, upvoteCount = 25000, tags = "orchestral, cats, symphony", ), FakeSongListData.cat.copy( id = Id("3"), title = "Purr-fect Harmony", playCount = 890000, upvoteCount = 15000, tags = "jazz, smooth, cats", ), FakeSongListData.cat.copy( id = Id("4"), title = "Whiskers Waltz", playCount = 750000, upvoteCount = 12000, tags = "classical, waltz, feline", ), FakeSongListData.cat.copy( id = Id("5"), title = "Tabby's Tango", playCount = 650000, upvoteCount = 9800, tags = "latin, tango, cats", ), ), playlists = emptyList(), userHooksState = UserHooksState.Loading, ), snackbarHostState = remember { SnackbarHostState() }, songActionsContainerHostState = rememberModalBottomSheetState(), onEvent = {}, isTopLevel = true, showSongActionContainerBottomSheet = false, onShowSongActionContainerBottomSheet = {}, onSettingsClicked = {}, onBackPressed = {}, onUpgradeClicked = {}, onEditProfileClicked = {}, onShareProfileClicked = {}, onPlaylistClicked = {}, onReusePromptClicked = {}, navigateToAssetEdit = {}, navigateToRemix = { _, _ -> }, onFollowersClicked = {}, onFollowingClicked = {}, ) } }