package com.suno.android.ui.screens.root.logged_in import android.content.Intent import android.widget.Toast import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState 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.graphics.Color 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.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavOptions import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import com.google.android.play.core.review.ReviewManagerFactory import com.suno.android.common_core_utils.Id import com.suno.android.common_core_utils.extensions.findActivity import com.suno.android.common_core_utils.model.UserHandle import com.suno.android.common_ui.R import com.suno.android.common_ui.animations.NavigationFadeInAnimation import com.suno.android.common_ui.animations.NavigationFadeOutAnimation import com.suno.android.common_ui.components.animation.ExitAwareAnimatedVisibility import com.suno.android.common_ui.components.banner.ExitDirection import com.suno.android.common_ui.components.banner.HookDownloadBanner import com.suno.android.common_ui.components.banner.SongGenerationBanner import com.suno.android.common_ui.components.dialogs.InAppReviewDialog import com.suno.android.common_ui.theme.ExtendedTheme import com.suno.android.common_ui.theme.SunoTheme import com.suno.android.ui.screens.create.CreateNavGraph import com.suno.android.ui.screens.create.CreatePromptForm import com.suno.android.ui.screens.create.audio.CreateAudioState import com.suno.android.ui.screens.home.HomeScreenNavGraph import com.suno.android.ui.screens.home.HomeScreenNavGraphVM import com.suno.android.ui.screens.home.library.search.LibrarySearchScreen import com.suno.android.ui.screens.home.mediaplayer.HomeBottomMediaPlayerLayout import com.suno.android.ui.screens.home.profile.edit.EditProfileScreen import com.suno.android.ui.screens.home.profile.settings.SettingsNavGraph import com.suno.android.ui.screens.home.profile.settings.billing.BillingNavGraph import com.suno.android.ui.screens.home.workspaces.WorkspacesScreen import com.suno.android.ui.screens.hooks.create.editor.camera.HooksCreateScreen import com.suno.android.ui.screens.hooks.feed.HooksFeedNavigationRequest import com.suno.android.ui.screens.hooks.feed.HooksFeedSource import com.suno.android.ui.screens.hooks.player.HooksPlayerScreen import com.suno.android.ui.screens.navigation.NavDestination import com.suno.android.ui.screens.omni.OmniScreen import com.suno.android.ui.screens.onboarding.OnboardingNavGraph import com.suno.android.ui.screens.orpheus.OrpheusChatScreen import com.suno.android.ui.screens.root.logged_in.LoggedInNavGraphVM.UiState.HookDownloadState import com.suno.android.ui.screens.share_asset.ShareAssetEditorScreen import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import com.suno.android.common_res.R as CommonResR @Composable fun LoggedInNavGraph( loggedInNavGraphVM: LoggedInNavGraphVM = hiltViewModel(), isOnboarding: Boolean, homeScreenNavGraphVM: HomeScreenNavGraphVM = hiltViewModel(), ) { val context = LocalContext.current val loggedInNavController = rememberNavController() LaunchedEffect(loggedInNavController) { loggedInNavController.addOnDestinationChangedListener { controller, destination, arguments -> loggedInNavGraphVM.trackScreen( screenName = destination.route, ) } } val viewState by loggedInNavGraphVM.viewStateFlow().collectAsState() fun navigateToProfile( artistHandle: UserHandle, ) { loggedInNavController.navigate( NavDestination.Home.HomeScreenDestination, ) homeScreenNavGraphVM.deepLinkToProfileByHandle( handle = artistHandle, ) } SunoTheme(themeMode = viewState.themeMode) { var showInAppReview by remember { mutableStateOf(false) } val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(Unit) { loggedInNavGraphVM.effectFlow().collect { loggedInEffect -> when (loggedInEffect) { LoggedInNavGraphVM.LoggedInEffects.OnRouteToOnboarding -> {} LoggedInNavGraphVM.LoggedInEffects.OnRouteToD0Create -> { loggedInNavController.navigate( route = NavDestination.Home.HomeScreenDestination, navOptions = NavOptions.Builder() .setLaunchSingleTop(true) .setPopUpTo( destinationId = 0, inclusive = true, ) .build(), ) /** * note from tomdroid: it seems that jetpack compose's navigation library doesn't support * building the backstack for deeplinks, which is frustrating... * This is the "easiest" workaround. However, we have upcoming requirements * for deeplinking into nested destinations. Might be wise to build our own custom thing? */ loggedInNavController.navigate( route = NavDestination.Create.CreateNavGraphDestination( createPromptFormJson = null, ), ) } LoggedInNavGraphVM.LoggedInEffects.OnRouteToExplore -> { loggedInNavController.navigate( route = NavDestination.Home.HomeScreenDestination, navOptions = NavOptions.Builder() .setLaunchSingleTop(true) .setPopUpTo( destinationId = 0, inclusive = true, ) .build(), ) } is LoggedInNavGraphVM.LoggedInEffects.OpenVideoInGallery -> { try { val intent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(loggedInEffect.videoUri, "video/mp4") flags = Intent.FLAG_GRANT_READ_URI_PERMISSION } context.startActivity(intent) } catch (_: Exception) { Toast.makeText( context, context.getString(CommonResR.string.failed_to_open_gallery), Toast.LENGTH_SHORT, ).show() } } } } } val context = LocalContext.current fun handleRemixNavigation( clipId: String, task: CreateAudioState.AudioCreateTask, ) { val createPromptForm = CreatePromptForm( remixClipId = clipId, task = task, ) loggedInNavController.navigate( route = NavDestination.Create.CreateNavGraphDestination( Json.encodeToString( createPromptForm, ), ), ) } Box { NavHost( modifier = Modifier.fillMaxSize(), navController = loggedInNavController, startDestination = if (isOnboarding) { NavDestination.Onboarding.OnboardingNavGraphDestination } else { NavDestination.Home.HomeScreenDestination }, enterTransition = { NavigationFadeInAnimation }, exitTransition = { NavigationFadeOutAnimation }, ) { composable { OnboardingNavGraph( onboardingComplete = { loggedInNavGraphVM.handleOnboardingComplete() }, ) } composable { HomeScreenNavGraph( vm = homeScreenNavGraphVM, loggedInNavHostController = loggedInNavController, isOrpheusChatEnabled = viewState.isOrpheusChatEnabled, onRedirectToCreate = { remixClipId, task -> if (remixClipId != null && task != null) { handleRemixNavigation(clipId = remixClipId, task = task) } else { loggedInNavController.navigate( route = NavDestination.Create.CreateNavGraphDestination( createPromptFormJson = null, ), ) } }, onRedirectToBilling = { loggedInNavController.navigate( route = NavDestination.Billing.BillingNavGraphDestination, ) }, onRedirectToPlayer = { loggedInNavController.navigate( NavDestination.LoggedInNav.OmniScreenDestination(), ) }, onReusePrompt = { createPromptForm: CreatePromptForm -> val jsonString = Json.encodeToString(createPromptForm) loggedInNavController.navigate( route = NavDestination.Create.CreateNavGraphDestination( createPromptFormJson = jsonString, ), ) }, ) } composable { args -> val route = args.toRoute() val promptFormJson = route.createPromptFormJson val form = runCatching { Json.decodeFromString(promptFormJson!!) }.getOrNull() WorkspacesScreen( starterPrompt = form, onUpgradeClicked = { loggedInNavController.navigate( route = NavDestination.Billing.BillingNavGraphDestination, ) }, onBackClicked = { loggedInNavController.popBackStack() }, onSearchClicked = { loggedInNavController.navigate( route = NavDestination.Create.WorkspacesSearchScreenDestination, ) }, onBottomMediaPlayerClicked = { loggedInNavController.navigate( route = NavDestination.LoggedInNav.OmniScreenDestination(), ) }, ) } composable { HomeBottomMediaPlayerLayout( onCardSurfaceClick = { loggedInNavController.navigate( NavDestination.LoggedInNav.OmniScreenDestination(), ) }, ) { LibrarySearchScreen( onUpgradeClicked = { loggedInNavController.navigate( route = NavDestination.Billing.BillingNavGraphDestination, ) }, onReusePromptClicked = {}, onBackPressed = { loggedInNavController.popBackStack() }, navigateToAssetEdit = { loggedInNavController.navigate( route = NavDestination.LoggedInNav.ShareAssetEditorDestination(it.value), ) }, navigateToRemix = { clipId, task -> handleRemixNavigation(clipId = clipId.value, task = task) }, ) } } composable { args -> val route = args.toRoute() val promptFormJson = route.createPromptFormJson LaunchedEffect(viewState.isCreateBottomSheetGateEnabled) { if (viewState.isCreateBottomSheetGateEnabled) { loggedInNavController.navigate( NavDestination.Create.WorkspacesScreenDestination(promptFormJson), ) { popUpTo(route) { inclusive = true } } } } if (viewState.isCreateBottomSheetGateEnabled) { // early return if we're redirecting anyway to avoid creating VMs and other overhead return@composable } CreateNavGraph( rootNavHostController = loggedInNavController, onSongGenJobStarted = { loggedInNavController.navigateUp() homeScreenNavGraphVM.onBottomBarDestinationClick( NavDestination.Home.LibraryScreenDestination, ) }, starterPrompt = promptFormJson, ) } composable { SettingsNavGraph( rootNavHostController = loggedInNavController, vm = hiltViewModel(), ) } composable { EditProfileScreen( vm = hiltViewModel(), onBackPressed = { loggedInNavController.popBackStack() }, ) } composable { BillingNavGraph( vm = hiltViewModel(), onBackClicked = { loggedInNavController.popBackStack() }, ) } composable { route -> OmniScreen( vm = hiltViewModel(), rootNavHostController = loggedInNavController, onReusePromptClicked = { promptForm -> loggedInNavController.navigate( NavDestination.Create.CreateNavGraphDestination( createPromptFormJson = Json.encodeToString(promptForm), ), ) }, onArtistHandleClicked = { artistHandle -> navigateToProfile(artistHandle) }, onUpgradeClicked = { loggedInNavController.navigate( route = NavDestination.Billing.BillingNavGraphDestination, ) }, navigateToAssetEdit = { loggedInNavController.navigate( route = NavDestination.LoggedInNav.ShareAssetEditorDestination( songId = it.value, ), ) }, showCommentsBottomSheetOnNavigate = route.toRoute().showCommentBottomSheet == true, navigateToRemix = { clipId, task -> handleRemixNavigation(clipId = clipId.value, task = task) }, ) } composable { ShareAssetEditorScreen( vm = hiltViewModel(), navigateBack = { loggedInNavController.popBackStack() }, ) } composable { HooksCreateScreen( vm = hiltViewModel(), navigateBack = { loggedInNavController.popBackStack() }, ) } composable { route -> val route = route.toRoute() HooksPlayerScreen( source = HooksFeedSource.Single(Id(route.hookId)), startIndex = 0, onNavigationRequest = { request -> when (request) { is HooksFeedNavigationRequest.AssetEdit -> { loggedInNavController.navigate( route = NavDestination.LoggedInNav.ShareAssetEditorDestination( songId = request.clipId.value, ), ) } is HooksFeedNavigationRequest.CreatorClick -> { navigateToProfile(request.creatorHandle) } is HooksFeedNavigationRequest.Remix -> { handleRemixNavigation(clipId = request.songId.value, task = request.task) } is HooksFeedNavigationRequest.ReusePrompt -> { loggedInNavController.navigate( route = NavDestination.Create.CreateNavGraphDestination( createPromptFormJson = Json.encodeToString(request.promptForm), ), ) } } }, showCommentsBottomSheetOnNavigate = route.showCommentBottomSheet, ) } composable { route -> val route = route.toRoute() HooksPlayerScreen( source = HooksFeedSource.User(route.userHandle.let(::UserHandle)), startIndex = route.startIndex, onNavigationRequest = { request -> when (request) { is HooksFeedNavigationRequest.AssetEdit -> { loggedInNavController.navigate( route = NavDestination.LoggedInNav.ShareAssetEditorDestination( songId = request.clipId.value, ), ) } is HooksFeedNavigationRequest.CreatorClick -> { if (request.creatorHandle.handle == route.userHandle) { loggedInNavController.popBackStack() } else { navigateToProfile(request.creatorHandle) } } is HooksFeedNavigationRequest.Remix -> { handleRemixNavigation(clipId = request.songId.value, task = request.task) } is HooksFeedNavigationRequest.ReusePrompt -> { loggedInNavController.navigate( route = NavDestination.Create.CreateNavGraphDestination( createPromptFormJson = Json.encodeToString(request.promptForm), ), ) } } }, ) } composable { OrpheusChatScreen( onBackClick = { loggedInNavController.popBackStack() }, onUpgradeClicked = { loggedInNavController.navigate( route = NavDestination.Billing.BillingNavGraphDestination, ) }, onReusePromptClicked = { promptForm -> loggedInNavController.navigate( route = NavDestination.Create.CreateNavGraphDestination( createPromptFormJson = Json.encodeToString(promptForm), ), ) }, navigateToAssetEdit = { songId -> loggedInNavController.navigate( route = NavDestination.LoggedInNav.ShareAssetEditorDestination( songId = songId.value, ), ) }, navigateToRemix = { clipId, task -> handleRemixNavigation(clipId = clipId.value, task = task) }, ) } } SnackbarHost( modifier = Modifier .align(Alignment.TopCenter) .statusBarsPadding() .padding( start = 4.dp, end = 4.dp, top = 4.dp, ), hostState = snackbarHostState, ) { Box( modifier = Modifier .height(60.dp) .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) .clickable { loggedInNavGraphVM.handleNewSongDialogClicked() loggedInNavController.navigate( route = NavDestination.LoggedInNav.OmniScreenDestination(), ) snackbarHostState.currentSnackbarData?.dismiss() }, ) { Image( modifier = Modifier .background(color = Color.Red) .fillMaxSize(), painter = painterResource(id = R.drawable.song_reveal_snackbar_bg), contentScale = ContentScale.FillBounds, contentDescription = "Alert", ) Box( modifier = Modifier .padding(4.dp) .fillMaxSize() .clip(RoundedCornerShape(16.dp)), contentAlignment = Alignment.Center, ) { Text( text = viewState.alertDialogMessageResId?.let { stringResource(it) }.orEmpty(), color = Color.White, style = ExtendedTheme.typography.body, ) } } } if (viewState.appReviewDialogActive) { Dialog( onDismissRequest = { loggedInNavGraphVM.dismissAppReviewDialog() }, ) { InAppReviewDialog( leftActionTitle = stringResource(CommonResR.string.meh), rightActionTitle = stringResource(CommonResR.string.yes_heart), onLeftActionClick = { loggedInNavGraphVM.dismissAppReviewDialog() }, onRightActionClick = { loggedInNavGraphVM.dismissAppReviewDialog() showInAppReview = true }, topDisplayContent = { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { Text( modifier = Modifier.width(250.dp), text = stringResource(CommonResR.string.is_suno_rocking_world), style = ExtendedTheme.typography.largeHeadline, color = Color.White, textAlign = TextAlign.Center, ) Text( modifier = Modifier .padding(top = 20.dp) .width(180.dp), text = stringResource(CommonResR.string.using_app_a_lot), style = ExtendedTheme.typography.smallBody, color = Color.White, textAlign = TextAlign.Center, ) } }, ) } } if (showInAppReview) { val manager = ReviewManagerFactory.create(context) val request = manager.requestReviewFlow() request.addOnCompleteListener { task -> runCatching { if (task.isSuccessful) { val reviewInfo = task.result val activity = context.findActivity() activity?.let { theActivity -> val flow = manager.launchReviewFlow(theActivity, reviewInfo) flow.addOnCompleteListener { _ -> showInAppReview = false } } } }.onFailure { loggedInNavGraphVM.logError(it) } } } val hasReadySongs = viewState.newlyGeneratedSongData.isNotEmpty() var songGenerationExitDirection by remember { mutableStateOf(null) } ExitAwareAnimatedVisibility( visible = viewState.showGenerationBanner, enter = fadeIn() + slideInVertically(), exit = when (songGenerationExitDirection) { ExitDirection.Up -> fadeOut() + slideOutVertically(targetOffsetY = { -it }) ExitDirection.Left -> fadeOut() + slideOutHorizontally(targetOffsetX = { -it }) ExitDirection.Right -> fadeOut() + slideOutHorizontally(targetOffsetX = { it }) else -> fadeOut() + slideOutVertically() }, onFullyExited = { loggedInNavGraphVM.resetSongGenerationBannerState() songGenerationExitDirection = null }, ) { SongGenerationBanner( modifier = Modifier .align(Alignment.TopCenter) .fillMaxWidth() .statusBarsPadding() .padding(horizontal = 16.dp, vertical = 8.dp), songGenerationReady = hasReadySongs, generationPreviewUrls = viewState.newlyGeneratedSongData.mapNotNull { it.albumImageUrl }, onClick = { if (hasReadySongs) { loggedInNavGraphVM.handleNewSongDialogClicked() loggedInNavController.navigate( route = NavDestination.LoggedInNav.OmniScreenDestination(), ) } else { loggedInNavController.navigate( route = NavDestination.Create.CreateNavGraphDestination( createPromptFormJson = null, ), ) } loggedInNavGraphVM.dismissSongGenerationBanner() }, onPlayButtonClick = { loggedInNavGraphVM.handleNewSongDialogClicked() loggedInNavGraphVM.dismissSongGenerationBanner() }, onDismiss = { direction -> loggedInNavGraphVM.dismissSongGenerationBanner() songGenerationExitDirection = direction }, ) } var hookDownloadExitDirection by remember { mutableStateOf(null) } val hookPreviewUrl = when (val state = viewState.hookDownloadState) { is HookDownloadState.Complete -> state.hookPreviewUrl is HookDownloadState.Downloading, HookDownloadState.Idle, -> null } ExitAwareAnimatedVisibility( visible = viewState.hookDownloadState !is HookDownloadState.Idle && !viewState.isHookDownloadBannerDismissed, enter = fadeIn() + slideInVertically(), exit = when (hookDownloadExitDirection) { ExitDirection.Up -> fadeOut() + slideOutVertically(targetOffsetY = { -it }) ExitDirection.Left -> fadeOut() + slideOutHorizontally(targetOffsetX = { -it }) ExitDirection.Right -> fadeOut() + slideOutHorizontally(targetOffsetX = { it }) else -> fadeOut() + slideOutVertically() }, onFullyExited = { loggedInNavGraphVM.resetHookDownloadBannerState() hookDownloadExitDirection = null }, ) { HookDownloadBanner( onClick = { val state = viewState.hookDownloadState if (state is HookDownloadState.Complete && state.videoUri != null) { loggedInNavGraphVM.openDownloadedHookInGallery(state.videoUri) } loggedInNavGraphVM.dismissHookDownloadBanner() }, onDismiss = { direction -> loggedInNavGraphVM.dismissHookDownloadBanner() hookDownloadExitDirection = direction }, modifier = Modifier .align(Alignment.TopCenter) .fillMaxWidth() .statusBarsPadding() .padding(horizontal = 16.dp, vertical = 8.dp), isDownloadComplete = viewState.hookDownloadState is HookDownloadState.Complete, hookPreviewUrl = hookPreviewUrl, ) } } } }