package com.suno.android.ui.screens.omni import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse import com.suno.android.clip.UpdateClipReactionUseCase import com.suno.android.common_core_utils.Id import com.suno.android.common_core_utils.SunoLogger import com.suno.android.common_core_utils.constants.ReactionType import com.suno.android.common_core_utils.extensions.xAsMinutesSecondsTimecode import com.suno.android.common_core_utils.model.Url import com.suno.android.common_data.extensions.xIsOwnedBy import com.suno.android.common_data.mappers.clips.LocalClipData import com.suno.android.common_data.mappers.clips.SongListData import com.suno.android.common_data.mappers.clips.xAsSongListDataOrNull import com.suno.android.common_data.repos.ClipAction import com.suno.android.common_data.repos.ClipMetricsRepository import com.suno.android.common_data.repos.ClipsRepository import com.suno.android.common_data.repos.ShareLinkRepository import com.suno.android.common_data.user.UserSessionRepository import com.suno.android.common_networking.extensions.toThrowable import com.suno.android.common_networking.remote.session.User import com.suno.android.common_ui.components.bottom_sheet.SharePlatformConstants import com.suno.android.common_ui.components.omni.RepeatMode import com.suno.android.gating.Feature import com.suno.android.gating.FeatureManager import com.suno.android.media.CastSessionState import com.suno.android.media.MediaManager import com.suno.android.media.MediaMetadataManager import com.suno.android.media.MediaPlayerState import com.suno.android.review.AppReviewManager import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class OmniScreenVM @Inject constructor( loggerFactory: SunoLogger.Factory, private val userSessionRepository: UserSessionRepository, private val mediaManager: MediaManager, private val clipsRepository: ClipsRepository, private val shareLinkRepository: ShareLinkRepository, private val appReviewManager: AppReviewManager, private val mediaMetadataManager: MediaMetadataManager, private val featureManager: FeatureManager, private val clipMetricsRepository: ClipMetricsRepository, private val updateClipReactionUseCase: UpdateClipReactionUseCase, ) : ViewModel() { private val logger = loggerFactory.create(this@OmniScreenVM) private val _viewStateFlow: MutableStateFlow = MutableStateFlow( UiState( songList = mediaManager.getMediaPlayerState().localClipDataQueue.toPersistentList(), currentSongIndex = mediaManager.getCurrentlyPlayingIndex(), // TODO: this needs to be fixed!!! isRemixGateEnabled = featureManager.hasFeature(Feature.RemixClip), ), ) private var _currentViewer: User? = null fun viewStateFlow(): StateFlow = _viewStateFlow.asStateFlow() private val _effects = MutableSharedFlow() fun effects() = _effects.asSharedFlow() init { mediaManager.mediaPlayerFlow() .onEach { mediaPlayerState: MediaPlayerState -> _viewStateFlow.update { oldState -> oldState.copy( currentSongIndex = mediaPlayerState.nowPlayingClipIndex, songList = mediaPlayerState.localClipDataQueue.toPersistentList(), isPlaying = mediaPlayerState.isPlaying, isShuffling = mediaPlayerState.isShuffling, repeatMode = mediaPlayerState.repeatMode, progressPercentage = mediaPlayerState.percentageComplete, timestamp = mediaPlayerState.playtimeDuration.xAsMinutesSecondsTimecode(), ) } }.onEach { it.nowPlayingClipData()?.let(::updateClipWithLyricsIfNeeded) }.catch { exception -> logger.e(exception) }.launchIn(viewModelScope) mediaManager.getCastManager().castSessionStateFlow() .onEach { castSessionState: CastSessionState -> _viewStateFlow.update { oldState -> oldState.copy( castSessionState = castSessionState, ) } }.catch { exception -> logger.e(exception) }.launchIn(viewModelScope) userSessionRepository.sessionConfigurationStateFlow() .onEach { sessionConfiguration -> _currentViewer = sessionConfiguration.user }.catch { exception -> logger.e(exception) }.launchIn(viewModelScope) mediaMetadataManager.mediaReactionFlow() .onEach { reaction -> _viewStateFlow.value.songList.find { it.clipId == reaction.clipId }?.let { song -> if (song.reaction != reaction.reaction) { mediaManager.updateClip( song, song.copy(reaction = reaction.reaction), ) } } }.catch { exception -> logger.e(exception) }.launchIn(viewModelScope) mediaMetadataManager.mediaVisibilityFlow() .onEach { visibility -> _viewStateFlow.value.songList.find { it.clipId == visibility.clipId }?.let { song -> if (song.isPublic != visibility.isPublic) { mediaManager.updateClip( song, song.copy(isPublic = visibility.isPublic), ) } } }.catch { exception -> logger.e(exception) }.launchIn(viewModelScope) mediaMetadataManager.mediaRemixabilityFlow() .onEach { canRemix -> _viewStateFlow.value.songList.find { it.clipId == canRemix.clipId }?.let { song -> if (song.canRemix != canRemix.canRemix) { mediaManager.updateClip( song, song.copy(canRemix = canRemix.canRemix), ) } } }.catch { exception -> logger.e(exception) }.launchIn(viewModelScope) _viewStateFlow.update { oldState -> oldState.copy( isShowRepeatGateEnabled = featureManager.hasFeature(Feature.ShowRepeatButton), isShowShuffleGateEnabled = featureManager.hasFeature(Feature.ShowShuffleButton), isShowShareVideoGateEnabled = featureManager.hasFeature(Feature.ShowShareVideo), isShowCastGateEnabled = featureManager.hasFeature(Feature.ShowCast), ) } } fun provideExoPlayer() = mediaManager.getPlayer() fun moveToNextSong() { mediaManager.moveToNextSong() } fun resetSnackBar() { _viewStateFlow.update { oldState -> oldState.copy( snackBarMessage = "", ) } } fun onPageSelected( pageIndex: Int?, ) { mediaManager.setCurrentlyPlayingIndex(pageIndex) } fun onShuffleClicked() { mediaManager.setIsShuffling(!mediaManager.isShuffling()) } fun onRepeatModeClicked() { mediaManager.cycleRepeatMode() } fun onPlayPauseClicked() { mediaManager.setIsPlaying(!mediaManager.isPlaying()) } fun onLeftBumperClicked() { mediaManager.scrubBackward() } fun onRightBumperClicked() { mediaManager.scrubForward() } fun onReplayClicked() { mediaManager.scrubToZero() } fun onCastClicked() { if (mediaManager.getCastManager().isCasting()) { mediaManager.getCastManager().stopCasting() } else { mediaManager.getCastManager().startCasting() } } fun onCastDialogDismissed() { mediaManager.getCastManager().dismissCastDialog() } fun onCastError( exception: Exception, ) { logger.e(exception) } fun onScrubbed( scrubPercentage: Float, ) { mediaManager.scrubToPercent(scrubPercentage) } fun onLikeClicked( localClipData: LocalClipData, ) { updateReaction(localClipData, requestedReactionType = ReactionType.LIKE) // Current reaction is not like, so this will update reaction to like if (localClipData.reaction != ReactionType.LIKE) { viewModelScope.launch { appReviewManager.onLike() } } } private fun updateReaction( localClipData: LocalClipData, requestedReactionType: ReactionType, ) { viewModelScope.launch { updateClipReactionUseCase( clipId = localClipData.clipId, currentReactionType = localClipData.reaction, requestedReactionType = requestedReactionType, ) } } fun handleSongDeleted( song: SongListData, ) { mediaManager.removeClipById(song.id) } fun handleSongRenamed( renamedSong: SongListData, ) { _viewStateFlow.value.songList.find { it.clipId == renamedSong.id }?.let { song -> if (song.nowPlayingTitle != renamedSong.title) { mediaManager.updateClip( song, song.copy(nowPlayingTitle = renamedSong.title), ) } } } fun startShare( songId: Id, platform: SharePlatformConstants, ) = when (platform) { is SharePlatformConstants.Link -> startShareLink(songId, platform) is SharePlatformConstants.Video -> startShareVideo(songId, platform) } private fun startShareLink( songId: Id, platform: SharePlatformConstants.Link, ) { viewModelScope.launch { val shareLink = shareLinkRepository.getSongShareLink(songId, platform.backendValue).getOrElse { error -> logger.e(error.toThrowable()) return@launch } shareLink?.let { link -> _effects.emit( OmniScreenEffects.ShareLink( songId = songId, sharePlatform = platform, link = link, ), ) } } } private fun startShareVideo( songId: Id, platform: SharePlatformConstants.Video, ) { viewModelScope.launch { _effects.emit( OmniScreenEffects.ShareVideo( songId = songId, sharePlatform = platform, ), ) } } fun onSongShared( songId: Id, ) = viewModelScope.launch { clipMetricsRepository.incrementActionCount( clipId = songId, action = ClipAction.SHARE, ) appReviewManager.onShare() } private fun updateClipWithLyricsIfNeeded( clip: LocalClipData, ) { if (clip.displayLyrics != null) return viewModelScope.launch { val result = clipsRepository.getLyricsForClip(clip.clipId).getOrElse { error -> logger.e(error) return@launch } val lyricsTextString = result.alignedWords?.joinToString("") { it.word } val updatedLocalClip = clip.copy( lyricsString = lyricsTextString, ) mediaManager.updateClip( oldLocalClipData = clip, newLocalClipData = updatedLocalClip, ) } } fun canRemixCurrentSong(): Boolean { val currentSong = _viewStateFlow.value.currentSong return currentSong?.canRemix == true || currentSong?.xAsSongListDataOrNull()?.xIsOwnedBy(_currentViewer) == true } data class UiState( val currentSongIndex: Int? = null, val songList: ImmutableList, val isPlaying: Boolean = true, val isShuffling: Boolean = true, val repeatMode: RepeatMode = RepeatMode.REPEAT_MODE_NONE, val isShowRepeatGateEnabled: Boolean = false, val isShowShuffleGateEnabled: Boolean = false, val isShowShareVideoGateEnabled: Boolean = false, val isShowCastGateEnabled: Boolean = false, val timestamp: String = "0:00", val progress: Float = 0f, val progressPercentage: Float = 0f, val snackBarMessage: String = "", val castSessionState: CastSessionState = CastSessionState(), val isRemixGateEnabled: Boolean = false, ) { @Stable val currentSong: LocalClipData? get() = currentSongIndex?.let { songList.getOrNull(it) } fun currentSongId() = currentSong?.clipId fun currentSongAuthorId() = currentSong?.artistUserId fun currentSongAuthorHandle() = currentSong?.handle @Stable val shouldShowCastButton: Boolean get() = isShowCastGateEnabled && castSessionState.isAvailable } sealed interface OmniScreenEffects { data class ShareLink( val songId: Id, val sharePlatform: SharePlatformConstants.Link, val link: Url, ) : OmniScreenEffects data class ShareVideo( val songId: Id, val sharePlatform: SharePlatformConstants.Video, ) : OmniScreenEffects } }