package com.suno.android.ui.screens.home.library.search import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row 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.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetState import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf 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.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.suno.android.common_core_utils.Id import com.suno.android.common_data.mappers.clips.SongListData import com.suno.android.common_ui.R import com.suno.android.common_ui.components.list_items.SongListItem import com.suno.android.common_ui.components.loading.SunoSpinner import com.suno.android.common_ui.components.omni.CircleIconButton import com.suno.android.common_ui.components.text_fields.SearchBarTextField import com.suno.android.common_ui.extensions.xAsFormattedCount import com.suno.android.common_ui.extensions.xBottomPlayerPadding import com.suno.android.common_ui.theme.ExtendedTheme import com.suno.android.common_ui.theme.SunoTheme import com.suno.android.extensions.xAsUiModelBadge 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.launch import com.suno.android.common_res.R as CommonResR @Composable fun LibrarySearchScreen( vm: LibrarySearchScreenVM? = hiltViewModel(), onUpgradeClicked: () -> Unit, onReusePromptClicked: (CreatePromptForm) -> Unit, navigateToAssetEdit: (songId: Id) -> Unit, navigateToRemix: (songId: Id, task: CreateAudioState.AudioCreateTask) -> Unit, onBackPressed: () -> Unit, ) { val viewState = vm?.state?.collectAsStateWithLifecycle()?.value val scope = rememberCoroutineScope() val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } var inputVal by remember { mutableStateOf("") } val lazyListState = rememberLazyListState() val endOfListReached: Boolean by remember { derivedStateOf { !lazyListState.canScrollForward } } LaunchedEffect(endOfListReached) { if (endOfListReached) { vm?.sendEvent( LibrarySearchScreenEvent.OnSearchLibrary( increment = true, term = inputVal, ), ) } } fun updateSearchQuery( query: String, ) { inputVal = query vm?.sendEvent(LibrarySearchScreenEvent.OnUpdateSearchQuery(query = query)) } var showSongActionContainerBottomSheet: Boolean by remember { mutableStateOf(false) } val songActionsContainerHostState: SheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true, // Allow partial expansion, ) LaunchedEffect(showSongActionContainerBottomSheet) { if (showSongActionContainerBottomSheet) { // Sheet is now in composition, animate it songActionsContainerHostState.show() } } // Bidirectional sync for sheet state so it'll animate LaunchedEffect(songActionsContainerHostState.isVisible) { showSongActionContainerBottomSheet = songActionsContainerHostState.isVisible } val snackbarHostState = remember { SnackbarHostState() } val songDeletedMsg = stringResource(id = CommonResR.string.song_deleted) val undoString = stringResource(CommonResR.string.undo) LaunchedEffect(viewState?.deletedSong) { if (viewState?.deletedSong != null) { val result = snackbarHostState.showSnackbar( message = songDeletedMsg, actionLabel = undoString, duration = SnackbarDuration.Short, ) when (result) { SnackbarResult.ActionPerformed -> { viewState.deletedSong.let { vm.sendEvent(LibrarySearchScreenEvent.OnUndoDeleteSong(songToUndoDelete = it)) } } SnackbarResult.Dismissed -> { // Do nothing } } } } LaunchedEffect(Unit) { vm?.effects?.collect { effect -> when (effect) { is LibrarySearchScreenEffect.ShowSnackbar -> { if (effect.message.isNotEmpty() == true) { snackbarHostState.showSnackbar(effect.message) } } } } } Scaffold( modifier = Modifier .background(ExtendedTheme.colors.backgroundPrimary) .fillMaxSize(), topBar = { Row( modifier = Modifier .fillMaxWidth() .padding(start = 12.dp, end = 12.dp, top = 48.dp), verticalAlignment = Alignment.CenterVertically, ) { CircleIconButton( onClick = { onBackPressed.invoke() }, vectorDrawableResId = R.drawable.arrow_left, contentDescription = "Back", ) SearchBarTextField( value = inputVal, placeholder = stringResource(CommonResR.string.search_library_placeholder), onValueChange = { updateSearchQuery(it) }, onSearchImeAction = { vm?.sendEvent(LibrarySearchScreenEvent.OnSearchLibrary(term = inputVal)) focusManager.clearFocus() }, modifier = Modifier .weight(1f) .focusRequester(focusRequester), ) // automatically focus on search field and pop keyboard LaunchedEffect(focusRequester) { focusRequester.requestFocus() } } }, snackbarHost = { SnackbarHost( hostState = snackbarHostState, modifier = Modifier.xBottomPlayerPadding(vm?.isPlayerVisible()), ) }, ) { innerPadding -> Box( modifier = Modifier .background(ExtendedTheme.colors.backgroundPrimary) .padding(innerPadding) .fillMaxSize(), ) { LazyColumn( modifier = Modifier.fillMaxSize(), state = lazyListState, ) { if (viewState?.loading == true) { item { Box( modifier = Modifier .fillMaxSize() .padding(top = 24.dp), contentAlignment = Alignment.Center, ) { SunoSpinner() } } } else { items(viewState?.songResults ?: emptyList()) { songResult -> SongListItem( clipImageUrl = songResult.displayImageUrl, songTitle = songResult.title?.takeIf { it.isNotEmpty() } ?: stringResource(id = CommonResR.string.untitled), songTags = songResult.tags ?: stringResource(CommonResR.string.no_style), playCount = songResult.playCount?.xAsFormattedCount(), upvoteCount = songResult.upvoteCount?.xAsFormattedCount(), modelBadge = songResult.modelBadge?.xAsUiModelBadge(), onClick = { vm?.sendEvent(LibrarySearchScreenEvent.OnSongClicked(song = songResult)) }, onOverflowClick = { vm?.sendEvent(LibrarySearchScreenEvent.OnSongOverflowOpened(songId = songResult.id)) showSongActionContainerBottomSheet = true }, onUpgradeClicked = { onUpgradeClicked.invoke() }, isPreviewClip = songResult.isPreview, ) HorizontalDivider( thickness = 1.dp, color = ExtendedTheme.colors.borderPrimary, ) } if (viewState?.loadingMore == true) { item { Box( modifier = Modifier .fillMaxWidth() .height(30.dp), contentAlignment = Alignment.Center, ) { SunoSpinner() } } } } } if (showSongActionContainerBottomSheet) { val currentlySelectedSong = viewState?.songResults?.find { it.id == viewState.songIdToOperate } currentlySelectedSong?.let { SongActionsBottomSheet( song = currentlySelectedSong, canRemoveFromPlaylist = false, isShowShareVideoGateEnabled = viewState.isShowShareVideoGateEnabled, songActionBottomSheetConsumerEvents = { event -> when (event) { SongActionBottomSheetConsumerEvent.DismissSongActionsContainerHost -> { scope.launch { songActionsContainerHostState.hide() } } is SongActionBottomSheetConsumerEvent.NavigateToRemix -> { scope.launch { songActionsContainerHostState.hide() showSongActionContainerBottomSheet = false } navigateToRemix(event.songId, event.task) } is SongActionBottomSheetConsumerEvent.NavigateToShareVideo -> { navigateToAssetEdit(event.songId) } is SongActionBottomSheetConsumerEvent.OnRemixClicked -> { Unit } is SongActionBottomSheetConsumerEvent.OnReusePromptClicked -> { onReusePromptClicked.invoke(event.formData) } is SongActionBottomSheetConsumerEvent.OnShowSnackbar -> { vm.triggerSnackbar(message = event.message) } is SongActionBottomSheetConsumerEvent.OnSongDeleted -> { vm.sendEvent( LibrarySearchScreenEvent.OnSongDeleted( deletedSong = event.song, ), ) } is SongActionBottomSheetConsumerEvent.OnSongRenamed -> { vm.sendEvent( LibrarySearchScreenEvent.OnSongRenamed( renamedSong = event.song, ), ) } is SongActionBottomSheetConsumerEvent.OnSongReported -> { scope.launch { snackbarHostState.showSnackbar( message = event.message, ) } } is SongActionBottomSheetConsumerEvent.OnSongUndoDeleted -> { Unit } is SongActionBottomSheetConsumerEvent.OnSongUndoReported -> { Unit } SongActionBottomSheetConsumerEvent.OnUpgradeClicked -> { onUpgradeClicked.invoke() } is SongActionBottomSheetConsumerEvent.OnRemoveFromPlaylistTapped -> Unit } }, ) } } } } } @Preview @Composable private fun LibrarySearchScreen_Preview() { SunoTheme { LibrarySearchScreen( vm = null, onReusePromptClicked = {}, onBackPressed = {}, onUpgradeClicked = {}, navigateToAssetEdit = {}, navigateToRemix = { _, _ -> }, ) } }