package com.suno.android.ui.screens.orpheus.components import androidx.annotation.FloatRange import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember 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.painter.ColorPainter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.crossfade import com.suno.android.common_core_utils.constants.ReactionType import com.suno.android.common_data.mappers.clips.LocalClipData import com.suno.android.common_ui.R import com.suno.android.common_ui.components.loading.SunoSpinner import com.suno.android.common_ui.theme.ExtendedTheme import com.suno.android.common_ui.theme.SunoTheme import com.suno.android.preview.FakeLocalClipData import com.suno.android.ui.screens.orpheus.OrpheusClipAction import com.suno.android.common_res.R as CommonR @Composable @Suppress("MagicNumber") internal fun GeneratedClipCard( clip: LocalClipData, onClipAction: (action: OrpheusClipAction) -> Unit, modifier: Modifier = Modifier, isSelected: Boolean = false, isPlaying: Boolean = false, @FloatRange(from = 0.0, to = 1.0) playbackProgress: Float = 0f, ) { val backgroundColor by animateColorAsState( targetValue = if (isSelected) { ExtendedTheme.colors.backgroundSecondary } else { Color.Transparent }, ) Column( modifier = modifier .clip(RoundedCornerShape(24.dp)) .background(color = backgroundColor) .padding(vertical = 8.dp, horizontal = 8.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { SongThumbnail( clip = clip, isPlaying = isPlaying, isSelected = isSelected, playbackProgress = playbackProgress, onPlayClick = { onClipAction(OrpheusClipAction.OnToggleClipPlayback(clip)) }, onScrubbed = { percent -> onClipAction(OrpheusClipAction.OnScrubClip(clip = clip, scrubPercent = percent)) }, ) Column( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(horizontal = 8.dp), ) { SongInfo( clip = clip, isSelected = isSelected, onSelectClick = { onClipAction(OrpheusClipAction.OnSelectClipClick(clip)) }, ) if (clip.status.isReady) { ActionButtons( clip = clip, onClipAction = onClipAction, ) } } } } @Composable private fun SongThumbnail( clip: LocalClipData, isPlaying: Boolean, isSelected: Boolean, @FloatRange(from = 0.0, to = 1.0) playbackProgress: Float, onPlayClick: () -> Unit, onScrubbed: (Float) -> Unit, ) { val context = LocalContext.current val imageRequest = remember(clip.albumImageUrl?.url) { ImageRequest.Builder(context) .data(clip.albumImageUrl) .memoryCacheKey(clip.albumImageUrl?.url) .crossfade(true) .build() } Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) .background(ExtendedTheme.colors.backgroundSecondary) .clickable( enabled = clip.status.isReady, onClick = onPlayClick, ), contentAlignment = Alignment.Center, ) { AsyncImage( model = imageRequest, contentDescription = null, modifier = Modifier .fillMaxWidth() .aspectRatio(ratio = 1f), placeholder = ColorPainter(ExtendedTheme.colors.backgroundSecondary), error = ColorPainter(ExtendedTheme.colors.backgroundSecondary), contentScale = ContentScale.Crop, ) if (clip.status.isReady) { Box( modifier = Modifier .matchParentSize() .background(Color.Black.copy(alpha = 0.5f)), ) Icon( imageVector = ImageVector.vectorResource( id = if (isPlaying) R.drawable.pause else R.drawable.play, ), contentDescription = stringResource(id = CommonR.string.play), tint = Color.White, modifier = Modifier.size(32.dp), ) AnimatedVisibility( visible = isSelected, modifier = Modifier.align(Alignment.BottomCenter), enter = fadeIn(), exit = fadeOut(), ) { OrpheusClipScrubber( percentageComplete = playbackProgress, onScrubbed = onScrubbed, modifier = Modifier.padding(horizontal = 16.dp), ) } } else { SunoSpinner( modifier = Modifier.size(24.dp), ) } } } @Composable private fun SongInfo( clip: LocalClipData, isSelected: Boolean, onSelectClick: () -> Unit, ) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.clickable( onClick = onSelectClick, indication = null, interactionSource = remember { MutableInteractionSource() }, ), ) { Text( text = clip.nowPlayingTitle?.takeIf { it.isNotEmpty() } ?: stringResource(id = CommonR.string.orpheus_song_untitled), style = ExtendedTheme.typography.smallSubtitle, color = animateColorAsState( if (isSelected) { ExtendedTheme.colors.aukPink } else { ExtendedTheme.colors.textPrimary }, ).value, maxLines = 1, overflow = TextOverflow.Ellipsis, ) clip.tags.takeUnless { it.isNullOrBlank() }?.let { tags -> Text( text = tags, style = ExtendedTheme.typography.smallSubtitle, color = ExtendedTheme.colors.textSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) } } } @Composable private fun ActionButtons( clip: LocalClipData, onClipAction: (action: OrpheusClipAction) -> Unit, ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, ) { ActionIconButton( iconRes = R.drawable.thumbs_up_v2, contentDescription = stringResource(id = CommonR.string.like), onClick = { onClipAction(OrpheusClipAction.OnLikeClipClick(clip)) }, tint = if (clip.reaction == ReactionType.LIKE) { ExtendedTheme.colors.aukPink } else { ExtendedTheme.colors.textPrimary }, ) ActionIconButton( iconRes = R.drawable.thumbs_down_v2, contentDescription = stringResource(id = CommonR.string.dislike), onClick = { onClipAction(OrpheusClipAction.OnDislikeClipClick(clip)) }, tint = if (clip.reaction == ReactionType.DISLIKE) { ExtendedTheme.colors.aukPink } else { ExtendedTheme.colors.textPrimary }, ) ActionIconButton( iconRes = R.drawable.share_arrow, contentDescription = stringResource(id = CommonR.string.share), onClick = { onClipAction(OrpheusClipAction.OnShareClipClick(clip)) }, tint = ExtendedTheme.colors.textPrimary, ) ActionIconButton( iconRes = R.drawable.ellipsis_horizontal, contentDescription = stringResource(id = CommonR.string.options), onClick = { onClipAction(OrpheusClipAction.OnClipOverflowClick(clip)) }, tint = ExtendedTheme.colors.textPrimary, ) } } @Composable private fun ActionIconButton( iconRes: Int, contentDescription: String, onClick: () -> Unit, tint: Color, ) { IconButton( onClick = onClick, modifier = Modifier.size(32.dp), ) { Icon( imageVector = ImageVector.vectorResource(id = iconRes), contentDescription = contentDescription, tint = tint, modifier = Modifier.size(20.dp), ) } } @PreviewLightDark @Composable private fun GeneratedClipCardPreview() { SunoTheme { Column( modifier = Modifier .background(ExtendedTheme.colors.backgroundPrimary) .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { GeneratedClipCard( clip = FakeLocalClipData.clip, onClipAction = {}, isSelected = false, isPlaying = false, playbackProgress = 0f, modifier = Modifier.fillMaxWidth(0.75f), ) GeneratedClipCard( clip = FakeLocalClipData.clip, onClipAction = {}, isSelected = true, isPlaying = true, playbackProgress = 0.35f, modifier = Modifier.fillMaxWidth(0.75f), ) } } }