mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-05 18:30:59 +01:00
Selecting a song on the search page navigates to a song's album with the song focused (#1661)
## Description Selecting a song from search now resolves to its album's detail page, scrolled to and focusing that song, instead of sending you to an "unsupported item type" page. Implemented in the shared BaseItem.destination() rather than in search specifically, so any caller that navigates to a song gets the same behavior. A song with no albumId plays directly while remaining in the search results page: NavigationManager.navigateTo() intercepts a bare AUDIO destination and hands it to a new MusicService.playSong(). Handling this in the single navigation funnel means every caller gets it for free with no loading-screen flash. Adds BaseItemDestinationTest covering the new destination() logic. ### Related issues https://github.com/damontecres/Wholphin/issues/1656 ### Testing Device: Sony Bravia 4K VH21 #### Tests on a search results page, song with albumId: 1. Select a song sends you to album page with cursor on song. Another press plays it 2. Back button brings you back to search results. Tested with both songs at the start of an album and songs deep into a 100+ songs album. #### Tests on a search results page, song with no albumId: 1. Select a songs plays the song while staying in the search results page. No loading screen. ## AI or LLM usage I used Cursor and Claude Code for planning, implementation and code review. --------- Co-authored-by: Damontecres <damontecres@gmail.com>
This commit is contained in:
parent
79cb93db08
commit
438c9dd704
9 changed files with 394 additions and 20 deletions
|
|
@ -272,6 +272,16 @@ data class BaseItem(
|
|||
}
|
||||
}
|
||||
|
||||
BaseItemKind.AUDIO -> {
|
||||
data.albumId?.let { albumId ->
|
||||
Destination.MediaItem(
|
||||
itemId = albumId,
|
||||
type = BaseItemKind.MUSIC_ALBUM,
|
||||
initialSongId = id,
|
||||
)
|
||||
} ?: Destination.MediaItem(this)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Destination.MediaItem(this)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,13 +75,7 @@ class CollectionFolderMusicViewModel
|
|||
index: Int,
|
||||
item: BaseItem,
|
||||
) {
|
||||
if (item.type == BaseItemKind.AUDIO) {
|
||||
viewModelScope.launchDefault {
|
||||
musicService.setQueue(listOf(item), false)
|
||||
}
|
||||
} else {
|
||||
navigationManager.navigateTo(item.destination())
|
||||
}
|
||||
navigationManager.navigateTo(item.destination())
|
||||
}
|
||||
|
||||
fun onClickPlayAll(shuffle: Boolean) {
|
||||
|
|
|
|||
|
|
@ -124,11 +124,15 @@ class AlbumViewModel
|
|||
private val backdropService: BackdropService,
|
||||
private val imageUrlService: ImageUrlService,
|
||||
private val extrasService: ExtrasService,
|
||||
@Assisted itemId: UUID,
|
||||
@Assisted("itemId") itemId: UUID,
|
||||
@Assisted("initialSongId") private val initialSongId: UUID?,
|
||||
) : MusicViewModel(itemId, context, api, musicService, navigationManager, mediaManagementService) {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(itemId: UUID): AlbumViewModel
|
||||
fun create(
|
||||
@Assisted("itemId") itemId: UUID,
|
||||
@Assisted("initialSongId") initialSongId: UUID?,
|
||||
): AlbumViewModel
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow(AlbumState.EMPTY)
|
||||
|
|
@ -158,6 +162,10 @@ class AlbumViewModel
|
|||
val songsDeferred = async { getPagerForAlbum(api, itemId) }
|
||||
val album = itemDeferred.await()
|
||||
val songs = songsDeferred.await()
|
||||
val initialSongIndex =
|
||||
initialSongId?.let { id ->
|
||||
songs.indexOfBlocking { it?.id == id }.takeIf { it >= 0 }
|
||||
}
|
||||
val imageUrl = imageUrlService.getItemImageUrl(album, ImageType.PRIMARY)
|
||||
val allArtists =
|
||||
album.data.artists.orEmpty() +
|
||||
|
|
@ -175,6 +183,7 @@ class AlbumViewModel
|
|||
isVariousArtists = isVariousArtists,
|
||||
imageUrl = imageUrl,
|
||||
songs = songs,
|
||||
initialSongIndex = initialSongIndex,
|
||||
loading = LoadingState.Success,
|
||||
)
|
||||
}
|
||||
|
|
@ -289,6 +298,7 @@ data class AlbumState(
|
|||
val songs: List<BaseItem?>,
|
||||
val similar: List<BaseItem>,
|
||||
val loading: LoadingState,
|
||||
val initialSongIndex: Int? = null,
|
||||
val musicVideos: List<BaseItem?> = emptyList(),
|
||||
val extras: List<ExtrasItem> = emptyList(),
|
||||
val canDelete: Boolean = false,
|
||||
|
|
@ -309,9 +319,10 @@ fun AlbumDetailsPage(
|
|||
itemId: UUID,
|
||||
preferences: UserPreferences,
|
||||
modifier: Modifier = Modifier,
|
||||
initialSongId: UUID? = null,
|
||||
viewModel: AlbumViewModel =
|
||||
hiltViewModel<AlbumViewModel, AlbumViewModel.Factory>(
|
||||
creationCallback = { it.create(itemId) },
|
||||
creationCallback = { it.create(itemId, initialSongId) },
|
||||
),
|
||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
) {
|
||||
|
|
@ -320,7 +331,6 @@ fun AlbumDetailsPage(
|
|||
val state by viewModel.state.collectAsState()
|
||||
val currentMusic by viewModel.currentMusic.collectAsState()
|
||||
|
||||
var position by rememberPosition(0, 0)
|
||||
val focusRequesters =
|
||||
remember { List(SIMILAR_ROW + 1) { FocusRequester() } }
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
|
@ -356,12 +366,20 @@ fun AlbumDetailsPage(
|
|||
|
||||
LoadingState.Success -> {
|
||||
val album = state.album!!
|
||||
val itemsBefore = 2
|
||||
val initialSongIndex = state.initialSongIndex
|
||||
var position by rememberPosition(
|
||||
row = if (initialSongIndex != null) SONG_ROW else 0,
|
||||
column = initialSongIndex ?: 0,
|
||||
)
|
||||
|
||||
val firstFocusRequester = remember { FocusRequester() }
|
||||
val firstBringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
val listState = rememberLazyListState()
|
||||
val itemsBefore = 2
|
||||
val listState =
|
||||
rememberLazyListState(
|
||||
initialFirstVisibleItemIndex = initialSongIndex?.let { itemsBefore + it } ?: 0,
|
||||
)
|
||||
|
||||
val songFocusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ fun MusicExpandableButtons(
|
|||
canDelete: Boolean,
|
||||
buttonOnFocusChanged: (FocusState) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
showShuffle: Boolean = true,
|
||||
) {
|
||||
val firstFocus = remember { FocusRequester() }
|
||||
LazyRow(
|
||||
|
|
@ -53,13 +54,15 @@ fun MusicExpandableButtons(
|
|||
.onFocusChanged(buttonOnFocusChanged),
|
||||
)
|
||||
}
|
||||
item("shuffle") {
|
||||
ExpandableFaButton(
|
||||
title = R.string.shuffle,
|
||||
iconStringRes = R.string.fa_shuffle,
|
||||
onClick = { actions.onClickPlay.invoke(true) },
|
||||
modifier = Modifier.onFocusChanged(buttonOnFocusChanged),
|
||||
)
|
||||
if (showShuffle) {
|
||||
item("shuffle") {
|
||||
ExpandableFaButton(
|
||||
title = R.string.shuffle,
|
||||
iconStringRes = R.string.fa_shuffle,
|
||||
onClick = { actions.onClickPlay.invoke(true) },
|
||||
modifier = Modifier.onFocusChanged(buttonOnFocusChanged),
|
||||
)
|
||||
}
|
||||
}
|
||||
item("instant_mix") {
|
||||
ExpandableFaButton(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,270 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.music
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.ImageUrlService
|
||||
import com.github.damontecres.wholphin.services.MediaManagementService
|
||||
import com.github.damontecres.wholphin.services.MusicService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
|
||||
import com.github.damontecres.wholphin.ui.components.ContextMenu
|
||||
import com.github.damontecres.wholphin.ui.components.ContextMenuDialog
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.MusicContextActions
|
||||
import com.github.damontecres.wholphin.ui.components.Optional
|
||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import java.util.UUID
|
||||
|
||||
@HiltViewModel(assistedFactory = SongViewModel.Factory::class)
|
||||
class SongViewModel
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@ApplicationContext context: Context,
|
||||
api: ApiClient,
|
||||
musicService: MusicService,
|
||||
navigationManager: NavigationManager,
|
||||
mediaManagementService: MediaManagementService,
|
||||
private val favoriteWatchManager: FavoriteWatchManager,
|
||||
private val imageUrlService: ImageUrlService,
|
||||
@Assisted itemId: UUID,
|
||||
) : MusicViewModel(itemId, context, api, musicService, navigationManager, mediaManagementService) {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(itemId: UUID): SongViewModel
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow(SongState.EMPTY)
|
||||
val state: StateFlow<SongState> = _state
|
||||
|
||||
init {
|
||||
init()
|
||||
viewModelScope.launchDefault {
|
||||
mediaManagementService.collectCanDelete(state.map { it.song }) { canDelete ->
|
||||
_state.update { it.copy(canDelete = canDelete) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
viewModelScope.launchIO {
|
||||
try {
|
||||
val song =
|
||||
api.userLibraryApi
|
||||
.getItem(itemId = itemId)
|
||||
.content
|
||||
.let { BaseItem(it, false) }
|
||||
val imageUrl = imageUrlService.getItemImageUrl(song, ImageType.PRIMARY)
|
||||
_state.update {
|
||||
it.copy(
|
||||
song = song,
|
||||
imageUrl = imageUrl,
|
||||
loading = LoadingState.Success,
|
||||
)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
_state.update { it.copy(loading = LoadingState.Error(ex)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setFavorite(
|
||||
itemId: UUID,
|
||||
favorite: Boolean,
|
||||
) = viewModelScope.launch(ExceptionHandler() + WholphinDispatchers.IO) {
|
||||
// The toggle returns the updated user data, so no refetch of the song is needed
|
||||
val userData = favoriteWatchManager.setFavorite(itemId, favorite)
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
song = state.song?.let { it.copy(data = it.data.copy(userData = userData)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SongState(
|
||||
val song: BaseItem?,
|
||||
val imageUrl: String?,
|
||||
val loading: LoadingState,
|
||||
val canDelete: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
val EMPTY = SongState(null, null, LoadingState.Pending)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Details page for a song that has no album, and therefore no album page to open.
|
||||
*/
|
||||
@Composable
|
||||
fun SongDetailsPage(
|
||||
itemId: UUID,
|
||||
preferences: UserPreferences,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SongViewModel =
|
||||
hiltViewModel<SongViewModel, SongViewModel.Factory>(
|
||||
creationCallback = { it.create(itemId) },
|
||||
),
|
||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||
val playlistState by playlistViewModel.playlistState.collectAsState()
|
||||
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
|
||||
val moreDialogActions =
|
||||
remember {
|
||||
MusicContextActions(
|
||||
navigateTo = { viewModel.navigationManager.navigateTo(it) },
|
||||
onClickPlay = { _, song -> viewModel.play(song) },
|
||||
onClickPlayNext = { _, song -> viewModel.playNext(song) },
|
||||
onClickAddToQueue = { item -> viewModel.addToQueue(item, -1) },
|
||||
onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) },
|
||||
onClickAddPlaylist = { id ->
|
||||
playlistViewModel.loadPlaylists()
|
||||
showPlaylistDialog.makePresent(id)
|
||||
},
|
||||
onClickRemoveFromQueue = { _, _ -> },
|
||||
onDeleteItem = viewModel::deleteItem,
|
||||
)
|
||||
}
|
||||
when (val loading = state.loading) {
|
||||
is LoadingState.Error -> {
|
||||
ErrorMessage(loading, modifier)
|
||||
}
|
||||
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> {
|
||||
LoadingPage(modifier)
|
||||
}
|
||||
|
||||
LoadingState.Success -> {
|
||||
val song = state.song!!
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
val buttonsFocusRequester = remember { FocusRequester() }
|
||||
RequestOrRestoreFocus(buttonsFocusRequester)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.bringIntoViewRequester(bringIntoViewRequester)
|
||||
.padding(bottom = 32.dp),
|
||||
) {
|
||||
AlbumHeader(
|
||||
album = song,
|
||||
imageUrl = state.imageUrl,
|
||||
overviewOnClick = {},
|
||||
bringIntoViewRequester = bringIntoViewRequester,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
MusicExpandableButtons(
|
||||
title = song.title ?: "",
|
||||
actions =
|
||||
remember(song) {
|
||||
MusicButtonActions(
|
||||
onClickPlay = { shuffled ->
|
||||
viewModel.play(song, shuffled = shuffled)
|
||||
},
|
||||
onClickInstantMix = { viewModel.startInstantMix(song.id) },
|
||||
onClickFavorite = {
|
||||
viewModel.setFavorite(song.id, !song.favorite)
|
||||
},
|
||||
onClickMore = {
|
||||
showContextMenu =
|
||||
ContextMenu.ForMusic(
|
||||
fromLongClick = false,
|
||||
item = song,
|
||||
index = 0,
|
||||
canDelete =
|
||||
viewModel.canDelete(
|
||||
song,
|
||||
preferences.appPreferences,
|
||||
),
|
||||
canRemoveFromQueue = false,
|
||||
actions = moreDialogActions,
|
||||
)
|
||||
},
|
||||
onConfirmDelete = { viewModel.deleteItem(song) },
|
||||
)
|
||||
},
|
||||
favorite = song.favorite,
|
||||
canDelete = state.canDelete,
|
||||
showShuffle = false,
|
||||
buttonOnFocusChanged = {},
|
||||
modifier = Modifier.focusRequester(buttonsFocusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
showContextMenu?.let { contextMenu ->
|
||||
ContextMenuDialog(
|
||||
onDismissRequest = { showContextMenu = null },
|
||||
getMediaSource = null,
|
||||
contextMenu = contextMenu,
|
||||
preferredSubtitleLanguage = null,
|
||||
)
|
||||
}
|
||||
showPlaylistDialog.compose { songId ->
|
||||
PlaylistDialog(
|
||||
title = stringResource(R.string.add_to_playlist),
|
||||
state = playlistState,
|
||||
onDismissRequest = { showPlaylistDialog.makeAbsent() },
|
||||
onClick = {
|
||||
playlistViewModel.addToPlaylist(it.id, songId)
|
||||
showPlaylistDialog.makeAbsent()
|
||||
},
|
||||
createEnabled = true,
|
||||
onCreatePlaylist = {
|
||||
playlistViewModel.createPlaylistAndAddItem(it, songId)
|
||||
showPlaylistDialog.makeAbsent()
|
||||
},
|
||||
onSearch = playlistViewModel::loadPlaylists,
|
||||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -74,6 +74,7 @@ sealed class Destination(
|
|||
val itemId: UUID,
|
||||
val type: BaseItemKind,
|
||||
val collectionType: CollectionType? = null,
|
||||
val initialSongId: UUID? = null,
|
||||
) : Destination() {
|
||||
constructor(item: BaseItem) : this(item.id, item.type, item.data.collectionType)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails
|
|||
import com.github.damontecres.wholphin.ui.detail.music.AlbumDetailsPage
|
||||
import com.github.damontecres.wholphin.ui.detail.music.ArtistDetailsPage
|
||||
import com.github.damontecres.wholphin.ui.detail.music.NowPlayingPage
|
||||
import com.github.damontecres.wholphin.ui.detail.music.SongDetailsPage
|
||||
import com.github.damontecres.wholphin.ui.detail.series.SeriesDetails
|
||||
import com.github.damontecres.wholphin.ui.detail.series.SeriesOverview
|
||||
import com.github.damontecres.wholphin.ui.discover.DiscoverPage
|
||||
|
|
@ -257,6 +258,7 @@ fun DestinationContent(
|
|||
AlbumDetailsPage(
|
||||
preferences = preferences,
|
||||
itemId = destination.itemId,
|
||||
initialSongId = destination.initialSongId,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -270,6 +272,15 @@ fun DestinationContent(
|
|||
)
|
||||
}
|
||||
|
||||
BaseItemKind.AUDIO -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
SongDetailsPage(
|
||||
preferences = preferences,
|
||||
itemId = destination.itemId,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.w("Unsupported item type: ${destination.type}")
|
||||
Text("Unsupported item type: ${destination.type}", modifier)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package com.github.damontecres.wholphin.test
|
||||
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import java.util.UUID
|
||||
|
||||
class BaseItemDestinationTest {
|
||||
@Test
|
||||
fun audioWithAlbumId_navigatesToAlbumWithInitialSongId() {
|
||||
val songId = UUID.randomUUID()
|
||||
val albumId = UUID.randomUUID()
|
||||
val item = BaseItem(song(id = songId, albumId = albumId), false)
|
||||
|
||||
val dest = item.destination()
|
||||
|
||||
assertEquals(Destination.MediaItem::class, dest::class)
|
||||
val media = dest as Destination.MediaItem
|
||||
assertEquals(albumId, media.itemId)
|
||||
assertEquals(BaseItemKind.MUSIC_ALBUM, media.type)
|
||||
assertEquals(songId, media.initialSongId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun audioWithoutAlbumId_returnsSelfAsMediaItem() {
|
||||
val songId = UUID.randomUUID()
|
||||
val item = BaseItem(song(id = songId, albumId = null), false)
|
||||
|
||||
val dest = item.destination()
|
||||
|
||||
assertEquals(songId, (dest as Destination.MediaItem).itemId)
|
||||
assertEquals(BaseItemKind.AUDIO, dest.type)
|
||||
assertNull(dest.initialSongId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movie_usesStandardDestination() {
|
||||
val movieId = UUID.randomUUID()
|
||||
val item = BaseItem(movie(id = movieId), false)
|
||||
|
||||
val dest = item.destination()
|
||||
|
||||
assertEquals(movieId, (dest as Destination.MediaItem).itemId)
|
||||
assertEquals(BaseItemKind.MOVIE, dest.type)
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,24 @@ fun episode(
|
|||
genreItems = genres,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a simple [BaseItemDto] song
|
||||
*/
|
||||
fun song(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
albumId: UUID? = null,
|
||||
name: String = "Test Song",
|
||||
genres: List<NameGuidPair>? = null,
|
||||
): BaseItemDto =
|
||||
BaseItemDto(
|
||||
id = id,
|
||||
type = BaseItemKind.AUDIO,
|
||||
name = name,
|
||||
albumId = albumId,
|
||||
seriesId = null,
|
||||
genreItems = genres,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a simple [BaseItemDto] playlist
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue