mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Buttons & instant mix
This commit is contained in:
parent
ee0c73f0e2
commit
11e28a605c
6 changed files with 184 additions and 18 deletions
|
|
@ -4,13 +4,16 @@ import android.content.Context
|
|||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.AudioItem
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.util.profile.Codec
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
|
|
@ -21,9 +24,11 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.instantMixApi
|
||||
import org.jellyfin.sdk.api.client.extensions.universalAudioApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -35,6 +40,7 @@ class MusicService
|
|||
@param:ApplicationContext private val context: Context,
|
||||
@param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient,
|
||||
private val api: ApiClient,
|
||||
private val serverRepository: ServerRepository,
|
||||
) {
|
||||
private val _state = MutableStateFlow(MusicServiceState.EMPTY)
|
||||
val state: StateFlow<MusicServiceState> = _state
|
||||
|
|
@ -53,6 +59,22 @@ class MusicService
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches instant mix items, replaces the queue, and begins playback
|
||||
*/
|
||||
suspend fun startInstantMix(itemId: UUID) {
|
||||
val items =
|
||||
api.instantMixApi
|
||||
.getInstantMixFromItem(
|
||||
userId = serverRepository.currentUser.value?.id,
|
||||
itemId = itemId,
|
||||
limit = 200,
|
||||
fields = DefaultItemFields,
|
||||
).content.items
|
||||
.map { BaseItem(it, false) }
|
||||
setQueue(items, false)
|
||||
}
|
||||
|
||||
suspend fun setQueue(
|
||||
items: List<BaseItem>,
|
||||
shuffled: Boolean,
|
||||
|
|
@ -138,8 +160,6 @@ private class MusicPlayerListener(
|
|||
Timber.v("MusicPlayerListener onIsPlayingChanged")
|
||||
state.update {
|
||||
it.copy(
|
||||
// TODO this is hack to force the UI to update, but is very inefficient
|
||||
queue = PlayerMediaItemList(player),
|
||||
isPlaying = isPlaying,
|
||||
)
|
||||
}
|
||||
|
|
@ -152,24 +172,38 @@ private class MusicPlayerListener(
|
|||
Timber.v("MusicPlayerListener onMediaItemTransition")
|
||||
state.update {
|
||||
it.copy(
|
||||
queue = PlayerMediaItemList(player),
|
||||
currentIndex = player.currentMediaItemIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTimelineChanged(
|
||||
timeline: Timeline,
|
||||
reason: Int,
|
||||
) {
|
||||
Timber.v("MusicPlayerListener onTimelineChanged")
|
||||
if (reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) {
|
||||
state.update {
|
||||
it.copy(
|
||||
queue = PlayerMediaItemList(player),
|
||||
currentIndex = player.currentMediaItemIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PlayerMediaItemList(
|
||||
private val player: Player,
|
||||
) : AbstractList<AudioItem>() {
|
||||
override fun get(index: Int): AudioItem {
|
||||
Timber.v("get %s", index)
|
||||
// Timber.v("get %s", index)
|
||||
return player.getMediaItemAt(index).localConfiguration?.tag as AudioItem
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() {
|
||||
Timber.v("size %s", player.mediaItemCount)
|
||||
// Timber.v("size %s", player.mediaItemCount)
|
||||
return player.mediaItemCount
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -29,6 +30,7 @@ import androidx.compose.ui.focus.FocusState
|
|||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
|
@ -59,6 +61,7 @@ import com.github.damontecres.wholphin.ui.components.OverviewText
|
|||
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||
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
|
||||
|
|
@ -68,6 +71,7 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -165,6 +169,17 @@ class AlbumViewModel
|
|||
}
|
||||
}
|
||||
|
||||
fun setFavorite(favorite: Boolean) =
|
||||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||
favoriteWatchManager.setFavorite(itemId, favorite)
|
||||
val album =
|
||||
api.userLibraryApi
|
||||
.getItem(itemId = itemId)
|
||||
.content
|
||||
.let { BaseItem(it, false) }
|
||||
_state.update { it.copy(album = album) }
|
||||
}
|
||||
|
||||
fun play(
|
||||
shuffled: Boolean,
|
||||
startIndex: Int = 0,
|
||||
|
|
@ -180,6 +195,14 @@ class AlbumViewModel
|
|||
musicService.setQueue(songsToAdd, shuffled)
|
||||
}
|
||||
}
|
||||
|
||||
fun startInstantMix() {
|
||||
viewModelScope.launchIO {
|
||||
Timber.v("Starting instant mix for %s", itemId)
|
||||
musicService.startInstantMix(itemId)
|
||||
navigationManager.navigateTo(Destination.NowPlaying)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class AlbumState(
|
||||
|
|
@ -194,7 +217,7 @@ data class AlbumState(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun AlbumDetails(
|
||||
fun AlbumDetailsPage(
|
||||
itemId: UUID,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: AlbumViewModel =
|
||||
|
|
@ -217,6 +240,9 @@ fun AlbumDetails(
|
|||
}
|
||||
|
||||
LoadingState.Success -> {
|
||||
val album = state.album!!
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
Box(modifier = modifier) {
|
||||
LazyColumn(
|
||||
|
|
@ -233,25 +259,38 @@ fun AlbumDetails(
|
|||
.padding(bottom = 32.dp),
|
||||
) {
|
||||
AlbumHeader(
|
||||
album = state.album!!,
|
||||
album = album,
|
||||
imageUrl = state.imageUrl,
|
||||
overviewOnClick = {},
|
||||
bringIntoViewRequester = bringIntoViewRequester,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
AlbumButtons(
|
||||
onClickPlay = { viewModel.play(it, 0) },
|
||||
onClickAddToPlaylist = {},
|
||||
onClickGoToArtist = {},
|
||||
onClickMore = { },
|
||||
buttonOnFocusChanged = {},
|
||||
modifier =
|
||||
Modifier.onFocusChanged {
|
||||
if (it.hasFocus) scope.launch { bringIntoViewRequester.bringIntoView() }
|
||||
MusicExpandableButtons(
|
||||
actions =
|
||||
remember {
|
||||
MusicButtonActions(
|
||||
onClickPlay = { viewModel.play(it, 0) },
|
||||
onClickInstantMix = viewModel::startInstantMix,
|
||||
onClickFavorite = { viewModel.setFavorite(!album.favorite) },
|
||||
onClickMore = {
|
||||
// TODO
|
||||
},
|
||||
)
|
||||
},
|
||||
favorite = album.favorite,
|
||||
modifier =
|
||||
Modifier
|
||||
.onFocusChanged {
|
||||
if (it.hasFocus) scope.launch { bringIntoViewRequester.bringIntoView() }
|
||||
}.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.songs),
|
||||
)
|
||||
}
|
||||
itemsIndexed(state.songs) { index, song ->
|
||||
SongListItem(
|
||||
song = song,
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.music
|
||||
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
|
||||
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Composable
|
||||
fun MusicExpandableButtons(
|
||||
actions: MusicButtonActions,
|
||||
favorite: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val firstFocus = remember { FocusRequester() }
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
modifier =
|
||||
modifier
|
||||
.focusGroup()
|
||||
.focusRestorer(firstFocus),
|
||||
) {
|
||||
item("play") {
|
||||
ExpandablePlayButton(
|
||||
title = R.string.play,
|
||||
resume = Duration.ZERO,
|
||||
icon = Icons.Default.PlayArrow,
|
||||
onClick = { actions.onClickPlay.invoke(false) },
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRequester(firstFocus),
|
||||
)
|
||||
}
|
||||
item("shuffle") {
|
||||
ExpandableFaButton(
|
||||
title = R.string.shuffle,
|
||||
iconStringRes = R.string.fa_shuffle,
|
||||
onClick = { actions.onClickPlay.invoke(true) },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
item("instant_mix") {
|
||||
ExpandableFaButton(
|
||||
title = R.string.instant_mix,
|
||||
iconStringRes = R.string.fa_compass,
|
||||
onClick = actions.onClickInstantMix,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
item("favorite") {
|
||||
ExpandableFaButton(
|
||||
title = if (favorite) R.string.remove_favorite else R.string.add_favorite,
|
||||
iconStringRes = R.string.fa_heart,
|
||||
onClick = actions.onClickFavorite,
|
||||
iconColor = if (favorite) Color.Red else Color.Unspecified,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
item("more") {
|
||||
ExpandablePlayButton(
|
||||
title = R.string.more,
|
||||
resume = Duration.ZERO,
|
||||
icon = Icons.Default.MoreVert,
|
||||
onClick = { actions.onClickMore.invoke() },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MusicButtonActions(
|
||||
val onClickPlay: (shuffle: Boolean) -> Unit,
|
||||
val onClickInstantMix: () -> Unit,
|
||||
val onClickFavorite: () -> Unit,
|
||||
val onClickMore: () -> Unit,
|
||||
)
|
||||
|
|
@ -29,7 +29,7 @@ import com.github.damontecres.wholphin.ui.detail.discover.DiscoverPersonPage
|
|||
import com.github.damontecres.wholphin.ui.detail.discover.DiscoverSeriesDetails
|
||||
import com.github.damontecres.wholphin.ui.detail.episode.EpisodeDetails
|
||||
import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails
|
||||
import com.github.damontecres.wholphin.ui.detail.music.AlbumDetails
|
||||
import com.github.damontecres.wholphin.ui.detail.music.AlbumDetailsPage
|
||||
import com.github.damontecres.wholphin.ui.detail.music.NowPlayingPage
|
||||
import com.github.damontecres.wholphin.ui.detail.series.SeriesDetails
|
||||
import com.github.damontecres.wholphin.ui.detail.series.SeriesOverview
|
||||
|
|
@ -212,7 +212,7 @@ fun DestinationContent(
|
|||
|
||||
BaseItemKind.MUSIC_ALBUM -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
AlbumDetails(
|
||||
AlbumDetailsPage(
|
||||
itemId = destination.itemId,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -51,4 +51,5 @@
|
|||
<string name="fa_clock" translatable="false"></string>
|
||||
<string name="fa_bell" translatable="false"></string>
|
||||
<string name="fa_check" translatable="false"></string>
|
||||
<string name="fa_compass" translatable="false"></string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -497,6 +497,7 @@
|
|||
<string name="songs">Songs</string>
|
||||
<string name="go_to_artist">Go to artist</string>
|
||||
<string name="now_playing">Now playing</string>
|
||||
<string name="instant_mix">Instant mix</string>
|
||||
|
||||
<string-array name="theme_song_volume">
|
||||
<item>Disabled</item>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue