package com.suno.android.ui.screens.explore import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxHeight 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.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf 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.graphics.Color import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState import androidx.paging.compose.collectAsLazyPagingItems import coil3.compose.AsyncImage import com.suno.android.common_core_utils.model.UserHandle import com.suno.android.common_data.mappers.clips.SongListData import com.suno.android.common_data.mappers.playlists.DiscoverFeedSection import com.suno.android.common_ui.R import com.suno.android.common_ui.components.loading.SunoPullToRefreshBox import com.suno.android.common_ui.components.loading.SunoSpinner import com.suno.android.common_ui.components.omni.CircleIconButton import com.suno.android.common_ui.extensions.windowInsetsPadding import com.suno.android.common_ui.theme.ExtendedTheme import com.suno.android.ui.screens.home.mediaplayer.omniPlayer import com.suno.android.common_res.R as CommonResR @Composable fun ExploreScreen( vm: ExploreScreenVM = hiltViewModel(), onFullScreenSongSelected: (clip: SongListData) -> Unit, onArtistClicked: (artistHandle: UserHandle) -> Unit, onNotificationsClicked: () -> Unit, onSearchClicked: () -> Unit, onPlaylistClicked: (playlistType: PlaylistType) -> Unit, onRedirect: (redirectUrl: String, styleTag: String) -> Unit, onRedirectToBilling: () -> Unit, ) { val viewState by vm.state.collectAsStateWithLifecycle() val sectionPagingItems = vm.pagingDataFlow.collectAsLazyPagingItems() val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val lazyFeedState = rememberLazyListState() val firstActivePlaylistIndex by remember { derivedStateOf { var index = lazyFeedState.firstVisibleItemIndex // Hard-coded number to check if first item is partially off-screen if (lazyFeedState.firstVisibleItemScrollOffset > 120) { index++ } index } } Scaffold( modifier = Modifier .background(ExtendedTheme.colors.backgroundPrimary) .fillMaxSize() .nestedScroll(topAppBarScrollBehavior.nestedScrollConnection), topBar = { TopAppBar( colors = TopAppBarDefaults.topAppBarColors( containerColor = ExtendedTheme.colors.backgroundPrimary, scrolledContainerColor = ExtendedTheme.colors.backgroundPrimary, ), scrollBehavior = topAppBarScrollBehavior, title = { Text( style = ExtendedTheme.typography.smallTrackName, text = stringResource( id = CommonResR.string.explore, ), ) }, actions = { if (viewState.isHooksFeedGateEnabled) { CircleIconButton( modifier = Modifier.padding(horizontal = 8.dp), vectorDrawableResId = R.drawable.notifications, contentDescription = stringResource(CommonResR.string.notifications), showBadge = viewState.hasUnreadNotifications == true, onClick = onNotificationsClicked, ) } CircleIconButton( modifier = Modifier.padding(horizontal = 8.dp), vectorDrawableResId = R.drawable.magnifying_glass, contentDescription = stringResource(CommonResR.string.search), onClick = onSearchClicked, ) }, ) }, ) { innerPadding -> SunoPullToRefreshBox( modifier = Modifier .background(ExtendedTheme.colors.backgroundPrimary) .windowInsetsPadding(innerPadding) .fillMaxSize(), isRefreshing = sectionPagingItems.loadState.refresh is LoadState.Loading && sectionPagingItems.itemCount != 0, onRefresh = { sectionPagingItems.refresh() }, ) { LazyColumn( state = lazyFeedState, horizontalAlignment = Alignment.CenterHorizontally, contentPadding = WindowInsets.omniPlayer.asPaddingValues(), modifier = Modifier .fillMaxSize() .consumeWindowInsets(WindowInsets.omniPlayer), ) { if (viewState.isBlackFridayBannerEnabled && sectionPagingItems.loadState.refresh !is LoadState.Loading ) { item { BlackFridayBanner( onUpgradeClicked = onRedirectToBilling, ) } } items(sectionPagingItems.itemCount) { sectionIndex -> sectionPagingItems[sectionIndex]?.let { section -> Column { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) .padding(top = 8.dp, bottom = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { Text( text = section.name, style = ExtendedTheme.typography.introCopy, ) } when (section) { is DiscoverFeedSection.PlaylistSection -> { ExplorePlaylistSection( vm = vm, section = section, sectionIndex = sectionIndex, firstActivePlaylistIndex = firstActivePlaylistIndex, onArtistClicked = onArtistClicked, onFullScreenSongSelected = onFullScreenSongSelected, ) } is DiscoverFeedSection.PlaylistListSection -> { ExplorePlaylistListSection( section = section, onArtistClicked = onArtistClicked, onPlaylistClicked = { playlistId -> onPlaylistClicked( PlaylistType.PlaylistId( playlistId, ), ) }, ) } is DiscoverFeedSection.StyleListSection -> { ExploreStyleListSection( section = section, onRedirect = { redirectUrl, styleTag -> onRedirect(redirectUrl, styleTag) }, ) } } } HorizontalDivider( modifier = Modifier.padding(vertical = 20.dp), thickness = 1.dp, ) } } item { sectionPagingItems.apply { when { loadState.refresh is LoadState.Loading && itemCount == 0 -> { Box( modifier = Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center, ) { SunoSpinner() } } loadState.append is LoadState.Loading -> { SunoSpinner() } // todo handle error states } } } } } } } @Composable private fun BlackFridayBanner( onUpgradeClicked: () -> Unit, ) { Box( modifier = Modifier .fillMaxWidth() .height(300.dp) .padding(horizontal = 16.dp, vertical = 8.dp) .background(color = Color.Black, shape = MaterialTheme.shapes.medium) .clip(shape = MaterialTheme.shapes.medium), ) { AsyncImage( model = BLACK_FRIDAY_BANNER_BG_URL, contentDescription = stringResource(CommonResR.string.black_friday_banner), modifier = Modifier.fillMaxSize(), contentScale = ContentScale.FillHeight, alignment = Alignment.CenterEnd, placeholder = ColorPainter(Color.Black), error = ColorPainter(Color.Black), ) Column( modifier = Modifier .fillMaxWidth(CONTENT_MARGIN) .fillMaxHeight() .padding(24.dp), verticalArrangement = Arrangement.Bottom, ) { Text( text = stringResource(CommonResR.string.black_friday_banner_header), color = Color.White, style = ExtendedTheme.typography.introCopy, ) Text( modifier = Modifier.padding(vertical = 8.dp), text = stringResource(CommonResR.string.black_friday_banner_body), style = ExtendedTheme.typography.body, color = Color.White, ) Button( modifier = Modifier.padding(top = 8.dp), onClick = onUpgradeClicked, shape = MaterialTheme.shapes.extraLarge, colors = ButtonDefaults.buttonColors( containerColor = Color.White, contentColor = Color.Black, ), ) { Row( verticalAlignment = Alignment.CenterVertically, ) { Icon( painter = painterResource(id = R.drawable.play), contentDescription = null, ) Text( text = stringResource(CommonResR.string.black_friday_banner_cta), style = ExtendedTheme.typography.body, ) } } } } } private const val CONTENT_MARGIN = 0.8f private const val BLACK_FRIDAY_BANNER_BG_URL = "https://cdn-o.suno.com/black_friday_2025_banner_ios_bg_2.jpg" sealed interface PlaylistType { data class PlaylistId( val playlistId: String, ) : PlaylistType data class SearchTag( val searchTag: String, ) : PlaylistType }