package com.suno.android.ui.screens.omni import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.VerticalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import com.suno.android.R import com.suno.android.common_core_utils.Id import com.suno.android.common_core_utils.constants.ReactionType import com.suno.android.common_core_utils.model.UserHandle import com.suno.android.common_data.comments.MediaEntity import com.suno.android.common_data.mappers.clips.ClipType import com.suno.android.common_data.mappers.clips.SongListData import com.suno.android.common_data.mappers.clips.xAsSongListDataOrNull import com.suno.android.common_ui.components.bottom_sheet.ShareLinkBottomSheet import com.suno.android.common_ui.components.bottom_sheet.handleShareClicked import com.suno.android.common_ui.components.dialogs.CastDeviceChooserDialog import com.suno.android.omniplayer.OmniPlayerPage import com.suno.android.ui.bottom_sheets.comments.CommentsBottomSheet import com.suno.android.ui.bottom_sheets.create.RemixTypeSelectionBottomSheet import com.suno.android.ui.bottom_sheets.song_actions.SongActionBottomSheetConsumerEvent import com.suno.android.ui.bottom_sheets.song_actions.SongActionsBottomSheet import com.suno.android.ui.screens.create.CreatePromptForm import com.suno.android.ui.screens.create.audio.CreateAudioState import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @Composable fun OmniScreen( vm: OmniScreenVM, rootNavHostController: NavHostController, onReusePromptClicked: (CreatePromptForm) -> Unit, onArtistHandleClicked: (handle: UserHandle) -> Unit, onUpgradeClicked: () -> Unit, navigateToAssetEdit: (songId: Id) -> Unit, navigateToRemix: (songId: Id, task: CreateAudioState.AudioCreateTask) -> Unit, showCommentsBottomSheetOnNavigate: Boolean = false, ) { val viewState by vm.viewStateFlow().collectAsStateWithLifecycle() val pagerState = rememberPagerState( initialPage = viewState.currentSongIndex ?: 0, pageCount = { viewState.songList.size }, ) val context = LocalContext.current val clipboardManager = LocalClipboardManager.current var currentlyPlayingPage by remember { mutableStateOf(null) } val processLifecycleOwner = ProcessLifecycleOwner.get() var screenJustOpened by remember { mutableStateOf(true) } var appJustForegrounded by remember { mutableStateOf(false) } val snackbarHostState = remember { SnackbarHostState() } DisposableEffect(Unit) { val observer = object : DefaultLifecycleObserver { override fun onResume( owner: LifecycleOwner, ) { appJustForegrounded = true // Mark only real foreground events } override fun onPause( owner: LifecycleOwner, ) { screenJustOpened = false } } processLifecycleOwner.lifecycle.addObserver(observer) onDispose { processLifecycleOwner.lifecycle.removeObserver(observer) } } LaunchedEffect(viewState.snackBarMessage) { if (viewState.snackBarMessage.isNotEmpty()) { snackbarHostState.showSnackbar(viewState.snackBarMessage) vm.resetSnackBar() } } LaunchedEffect(viewState.currentSongIndex) { if (pagerState.currentPage != viewState.currentSongIndex) { if (appJustForegrounded && !screenJustOpened) { viewState.currentSongIndex?.let { pagerState.scrollToPage(it) } appJustForegrounded = false } else { screenJustOpened = false viewState.currentSongIndex?.let { pagerState.animateScrollToPage(it) } } } } LaunchedEffect(pagerState) { snapshotFlow { if (pagerState.pageCount == 0) { null } else { pagerState.currentPage } }.collect { page -> currentlyPlayingPage = page vm.onPageSelected(pageIndex = page) } } // Cast device chooser dialog CastDeviceChooserDialog( isVisible = viewState.castSessionState.showDialog, castAppId = stringResource(R.string.cast_app_id), onDismiss = { vm.onCastDialogDismissed() }, onError = { exception -> vm.onCastError(exception) }, ) val songActionsSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true, // Bypass partial expansion, ) val songQualityReportSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = false, // Allow partial expansion, ) val songRenameSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = false, // Allow partial expansion, ) val addToPlaylistSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = false, // Allow partial expansion, ) val commentsSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true, // Allow partial expansion, ) val shareLinkSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = false, // Allow partial expansion, ) val remixTypeSelectionSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = false, // Allow partial expansion, ) var showSongActionBottomSheet by remember { mutableStateOf(false) } var showCommentsBottomSheet by remember { mutableStateOf(false) } var showReportQualityBottomSheet by remember { mutableStateOf(false) } var showRenameBottomSheet by remember { mutableStateOf(false) } var showShareLinkBottomSheet by remember { mutableStateOf(false) } var showAddToPlaylistBottomSheet by remember { mutableStateOf(false) } var showRemixTypeSelectionBottomSheet by remember { mutableStateOf(false) } LaunchedEffect(Unit) { vm.effects().collectLatest { effect -> when (effect) { is OmniScreenVM.OmniScreenEffects.ShareLink -> { launch { handleShareClicked( platform = effect.sharePlatform, shareLink = effect.link.url, context = context, clipboardManager = clipboardManager, ) vm.onSongShared(effect.songId) } shareLinkSheetState.hide() showShareLinkBottomSheet = false } is OmniScreenVM.OmniScreenEffects.ShareVideo -> { navigateToAssetEdit(effect.songId) } } } } val scope = rememberCoroutineScope() // We need this launched effect for the initial setting of the modal sheet state so it'll animate properly for FIRST launch LaunchedEffect(showSongActionBottomSheet) { if (showSongActionBottomSheet) { // Sheet is now in composition, animate it songActionsSheetState.show() } } LaunchedEffect(showRemixTypeSelectionBottomSheet) { if (showRemixTypeSelectionBottomSheet) { // Sheet is now in composition, animate it remixTypeSelectionSheetState.show() } } LaunchedEffect(showCommentsBottomSheet) { if (showCommentsBottomSheet) { // Sheet is now in composition, animate it commentsSheetState.show() } } LaunchedEffect(showShareLinkBottomSheet) { if (showShareLinkBottomSheet) { // Sheet is now in composition, animate it shareLinkSheetState.show() } } // Bidirectional sync for sheet state so it'll animate LaunchedEffect(songActionsSheetState.isVisible) { showSongActionBottomSheet = songActionsSheetState.isVisible } // Bidirectional sync for sheet state so it'll animate LaunchedEffect(remixTypeSelectionSheetState.isVisible) { showRemixTypeSelectionBottomSheet = remixTypeSelectionSheetState.isVisible } // We need this launched effect for the initial setting of the modal sheet state so it'll animate properly for FIRST launch LaunchedEffect(showReportQualityBottomSheet) { if (showReportQualityBottomSheet) { // Sheet is now in composition, animate it songQualityReportSheetState.show() } } // Bidirectional sync for sheet state so it'll animate LaunchedEffect(songQualityReportSheetState.isVisible) { showReportQualityBottomSheet = songQualityReportSheetState.isVisible } LaunchedEffect(addToPlaylistSheetState.isVisible) { showAddToPlaylistBottomSheet = addToPlaylistSheetState.isVisible } LaunchedEffect(commentsSheetState.isVisible) { showCommentsBottomSheet = commentsSheetState.isVisible } LaunchedEffect(shareLinkSheetState.isVisible) { showShareLinkBottomSheet = shareLinkSheetState.isVisible } Scaffold( modifier = Modifier .fillMaxSize() .background(Color.Black), snackbarHost = { SnackbarHost(snackbarHostState) }, ) { innerPadding -> val onRemixClicked = { showRemixTypeSelectionBottomSheet = true } Column( modifier = Modifier .background(Color.Black) .padding(innerPadding), ) { VerticalPager( contentPadding = PaddingValues(vertical = 64.dp), state = pagerState, key = { page -> viewState.songList.getOrNull(page)?.clipId?.toString() ?: "null $page" }, beyondViewportPageCount = 1, ) { currentPageIndex -> if (viewState.songList.isEmpty()) return@VerticalPager val isCurrentPage = pagerState.currentPage == currentlyPlayingPage val song = viewState.songList.getOrNull(currentPageIndex) if (song != null) { val isPreviewClip = remember(song.type) { song.type == ClipType.PREVIEW.value } OmniPlayerPage( parentPagerState = pagerState, page = currentPageIndex, exoPlayer = vm .provideExoPlayer() .takeIf { isCurrentPage }, // attach only when visible mediaType = song.mediaType, timestamp = viewState.timestamp, percentageComplete = viewState.progressPercentage, albumArtImageUrl = song.albumImageUrl, videoCoverUrl = song.videoCoverUrl, titleTextString = song.nowPlayingTitle, commentCount = song.commentCount, upvoteCount = song.upvoteCount, artistAvatarImageUrl = song.artistAvatarUrl, artistName = song.artistName, isPreviewClip = isPreviewClip, clipModelName = song.majorModelVersion, onArtistHandleClicked = { targetHandle -> val handle = targetHandle ?: song.handle handle?.let { onArtistHandleClicked.invoke(it) } }, isPlaying = viewState.isPlaying, isShuffling = viewState.isShuffling, repeatMode = viewState.repeatMode, isShowRepeatGateEnabled = viewState.isShowRepeatGateEnabled, isShowShuffleGateEnabled = viewState.isShowShuffleGateEnabled, lyricsString = song.displayLyrics, caption = song.caption, captionMentions = song.captionMentions, liked = song.reaction == ReactionType.LIKE, showCastButton = viewState.shouldShowCastButton, isCasting = viewState.castSessionState.isConnected, onRemixClicked = if (viewState.isRemixGateEnabled && vm.canRemixCurrentSong()) { { showRemixTypeSelectionBottomSheet = true } } else { null }, onUpgradeClicked = { onUpgradeClicked.invoke() }, onPlayPauseClicked = { vm.onPlayPauseClicked() }, onDismissClicked = { rootNavHostController.popBackStack() }, onLeftTap = { vm.onLeftBumperClicked() }, onRightTap = { vm.onRightBumperClicked() }, onScrubbed = { percentage -> vm.onScrubbed( percentage, ) }, onShuffleClicked = { vm.onShuffleClicked() }, onRepeatModeClicked = { vm.onRepeatModeClicked() }, onCommentsClicked = { showCommentsBottomSheet = true }, onOverflowMenuClicked = { showSongActionBottomSheet = true }, onLikeClicked = { vm.onLikeClicked(song) }, onShareClicked = { showShareLinkBottomSheet = true }, onCastClicked = { vm.onCastClicked() }, onReplayClicked = { vm.onReplayClicked() }, onCenterTap = { vm.onPlayPauseClicked() }, onCenterDoubleTap = { vm.moveToNextSong() }, ) } else { Spacer(Modifier.fillMaxSize()) } } } if (showSongActionBottomSheet) { if (viewState.songList.isNotEmpty()) { viewState.currentSong?.let { song -> val songListData = song.xAsSongListDataOrNull() ?: return@let null SongActionsBottomSheet( song = songListData, canRemoveFromPlaylist = false, isShowShareVideoGateEnabled = viewState.isShowShareVideoGateEnabled, songActionBottomSheetConsumerEvents = { event -> when (event) { SongActionBottomSheetConsumerEvent.DismissSongActionsContainerHost -> { showSongActionBottomSheet = false } is SongActionBottomSheetConsumerEvent.NavigateToRemix -> { scope.launch { songActionsSheetState.hide() showSongActionBottomSheet = false } navigateToRemix.invoke(event.songId, event.task) } is SongActionBottomSheetConsumerEvent.NavigateToShareVideo -> { navigateToAssetEdit.invoke(event.songId) } is SongActionBottomSheetConsumerEvent.OnRemixClicked -> Unit is SongActionBottomSheetConsumerEvent.OnReusePromptClicked -> { onReusePromptClicked.invoke(event.formData) } is SongActionBottomSheetConsumerEvent.OnShowSnackbar -> { scope.launch { snackbarHostState.showSnackbar(event.message) } } is SongActionBottomSheetConsumerEvent.OnSongDeleted -> { vm.handleSongDeleted(event.song) } is SongActionBottomSheetConsumerEvent.OnSongRenamed -> { vm.handleSongRenamed(event.song) } is SongActionBottomSheetConsumerEvent.OnSongReported -> { scope.launch { snackbarHostState.showSnackbar(event.message) } } is SongActionBottomSheetConsumerEvent.OnSongUndoDeleted -> Unit is SongActionBottomSheetConsumerEvent.OnSongUndoReported -> Unit SongActionBottomSheetConsumerEvent.OnUpgradeClicked -> { onUpgradeClicked.invoke() } is SongActionBottomSheetConsumerEvent.OnRemoveFromPlaylistTapped -> Unit } }, ) } } } if (showCommentsBottomSheet) { viewState.currentSongId()?.let { songId -> CommentsBottomSheet( mediaEntity = MediaEntity.ClipEntity(clipId = songId.map()), authorId = viewState.currentSongAuthorId(), authorHandle = viewState.currentSongAuthorHandle(), onDismiss = { scope.launch { commentsSheetState.hide() showCommentsBottomSheet = false } }, onProfileClicked = { userHandle -> onArtistHandleClicked(userHandle) }, ) } } if (showShareLinkBottomSheet) { ShareLinkBottomSheet( sheetState = shareLinkSheetState, isShowShareVideoGateEnabled = viewState.isShowShareVideoGateEnabled, onShareClicked = { platform -> val songId = viewState.currentSongId() ?: return@ShareLinkBottomSheet vm.startShare( songId = songId, platform = platform, ) }, onDismiss = { showShareLinkBottomSheet = false }, ) } if (viewState.songList.isNotEmpty()) { viewState.currentSong?.let { clip -> if (showRemixTypeSelectionBottomSheet) { RemixTypeSelectionBottomSheet( clip = clip, onDismiss = { scope.launch { showRemixTypeSelectionBottomSheet = false remixTypeSelectionSheetState.hide() } }, onOptionClick = { scope.launch { showRemixTypeSelectionBottomSheet = false remixTypeSelectionSheetState.hide() } navigateToRemix(clip.clipId, it.task) }, ) } } } } // this needs to be at the bottom of the function // pop open the comments bottom sheet AFTER loading the new songId in CommentsBottomSheet func invocation LaunchedEffect(showCommentsBottomSheetOnNavigate) { if (showCommentsBottomSheetOnNavigate == true) { showCommentsBottomSheet = true } } }