package com.suno.android.ui.screens.home import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.TopAppBarDefaults 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.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import com.suno.android.common_core_utils.environment.ThemeMode import com.suno.android.common_ui.animations.NavigationFadeInAnimation import com.suno.android.common_ui.animations.NavigationFadeOutAnimation import com.suno.android.common_ui.components.IconBadge import com.suno.android.common_ui.components.buttons.SunoCreateFab import com.suno.android.common_ui.components.dialogs.PromoDialog import com.suno.android.common_ui.components.dialogs.V5DialogContent import com.suno.android.common_ui.components.images.ProfileAvatarImage import com.suno.android.common_ui.extensions.windowInsetsPadding import com.suno.android.common_ui.theme.ExtendedTheme import com.suno.android.common_ui.theme.NavBarSelection import com.suno.android.common_ui.theme.SunoTheme import com.suno.android.ui.effects.SystemBarsEffect import com.suno.android.ui.screens.create.CreatePromptForm import com.suno.android.ui.screens.create.audio.CreateAudioState import com.suno.android.ui.screens.explore.ExploreScreen import com.suno.android.ui.screens.explore.PlaylistType import com.suno.android.ui.screens.following_clips.FollowingClipsNavigationRequest import com.suno.android.ui.screens.following_clips.FollowingClipsScreen import com.suno.android.ui.screens.home.library.LibraryScreen 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.notifications.NotificationScreen import com.suno.android.ui.screens.home.profile.ProfileNavGraph import com.suno.android.ui.screens.home.tabs.BottomNavTabItem import com.suno.android.ui.screens.hooks.home.HooksHomeEvent import com.suno.android.ui.screens.hooks.home.HooksHomeNavigationRequest import com.suno.android.ui.screens.hooks.home.HooksHomeScreen import com.suno.android.ui.screens.hooks.home.HooksHomeVM import com.suno.android.ui.screens.navigation.NavDestination import com.suno.android.ui.screens.navigation.NavDestination.Create.CreateNavGraphDestination import com.suno.android.ui.screens.navigation.NavDestination.Home.PlaylistScreenDestination import com.suno.android.ui.screens.navigation.NavDestination.LoggedInNav.HooksCreateDestination import com.suno.android.ui.screens.navigation.NavDestination.LoggedInNav.ShareAssetEditorDestination import com.suno.android.ui.screens.navigation.NavDestination.Profile.ProfileNavGraphDestination import com.suno.android.ui.screens.playlist.LIKED_PLAYLIST_ID import com.suno.android.ui.screens.playlist.PlaylistScreen import com.suno.android.ui.screens.playlist_collection.PlaylistCollectionScreen import com.suno.android.ui.screens.search.ExploreSearchScreen import com.suno.android.utils.mvi.MviEffectHandler import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import com.suno.android.common_res.R as CommonResR @Suppress("CyclomaticComplexMethod", "LongMethod") @Composable fun HomeScreenNavGraph( vm: HomeScreenNavGraphVM, loggedInNavHostController: NavHostController, isOrpheusChatEnabled: Boolean, onRedirectToCreate: (clipId: String?, task: CreateAudioState.AudioCreateTask?) -> Unit, onRedirectToPlayer: () -> Unit, onRedirectToBilling: () -> Unit, onReusePrompt: (CreatePromptForm) -> Unit, ) { val homeNavController = rememberNavController() val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val viewState by vm.viewStateFlow().collectAsStateWithLifecycle() val currentBackStackEntry by homeNavController.currentBackStackEntryAsState() val isOnHooksFeed = currentBackStackEntry?.destination?.route == NavDestination.Home.HooksFeedScreenDestination::class.qualifiedName SystemBarsEffect( themeMode = viewState.themeMode, forceDarkMode = isOnHooksFeed, ) val tabs = remember(vm.dynamicBottomNavConfiguration) { vm.dynamicBottomNavConfiguration.asSet() } val hooksVm: HooksHomeVM? = if (BottomNavTabItem.HooksFeed in tabs) { hiltViewModel() } else { null } Scaffold( contentWindowInsets = WindowInsets.navigationBars, bottomBar = { val themeMode = if (isOnHooksFeed) ThemeMode.DARK else viewState.themeMode SunoTheme(themeMode) { NavigationBar(containerColor = ExtendedTheme.colors.backgroundPrimary) { TabBarItem(vm.dynamicBottomNavConfiguration.firstTab, vm, viewState) TabBarItem(vm.dynamicBottomNavConfiguration.secondTab, vm, viewState) SunoCreateFab( modifier = Modifier.size(64.dp), onClick = { vm.onBottomBarDestinationClick(CreateNavGraphDestination()) val route = if (isOrpheusChatEnabled) { NavDestination.LoggedInNav.OrpheusChatDestination } else { CreateNavGraphDestination(createPromptFormJson = null) } loggedInNavHostController.navigate(route = route) }, ) TabBarItem(vm.dynamicBottomNavConfiguration.fourthTab, vm, viewState) TabBarItem(BottomNavTabItem.MyProfile, vm, viewState) } } }, ) { innerPadding -> Box( modifier = Modifier .windowInsetsPadding(innerPadding), ) { HomeBottomMediaPlayerLayout( onCardSurfaceClick = { loggedInNavHostController.navigate( NavDestination.LoggedInNav.OmniScreenDestination(), ) }, hideMediaPlayer = isOnHooksFeed, ) { NavHost( modifier = Modifier.fillMaxSize(), navController = homeNavController, startDestination = vm.dynamicBottomNavConfiguration.firstTab.destination, enterTransition = { NavigationFadeInAnimation }, exitTransition = { NavigationFadeOutAnimation }, ) { composable { hooksVm ?: return@composable HooksHomeScreen( vm = hooksVm, onNavigationRequest = { request -> when (request) { is HooksHomeNavigationRequest.CreatorClick -> { homeNavController.navigate( ProfileNavGraphDestination( userHandle = request.creatorHandle.handle, isSelf = false, ), ) } is HooksHomeNavigationRequest.AssetEdit -> { loggedInNavHostController.navigate( route = ShareAssetEditorDestination( request.songId.value, ), ) } is HooksHomeNavigationRequest.ReusePrompt -> { loggedInNavHostController.navigate( route = CreateNavGraphDestination( createPromptFormJson = Json.encodeToString(request.promptForm), ), ) } is HooksHomeNavigationRequest.Remix -> { onRedirectToCreate(request.songId.value, request.task) } is HooksHomeNavigationRequest.CreateHookWithClip -> { loggedInNavHostController.navigate( HooksCreateDestination( songId = request.clip.clipId.value, ), ) } is HooksHomeNavigationRequest.Redirect -> { vm.handleRedirectUrl(request.url.url) } is HooksHomeNavigationRequest.MySongs -> { homeNavController.navigate( ProfileNavGraphDestination( userHandle = request.myUserHandle.handle, isSelf = true, ), ) } HooksHomeNavigationRequest.LikedSongs -> { vm.handleRedirectUrl("/playlist/liked") } HooksHomeNavigationRequest.Notifications -> { homeNavController.navigate( NavDestination.Home.ExploreNotificationScreenDestination, ) } HooksHomeNavigationRequest.Search -> { homeNavController.navigate( NavDestination.Home.ExploreSearchScreenDestination, ) } HooksHomeNavigationRequest.FollowingClips -> { homeNavController.navigate(NavDestination.Home.FollowingClipsScreenDestination) } } }, ) } composable { ExploreScreen( onFullScreenSongSelected = { loggedInNavHostController.navigate( NavDestination.LoggedInNav.OmniScreenDestination(), ) }, onArtistClicked = { artistHandle -> homeNavController.navigate( NavDestination.Profile.ProfileNavGraphDestination( userHandle = artistHandle.handle, isSelf = false, ), ) }, onPlaylistClicked = { playlistType -> when (playlistType) { is PlaylistType.PlaylistId -> { homeNavController.navigate( PlaylistScreenDestination( playlistId = playlistType.playlistId, ), ) } is PlaylistType.SearchTag -> { homeNavController.navigate( PlaylistScreenDestination( searchId = playlistType.searchTag, ), ) } } }, onNotificationsClicked = { homeNavController.navigate( NavDestination.Home.ExploreNotificationScreenDestination, ) }, onSearchClicked = { homeNavController.navigate( NavDestination.Home.ExploreSearchScreenDestination, ) }, onRedirect = { redirectUrl, styleTag -> vm.handleRedirectUrl(url = redirectUrl, styleTag = styleTag) }, onRedirectToBilling = onRedirectToBilling, ) } composable { NotificationScreen( topAppBarScrollBehavior = topAppBarScrollBehavior, onProfileClick = { userHandle -> homeNavController.navigate( NavDestination.Profile.ProfileNavGraphDestination( userHandle = userHandle, isSelf = false, ), ) }, onClipClick = { openCommentsSheet -> loggedInNavHostController.navigate( NavDestination.LoggedInNav.OmniScreenDestination( showCommentBottomSheet = openCommentsSheet, ), ) }, onHookClick = { hookId, showCommentsSheet -> loggedInNavHostController.navigate( NavDestination.LoggedInNav.HooksSingleDestination( hookId = hookId.value, showCommentBottomSheet = showCommentsSheet, ), ) }, onBackClick = { homeNavController.popBackStack() }, ) } composable { ExploreSearchScreen( onUserClick = { handle, searchKey -> homeNavController.navigate( ProfileNavGraphDestination( userHandle = handle.handle, isSelf = false, searchKey = searchKey, ), ) }, onPlaylistClick = { playlistId, searchKey -> homeNavController.navigate( PlaylistScreenDestination( playlistId = playlistId.value, searchKey = searchKey, ), ) }, onReusePromptClick = { createPromptForm: CreatePromptForm -> onReusePrompt.invoke(createPromptForm) }, onUpgradeClick = { onRedirectToBilling.invoke() }, onBackPress = { homeNavController.popBackStack() }, navigateToAssetEdit = { songId -> loggedInNavHostController.navigate( route = ShareAssetEditorDestination(songId.value), ) }, navigateToRemix = { clipId, task -> onRedirectToCreate(clipId.value, task) }, ) } composable { LibrarySearchScreen( onUpgradeClicked = { onRedirectToBilling.invoke() }, onReusePromptClicked = { createPromptForm: CreatePromptForm -> onReusePrompt.invoke(createPromptForm) }, onBackPressed = { homeNavController.popBackStack() }, navigateToAssetEdit = { loggedInNavHostController.navigate( route = NavDestination.LoggedInNav.ShareAssetEditorDestination(it.value), ) }, navigateToRemix = { clipId, task -> onRedirectToCreate(clipId.value, task) }, ) } composable { LibraryScreen( onPlaylistsClicked = { homeNavController.navigate( NavDestination.Home.PlaylistsCollectionScreenDestination, ) }, onLikedPlaylistClicked = { homeNavController.navigate( PlaylistScreenDestination(playlistId = LIKED_PLAYLIST_ID), ) }, onUpgradeClicked = { onRedirectToBilling.invoke() }, onReusePromptClicked = { createPromptForm: CreatePromptForm -> onReusePrompt.invoke(createPromptForm) }, onSearchClicked = { homeNavController.navigate( NavDestination.Home.LibrarySearchScreenDestination, ) }, navigateToAssetEdit = { loggedInNavHostController.navigate( route = NavDestination.LoggedInNav.ShareAssetEditorDestination(it.value), ) }, navigateToRemix = { clipId, task -> val createPromptForm = CreatePromptForm( remixClipId = clipId.value, task = task, ) loggedInNavHostController.navigate( route = NavDestination.Create.CreateNavGraphDestination( Json.encodeToString( createPromptForm, ), ), ) }, ) } composable { LibrarySearchScreen( onUpgradeClicked = { onRedirectToBilling.invoke() }, onReusePromptClicked = { createPromptForm: CreatePromptForm -> onReusePrompt.invoke(createPromptForm) }, navigateToAssetEdit = { loggedInNavHostController.navigate( route = NavDestination.LoggedInNav.ShareAssetEditorDestination(it.value), ) }, onBackPressed = { homeNavController.popBackStack() }, navigateToRemix = { clipId, task -> onRedirectToCreate(clipId.value, task) }, ) } composable { PlaylistCollectionScreen( onBackPressed = { homeNavController.popBackStack() }, onPlaylistSelected = { playlistId -> homeNavController.navigate( route = PlaylistScreenDestination( playlistId = playlistId, ), ) }, ) } composable { PlaylistScreen( onBackClicked = { homeNavController.popBackStack() }, onPlaylistDeletion = { homeNavController.navigate( NavDestination.Home.PlaylistsCollectionScreenDestination, ) { popUpTo(NavDestination.Home.PlaylistsCollectionScreenDestination) { inclusive = true } launchSingleTop = true } }, onReusePromptClicked = { createPromptForm: CreatePromptForm -> onReusePrompt.invoke(createPromptForm) }, onUserClicked = { handle -> homeNavController.navigate( NavDestination.Profile.ProfileNavGraphDestination( userHandle = handle.handle, isSelf = false, ), ) }, onUpgradeClicked = { onRedirectToBilling.invoke() }, navigateToAssetEdit = { loggedInNavHostController.navigate( route = NavDestination.LoggedInNav.ShareAssetEditorDestination(it.value), ) }, navigateToRemix = { clipId, task -> onRedirectToCreate(clipId.value, task) }, ) } composable { NotificationScreen( topAppBarScrollBehavior = topAppBarScrollBehavior, onProfileClick = { userHandle -> homeNavController.navigate( NavDestination.Profile.ProfileNavGraphDestination( userHandle = userHandle, isSelf = false, ), ) }, onClipClick = { openCommentsSheet -> loggedInNavHostController.navigate( NavDestination.LoggedInNav.OmniScreenDestination( showCommentBottomSheet = openCommentsSheet, ), ) }, onHookClick = { hookId, openCommentsSheet -> loggedInNavHostController.navigate( NavDestination.LoggedInNav.HooksSingleDestination( hookId = hookId.value, showCommentBottomSheet = openCommentsSheet, ), ) }, ) } composable { ProfileNavGraph( isTopLevel = true, userHandle = viewState.myUserHandle ?: "", isSelf = true, onBackPressed = { homeNavController.popBackStack() }, onUpgradeClicked = { onRedirectToBilling.invoke() }, onSettingsClicked = { loggedInNavHostController.navigate( route = NavDestination.Settings.SettingsNavGraphDestination, ) }, onEditProfileClicked = { loggedInNavHostController.navigate( route = NavDestination.Profile.EditProfileScreenDestination, ) }, onPlaylistClicked = { playlistId: String -> homeNavController.navigate( PlaylistScreenDestination( playlistId = playlistId, ), ) }, onReusePromptClicked = { createPromptForm -> onReusePrompt.invoke(createPromptForm) }, navigateToAssetEdit = { loggedInNavHostController.navigate( route = NavDestination.LoggedInNav.ShareAssetEditorDestination(it.value), ) }, navigateToRemix = { clipId, task -> onRedirectToCreate(clipId.value, task) }, navigateToUserHooksFeed = { userHandle, hookIndex -> loggedInNavHostController.navigate( NavDestination.LoggedInNav.HooksProfileFeedDestination( userHandle = userHandle.handle, startIndex = hookIndex, ), ) }, ) } composable { route -> val toRoute = route.toRoute() val userHandle = toRoute.userHandle val isSelf = toRoute.isSelf val searchKey = toRoute.searchKey ProfileNavGraph( userHandle = userHandle, isSelf = isSelf, onBackPressed = { homeNavController.popBackStack() }, onSettingsClicked = { loggedInNavHostController.navigate( NavDestination.Settings.SettingsNavGraphDestination, ) }, onEditProfileClicked = { loggedInNavHostController.navigate( NavDestination.Profile.EditProfileScreenDestination, ) }, onPlaylistClicked = { playlistId: String -> homeNavController.navigate( PlaylistScreenDestination( playlistId = playlistId, ), ) }, onReusePromptClicked = { createPromptForm -> onReusePrompt.invoke(createPromptForm) }, onUpgradeClicked = { onRedirectToBilling.invoke() }, navigateToAssetEdit = { loggedInNavHostController.navigate( ShareAssetEditorDestination(it.value), ) }, navigateToRemix = { clipId, task -> onRedirectToCreate(clipId.value, task) }, navigateToUserHooksFeed = { userHandle, hookIndex -> loggedInNavHostController.navigate( NavDestination.LoggedInNav.HooksProfileFeedDestination( userHandle = userHandle.handle, startIndex = hookIndex, ), ) }, searchKey = searchKey, ) } composable { FollowingClipsScreen( vm = hiltViewModel(), onNavigationRequest = { request -> when (request) { FollowingClipsNavigationRequest.Back -> homeNavController.popBackStack() FollowingClipsNavigationRequest.Billing -> onRedirectToBilling() } }, ) } } if ( !viewState.isPromoDialogGateEnabled && // If PromoDialog is enabled, don't show V5Dialog viewState.showingV5Dialog && viewState.isV5DialogGateEnabled ) { Dialog( onDismissRequest = { vm.onV5DialogDismissed() }, ) { V5DialogContent( ctaText = if (viewState.promoDialogType == HomeScreenNavGraphVM.PromoDialogType.FreeUser ) { stringResource(CommonResR.string.v5_dialog_upgrade_cta) } else { stringResource(CommonResR.string.v5_dialog_create_cta) }, onLeftActionClick = { vm.onV5DialogDismissed() if (viewState.promoDialogType == HomeScreenNavGraphVM.PromoDialogType.FreeUser) { onRedirectToBilling.invoke() } else { vm.onCreateWithModelClicked("v5") } }, onDismiss = { vm.onV5DialogDismissed() }, ) } } if (viewState.showingPromoDialog && viewState.isPromoDialogGateEnabled) { val promos = viewState.promos PromoDialog( promos = promos, onDismiss = vm::onPromoDialogDismissed, onSeen = vm::onPromoBannerSeen, ) } } } // this needs to be at the top so that it happens _before_ any other navigation LaunchedEffect(viewState.selectedTabDestination) { homeNavController.navigate(viewState.selectedTabDestination) { launchSingleTop = true restoreState = true popUpTo(homeNavController.graph.findStartDestination().id) { saveState = true } } } LaunchedEffect(homeNavController) { homeNavController.addOnDestinationChangedListener { controller, destination, arguments -> vm.onNavDestinationChanged(screenName = destination.route) vm.trackScreen(screenName = destination.route) } } MviEffectHandler(vm.effects) { effect -> when (effect) { HomeScreenNavGraphVM.HomeNavScreenEffects.RedirectToCreate -> { onRedirectToCreate.invoke(null, null) } HomeScreenNavGraphVM.HomeNavScreenEffects.RedirectToBilling -> { onRedirectToBilling.invoke() } is HomeScreenNavGraphVM.HomeNavScreenEffects.RedirectToPlaylist -> { homeNavController.navigate( PlaylistScreenDestination( playlistId = effect.playlistId, styleId = effect.styleTag, ), ) } is HomeScreenNavGraphVM.HomeNavScreenEffects.ReloadDestination -> { when (effect.destination) { // clear backstack and surface NavDestination.Home.ExploreScreenDestination, NavDestination.Home.LibraryScreenDestination, NavDestination.Home.MyProfileScreenDestination, -> homeNavController.navigate(viewState.selectedTabDestination) { launchSingleTop = true popUpTo(homeNavController.graph.findStartDestination().id) } NavDestination.Home.HooksFeedScreenDestination -> { if (isOnHooksFeed) { hooksVm?.sendEvent(HooksHomeEvent.NavReTapped) } else { homeNavController.popBackStack( route = NavDestination.Home.HooksFeedScreenDestination, inclusive = false, ) } } else -> Unit } } } } // TODO: investigate this later. It feels like a terrible solution but I tried so many things... LaunchedEffect(Unit) { vm.redirectToProfileStateFlow().collect { userHandle -> if (userHandle != null) { homeNavController.navigate( NavDestination.Profile.ProfileNavGraphDestination( userHandle = userHandle.handle, isSelf = false, ), ) vm.resetRedirectToProfileStateFlow() } } } LaunchedEffect(Unit) { vm.redirectToHookStateFlow().collect { deeplink -> deeplink?.let { link -> loggedInNavHostController.navigate( NavDestination.LoggedInNav.HooksSingleDestination( hookId = link.hookId.value, showCommentBottomSheet = !link.commentId?.value.isNullOrEmpty(), ), ) } vm.resetRedirectToHookStateFlow() } } LaunchedEffect(Unit) { vm.redirectToHooksFeedStateFlow().collect { redirectToHooksFeed -> if (redirectToHooksFeed == true) { homeNavController.navigate(NavDestination.Home.HooksFeedScreenDestination) vm.resetRedirectToHooksFeedStateFlow() } } } LaunchedEffect(Unit) { vm.redirectToCreateStateFlow().collect { redirectToCreate -> if (redirectToCreate == true) { onRedirectToCreate.invoke(null, null) vm.resetRedirectToCreateStateFlow() } } } LaunchedEffect(Unit) { vm.redirectToBillingStateFlow().collect { redirectToBilling -> if (redirectToBilling == true) { onRedirectToBilling.invoke() vm.resetRedirectToBillingStateFlow() } } } LaunchedEffect(Unit) { vm.redirectToPlayerStateFlow().collect { redirectToPlayer -> if (redirectToPlayer == true) { onRedirectToPlayer.invoke() vm.resetRedirectToPlayerStateFlow() } } } } } @Composable private fun RowScope.TabBarItem( item: BottomNavTabItem, vm: HomeScreenNavGraphVM, viewState: HomeScreenNavGraphVM.UiState, ) { val isNotificationsDestination = item.destination == NavDestination.Home.NotificationScreenDestination NavigationBarItem( selected = viewState.selectedTabDestination == item.destination, colors = NavigationBarItemDefaults.colors( selectedIconColor = NavBarSelection, indicatorColor = ExtendedTheme.colors.backgroundSecondary, ), onClick = { if (isNotificationsDestination) { vm.clearReadNotificationIndicator() } vm.onBottomBarDestinationClick(item.destination) }, icon = { if (item.destination == NavDestination.Home.MyProfileScreenDestination) { ProfileAvatarImage( avatarUrl = viewState.myUserAvatarUrl, displayName = "", modifier = Modifier.size(24.dp), contentDescription = stringResource(item.contentDescription), ) } else { item.iconResId?.let { iconResId -> Box { Icon( imageVector = ImageVector.vectorResource(id = iconResId), contentDescription = stringResource(item.contentDescription), ) if (isNotificationsDestination && viewState.hasUnreadNotifications) { IconBadge( Modifier.align(Alignment.TopEnd), ) } } } } }, label = {}, ) }