From 9fb77854e4473b4ce85449d6a98f3ada15b059eb Mon Sep 17 00:00:00 2001 From: Damontecres Date: Sun, 1 Mar 2026 10:34:14 -0500 Subject: [PATCH] Unify logic for albums & artists --- .../ui/detail/music/AlbumDetailsPage.kt | 50 ++------ .../ui/detail/music/ArtistDetailsPage.kt | 113 +----------------- .../ui/detail/music/MusicViewModel.kt | 99 +++++++++++++++ .../ui/detail/music/NowPlayingViewModel.kt | 2 +- .../damontecres/wholphin/ui/nav/NavDrawer.kt | 2 +- 5 files changed, 114 insertions(+), 152 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/MusicViewModel.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt index c09c022e..03a18dc2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt @@ -35,7 +35,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text @@ -61,10 +60,8 @@ import com.github.damontecres.wholphin.ui.components.QuickDetails import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState -import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.letNotEmpty -import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState @@ -82,28 +79,26 @@ import kotlinx.coroutines.launch import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi -import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.MediaType -import timber.log.Timber import java.util.UUID @HiltViewModel(assistedFactory = AlbumViewModel.Factory::class) class AlbumViewModel @AssistedInject constructor( - private val api: ApiClient, + api: ApiClient, + musicService: MusicService, + navigationManager: NavigationManager, @param:ApplicationContext private val context: Context, - val navigationManager: NavigationManager, val serverRepository: ServerRepository, val mediaReportService: MediaReportService, private val favoriteWatchManager: FavoriteWatchManager, private val userPreferencesService: UserPreferencesService, private val backdropService: BackdropService, private val imageUrlService: ImageUrlService, - private val musicService: MusicService, @Assisted val itemId: UUID, - ) : ViewModel() { + ) : MusicViewModel(api, musicService, navigationManager) { @AssistedFactory interface Factory { fun create(itemId: UUID): AlbumViewModel @@ -175,39 +170,8 @@ class AlbumViewModel shuffled: Boolean, startIndex: Int = 0, ) { - viewModelScope.launchIO { - Timber.v("Playing album %s from %s", itemId, startIndex) - val songs = state.value.songs as ApiRequestPager<*> - musicService.setQueue(songs, startIndex, shuffled) - } - } - - fun playNext(song: BaseItem) { - viewModelScope.launchDefault { - musicService.playNext(song) - } - } - - fun addToQueue( - item: BaseItem, - index: Int, - ) { - viewModelScope.launchIO { - val songs = state.value.songs as ApiRequestPager<*> - if (item.id == this@AlbumViewModel.itemId) { - musicService.addAllToQueue(songs, 0) - } else if (item.type == BaseItemKind.AUDIO) { - musicService.addToQueue(item) - } - } - } - - fun startInstantMix() { - viewModelScope.launchIO { - Timber.v("Starting instant mix for %s", itemId) - musicService.startInstantMix(itemId) - navigationManager.navigateTo(Destination.NowPlaying) - } + val songs = state.value.songs as ApiRequestPager<*> + play(songs, startIndex, shuffled) } } @@ -297,7 +261,7 @@ fun AlbumDetailsPage( remember { MusicButtonActions( onClickPlay = { viewModel.play(it, 0) }, - onClickInstantMix = viewModel::startInstantMix, + onClickInstantMix = { viewModel.startInstantMix(album.id) }, onClickFavorite = { viewModel.setFavorite( album.id, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt index 601d1f0d..7b29a571 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text @@ -69,10 +68,8 @@ import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.letNotEmpty -import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.toBaseItems import com.github.damontecres.wholphin.util.ApiRequestPager -import com.github.damontecres.wholphin.util.BlockingList import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.LoadingState @@ -95,25 +92,24 @@ import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.request.GetItemsRequest -import timber.log.Timber import java.util.UUID @HiltViewModel(assistedFactory = ArtistViewModel.Factory::class) class ArtistViewModel @AssistedInject constructor( - private val api: ApiClient, + api: ApiClient, + musicService: MusicService, + navigationManager: NavigationManager, @param:ApplicationContext private val context: Context, - val navigationManager: NavigationManager, val serverRepository: ServerRepository, val mediaReportService: MediaReportService, private val favoriteWatchManager: FavoriteWatchManager, private val userPreferencesService: UserPreferencesService, private val backdropService: BackdropService, private val imageUrlService: ImageUrlService, - private val musicService: MusicService, @Assisted val itemId: UUID, - ) : ViewModel() { + ) : MusicViewModel(api, musicService, navigationManager) { @AssistedFactory interface Factory { fun create(itemId: UUID): ArtistViewModel @@ -211,103 +207,6 @@ class ArtistViewModel .let { BaseItem(it, false) } _state.update { it.copy(artist = artist) } } - - fun playArtist(shuffled: Boolean) { - viewModelScope.launchIO { - Timber.v("Playing artist %s", itemId) - val request = - GetItemsRequest( - parentId = itemId, - fields = DefaultItemFields, - recursive = true, - includeItemTypes = listOf(BaseItemKind.AUDIO), - sortBy = - listOf( - ItemSortBy.PREMIERE_DATE, - ItemSortBy.INDEX_NUMBER, - ), - sortOrder = listOf(SortOrder.DESCENDING, SortOrder.ASCENDING), - ) - val pager = - ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope).init() - musicService.setQueue(pager, 0, shuffled) - } - } - - fun addToQueue( - itemId: UUID, - index: Int, - ) { - viewModelScope.launchIO { - if (itemId == this@ArtistViewModel.itemId) { - // TODO - } else { - } - } - } - - fun startInstantMix() { - viewModelScope.launchIO { - Timber.v("Starting instant mix for %s", itemId) - musicService.startInstantMix(itemId) - navigationManager.navigateTo(Destination.NowPlaying) - } - } - - fun play(item: BaseItem) { - viewModelScope.launchIO { - Timber.v("Playing %s %s", item.type, item.id) - when (item.type) { - BaseItemKind.AUDIO -> { - musicService.setQueue(listOf(item), false) - } - - BaseItemKind.MUSIC_ALBUM -> { - val pager = getPagerForAlbum(api, item.id) - musicService.setQueue(pager, 0, false) - } - - BaseItemKind.MUSIC_ARTIST -> { - val pager = getPagerForArtist(api, item.id) - musicService.setQueue(pager, 0, false) - } - - else -> {} - } - } - } - - fun playNext(song: BaseItem) { - viewModelScope.launchDefault { - musicService.playNext(song) - } - } - - fun addToQueue( - item: BaseItem, - index: Int, - ) { - viewModelScope.launchIO { - Timber.v("addToQueue %s %s", item.type, item.id) - when (item.type) { - BaseItemKind.AUDIO -> { - musicService.addAllToQueue(BlockingList.of(listOf(item)), 0) - } - - BaseItemKind.MUSIC_ALBUM -> { - val pager = getPagerForAlbum(api, itemId) - musicService.addAllToQueue(pager, 0) - } - - BaseItemKind.MUSIC_ARTIST -> { - val pager = getPagerForArtist(api, item.id) - musicService.addAllToQueue(pager, 0) - } - - else -> {} - } - } - } } data class ArtistState( @@ -403,8 +302,8 @@ fun ArtistDetailsPage( actions = remember { MusicButtonActions( - onClickPlay = { viewModel.playArtist(it) }, - onClickInstantMix = viewModel::startInstantMix, + onClickPlay = { viewModel.play(artist) }, + onClickInstantMix = { viewModel.startInstantMix(artist.id) }, onClickFavorite = { viewModel.setFavorite( artist.id, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/MusicViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/MusicViewModel.kt new file mode 100644 index 00000000..c9823371 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/MusicViewModel.kt @@ -0,0 +1,99 @@ +package com.github.damontecres.wholphin.ui.detail.music + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.services.MusicService +import com.github.damontecres.wholphin.services.NavigationManager +import com.github.damontecres.wholphin.ui.launchDefault +import com.github.damontecres.wholphin.ui.launchIO +import com.github.damontecres.wholphin.ui.nav.Destination +import com.github.damontecres.wholphin.util.ApiRequestPager +import com.github.damontecres.wholphin.util.BlockingList +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.model.api.BaseItemKind +import timber.log.Timber +import java.util.UUID + +abstract class MusicViewModel( + internal val api: ApiClient, + internal val musicService: MusicService, + internal val navigationManager: NavigationManager, +) : ViewModel() { + fun play( + pager: ApiRequestPager<*>, + startIndex: Int = 0, + shuffled: Boolean = false, + ) { + viewModelScope.launchIO { + musicService.setQueue(pager, startIndex, shuffled) + } + } + + fun play( + item: BaseItem, + startIndex: Int = 0, + shuffled: Boolean = false, + ) { + viewModelScope.launchIO { + Timber.v("Playing %s %s", item.type, item.id) + when (item.type) { + BaseItemKind.AUDIO -> { + musicService.setQueue(listOf(item), shuffled) + } + + BaseItemKind.MUSIC_ALBUM -> { + val pager = getPagerForAlbum(api, item.id) + musicService.setQueue(pager, startIndex, shuffled) + } + + BaseItemKind.MUSIC_ARTIST -> { + val pager = getPagerForArtist(api, item.id) + musicService.setQueue(pager, startIndex, shuffled) + } + + else -> {} + } + } + } + + fun playNext(song: BaseItem) { + viewModelScope.launchDefault { + musicService.playNext(song) + } + } + + fun addToQueue( + item: BaseItem, + index: Int, + ) { + viewModelScope.launchIO { + Timber.v("addToQueue %s %s", item.type, item.id) + when (item.type) { + BaseItemKind.AUDIO -> { + musicService.addAllToQueue(BlockingList.of(listOf(item)), 0) + } + + BaseItemKind.MUSIC_ALBUM -> { + val pager = getPagerForAlbum(api, item.id) + musicService.addAllToQueue(pager, 0) + } + + BaseItemKind.MUSIC_ARTIST -> { + val pager = getPagerForArtist(api, item.id) + musicService.addAllToQueue(pager, 0) + } + + else -> {} + } + } + } + + fun startInstantMix(itemId: UUID) { + viewModelScope.launchIO { + Timber.v("Starting instant mix for %s", itemId) + musicService.startInstantMix(itemId) + navigationManager.navigateTo(Destination.NowPlaying) + } + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt index d78ca31f..931f2d5f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt @@ -90,7 +90,7 @@ class NowPlayingViewModel val mediaItem = onMain { player.currentMediaItemIndex - .takeIf { it >= 0 } + .takeIf { it in 0..