mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Implement various queue functions
This commit is contained in:
parent
ea243975ad
commit
7a869d9f47
5 changed files with 202 additions and 41 deletions
|
|
@ -209,6 +209,12 @@ class MusicService
|
|||
player.moveMediaItem(index, if (direction == MoveDirection.UP) index - 1 else index + 1)
|
||||
updateQueueSize()
|
||||
}
|
||||
|
||||
suspend fun playNext(song: BaseItem) {
|
||||
val mediaItem = convert(song)
|
||||
onMain { player.addMediaItem(state.value.currentIndex + 1, mediaItem) }
|
||||
updateQueueSize()
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
|
|
@ -49,20 +50,23 @@ import com.github.damontecres.wholphin.services.MediaReportService
|
|||
import com.github.damontecres.wholphin.services.MusicService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.GenreText
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.Optional
|
||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||
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.GetItemsRequestHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -80,8 +84,7 @@ 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.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
|
|
@ -121,22 +124,7 @@ class AlbumViewModel
|
|||
.content
|
||||
.let { BaseItem(it, false) }
|
||||
}
|
||||
val songsDeferred =
|
||||
async {
|
||||
val request =
|
||||
GetItemsRequest(
|
||||
parentId = itemId,
|
||||
includeItemTypes = listOf(BaseItemKind.AUDIO),
|
||||
fields = DefaultItemFields,
|
||||
sortBy =
|
||||
listOf(
|
||||
ItemSortBy.PARENT_INDEX_NUMBER,
|
||||
ItemSortBy.INDEX_NUMBER,
|
||||
ItemSortBy.SORT_NAME,
|
||||
),
|
||||
)
|
||||
ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope).init()
|
||||
}
|
||||
val songsDeferred = async { getPagerForAlbum(api, itemId) }
|
||||
val album = itemDeferred.await()
|
||||
val songs = songsDeferred.await()
|
||||
val imageUrl = imageUrlService.getItemImageUrl(album, ImageType.PRIMARY)
|
||||
|
|
@ -194,18 +182,22 @@ class AlbumViewModel
|
|||
}
|
||||
}
|
||||
|
||||
fun playNext(song: BaseItem) {
|
||||
viewModelScope.launchDefault {
|
||||
musicService.playNext(song)
|
||||
}
|
||||
}
|
||||
|
||||
fun addToQueue(
|
||||
itemId: UUID,
|
||||
item: BaseItem,
|
||||
index: Int,
|
||||
) {
|
||||
viewModelScope.launchIO {
|
||||
val songs = state.value.songs as ApiRequestPager<*>
|
||||
if (itemId == this@AlbumViewModel.itemId) {
|
||||
if (item.id == this@AlbumViewModel.itemId) {
|
||||
musicService.addAllToQueue(songs, 0)
|
||||
} else {
|
||||
songs.getBlocking(index)?.let {
|
||||
musicService.addToQueue(it)
|
||||
}
|
||||
} else if (item.type == BaseItemKind.AUDIO) {
|
||||
musicService.addToQueue(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -238,21 +230,28 @@ fun AlbumDetailsPage(
|
|||
hiltViewModel<AlbumViewModel, AlbumViewModel.Factory>(
|
||||
creationCallback = { it.create(itemId) },
|
||||
),
|
||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val state by viewModel.state.collectAsState()
|
||||
val currentMusic by viewModel.currentMusic.collectAsState()
|
||||
|
||||
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
val moreDialogActions =
|
||||
remember {
|
||||
MusicMoreDialogActions(
|
||||
onNavigate = { viewModel.navigationManager.navigateTo(it) },
|
||||
onClickPlay = { index, _ -> viewModel.play(false, index) },
|
||||
onClickAddToQueue = { index, itemId -> viewModel.addToQueue(itemId, index) },
|
||||
onClickPlayNext = { _, song -> viewModel.playNext(song) },
|
||||
onClickAddToQueue = { index, item -> viewModel.addToQueue(item, index) },
|
||||
onClickFavorite = { itemId, favorite -> viewModel.setFavorite(itemId, favorite) },
|
||||
onClickAddPlaylist = {},
|
||||
onClickAddPlaylist = { itemId ->
|
||||
playlistViewModel.loadPlaylists(MediaType.AUDIO)
|
||||
showPlaylistDialog.makePresent(itemId)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -377,6 +376,23 @@ fun AlbumDetailsPage(
|
|||
waitToLoad = params.fromLongClick,
|
||||
)
|
||||
}
|
||||
showPlaylistDialog.compose { itemId ->
|
||||
PlaylistDialog(
|
||||
title = stringResource(R.string.add_to_playlist),
|
||||
state = playlistState,
|
||||
onDismissRequest = { showPlaylistDialog.makeAbsent() },
|
||||
onClick = {
|
||||
playlistViewModel.addToPlaylist(it.id, itemId)
|
||||
showPlaylistDialog.makeAbsent()
|
||||
},
|
||||
createEnabled = true,
|
||||
onCreatePlaylist = {
|
||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||
showPlaylistDialog.makeAbsent()
|
||||
},
|
||||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
|
|
@ -59,14 +60,19 @@ import com.github.damontecres.wholphin.ui.components.DialogPopup
|
|||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.GenreText
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.Optional
|
||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||
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.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
|
||||
|
|
@ -86,6 +92,7 @@ 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.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
|
||||
|
|
@ -205,7 +212,7 @@ class ArtistViewModel
|
|||
_state.update { it.copy(artist = artist) }
|
||||
}
|
||||
|
||||
fun play(shuffled: Boolean) {
|
||||
fun playArtist(shuffled: Boolean) {
|
||||
viewModelScope.launchIO {
|
||||
Timber.v("Playing artist %s", itemId)
|
||||
val request =
|
||||
|
|
@ -247,9 +254,58 @@ class ArtistViewModel
|
|||
}
|
||||
}
|
||||
|
||||
fun playSong(song: BaseItem) {
|
||||
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.setQueue(listOf(song), false)
|
||||
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 -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -275,21 +331,28 @@ fun ArtistDetailsPage(
|
|||
hiltViewModel<ArtistViewModel, ArtistViewModel.Factory>(
|
||||
creationCallback = { it.create(itemId) },
|
||||
),
|
||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val state by viewModel.state.collectAsState()
|
||||
val currentMusic by viewModel.currentMusic.collectAsState()
|
||||
|
||||
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
val moreDialogActions =
|
||||
remember {
|
||||
MusicMoreDialogActions(
|
||||
onNavigate = { viewModel.navigationManager.navigateTo(it) },
|
||||
onClickPlay = { index, song -> TODO() },
|
||||
onClickAddToQueue = { index, itemId -> viewModel.addToQueue(itemId, index) },
|
||||
onClickPlay = { index, item -> viewModel.play(item) },
|
||||
onClickPlayNext = { _, item -> viewModel.playNext(item) },
|
||||
onClickAddToQueue = { index, item -> viewModel.addToQueue(item, index) },
|
||||
onClickFavorite = { itemId, favorite -> viewModel.setFavorite(itemId, favorite) },
|
||||
onClickAddPlaylist = {},
|
||||
onClickAddPlaylist = { itemId ->
|
||||
playlistViewModel.loadPlaylists(MediaType.AUDIO)
|
||||
showPlaylistDialog.makePresent(itemId)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -340,7 +403,7 @@ fun ArtistDetailsPage(
|
|||
actions =
|
||||
remember {
|
||||
MusicButtonActions(
|
||||
onClickPlay = { viewModel.play(it) },
|
||||
onClickPlay = { viewModel.playArtist(it) },
|
||||
onClickInstantMix = viewModel::startInstantMix,
|
||||
onClickFavorite = {
|
||||
viewModel.setFavorite(
|
||||
|
|
@ -385,7 +448,7 @@ fun ArtistDetailsPage(
|
|||
itemsIndexed(state.topSongs) { index, song ->
|
||||
SongListItem(
|
||||
song = song,
|
||||
onClick = { song?.let { viewModel.playSong(it) } },
|
||||
onClick = { song?.let { viewModel.play(it) } },
|
||||
onLongClick = {
|
||||
if (song != null) {
|
||||
moreDialog =
|
||||
|
|
@ -444,6 +507,23 @@ fun ArtistDetailsPage(
|
|||
waitToLoad = params.fromLongClick,
|
||||
)
|
||||
}
|
||||
showPlaylistDialog.compose { itemId ->
|
||||
PlaylistDialog(
|
||||
title = stringResource(R.string.add_to_playlist),
|
||||
state = playlistState,
|
||||
onDismissRequest = { showPlaylistDialog.makeAbsent() },
|
||||
onClick = {
|
||||
playlistViewModel.addToPlaylist(it.id, itemId)
|
||||
showPlaylistDialog.makeAbsent()
|
||||
},
|
||||
createEnabled = true,
|
||||
onCreatePlaylist = {
|
||||
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||
showPlaylistDialog.makeAbsent()
|
||||
},
|
||||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -6,17 +6,26 @@ import androidx.compose.material.icons.filled.Add
|
|||
import androidx.compose.material.icons.filled.ArrowForward
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import java.util.UUID
|
||||
|
||||
data class MusicMoreDialogActions(
|
||||
val onNavigate: (Destination) -> Unit,
|
||||
val onClickPlay: (Int, UUID) -> Unit,
|
||||
val onClickAddToQueue: (Int, UUID) -> Unit,
|
||||
val onClickPlay: (Int, BaseItem) -> Unit,
|
||||
val onClickPlayNext: (Int, BaseItem) -> Unit,
|
||||
val onClickAddToQueue: (Int, BaseItem) -> Unit,
|
||||
val onClickFavorite: (UUID, Boolean) -> Unit,
|
||||
val onClickAddPlaylist: (UUID) -> Unit,
|
||||
val onClickGoToAlbum: (UUID) -> Unit = {
|
||||
|
|
@ -40,7 +49,16 @@ fun buildMoreDialogForMusic(
|
|||
Icons.Default.PlayArrow,
|
||||
iconColor = Color.Green.copy(alpha = .8f),
|
||||
) {
|
||||
actions.onClickPlay(index, item.id)
|
||||
actions.onClickPlay(index, item)
|
||||
},
|
||||
)
|
||||
add(
|
||||
DialogItem(
|
||||
context.getString(R.string.play_next),
|
||||
Icons.Default.PlayArrow,
|
||||
iconColor = Color.Green.copy(alpha = .8f),
|
||||
) {
|
||||
actions.onClickPlayNext(index, item)
|
||||
},
|
||||
)
|
||||
add(
|
||||
|
|
@ -48,7 +66,7 @@ fun buildMoreDialogForMusic(
|
|||
context.getString(R.string.add_to_queue),
|
||||
Icons.Default.Add,
|
||||
) {
|
||||
actions.onClickAddToQueue(index, item.id)
|
||||
actions.onClickAddToQueue(index, item)
|
||||
},
|
||||
)
|
||||
add(
|
||||
|
|
@ -93,3 +111,43 @@ fun buildMoreDialogForMusic(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun ViewModel.getPagerForAlbum(
|
||||
api: ApiClient,
|
||||
albumId: UUID,
|
||||
): ApiRequestPager<GetItemsRequest> {
|
||||
val request =
|
||||
GetItemsRequest(
|
||||
parentId = albumId,
|
||||
includeItemTypes = listOf(BaseItemKind.AUDIO),
|
||||
fields = DefaultItemFields,
|
||||
sortBy =
|
||||
listOf(
|
||||
ItemSortBy.PARENT_INDEX_NUMBER,
|
||||
ItemSortBy.INDEX_NUMBER,
|
||||
ItemSortBy.SORT_NAME,
|
||||
),
|
||||
)
|
||||
return ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope).init()
|
||||
}
|
||||
|
||||
suspend fun ViewModel.getPagerForArtist(
|
||||
api: ApiClient,
|
||||
artistId: UUID,
|
||||
): ApiRequestPager<GetItemsRequest> {
|
||||
val request =
|
||||
GetItemsRequest(
|
||||
parentId = artistId,
|
||||
recursive = true,
|
||||
includeItemTypes = listOf(BaseItemKind.AUDIO),
|
||||
fields = DefaultItemFields,
|
||||
// TODO better sort
|
||||
sortBy =
|
||||
listOf(
|
||||
ItemSortBy.PARENT_INDEX_NUMBER,
|
||||
ItemSortBy.INDEX_NUMBER,
|
||||
ItemSortBy.SORT_NAME,
|
||||
),
|
||||
)
|
||||
return ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope).init()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -727,5 +727,6 @@
|
|||
<string name="album_artist">Album artist</string>
|
||||
<string name="album">Album</string>
|
||||
<string name="popular_songs">Popular songs</string>
|
||||
<string name="play_next">Play next</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue