package com.suno.android.ui.screens.create.text import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.suno.android.common_data.mappers.projects.ProjectMetadata import com.suno.android.common_ui.R import com.suno.android.common_ui.components.dialogs.AukDialogScaffold import com.suno.android.common_ui.components.dialogs.NoCreditsRemainingDialogScaffold import com.suno.android.common_ui.components.dialogs.V45PlusDialogContent import com.suno.android.common_ui.components.dialogs.V5DialogContent import com.suno.android.common_ui.extensions.plus import com.suno.android.common_ui.models.SeekInteraction import com.suno.android.common_ui.theme.AukPink import com.suno.android.common_ui.theme.ExtendedTheme import com.suno.android.common_ui.theme.SunoTheme import com.suno.android.ui.bottom_sheets.create.createBottomButtons import com.suno.android.ui.screens.create.CreatePromptForm import com.suno.android.ui.screens.create.audio.CreateAudioEvent import com.suno.android.ui.screens.create.audio.recording.AudioCreateBottomSheet import kotlinx.serialization.json.Json import kotlin.time.Duration import com.suno.android.common_res.R as CommonResR @Composable fun CreateTextScreen( vm: CreateTextScreenVM? = hiltViewModel(), modifier: Modifier = Modifier, starterPrompt: String? = null, onGenerationResponse: ((Boolean) -> Unit)? = null, onUpsellClicked: (() -> Unit)? = null, onCreateSubmissionComplete: (() -> Unit)? = null, selectedProject: ProjectMetadata? = null, showBottomBar: Boolean = true, onCreateClick: () -> Unit = {}, onPlusClick: () -> Unit = {}, isFromOrpheus: Boolean = false, ) { LaunchedEffect(Unit) { vm?.effects?.collect { effect -> when (effect) { is CreateTextEffect.TextGenCreationSuccess -> { onGenerationResponse?.invoke(effect.isSuccessful) } is CreateTextEffect.RedirectToBilling -> { onUpsellClicked?.invoke() } is CreateTextEffect.CreateSubmissionComplete -> { onCreateSubmissionComplete?.invoke() } // tracking no-ops // todo: tomdroid to create effect tracking function for MviViewModel is CreateTextEffect.TrackWeirdnessChange -> {} is CreateTextEffect.TrackStyleInfluenceChange -> {} is CreateTextEffect.TrackAudioInfluenceChange -> {} } } } LaunchedEffect(Unit) { starterPrompt?.let { runCatching { Json.decodeFromString(starterPrompt) }.getOrNull()?.let { form -> vm?.sendEvent(CreateTextEvent.OnHandleStarterPrompt(form)) } } } LaunchedEffect(selectedProject) { selectedProject?.let { vm?.setProject(it) } } val viewState = vm?.state?.collectAsStateWithLifecycle()?.value val keyboardController = LocalSoftwareKeyboardController.current LaunchedEffect(Unit) { // Automatically show keyboard when the screen loads keyboardController?.show() } // todo: tomdroid look into char limits that arent hardcoded val bgColor = if (viewState?.isWorkspacesGateEnabled == true || isFromOrpheus) { ExtendedTheme.colors.backgroundSecondary } else { ExtendedTheme.colors.backgroundPrimary } Scaffold( containerColor = bgColor, contentWindowInsets = if (showBottomBar) { WindowInsets.safeDrawing.add(WindowInsets.createBottomButtons) } else { WindowInsets.safeDrawing }, topBar = { Surface( color = ExtendedTheme.colors.backgroundSecondary, ) { CreateTextTopBar( toggleState = viewState?.isCustom == true, onCustomToggled = { isToggled -> vm?.sendEvent(CreateTextEvent.OnCustomModeToggled(isCustomMode = isToggled)) }, creditCount = viewState?.creditCount, onCreditClick = { onUpsellClicked?.invoke() }, activeModel = viewState?.activeModel, availableModels = viewState?.availableModels ?: emptyList(), onModelNameSelected = { selectedModel -> vm?.sendEvent( CreateTextEvent.OnAttemptToSelectModel(selectedModelName = selectedModel), ) }, modifier = Modifier .windowInsetsPadding( WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), ) .clipToBounds(), backgroundColor = bgColor, isFromOrpheus = isFromOrpheus, onCreateClick = onCreateClick, isCreateButtonEnabled = viewState?.isCreateButtonEnabled == true, onPlusClick = onPlusClick, ) } }, bottomBar = { if (showBottomBar) { BottomBar( viewState = viewState, vm = vm, modifier = Modifier.windowInsetsPadding( WindowInsets.systemBars.only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal), ), ) } }, modifier = modifier, ) { contentPadding -> viewState?.let { val seekInteraction = remember(vm) { object : SeekInteraction { override fun onSeekStarted() { vm.sendEvent(CreateTextEvent.AudioEvent(CreateAudioEvent.OnSeekStart)) } override fun onSeekPositionChanged( newSeekPosition: Duration, ) { vm.sendEvent( CreateTextEvent.AudioEvent( CreateAudioEvent.OnSeekUpdate( newSeekPosition, ), ), ) } override fun onSeekStopped() { vm.sendEvent(CreateTextEvent.AudioEvent(CreateAudioEvent.OnSeekStop)) } } } if (viewState.isCustom) { CreateTextCustomMode( modifier = Modifier.padding(horizontal = 16.dp), viewState = viewState, sendEvent = vm::sendEvent, audioEffects = vm.audioEffects, seekInteraction = seekInteraction, contentPadding = contentPadding + PaddingValues(top = 16.dp), ) } else { CreateTextSimpleMode( contentPadding = contentPadding + PaddingValues(top = 16.dp), viewState = viewState, sendEvent = vm::sendEvent, audioEffects = vm.audioEffects, seekInteraction = seekInteraction, modifier = Modifier.padding(horizontal = 16.dp), ) } } } if (viewState?.showingNoCreditsRemainingDialog == true) { Dialog( onDismissRequest = { vm.sendEvent(CreateTextEvent.OnNoCreditsRemainingDialogDismissed) }, ) { NoCreditsRemainingDialogScaffold( onCloseClick = { vm.sendEvent(CreateTextEvent.OnNoCreditsRemainingDialogDismissed) }, onUpgradeClick = { vm.sendEvent(CreateTextEvent.OnNoCreditsRemainingDialogUpgradeClicked) }, ) } } if (viewState?.showAukUpsellDialog == true) { Dialog( onDismissRequest = { vm.sendEvent(CreateTextEvent.OnUpsellDialogDismissed) }, ) { AukDialogScaffold( leftActionTitle = stringResource(CommonResR.string.close), rightActionTitle = stringResource(CommonResR.string.upgrade), onLeftActionClick = { vm.sendEvent(CreateTextEvent.OnUpsellDialogDismissed) }, onRightActionClick = { vm.sendEvent(CreateTextEvent.OnUpsellDialogDismissed) onUpsellClicked?.invoke() }, topDisplayContent = { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), ) { Text( modifier = Modifier .background( color = AukPink, shape = RoundedCornerShape(16.dp), ) .padding(vertical = 4.dp, horizontal = 6.dp), text = stringResource(CommonResR.string.NEW_MODEL), style = ExtendedTheme.typography.smallBody, color = Color.Black, ) Text( modifier = Modifier.width(300.dp), text = stringResource(CommonResR.string.v45_upsell_dialog_header), style = ExtendedTheme.typography.mediumHeadline, color = Color.White, textAlign = TextAlign.Center, ) Text( modifier = Modifier.width(250.dp), text = stringResource(CommonResR.string.v45_upsell_dialog_body), style = ExtendedTheme.typography.smallBody.copy(fontSize = 12.sp), color = Color.White, textAlign = TextAlign.Center, ) } }, ) } } if (viewState?.showV5UpsellDialog == true) { Dialog( onDismissRequest = { vm.sendEvent(CreateTextEvent.OnUpsellDialogDismissed) }, ) { V5DialogContent( ctaText = stringResource(CommonResR.string.v5_dialog_upgrade_cta), onLeftActionClick = { onUpsellClicked?.invoke() vm.sendEvent(CreateTextEvent.OnUpsellDialogDismissed) }, onDismiss = { vm.sendEvent(CreateTextEvent.OnUpsellDialogDismissed) }, ) } } if (viewState?.showV45PlusUpsellDialog == true) { Dialog( onDismissRequest = { vm.sendEvent(CreateTextEvent.OnUpsellDialogDismissed) }, ) { V45PlusDialogContent( ctaText = stringResource(CommonResR.string.upgrade_now), onLeftActionClick = { onUpsellClicked?.invoke() vm.sendEvent(CreateTextEvent.OnUpsellDialogDismissed) }, onDismiss = { vm.sendEvent(CreateTextEvent.OnUpsellDialogDismissed) }, ) } } if (viewState != null) { AudioCreateBottomSheet( state = viewState.bottomSheetState, onDismiss = { vm.sendEvent(CreateTextEvent.DismissBottomSheet) }, ) } } @Composable fun InstrumentalToggleButton( modifier: Modifier = Modifier, label: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, ) { val backgroundColor by animateColorAsState( if (isChecked) ExtendedTheme.colors.backgroundInvert else Color.Transparent, ) val borderColor by animateColorAsState( if (isChecked) Color.Transparent else ExtendedTheme.colors.textPrimary.copy(alpha = 0.1f), ) val iconTintColor by animateColorAsState( if (isChecked) ExtendedTheme.colors.aukPink else ExtendedTheme.colors.backgroundQuarternary, ) val textColor by animateColorAsState( if (isChecked) ExtendedTheme.colors.backgroundPrimary else ExtendedTheme.colors.textPrimary, ) Row( modifier = modifier .height(40.dp) .border( width = if (isChecked) 0.dp else 1.dp, color = borderColor, shape = RoundedCornerShape(25.dp), ) .background( color = backgroundColor, shape = RoundedCornerShape(25.dp), ) .clip(RoundedCornerShape(25.dp)) .clickable { onCheckedChange.invoke(!isChecked) } .padding(horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { Icon( imageVector = ImageVector.vectorResource( id = R.drawable.circle_check, ), contentDescription = "Instrumentals only", tint = iconTintColor, ) Text( modifier = Modifier.padding(horizontal = 4.dp), text = label, style = TextStyle(fontSize = 16.sp), color = textColor, ) } } @Preview(showSystemUi = true) @Composable private fun CreateTextScreen_Preview() { SunoTheme { CreateTextScreen( vm = null, starterPrompt = null, ) } }