Add long click queue operations

This commit is contained in:
Damontecres 2026-03-03 22:27:39 -05:00
parent 6471b198b2
commit 73b626facd
No known key found for this signature in database
10 changed files with 176 additions and 22 deletions

View file

@ -9,6 +9,7 @@ import kotlin.time.Duration
data class AudioItem(
val id: UUID,
val albumId: UUID?,
val artistId: UUID?,
val title: String?,
val albumTitle: String?,
val artistNames: String?,
@ -24,6 +25,10 @@ data class AudioItem(
AudioItem(
id = item.id,
albumId = item.data.albumId,
artistId =
item.data.artistItems
?.firstOrNull()
?.id,
title = item.title,
albumTitle = item.data.album,
artistNames = item.data.albumArtist,

View file

@ -244,12 +244,30 @@ class MusicService
updateQueueSize()
}
suspend fun moveQueue(
index: Int,
newIndex: Int,
) = withContext(Dispatchers.Main) {
player.moveMediaItem(index, newIndex)
updateQueueSize()
}
suspend fun playIndex(index: Int) {
onMain { player.seekTo(index, 0L) }
// MusicPlayerListener will update state
}
suspend fun playNext(song: BaseItem) {
val mediaItem = convert(song)
onMain { player.addMediaItem(state.value.currentIndex + 1, mediaItem) }
updateQueueSize()
}
suspend fun removeFromQueue(index: Int) {
onMain { player.removeMediaItem(index) }
updateQueueSize()
}
private suspend fun <T> loading(block: suspend () -> T): T {
_state.update { it.copy(loadingState = LoadingState.Loading) }
val result = block.invoke()
@ -336,17 +354,6 @@ private class MusicPlayerListener(
}
}
data class PlayerMediaItemList(
private val player: Player,
) {
val size: Int get() = player.mediaItemCount
operator fun get(index: Int): AudioItem {
// Timber.v("get %s", index)
return player.getMediaItemAt(index).localConfiguration?.tag as AudioItem
}
}
@Composable
fun rememberQueue(
player: Player,

View file

@ -282,6 +282,7 @@ fun AlbumDetailsPage(
playlistViewModel.loadPlaylists(MediaType.AUDIO)
showPlaylistDialog.makePresent(itemId)
},
onClickRemoveFromQueue = {},
)
}
@ -362,6 +363,7 @@ fun AlbumDetailsPage(
actions = moreDialogActions,
item = album,
index = 0,
canRemove = false,
),
)
},
@ -404,6 +406,7 @@ fun AlbumDetailsPage(
actions = moreDialogActions,
item = song,
index = index,
canRemove = false,
),
)
}

View file

@ -300,6 +300,7 @@ fun ArtistDetailsPage(
playlistViewModel.loadPlaylists(MediaType.AUDIO)
showPlaylistDialog.makePresent(itemId)
},
onClickRemoveFromQueue = {},
)
}
@ -369,6 +370,7 @@ fun ArtistDetailsPage(
actions = moreDialogActions,
item = artist,
index = 0,
canRemove = false,
),
)
},
@ -408,6 +410,7 @@ fun ArtistDetailsPage(
actions = moreDialogActions,
item = song,
index = index,
canRemove = false,
),
)
}

View file

@ -59,6 +59,8 @@ fun NowPlayingOverlay(
current: AudioItem?,
queue: List<AudioItem>,
controllerViewState: ControllerViewState,
onClickSong: (Int, AudioItem) -> Unit,
onLongClickSong: (Int, AudioItem) -> Unit,
onClickMore: () -> Unit,
onMoveQueue: (Int, MoveDirection) -> Unit,
modifier: Modifier = Modifier,
@ -159,10 +161,8 @@ fun NowPlayingOverlay(
runtime = song.runtime?.roundSeconds,
showArtist = true,
isPlaying = current?.id == song.id,
onClick = {
player.seekTo(index, 0L)
},
onLongClick = {},
onClick = { onClickSong.invoke(index, song) },
onLongClick = { onLongClickSong.invoke(index, song) },
modifier =
Modifier
.weight(1f)

View file

@ -30,6 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -42,6 +43,8 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.rememberQueue
import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.playback.BottomDialog
import com.github.damontecres.wholphin.ui.playback.BottomDialogItem
@ -59,6 +62,7 @@ fun NowPlayingPage(
creationCallback = { it.create() },
),
) {
val context = LocalContext.current
val state by viewModel.state.collectAsState()
val player = viewModel.player
val queue = rememberQueue(player, state.musicServiceState.queueSize)
@ -97,9 +101,20 @@ fun NowPlayingPage(
)
}
val actions =
remember {
MusicQueueDialogActions(
onNavigate = { viewModel.navigationManager.navigateTo(it) },
onClickPlay = { index, _ -> viewModel.play(index) },
onClickPlayNext = { index, _ -> viewModel.playNext(index) },
onClickRemoveFromQueue = { index, _ -> viewModel.removeFromQueue(index) },
)
}
var showMoreDialog by remember { mutableStateOf(false) }
var lyricsActive by remember { mutableStateOf(true) }
var showDebugInfo by remember { mutableStateOf(true) }
var itemMoreDialog by remember { mutableStateOf<DialogParams?>(null) }
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
@ -189,6 +204,22 @@ fun NowPlayingPage(
controllerViewState = controllerViewState,
onMoveQueue = { index, direction -> viewModel.moveQueue(index, direction) },
onClickMore = { showMoreDialog = true },
onClickSong = { index, _ -> viewModel.play(index) },
onLongClickSong = { index, song ->
itemMoreDialog =
DialogParams(
title = song.title ?: "",
fromLongClick = true,
items =
buildMoreDialogForMusicQueue(
context = context,
actions = actions,
item = song,
index = index,
canRemove = true,
),
)
},
modifier =
Modifier
.background(AppColors.TransparentBlack50)
@ -199,6 +230,16 @@ fun NowPlayingPage(
LoadingPage(focusEnabled = false)
}
}
itemMoreDialog?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { itemMoreDialog = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
)
}
if (showMoreDialog) {
NowPlayingBottomDialog(
showDebugInfo = showDebugInfo,

View file

@ -44,7 +44,7 @@ class NowPlayingViewModel
constructor(
private val api: ApiClient,
@param:ApplicationContext private val context: Context,
private val navigationManager: NavigationManager,
val navigationManager: NavigationManager,
private val favoriteWatchManager: FavoriteWatchManager,
private val backdropService: BackdropService,
private val imageUrlService: ImageUrlService,
@ -127,7 +127,7 @@ class NowPlayingViewModel
} else {
null
}
Timber.v("lyricIndex=$lyricIndex")
// Timber.v("lyricIndex=$lyricIndex")
state.update {
it.copy(
lyrics = lyrics,
@ -178,6 +178,12 @@ class NowPlayingViewModel
direction: MoveDirection,
) = viewModelScope.launchDefault { musicService.moveQueue(index, direction) }
fun play(index: Int) = viewModelScope.launchDefault { musicService.playIndex(index) }
fun playNext(index: Int) = viewModelScope.launchDefault { musicService.moveQueue(index, 1) }
fun removeFromQueue(index: Int) = viewModelScope.launchDefault { musicService.removeFromQueue(index) }
fun stop() {
player.stop()
navigationManager.goBack()

View file

@ -25,6 +25,7 @@ import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.roundMinutes
import com.github.damontecres.wholphin.ui.roundSeconds
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import org.jellyfin.sdk.model.extensions.ticks
import kotlin.time.Duration
@ -68,11 +69,13 @@ fun SongListItem(
artist = if (showArtist) song?.data?.albumArtist else null,
indexNumber = song?.data?.indexNumber,
runtime =
remember(song) {
song
?.data
?.runTimeTicks
?.ticks
?.roundMinutes,
?.roundSeconds
},
onClick = onClick,
onLongClick = onLongClick,
modifier = modifier,

View file

@ -4,11 +4,13 @@ import android.content.Context
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.Delete
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.AudioItem
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.components.DialogItem
@ -34,6 +36,7 @@ data class MusicMoreDialogActions(
val onClickGoToArtist: (UUID) -> Unit = {
onNavigate.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ARTIST))
},
val onClickRemoveFromQueue: (Int) -> Unit,
)
fun buildMoreDialogForMusic(
@ -41,6 +44,7 @@ fun buildMoreDialogForMusic(
actions: MusicMoreDialogActions,
item: BaseItem,
index: Int,
canRemove: Boolean,
): List<DialogItem> =
buildList {
add(
@ -61,6 +65,16 @@ fun buildMoreDialogForMusic(
actions.onClickPlayNext(index, item)
},
)
if (canRemove) {
add(
DialogItem(
context.getString(R.string.remove_from_queue),
Icons.Default.Delete,
) {
actions.onClickRemoveFromQueue(index)
},
)
}
add(
DialogItem(
context.getString(R.string.add_to_queue),
@ -112,6 +126,77 @@ fun buildMoreDialogForMusic(
}
}
data class MusicQueueDialogActions(
val onNavigate: (Destination) -> Unit,
val onClickPlay: (Int, AudioItem) -> Unit,
val onClickPlayNext: (Int, AudioItem) -> Unit,
val onClickGoToAlbum: (UUID) -> Unit = {
onNavigate.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ALBUM))
},
val onClickGoToArtist: (UUID) -> Unit = {
onNavigate.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ARTIST))
},
val onClickRemoveFromQueue: (Int, AudioItem) -> Unit,
)
fun buildMoreDialogForMusicQueue(
context: Context,
actions: MusicQueueDialogActions,
item: AudioItem,
index: Int,
canRemove: Boolean,
): List<DialogItem> =
buildList {
add(
DialogItem(
context.getString(R.string.play),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
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)
},
)
if (canRemove) {
add(
DialogItem(
context.getString(R.string.remove_from_queue),
Icons.Default.Delete,
) {
actions.onClickRemoveFromQueue(index, item)
},
)
}
if (item.albumId != null) {
add(
DialogItem(
context.getString(R.string.go_to_album),
Icons.Default.ArrowForward,
) {
actions.onClickGoToAlbum.invoke(item.albumId)
},
)
}
if (item.artistId != null) {
add(
DialogItem(
context.getString(R.string.go_to_artist),
Icons.Default.ArrowForward,
) {
actions.onClickGoToArtist.invoke(item.artistId)
},
)
}
}
suspend fun ViewModel.getPagerForAlbum(
api: ApiClient,
albumId: UUID,

View file

@ -737,6 +737,7 @@
<string name="hide_lyrics">Hide lyrics</string>
<string name="show_lyrics">Show lyrics</string>
<string name="song_has_lyrics">Song has lyrics</string>
<string name="remove_from_queue">Remove from queue</string>
</resources>