package com.example.ui.viewmodel import android.app.Application import android.content.Intent import android.net.Uri import android.os.Build import androidx.core.content.ContextCompat import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import androidx.media3.common.util.UnstableApi import com.example.data.db.FavoriteEntity import com.example.data.db.PlayHistoryEntity import com.example.data.db.PlaylistEntity import com.example.data.db.PlaylistItemEntity import com.example.data.db.SwipePlayDatabase import com.example.data.mediastore.MediaStoreScanner import com.example.data.model.MediaItemData import com.example.data.model.MediaTypeFilter import com.example.data.model.RepeatMode import com.example.data.model.SortOption import com.example.data.model.SortOrder import com.example.data.model.VideoScaleMode import com.example.data.prefs.UserPreferencesRepository import com.example.playback.PlaybackManager import com.example.playback.SwipePlayMediaService import com.example.ui.navigation.Screen import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @UnstableApi class MainViewModel(application: Application) : AndroidViewModel(application) { private val database = SwipePlayDatabase.getDatabase(application) private val dao = database.swipePlayDao() private val scanner = MediaStoreScanner(application) val prefs = UserPreferencesRepository(application) private val _currentScreen = MutableStateFlow(Screen.SwipeFeed) val currentScreen: StateFlow = _currentScreen.asStateFlow() private val _allMedia = MutableStateFlow>(emptyList()) val allMedia: StateFlow> = _allMedia.asStateFlow() private val _isScanning = MutableStateFlow(false) val isScanning: StateFlow = _isScanning.asStateFlow() private val _searchQuery = MutableStateFlow("") val searchQuery: StateFlow = _searchQuery.asStateFlow() val feedFilter: StateFlow = prefs.feedFilterFlow .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MediaTypeFilter.ALL) val videoScaleMode: StateFlow = prefs.videoScaleModeFlow .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), VideoScaleMode.FIT) val musicSortOption: StateFlow = prefs.musicSortOptionFlow .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), SortOption.NAME) val musicSortOrder: StateFlow = prefs.musicSortOrderFlow .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), SortOrder.ASCENDING) val isGridLayout: StateFlow = prefs.isGridLayoutFlow .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) val resumePlaybackPref: StateFlow = prefs.resumePlaybackFlow .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true) val backgroundAudioPref: StateFlow = prefs.backgroundAudioFlow .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true) val playbackSpeedPref: StateFlow = prefs.playbackSpeedFlow .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 1.0f) val reducedMotionPref: StateFlow = prefs.reducedMotionFlow .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) val favorites: StateFlow> = dao.getAllFavorites() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) val history: StateFlow> = dao.getHistory() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) val playlists: StateFlow> = dao.getAllPlaylists() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) private val _selectedPlaylistId = MutableStateFlow(null) val selectedPlaylistId: StateFlow = _selectedPlaylistId.asStateFlow() private val _playlistItems = MutableStateFlow>(emptyList()) val playlistItems: StateFlow> = _playlistItems.asStateFlow() private val _activeFolder = MutableStateFlow(null) val activeFolder: StateFlow = _activeFolder.asStateFlow() private val _showAudioPlayerSheet = MutableStateFlow(false) val showAudioPlayerSheet: StateFlow = _showAudioPlayerSheet.asStateFlow() private val _mediaDetailsItem = MutableStateFlow(null) val mediaDetailsItem: StateFlow = _mediaDetailsItem.asStateFlow() // Feed items based on current feedFilter and allMedia val feedMediaItems: StateFlow> = combine(_allMedia, feedFilter) { list, filter -> when (filter) { MediaTypeFilter.ALL -> list MediaTypeFilter.MUSIC -> list.filter { !it.isVideo } MediaTypeFilter.VIDEOS -> list.filter { it.isVideo } } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) // Music filtered and sorted val musicItems: StateFlow> = combine(_allMedia, _searchQuery, musicSortOption, musicSortOrder) { list, query, sort, order -> val filtered = list.filter { !it.isVideo && matchesQuery(it, query) } sortMedia(filtered, sort, order) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) // Videos filtered and sorted val videoItems: StateFlow> = combine(_allMedia, _searchQuery, musicSortOption, musicSortOrder) { list, query, sort, order -> val filtered = list.filter { it.isVideo && matchesQuery(it, query) } sortMedia(filtered, sort, order) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) private val _libraryFilter = MutableStateFlow(MediaTypeFilter.ALL) val libraryFilter: StateFlow = _libraryFilter.asStateFlow() fun setLibraryFilter(filter: MediaTypeFilter) { _libraryFilter.value = filter } // Unified library media items combining both audio and video filtered by title/artist val libraryItems: StateFlow> = combine( _allMedia, _searchQuery, _libraryFilter, musicSortOption, musicSortOrder ) { list, query, filter, sort, order -> val byType = when (filter) { MediaTypeFilter.ALL -> list MediaTypeFilter.MUSIC -> list.filter { !it.isVideo } MediaTypeFilter.VIDEOS -> list.filter { it.isVideo } } val filtered = byType.filter { matchesQuery(it, query) } sortMedia(filtered, sort, order) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) private val _initialFeedIndex = MutableStateFlow(0) val initialFeedIndex: StateFlow = _initialFeedIndex.asStateFlow() init { PlaybackManager.getOrCreatePlayer(application) // Wire position save callback to Room database and UserPreferences PlaybackManager.onSaveHistoryCallback = { item, posMs, completed -> viewModelScope.launch { dao.insertOrUpdateHistory( PlayHistoryEntity( id = item.id, mediaUri = item.contentUri, isVideo = item.isVideo, title = item.title, artist = item.artist, album = item.album, durationMs = item.durationMs, lastPositionMs = posMs, playedAt = System.currentTimeMillis(), completed = completed ) ) prefs.saveLastPlayed(item.id, item.contentUri, posMs, item.isVideo, item.title) } } // Initialize initial preferences into PlaybackManager viewModelScope.launch { val scaleMode = prefs.videoScaleModeFlow.first() PlaybackManager.setVideoScaleMode(scaleMode) val speed = prefs.playbackSpeedFlow.first() PlaybackManager.setPlaybackSpeed(speed) val repeat = prefs.repeatModeFlow.first() PlaybackManager.setRepeatMode(repeat) val shuffle = prefs.shuffleFlow.first() if (shuffle) PlaybackManager.toggleShuffle() } // Start background media service startMediaService() // Run initial scan scanMedia() } fun navigateTo(screen: Screen) { _currentScreen.value = screen } fun setSearchQuery(query: String) { _searchQuery.value = query } fun setFeedFilter(filter: MediaTypeFilter) { viewModelScope.launch { prefs.setFeedFilter(filter) } } fun setVideoScaleMode(mode: VideoScaleMode) { viewModelScope.launch { prefs.setVideoScaleMode(mode) PlaybackManager.setVideoScaleMode(mode) } } fun toggleVideoScaleMode() { val newMode = if (PlaybackManager.videoScaleMode.value == VideoScaleMode.FIT) VideoScaleMode.FILL else VideoScaleMode.FIT setVideoScaleMode(newMode) } fun setMusicSort(option: SortOption, order: SortOrder) { viewModelScope.launch { prefs.setMusicSort(option, order) } } fun setIsGridLayout(isGrid: Boolean) { viewModelScope.launch { prefs.setIsGridLayout(isGrid) } } fun setResumePlayback(enabled: Boolean) { viewModelScope.launch { prefs.setResumePlayback(enabled) } } fun setBackgroundAudio(enabled: Boolean) { viewModelScope.launch { prefs.setBackgroundAudio(enabled) } } fun setPlaybackSpeed(speed: Float) { viewModelScope.launch { prefs.setPlaybackSpeed(speed) PlaybackManager.setPlaybackSpeed(speed) } } fun setRepeatMode(mode: RepeatMode) { viewModelScope.launch { prefs.setRepeatMode(mode) PlaybackManager.setRepeatMode(mode) } } fun toggleShuffle() { viewModelScope.launch { PlaybackManager.toggleShuffle() prefs.setShuffle(PlaybackManager.isShuffleEnabled.value) } } fun setReducedMotion(enabled: Boolean) { viewModelScope.launch { prefs.setReducedMotion(enabled) } } fun scanMedia() { viewModelScope.launch { _isScanning.value = true try { val scanned = scanner.scanAllMedia(includeDemoIfEmpty = true) _allMedia.value = scanned restoreLastPlayedMedia(scanned) } catch (_: Exception) { } finally { _isScanning.value = false } } } private suspend fun restoreLastPlayedMedia(mediaList: List) { if (mediaList.isEmpty()) return val lastInfo = prefs.lastPlayedFlow.first() val latestHistory = if (lastInfo == null) dao.getLatestHistoryItem() else null val targetId = lastInfo?.mediaId ?: latestHistory?.id val targetUri = lastInfo?.mediaUri ?: latestHistory?.mediaUri val savedPos = lastInfo?.positionMs ?: latestHistory?.lastPositionMs ?: 0L val isCompleted = latestHistory?.completed ?: false val matchedItem = mediaList.find { it.id == targetId || it.contentUri == targetUri } ?: mediaList.firstOrNull() if (matchedItem != null) { val startPos = if (isCompleted) 0L else savedPos.coerceAtLeast(0L) val currentFilter = feedFilter.value val filteredList = when (currentFilter) { MediaTypeFilter.ALL -> mediaList MediaTypeFilter.MUSIC -> mediaList.filter { !it.isVideo } MediaTypeFilter.VIDEOS -> mediaList.filter { it.isVideo } }.ifEmpty { mediaList } val index = filteredList.indexOfFirst { it.id == matchedItem.id }.let { if (it >= 0) it else 0 } _initialFeedIndex.value = index if (PlaybackManager.currentMediaItem.value == null) { PlaybackManager.setQueue(filteredList, index, autoPlay = false, startPosMs = startPos) } } } fun importMediaUri(uri: Uri) { viewModelScope.launch { try { val item = scanner.resolveMediaUri(uri) val current = _allMedia.value.toMutableList() current.add(0, item) _allMedia.value = current PlaybackManager.playMediaItem(item, 0L, autoPlay = true) } catch (_: Exception) { } } } fun playMedia(item: MediaItemData, playlistContext: List? = null) { viewModelScope.launch { val queueList = playlistContext ?: _allMedia.value val index = queueList.indexOfFirst { it.id == item.id } var startPos = 0L if (resumePlaybackPref.value) { val savedHistory = dao.getHistoryItem(item.id) if (savedHistory != null && !savedHistory.completed && savedHistory.lastPositionMs > 3000) { startPos = savedHistory.lastPositionMs } } if (index >= 0) { PlaybackManager.setQueue(queueList, index, autoPlay = true, startPosMs = startPos) } else { PlaybackManager.playMediaItem(item, startPositionMs = startPos, autoPlay = true) } if (!item.isVideo) { _showAudioPlayerSheet.value = true } } } fun resumeFromHistory(historyItem: PlayHistoryEntity) { val match = _allMedia.value.find { it.id == historyItem.id || it.contentUri == historyItem.mediaUri } if (match != null) { val startPos = if (historyItem.completed) 0L else historyItem.lastPositionMs PlaybackManager.playMediaItem(match, startPositionMs = startPos, autoPlay = true) if (!match.isVideo) { _showAudioPlayerSheet.value = true } } } fun showAudioPlayer(show: Boolean) { _showAudioPlayerSheet.value = show } fun showMediaDetails(item: MediaItemData?) { _mediaDetailsItem.value = item } fun setActiveFolder(folder: String?) { _activeFolder.value = folder } fun selectPlaylist(playlistId: Long?) { _selectedPlaylistId.value = playlistId if (playlistId != null) { viewModelScope.launch { dao.getItemsForPlaylist(playlistId).collect { _playlistItems.value = it } } } else { _playlistItems.value = emptyList() } } fun createPlaylist(name: String) { if (name.isBlank()) return viewModelScope.launch { val id = dao.insertPlaylist(PlaylistEntity(name = name.trim())) selectPlaylist(id) } } fun renamePlaylist(playlistId: Long, newName: String) { if (newName.isBlank()) return viewModelScope.launch { dao.renamePlaylist(playlistId, newName.trim()) } } fun deletePlaylist(playlistId: Long) { viewModelScope.launch { dao.deletePlaylist(playlistId) dao.clearPlaylistItems(playlistId) if (_selectedPlaylistId.value == playlistId) { _selectedPlaylistId.value = null } } } fun addToPlaylist(playlistId: Long, item: MediaItemData) { viewModelScope.launch { dao.insertPlaylistItem( PlaylistItemEntity( playlistId = playlistId, mediaUri = item.contentUri, isVideo = item.isVideo, title = item.title, artist = item.artist, album = item.album, durationMs = item.durationMs, orderIndex = _playlistItems.value.size ) ) } } fun removeFromPlaylist(playlistId: Long, mediaUri: String) { viewModelScope.launch { dao.removePlaylistItem(playlistId, mediaUri) } } fun toggleFavorite(item: MediaItemData) { viewModelScope.launch { val isFav = favorites.value.any { it.id == item.id } if (isFav) { dao.removeFavorite(item.id) } else { dao.insertFavorite( FavoriteEntity( id = item.id, mediaUri = item.contentUri, isVideo = item.isVideo, title = item.title, artist = item.artist, album = item.album, durationMs = item.durationMs ) ) } } } fun isFavorite(itemId: String): Boolean { return favorites.value.any { it.id == itemId } } fun clearHistory() { viewModelScope.launch { dao.clearHistory() } } fun clearAppCache() { viewModelScope.launch { try { getApplication().cacheDir.deleteRecursively() } catch (_: Exception) {} } } private fun startMediaService() { try { val intent = Intent(getApplication(), SwipePlayMediaService::class.java) getApplication().startService(intent) } catch (_: Exception) {} } private fun matchesQuery(item: MediaItemData, query: String): Boolean { if (query.isBlank()) return true val q = query.lowercase() return item.title.lowercase().contains(q) || item.artist.lowercase().contains(q) || item.album.lowercase().contains(q) || item.folderName.lowercase().contains(q) } private fun sortMedia(list: List, option: SortOption, order: SortOrder): List { val sorted = when (option) { SortOption.NAME -> list.sortedBy { it.title.lowercase() } SortOption.ARTIST -> list.sortedBy { it.artist.lowercase() } SortOption.ALBUM -> list.sortedBy { it.album.lowercase() } SortOption.DURATION -> list.sortedBy { it.durationMs } SortOption.DATE_ADDED -> list.sortedBy { it.dateAdded } SortOption.SIZE -> list.sortedBy { it.sizeBytes } } return if (order == SortOrder.DESCENDING) sorted.reversed() else sorted } }