package com.suno.android.ui.screens.hooks.home import androidx.annotation.OptIn import androidx.lifecycle.viewModelScope import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.ProgressiveMediaSource import com.suno.android.common_core_utils.DispatcherIO import com.suno.android.common_core_utils.environment.UserPrefsDataStoreManager import com.suno.android.common_data.alerts.AlertsRepo import com.suno.android.common_mvi.MviProcessorFactory import com.suno.android.common_mvi.MviViewModel import com.suno.android.hooks.HooksFeatureGateManager import com.suno.android.hooks.HooksFeedTab import com.suno.android.hooks.HooksFeedType import com.suno.android.media.HooksOnboardingVideoPlayer import com.suno.android.media.VideoPreview import com.suno.android.ui.screens.hooks.create.song_select.HooksSongSelectController import com.suno.android.ui.screens.hooks.feed.HooksFeedController import com.suno.android.ui.screens.hooks.feed.HooksFeedEvent import com.suno.android.ui.screens.hooks.feed.HooksFeedSource import com.suno.android.ui.screens.hooks.feed.HooksFeedUiState import com.suno.android.ui.screens.hooks.home.HooksHomeState.BottomSheetState.CreateSongSelect import com.suno.android.ui.screens.hooks.shortcut_carousel.ShortcutCarouselController import dagger.Lazy import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject private const val HOOKS_ONBOARDING_VIDEO_URL = "https://cdn-o.suno.com/suno_mobile_onboarding.mp4" @OptIn(UnstableApi::class) @HiltViewModel class HooksHomeVM @Inject constructor( mviProcessorFactory: MviProcessorFactory, hooksFeedControllerFactory: HooksFeedController.Factory, shortcutCarouselControllerFactory: ShortcutCarouselController.Factory, @VideoPreview val exoPlayer: Lazy, @HooksOnboardingVideoPlayer val dataSourceFactory: CacheDataSource.Factory, private val userPrefsDataStoreManager: UserPrefsDataStoreManager, private val hooksFeatureGateManager: HooksFeatureGateManager, private val hooksSongSelectControllerFactory: HooksSongSelectController.Factory, private val alertsRepo: AlertsRepo, @DispatcherIO private val ioDispatcher: CoroutineDispatcher, ) : MviViewModel( processorFactory = mviProcessorFactory, initialState = HooksHomeState( hooksFeedState = HooksFeedUiState( isLoading = true, ), isShowHooksCreateEntryPointGateEnabled = hooksFeatureGateManager.isHooksCreateEnabled, onboardingState = null, ), ) { init { if (hooksFeatureGateManager.hooksFeedTab != HooksFeedTab.Disabled) { updateUnreadNotifications() } } private fun updateUnreadNotifications() { alertsRepo.inAppNotificationsStateFlow() .onEach { notificationsState -> updateState { oldState -> oldState.copy(hasUnreadNotifications = notificationsState.hasUnreadNotifications) } } .catch { logger.e(it) } .launchIn(viewModelScope) } val hooksFeedController = hooksFeedControllerFactory.create( coroutineScope = viewModelScope, source = HooksFeedSource.Home, startIndex = 0, ).also { it.state.onEach { hooksFeedState -> updateState { currentState -> currentState.copy(hooksFeedState = hooksFeedState) } }.launchIn(viewModelScope) } val shortcutCarouselController = shortcutCarouselControllerFactory.create( coroutineScope = viewModelScope, ) @OptIn(UnstableApi::class) override suspend fun reduceEvent( currentState: HooksHomeState, event: HooksHomeEvent, emitEffect: suspend (HooksHomeEffect) -> Unit, ): HooksHomeState = when (event) { HooksHomeEvent.OnOnboardingShow -> { val onboardingState = currentState.onboardingState if (onboardingState != null) { currentState.apply { logger.d { "Resuming onboarding video" } onboardingState.player.play() } } else { logger.d { "Initializing onboarding video" } currentState.copy( onboardingState = HooksHomeState.OnboardingState( player = exoPlayer.get().apply { addMediaSource( ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource( MediaItem.fromUri( HOOKS_ONBOARDING_VIDEO_URL, ), ), ) repeatMode = Player.REPEAT_MODE_ONE playWhenReady = true prepare() }, ), ) } } is HooksHomeEvent.OnHooksFeedEvent -> { hooksFeedController.sendEvent(event.hooksFeedEvent) currentState } HooksHomeEvent.OnOnboardingDismiss -> { viewModelScope.launch { userPrefsDataStoreManager.setHasSeenHooksOnboarding() } viewModelScope.launch(ioDispatcher) { dataSourceFactory.cache?.removeResource(HOOKS_ONBOARDING_VIDEO_URL) } currentState.onboardingState?.player?.release() hooksFeedController.sendEvent(HooksFeedEvent.DismissOnboardingModalToUnmute) currentState.copy( onboardingState = null, ) } HooksHomeEvent.NavReTapped -> { if (currentState.hooksFeedState.currentIndex == 0 && hooksFeatureGateManager.hooksFeedType == HooksFeedType.Carousel ) { emitEffect(HooksHomeEffect.ExpandHooksCarousel) } else { hooksFeedController.sendEvent(HooksFeedEvent.LoadFeed(resetScroll = true)) currentState.hooksFeedState.currentHook?.let { hook -> emitAnalyticEffect(HooksHomeAnalyticEffect.OnRefreshFeedTapped(hook)) } } currentState } HooksHomeEvent.OnCreateClick -> { currentState.copy( bottomSheetState = CreateSongSelect( hooksSongSelectControllerFactory.create( parentCoroutineScope = viewModelScope, ), ), ) } is HooksHomeEvent.OnCloseBottomSheetRequested -> { val currentBottomState = currentState.bottomSheetState if (currentBottomState is HooksHomeState.BottomSheetState.HasController) { currentBottomState.controller.onClear() } currentState.copy( bottomSheetState = HooksHomeState.BottomSheetState.Hidden, ) } HooksHomeEvent.OnUiHidden -> { currentState.onboardingState?.run { logger.d { "Pausing onboarding video" } player.pause() } currentState } HooksHomeEvent.OnUiVisible -> { viewModelScope.launch { val hasSeenOnboarding = userPrefsDataStoreManager.getHasSeenHooksOnboarding().firstOrNull() if (hasSeenOnboarding != true) { sendEvent(HooksHomeEvent.OnOnboardingShow) } } currentState } } override fun onCleared() { super.onCleared() state.value.onboardingState?.player?.release() } }