package com.suno.android.ui.screens.explore import androidx.lifecycle.viewModelScope import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer import androidx.paging.PagingData import androidx.paging.cachedIn import com.suno.android.common_analytics.listening_source.ListeningSource import com.suno.android.common_analytics.listening_source.ListeningSourceCache 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.Url import com.suno.android.common_data.alerts.AlertsRepo import com.suno.android.common_data.discover.DiscoverRepository import com.suno.android.common_data.mappers.clips.SongListData import com.suno.android.common_data.mappers.clips.xAsLocalClipData import com.suno.android.common_data.mappers.playlists.DiscoverFeedSection import com.suno.android.common_data.user.UserSessionRepository import com.suno.android.common_mvi.MviEffect import com.suno.android.common_mvi.MviProcessorFactory import com.suno.android.common_mvi.MviViewModel import com.suno.android.gating.Feature import com.suno.android.gating.FeatureManager import com.suno.android.hooks.HooksFeatureGateManager import com.suno.android.hooks.HooksFeedTab import com.suno.android.media.MediaManager import com.suno.android.media.MediaMetadataManager import com.suno.android.media.VideoPreview import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import javax.inject.Inject @HiltViewModel class ExploreScreenVM @Inject constructor( processorFactory: MviProcessorFactory, private val mediaManager: MediaManager, private val featureManager: FeatureManager, private val alertsRepo: AlertsRepo, private val listeningSourceCache: ListeningSourceCache, @VideoPreview videoPreviewPlayer: ExoPlayer, discoverRepository: DiscoverRepository, userSessionRepository: UserSessionRepository, mediaMetadataManager: MediaMetadataManager, hooksFeatureGateManager: HooksFeatureGateManager, ) : MviViewModel( processorFactory = processorFactory, initialState = ExploreScreenMVIState( isHooksFeedGateEnabled = hooksFeatureGateManager.hooksFeedTab != HooksFeedTab.Disabled, isBlackFridayBannerEnabled = featureManager.hasFeature(Feature.BlackFridayBanner), ), ) { val pagingDataFlow: Flow> = discoverRepository.getFeedStream().cachedIn(viewModelScope) // TODO AND-1033 temporary solution to avoid updating explore items due to reactions private val mediaReactionsMap: MutableMap, ReactionType?> = mutableMapOf() private val firstVisibleClipFlow = MutableStateFlow(Pair(-1, -1)) private val videoPreviewPlayerDelegate = lazy { videoPreviewPlayer.apply { prepare() repeatMode = Player.REPEAT_MODE_ONE playWhenReady = true addListener( object : Player.Listener { override fun onPlayerError( error: PlaybackException, ) { logger.e(error) } }, ) } } // Only maintains a single video player that gets passed to the first visible track fun getVideoPreviewPlayerState( playlistIndex: Int, clipIndex: Int, videoPreviewUrl: Url?, ): StateFlow { val isVideoCoverPreviewGateEnabled = featureManager.hasFeature(Feature.VideoCoverPreview) return firstVisibleClipFlow.map { val canShowVideoPreview = isVideoCoverPreviewGateEnabled && videoPreviewUrl != null && it.first == playlistIndex && it.second == clipIndex if (canShowVideoPreview) { videoPreviewPlayerDelegate.value } else { null } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) } init { userSessionRepository.sessionConfigurationStateFlow() .onEach { sessionConfiguration -> val user = sessionConfiguration.user updateState { oldState -> oldState.copy( userAvatarUrl = user?.avatarImageUrl, userDisplayName = user?.displayName, ) } }.catch { exception -> logger.e(exception) }.launchIn(viewModelScope) mediaMetadataManager.mediaReactionFlow() .onEach { reaction -> mediaReactionsMap[reaction.clipId] = reaction.reaction }.catch { exception -> logger.e(exception) }.launchIn(viewModelScope) if (state.value.isHooksFeedGateEnabled) { // Check if there are any unread notifications for notifications icon badging updateUnreadNotifications() } } private fun updateUnreadNotifications() { alertsRepo.inAppNotificationsStateFlow() .onEach { notificationsState -> updateState { oldState -> oldState.copy(hasUnreadNotifications = notificationsState.hasUnreadNotifications) } } .catch { logger.e(it) } .launchIn(viewModelScope) } fun handleVisibleIndexChange( playlistIndex: Int? = null, clipIndex: Int? = null, ) { firstVisibleClipFlow.update { it.copy(first = playlistIndex ?: it.first, second = clipIndex ?: it.second) } } override suspend fun reduceEvent( currentState: ExploreScreenMVIState, event: ExploreScreenEvent, emitEffect: suspend (MviEffect) -> Unit, ): ExploreScreenMVIState = when (event) { is ExploreScreenEvent.SongPress -> { handleSongPress(playlist = event.playlist, clip = event.clip) currentState } } private fun handleSongPress( playlist: DiscoverFeedSection.PlaylistSection, clip: SongListData, ) { val items = playlist.items val songIndex = items.indexOf(clip).takeIf { it in 0..