package com.suno.android.ui.screens.songdetails import android.Manifest import android.content.pm.PackageManager import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row 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.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.draw.rotate import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.painter.ColorPainter 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.unit.dp import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.suno.android.common_ui.components.loading.SunoSpinner import com.suno.android.common_ui.theme.ExtendedTheme import java.io.File import com.suno.android.common_res.R as CommonResR import com.suno.android.common_ui.R as CommonUiR @Composable fun SongDetailsSongArtScreen( viewModel: SongDetailsViewModel, isInPostingMode: Boolean, ) { val state by viewModel.state.collectAsStateWithLifecycle() val context = LocalContext.current var currentPhotoUri by remember { mutableStateOf(null) } var showPromptField by remember { mutableStateOf(false) } var imageGenPrompt by remember { mutableStateOf("") } DisposableEffect(Unit) { onDispose { currentPhotoUri?.let { uri -> runCatching { context.contentResolver.delete(uri, null, null) }.onFailure { viewModel.logError(it) } } } } val createPhotoUri: () -> Uri? = remember(context) { { try { val tempFile = File.createTempFile("temp_song_image", ".jpg", context.cacheDir) tempFile.apply { deleteOnExit() } FileProvider.getUriForFile( context, "${context.packageName}.fileprovider", tempFile, ) } catch (e: Exception) { null } } } val takePictureLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.TakePicture(), ) { success -> if (success) { currentPhotoUri?.let { uri -> viewModel.sendEvent( SongDetailsMviEvent.EditSongArtUpload( isInPostingMode, uri, ), ) } } } val launchCamera = { val uri = createPhotoUri() if (uri != null) { currentPhotoUri = uri takePictureLauncher.launch(uri) } } val permissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission(), ) { isGranted -> if (isGranted) { launchCamera() } else { viewModel.sendEvent(SongDetailsMviEvent.ShowPermissionRationaleDialog(isInPostingMode)) } } val imagePickerLauncher = rememberLauncherForActivityResult( ActivityResultContracts.GetContent(), ) { uri -> uri?.let { uri -> viewModel.sendEvent( SongDetailsMviEvent.EditSongArtUpload( isInPostingMode, uri, ), ) } } Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { AsyncImage( model = state.songImageUrl, contentDescription = stringResource(CommonResR.string.song_cover), modifier = Modifier .height(272.dp) .width(184.dp) .clip(RoundedCornerShape(10.dp)), placeholder = ColorPainter(ExtendedTheme.colors.backgroundSecondary), error = ColorPainter(ExtendedTheme.colors.backgroundSecondary), contentScale = ContentScale.Crop, ) Text( modifier = Modifier.padding(vertical = 12.dp), text = state.songTitle, ) HorizontalDivider(color = ExtendedTheme.colors.backgroundTertiary) val iconRotation by animateFloatAsState( targetValue = if (showPromptField) 90f else 0f, animationSpec = tween(durationMillis = 300), ) SongDetailsOptionRow( text = stringResource(CommonResR.string.reprompt), iconRes = CommonUiR.drawable.remaster, onClick = { showPromptField = showPromptField.not() }, rightContent = { Icon( painterResource(CommonUiR.drawable.chevron_right), contentDescription = null, tint = ExtendedTheme.colors.textPrimary, modifier = Modifier .size(24.dp) .rotate(iconRotation), ) }, ) AnimatedVisibility( visible = showPromptField, ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { BasicTextField( modifier = Modifier .padding(start = 16.dp, top = 12.dp, bottom = 12.dp) .weight(1f) .height(44.dp) .background( color = ExtendedTheme.colors.backgroundTertiary, shape = RoundedCornerShape(8.dp), ), value = imageGenPrompt, onValueChange = { imageGenPrompt = it }, textStyle = ExtendedTheme.typography.body.copy(color = ExtendedTheme.colors.textPrimary), cursorBrush = SolidColor(ExtendedTheme.colors.textPrimary), maxLines = 1, singleLine = true, enabled = state.isImageGenInProgress.not(), decorationBox = @Composable { innerTextField -> Row( modifier = Modifier .fillMaxSize() .padding(start = 8.dp) .background( color = ExtendedTheme.colors.backgroundTertiary, shape = RoundedCornerShape(8.dp), ), verticalAlignment = Alignment.CenterVertically, ) { Row( modifier = Modifier .weight(1f) .padding(start = 4.dp), ) { innerTextField() } } }, ) Button( modifier = Modifier.height(44.dp), enabled = state.isImageGenInProgress.not(), colors = ButtonDefaults.buttonColors( containerColor = ExtendedTheme.colors.textPrimary, contentColor = ExtendedTheme.colors.backgroundPrimary, ), onClick = { viewModel.sendEvent( SongDetailsMviEvent.EditSongArtPrompt( isInPostingMode, imageGenPrompt, ), ) }, ) { if (state.isImageGenInProgress) { SunoSpinner( modifier = Modifier.size(24.dp), ) } else { Text( text = stringResource(CommonResR.string.generate), style = ExtendedTheme.typography.button, ) } } } } HorizontalDivider(color = ExtendedTheme.colors.backgroundTertiary) SongDetailsOptionRow( text = stringResource(CommonResR.string.upload_image), iconRes = CommonUiR.drawable.images, onClick = { imagePickerLauncher.launch("image/*") }, enabled = state.isImageGenInProgress.not(), ) HorizontalDivider(color = ExtendedTheme.colors.backgroundTertiary) SongDetailsOptionRow( text = stringResource(CommonResR.string.take_a_photo), iconRes = CommonUiR.drawable.camera, onClick = { if (ContextCompat.checkSelfPermission( context, Manifest.permission.CAMERA, ) == PackageManager.PERMISSION_GRANTED ) { launchCamera() } else { permissionLauncher.launch(Manifest.permission.CAMERA) } }, enabled = state.isImageGenInProgress.not(), ) HorizontalDivider(color = ExtendedTheme.colors.backgroundTertiary) } }