Refactor context menu code (#1302)

## Description
This PR refactors most of the context menu dialog code moving more of
the display into a Composable. This then allows for embedding the
confirm delete or choose version sub-dialogs. This will allow for easier
future expansion of the context menu.

Now instead of the page using a combination of `DiaglogPopup` &
`buildMoreFor` functions, pages create a `ContextMenu` object and pass
it to a composable.

There's no significant user facing changes.

### Related issues
None

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-04-27 18:58:54 -04:00 committed by GitHub
parent a159526296
commit c718d903a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1707 additions and 1647 deletions

View file

@ -66,6 +66,7 @@ import com.github.damontecres.wholphin.services.MediaManagementService
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.StreamChoiceService
import com.github.damontecres.wholphin.services.ThemeSongPlayer
import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.services.deleteItem
@ -74,13 +75,13 @@ import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.cards.GridCard
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.CardGrid
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.detail.music.addToQueue
import com.github.damontecres.wholphin.ui.equalsNotNull
import com.github.damontecres.wholphin.ui.launchDefault
@ -133,7 +134,7 @@ class CollectionFolderViewModel
private val savedStateHandle: SavedStateHandle,
api: ApiClient,
@param:ApplicationContext private val context: Context,
private val serverRepository: ServerRepository,
val serverRepository: ServerRepository,
private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
private val favoriteWatchManager: FavoriteWatchManager,
private val backdropService: BackdropService,
@ -142,6 +143,7 @@ class CollectionFolderViewModel
private val userPreferencesService: UserPreferencesService,
private val mediaManagementService: MediaManagementService,
private val musicService: MusicService,
val streamChoiceService: StreamChoiceService,
val mediaReportService: MediaReportService,
@Assisted itemId: String,
@Assisted initialSortAndDirection: SortAndDirection?,
@ -662,18 +664,58 @@ fun CollectionFolderGrid(
val item by viewModel.item.observeAsState()
val viewOptions by viewModel.viewOptions.observeAsState(defaultViewOptions)
var moreDialog by remember { mutableStateOf<Optional<PositionItem>>(Optional.absent()) }
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
var showDeleteDialog by remember { mutableStateOf<PositionItem?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
val contextActions =
remember {
ContextMenuActions(
navigateTo = viewModel::navigateTo,
onClickWatch = { itemId, watched ->
viewModel.setWatched(viewModel.position, itemId, watched)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(viewModel.position, itemId, favorite)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onDeleteItem = { viewModel.deleteItem(viewModel.position, it) },
onShowOverview = { overviewDialog = ItemDetailsDialogInfo(it) },
onChooseVersion = { _, _ ->
// Not supported on this page
},
onChooseTracks = { result ->
// Not supported on this page
},
onClearChosenStreams = {
// Not supported on this page
},
)
}
val gridActions =
remember(actions) {
GridClickActions(
onClickItem = actions.onClickItem,
onLongClickItem =
actions.onLongClickItem ?: { position, item ->
moreDialog.makePresent(PositionItem(position, item))
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = true,
item = item,
chosenStreams = null,
showGoTo = true,
showStreamChoices = false,
canDelete = viewModel.canDelete(item, preferences.appPreferences),
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = contextActions,
)
},
onClickPlayAll =
actions.onClickPlayAll ?: { shuffle ->
@ -802,47 +844,22 @@ fun CollectionFolderGrid(
}
}
}
moreDialog.compose { (position, item) ->
DialogPopup(
showDialog = true,
title = item.title ?: "",
dialogItems =
buildMoreDialogItemsForHome(
context = context,
item = item,
seriesId = null,
playbackPosition = item.playbackPosition,
watched = item.played,
favorite = item.favorite,
canDelete = viewModel.canDelete(item, preferences.appPreferences),
actions =
MoreDialogActions(
navigateTo = { viewModel.navigateTo(it) },
onClickWatch = { itemId, watched ->
viewModel.setWatched(position, itemId, watched)
},
onClickFavorite = { itemId, watched ->
viewModel.setFavorite(position, itemId, watched)
},
onClickAddPlaylist = {
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(it)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onClickDelete = {
showDeleteDialog = PositionItem(position, item)
},
onClickGoTo = {
gridActions.onClickItem.invoke(position, it)
},
onClickAddToQueue = {
viewModel.addToQueue(it, 0)
},
),
),
onDismissRequest = { moreDialog.makeAbsent() },
dismissOnClick = true,
waitToLoad = true,
overviewDialog?.let { info ->
ItemDetailsDialog(
info = info,
showFilePath =
viewModel.serverRepository.currentUserDto.value
?.policy
?.isAdministrator == true,
onDismissRequest = { overviewDialog = null },
)
}
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = null,
contextMenu = contextMenu,
preferredSubtitleLanguage = null,
)
}
showPlaylistDialog.compose { itemId ->
@ -862,16 +879,6 @@ fun CollectionFolderGrid(
elevation = 3.dp,
)
}
showDeleteDialog?.let { (position, item) ->
ConfirmDeleteDialog(
itemTitle = listOfNotNull(item.title, item.subtitle).joinToString(" - "),
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(position, item)
showDeleteDialog = null
},
)
}
}
@OptIn(ExperimentalFoundationApi::class)

View file

@ -0,0 +1,861 @@
package com.github.damontecres.wholphin.ui.components
import android.content.Context
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
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.Info
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.model.AudioItem
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.util.supportedPlayableTypes
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaSourceInfo
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import kotlin.time.Duration
sealed interface ContextMenu {
data class ForBaseItem(
val fromLongClick: Boolean,
val item: BaseItem,
val chosenStreams: ChosenStreams?,
val showGoTo: Boolean,
val showStreamChoices: Boolean,
val canDelete: Boolean,
val canRemoveContinueWatching: Boolean,
val canRemoveNextUp: Boolean,
val actions: ContextMenuActions,
) : ContextMenu
data class ForPerson(
val fromLongClick: Boolean,
val person: Person,
val actions: PersonContextActions,
) : ContextMenu
data class ForMusic(
val fromLongClick: Boolean,
val item: BaseItem,
val index: Int,
val canDelete: Boolean,
val canRemoveFromQueue: Boolean,
val actions: MusicContextActions,
) : ContextMenu
data class ForQueue(
val fromLongClick: Boolean,
val item: AudioItem,
val index: Int,
val actions: QueueContextActions,
) : ContextMenu
}
data class ContextMenuActions(
val navigateTo: (Destination) -> Unit,
val onShowOverview: (BaseItem) -> Unit,
val onClickWatch: (UUID, Boolean) -> Unit,
val onClickFavorite: (UUID, Boolean) -> Unit,
val onClickAddPlaylist: (UUID) -> Unit,
val onSendMediaInfo: (UUID) -> Unit,
val onDeleteItem: (BaseItem) -> Unit,
val onChooseVersion: (BaseItem, MediaSourceInfo) -> Unit,
val onChooseTracks: (ChosenTrackResult) -> Unit,
val onClearChosenStreams: (ChosenStreams?) -> Unit,
val onClickGoTo: (BaseItem) -> Unit = { navigateTo(it.destination()) },
val onClickRemoveFromNextUp: (BaseItem) -> Unit = {},
val onClickAddToQueue: (BaseItem) -> Unit = {},
)
data class PersonContextActions(
val navigateTo: (Destination) -> Unit,
val onClickFavorite: (UUID, Boolean) -> Unit,
)
data class MusicContextActions(
val navigateTo: (Destination) -> Unit,
val onClickPlay: (Int, BaseItem) -> Unit,
val onClickPlayNext: (Int, BaseItem) -> Unit,
val onClickFavorite: (UUID, Boolean) -> Unit,
val onClickAddPlaylist: (UUID) -> Unit,
val onDeleteItem: (BaseItem) -> Unit,
val onClickAddToQueue: (BaseItem) -> Unit,
val onClickRemoveFromQueue: (Int, BaseItem) -> Unit,
val onClickGoToAlbum: (UUID) -> Unit = {
navigateTo.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ALBUM))
},
val onClickGoToArtist: (UUID) -> Unit = {
navigateTo.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ARTIST))
},
)
data class QueueContextActions(
val onNavigate: (Destination) -> Unit,
val onClickPlay: (Int, AudioItem) -> Unit,
val onClickPlayNext: (Int, AudioItem) -> Unit,
val onClickGoToAlbum: (java.util.UUID) -> Unit = {
onNavigate.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ALBUM))
},
val onClickGoToArtist: (java.util.UUID) -> Unit = {
onNavigate.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ARTIST))
},
val onClickRemoveFromQueue: (Int, AudioItem) -> Unit,
)
data class ChosenTrackResult(
val item: BaseItem,
val streamType: MediaStreamType,
val trackIndex: Int,
val itemPlayback: ItemPlayback?,
)
@Composable
fun ContextMenuDialog(
onDismissRequest: () -> Unit,
contextMenu: ContextMenu,
getMediaSource: ((dto: BaseItemDto, itemPlayback: ItemPlayback?) -> MediaSourceInfo?)?,
preferredSubtitleLanguage: String?,
) {
when (contextMenu) {
is ContextMenu.ForBaseItem -> {
ContextMenu(
onDismissRequest,
contextMenu,
getMediaSource,
preferredSubtitleLanguage,
contextMenu.actions,
)
}
is ContextMenu.ForPerson -> {
ContextMenu(
onDismissRequest = onDismissRequest,
item = contextMenu,
actions = contextMenu.actions,
)
}
is ContextMenu.ForMusic -> {
ContextMenu(
onDismissRequest = onDismissRequest,
item = contextMenu,
actions = contextMenu.actions,
)
}
is ContextMenu.ForQueue -> {
ContextMenu(
onDismissRequest = onDismissRequest,
item = contextMenu,
actions = contextMenu.actions,
)
}
}
}
@Composable
fun ContextMenu(
onDismissRequest: () -> Unit,
contextMenu: ContextMenu.ForBaseItem,
getMediaSource: ((dto: BaseItemDto, itemPlayback: ItemPlayback?) -> MediaSourceInfo?)?,
preferredSubtitleLanguage: String?,
actions: ContextMenuActions,
) {
val item = contextMenu.item
val chosenStreams = contextMenu.chosenStreams
val context = LocalContext.current
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
var showDeleteDialog by remember { mutableStateOf(false) }
val dialogItems =
remember(context, item, chosenStreams) {
buildContextMenuItems(
context = context,
item = item,
watched = item.played,
favorite = item.favorite,
seriesId = item.data.seriesId,
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
canClearChosenStreams = chosenStreams.let { it?.itemPlayback != null || it?.plc != null },
showGoTo = contextMenu.showGoTo,
showStreamChoices = contextMenu.showStreamChoices,
canDelete = contextMenu.canDelete,
canRemoveContinueWatching = contextMenu.canRemoveContinueWatching,
canRemoveNextUp = contextMenu.canRemoveNextUp,
actions = actions,
onChooseVersion = {
chooseVersion =
chooseVersionParams(
context,
item.data.mediaSources.orEmpty(),
chosenStreams?.source?.id?.toUUIDOrNull(),
) { idx ->
val source = item.data.mediaSources!![idx]
actions.onChooseVersion.invoke(item, source)
}
},
onChooseTracks = { type ->
getMediaSource
?.invoke(
item.data,
chosenStreams?.itemPlayback,
)?.let { source ->
chooseVersion =
chooseStream(
context = context,
streams = source.mediaStreams.orEmpty(),
type = type,
currentIndex =
if (type == MediaStreamType.AUDIO) {
chosenStreams?.audioStream?.index
} else {
chosenStreams?.subtitleStream?.index
},
onClick = { trackIndex ->
actions.onChooseTracks.invoke(
ChosenTrackResult(
item = item,
streamType = type,
trackIndex = trackIndex,
itemPlayback = chosenStreams?.itemPlayback,
),
)
},
preferredSubtitleLanguage = preferredSubtitleLanguage,
)
}
},
onShowOverview = { actions.onShowOverview.invoke(item) },
onClearChosenStreams = {
actions.onClearChosenStreams.invoke(chosenStreams)
},
onClickDelete = { showDeleteDialog = true },
)
}
DialogPopup(
showDialog = true,
title = item.title ?: "",
dialogItems = dialogItems,
onDismissRequest = onDismissRequest,
dismissOnClick = false,
waitToLoad = contextMenu.fromLongClick,
)
if (chooseVersion != null) {
chooseVersion?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { chooseVersion = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
)
}
}
if (showDeleteDialog) {
ConfirmDeleteDialog(
itemTitle = item.title ?: "",
onCancel = { showDeleteDialog = false },
onConfirm = {
actions.onDeleteItem.invoke(item)
onDismissRequest.invoke()
},
)
}
}
private fun buildContextMenuItems(
context: Context,
item: BaseItem,
seriesId: UUID?,
sourceId: UUID?,
watched: Boolean,
favorite: Boolean,
canClearChosenStreams: Boolean,
showGoTo: Boolean,
showStreamChoices: Boolean,
canDelete: Boolean,
canRemoveContinueWatching: Boolean,
canRemoveNextUp: Boolean,
actions: ContextMenuActions,
onChooseVersion: () -> Unit,
onChooseTracks: (MediaStreamType) -> Unit,
onShowOverview: () -> Unit,
onClearChosenStreams: () -> Unit,
onClickDelete: () -> Unit,
): List<DialogItem> =
buildList {
if (showGoTo) {
add(
DialogItem(
context.getString(R.string.go_to),
Icons.Default.ArrowForward,
dismissOnClick = true,
) {
actions.onClickGoTo(item)
},
)
}
if (item.type in supportedPlayableTypes) {
if (item.playbackPosition >= Duration.ZERO) {
add(
DialogItem(
context.getString(R.string.resume),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
dismissOnClick = true,
) {
actions.navigateTo(
Destination.Playback(
item.id,
item.playbackPosition.inWholeMilliseconds,
),
)
},
)
add(
DialogItem(
context.getString(R.string.restart),
Icons.Default.Refresh,
dismissOnClick = true,
) {
actions.navigateTo(
Destination.Playback(
item.id,
0L,
),
)
},
)
} else {
add(
DialogItem(
context.getString(R.string.play),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
dismissOnClick = true,
) {
actions.navigateTo(
Destination.Playback(
item.id,
0L,
),
)
},
)
}
}
if (showStreamChoices) {
item.data.mediaSources?.letNotEmpty { sources ->
val source =
sourceId?.let { sources.firstOrNull { it.id?.toUUIDOrNull() == sourceId } }
?: sources.firstOrNull()
source?.mediaStreams?.letNotEmpty { streams ->
val audioCount = streams.count { it.type == MediaStreamType.AUDIO }
val subtitleCount = streams.count { it.type == MediaStreamType.SUBTITLE }
if (audioCount > 1) {
add(
DialogItem(
context.getString(
R.string.choose_stream,
context.getString(R.string.audio),
),
R.string.fa_volume_low,
dismissOnClick = false,
) {
onChooseTracks.invoke(MediaStreamType.AUDIO)
},
)
}
if (subtitleCount > 0) {
add(
DialogItem(
context.getString(
R.string.choose_stream,
context.getString(R.string.subtitles),
),
R.string.fa_closed_captioning,
dismissOnClick = false,
) {
onChooseTracks.invoke(MediaStreamType.SUBTITLE)
},
)
}
}
if (sources.size > 1) {
add(
DialogItem(
context.getString(
R.string.choose_stream,
context.getString(R.string.version),
),
R.string.fa_file_video,
dismissOnClick = false,
) {
onChooseVersion.invoke()
},
)
}
}
}
if (item.type == BaseItemKind.MUSIC_ALBUM) {
add(
DialogItem(
context.getString(R.string.add_to_queue),
Icons.Default.Add,
dismissOnClick = true,
) {
actions.onClickAddToQueue(item)
},
)
}
add(
DialogItem(
text = R.string.add_to_playlist,
iconStringRes = R.string.fa_list_ul,
dismissOnClick = true,
) {
actions.onClickAddPlaylist.invoke(item.id)
},
)
if (canDelete) {
add(
DialogItem(
context.getString(R.string.delete),
Icons.Default.Delete,
iconColor = Color.Red.copy(alpha = .8f),
dismissOnClick = false,
) {
onClickDelete.invoke()
},
)
}
if (canRemoveContinueWatching && !watched && item.playbackPosition > Duration.ZERO) {
add(
DialogItem(
text = R.string.remove_continue_watching,
iconStringRes = R.string.fa_eye,
dismissOnClick = true,
) {
actions.onClickWatch.invoke(item.id, false)
},
)
}
if (canRemoveNextUp && item.type == BaseItemKind.EPISODE && item.data.seriesId != null) {
add(
DialogItem(
text = R.string.remove_next_up,
iconStringRes = R.string.fa_tag,
dismissOnClick = true,
) {
actions.onClickRemoveFromNextUp.invoke(item)
},
)
}
add(
DialogItem(
text = if (watched) R.string.mark_unwatched else R.string.mark_watched,
iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash,
dismissOnClick = true,
) {
actions.onClickWatch.invoke(item.id, !watched)
},
)
add(
DialogItem(
text = if (favorite) R.string.remove_favorite else R.string.add_favorite,
iconStringRes = R.string.fa_heart,
iconColor = if (favorite) Color.Red else Color.Unspecified,
dismissOnClick = true,
) {
actions.onClickFavorite.invoke(item.id, !favorite)
},
)
item.data.albumId?.let { albumId ->
add(
DialogItem(
context.getString(R.string.go_to_album),
R.string.fa_compact_disc,
dismissOnClick = true,
) {
actions.navigateTo(
Destination.MediaItem(
albumId,
BaseItemKind.MUSIC_ALBUM,
null,
),
)
},
)
}
item.data.artistItems?.firstOrNull()?.id?.let { artistId ->
add(
DialogItem(
context.getString(R.string.go_to_artist),
R.string.fa_user,
dismissOnClick = true,
) {
actions.navigateTo(
Destination.MediaItem(
artistId,
BaseItemKind.MUSIC_ARTIST,
null,
),
)
},
)
}
seriesId?.let {
add(
DialogItem(
context.getString(R.string.go_to_series),
Icons.AutoMirrored.Filled.ArrowForward,
dismissOnClick = true,
) {
actions.navigateTo(
Destination.MediaItem(
seriesId,
BaseItemKind.SERIES,
null,
),
)
},
)
}
if (item.data.mediaSources?.isNotEmpty() == true) {
add(
DialogItem(
context.getString(R.string.media_information),
Icons.Default.Info,
dismissOnClick = true,
) {
onShowOverview.invoke()
},
)
}
if (showStreamChoices && canClearChosenStreams) {
add(
DialogItem(
context.getString(R.string.clear_track_choices),
Icons.Default.Delete,
dismissOnClick = true,
) {
onClearChosenStreams()
},
)
}
if (item.type in supportedPlayableTypes) {
add(
DialogItem(
context.getString(R.string.play_with_transcoding),
Icons.Default.PlayArrow,
dismissOnClick = true,
) {
actions.navigateTo(
Destination.Playback(
item.id,
item.resumeMs ?: 0L,
forceTranscoding = true,
),
)
},
)
}
if (item.data.mediaSources?.isNotEmpty() == true) {
add(
DialogItem(
text = R.string.send_media_info_log_to_server,
iconStringRes = R.string.fa_file_video,
dismissOnClick = true,
) {
actions.onSendMediaInfo.invoke(item.id)
},
)
}
}
@Composable
fun ContextMenu(
onDismissRequest: () -> Unit,
item: ContextMenu.ForPerson,
actions: PersonContextActions,
) {
val person = item.person
val dialogItems =
buildList {
val itemId = person.id
add(
DialogItem(
stringResource(R.string.go_to),
Icons.Default.ArrowForward,
) {
actions.navigateTo(Destination.MediaItem(itemId, BaseItemKind.PERSON, null))
},
)
add(
DialogItem(
text = if (person.favorite) R.string.remove_favorite else R.string.add_favorite,
iconStringRes = R.string.fa_heart,
iconColor = if (person.favorite) Color.Red else Color.Unspecified,
) {
actions.onClickFavorite.invoke(itemId, !person.favorite)
},
)
}
DialogPopup(
showDialog = true,
title = person.name ?: "",
dialogItems = dialogItems,
onDismissRequest = onDismissRequest,
dismissOnClick = true,
waitToLoad = item.fromLongClick,
)
}
@Composable
fun ContextMenu(
onDismissRequest: () -> Unit,
item: ContextMenu.ForMusic,
actions: MusicContextActions,
) {
val context = LocalContext.current
var showDeleteDialog by remember { mutableStateOf(false) }
val dialogItems =
remember(context, item, actions) {
buildContextForMusic(context, item, actions, onClickDelete = {
showDeleteDialog = true
})
}
DialogPopup(
showDialog = true,
title = item.item.title ?: "",
dialogItems = dialogItems,
onDismissRequest = onDismissRequest,
dismissOnClick = false,
waitToLoad = item.fromLongClick,
)
if (showDeleteDialog) {
ConfirmDeleteDialog(
itemTitle = item.item.title ?: "",
onCancel = { showDeleteDialog = false },
onConfirm = {
actions.onDeleteItem.invoke(item.item)
onDismissRequest.invoke()
},
)
}
}
fun buildContextForMusic(
context: Context,
music: ContextMenu.ForMusic,
actions: MusicContextActions,
onClickDelete: () -> Unit,
): List<DialogItem> =
buildList {
val item = music.item
val index = music.index
add(
DialogItem(
context.getString(R.string.play),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
dismissOnClick = true,
) {
actions.onClickPlay(index, item)
},
)
add(
DialogItem(
context.getString(R.string.play_next),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
dismissOnClick = true,
) {
actions.onClickPlayNext(index, item)
},
)
if (music.canRemoveFromQueue) {
add(
DialogItem(
context.getString(R.string.remove_from_queue),
Icons.Default.Delete,
dismissOnClick = true,
) {
actions.onClickRemoveFromQueue(index, item)
},
)
} else {
add(
DialogItem(
context.getString(R.string.add_to_queue),
Icons.Default.Add,
dismissOnClick = true,
) {
actions.onClickAddToQueue(item)
},
)
}
add(
DialogItem(
text = R.string.add_to_playlist,
iconStringRes = R.string.fa_list_ul,
dismissOnClick = true,
) {
actions.onClickAddPlaylist.invoke(item.id)
},
)
if (music.canDelete) {
add(
DialogItem(
context.getString(R.string.delete),
Icons.Default.Delete,
iconColor = Color.Red.copy(alpha = .8f),
dismissOnClick = false,
) {
onClickDelete.invoke()
},
)
}
add(
DialogItem(
text = if (item.favorite) R.string.remove_favorite else R.string.add_favorite,
iconStringRes = R.string.fa_heart,
iconColor = if (item.favorite) Color.Red else Color.Unspecified,
dismissOnClick = true,
) {
actions.onClickFavorite.invoke(item.id, !item.favorite)
},
)
if (item.type == BaseItemKind.AUDIO && item.data.albumId != null) {
add(
DialogItem(
context.getString(R.string.go_to_album),
R.string.fa_compact_disc,
dismissOnClick = true,
) {
actions.onClickGoToAlbum.invoke(item.data.albumId!!)
},
)
}
if ((item.type == BaseItemKind.AUDIO || item.type == BaseItemKind.MUSIC_ALBUM) && item.data.artistItems?.isNotEmpty() == true) {
add(
DialogItem(
context.getString(R.string.go_to_artist),
R.string.fa_user,
dismissOnClick = true,
) {
actions.onClickGoToArtist.invoke(
item.data.artistItems!!
.first()
.id,
)
},
)
}
}
@Composable
fun ContextMenu(
onDismissRequest: () -> Unit,
item: ContextMenu.ForQueue,
actions: QueueContextActions,
) {
val context = LocalContext.current
val dialogItems =
remember(context, item, actions) {
buildContextForMusicQueue(
context,
item.item,
item.index,
actions,
)
}
DialogPopup(
showDialog = true,
title = item.item.title ?: "",
dialogItems = dialogItems,
onDismissRequest = onDismissRequest,
dismissOnClick = false,
waitToLoad = item.fromLongClick,
)
}
fun buildContextForMusicQueue(
context: Context,
item: AudioItem,
index: Int,
actions: QueueContextActions,
): List<DialogItem> =
buildList {
add(
DialogItem(
context.getString(R.string.play),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
dismissOnClick = true,
) {
actions.onClickPlay(index, item)
},
)
add(
DialogItem(
context.getString(R.string.play_next),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
dismissOnClick = true,
) {
actions.onClickPlayNext(index, item)
},
)
add(
DialogItem(
context.getString(R.string.remove_from_queue),
Icons.Default.Delete,
dismissOnClick = true,
) {
actions.onClickRemoveFromQueue(index, item)
},
)
if (item.albumId != null) {
add(
DialogItem(
context.getString(R.string.go_to_album),
Icons.Default.ArrowForward,
dismissOnClick = true,
) {
actions.onClickGoToAlbum.invoke(item.albumId)
},
)
}
if (item.artistId != null) {
add(
DialogItem(
context.getString(R.string.go_to_artist),
Icons.Default.ArrowForward,
dismissOnClick = true,
) {
actions.onClickGoToArtist.invoke(item.artistId)
},
)
}
}

View file

@ -104,11 +104,13 @@ data class DialogItem(
val trailingContent: @Composable (() -> Unit)? = null,
val enabled: Boolean = true,
val selected: Boolean = false,
val dismissOnClick: Boolean = false,
) : DialogItemEntry {
constructor(
@StringRes text: Int,
@StringRes iconStringRes: Int,
iconColor: Color = Color.Unspecified,
dismissOnClick: Boolean = false,
onClick: () -> Unit,
) : this(
headlineContent = {
@ -124,11 +126,13 @@ data class DialogItem(
)
},
onClick = onClick,
dismissOnClick = dismissOnClick,
)
constructor(
text: String,
@StringRes iconStringRes: Int,
dismissOnClick: Boolean = false,
onClick: () -> Unit,
) : this(
headlineContent = {
@ -145,12 +149,14 @@ data class DialogItem(
)
},
onClick = onClick,
dismissOnClick = dismissOnClick,
)
constructor(
text: String,
icon: ImageVector,
iconColor: Color? = null,
dismissOnClick: Boolean = false,
onClick: () -> Unit,
) : this(
headlineContent = {
@ -167,10 +173,12 @@ data class DialogItem(
)
},
onClick = onClick,
dismissOnClick = dismissOnClick,
)
constructor(
text: String,
dismissOnClick: Boolean = false,
onClick: () -> Unit,
) : this(
headlineContent = {
@ -180,6 +188,7 @@ data class DialogItem(
)
},
onClick = onClick,
dismissOnClick = dismissOnClick,
)
companion object {
@ -201,7 +210,7 @@ fun DialogPopup(
dismissOnClick: Boolean = true,
waitToLoad: Boolean = true,
properties: DialogProperties = DialogProperties(),
elevation: Dp = 8.dp,
elevation: Dp = 1.dp,
) {
var waiting by remember { mutableStateOf(waitToLoad) }
if (showDialog) {
@ -296,7 +305,7 @@ fun DialogPopupContent(
selected = item.selected,
enabled = !waiting && item.enabled,
onClick = {
if (dismissOnClick) {
if (dismissOnClick || item.dismissOnClick) {
onDismissRequest.invoke()
}
item.onClick.invoke()
@ -349,7 +358,7 @@ fun DialogPopup(
onDismissRequest: () -> Unit,
dismissOnClick: Boolean = true,
properties: DialogProperties = DialogProperties(),
elevation: Dp = 8.dp,
elevation: Dp = 1.dp,
) = DialogPopup(
showDialog = true,
waitToLoad = params.fromLongClick,
@ -427,7 +436,7 @@ fun ScrollableDialog(
fun BasicDialog(
onDismissRequest: () -> Unit,
properties: DialogProperties = DialogProperties(),
elevation: Dp = 8.dp,
elevation: Dp = 1.dp,
content: @Composable () -> Unit,
) {
Dialog(
@ -458,7 +467,7 @@ fun ConfirmDialog(
onCancel: () -> Unit,
onConfirm: () -> Unit,
properties: DialogProperties = DialogProperties(),
elevation: Dp = 8.dp,
elevation: Dp = 1.dp,
bodyColor: Color = MaterialTheme.colorScheme.onSurface,
) = BasicDialog(
onDismissRequest = onCancel,
@ -527,10 +536,12 @@ fun ConfirmDeleteDialog(
itemTitle: String,
onCancel: () -> Unit,
onConfirm: () -> Unit,
elevation: Dp = 3.dp,
) {
BasicDialog(
onDismissRequest = onCancel,
properties = DialogProperties(usePlatformDefaultWidth = false),
elevation = elevation,
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,

View file

@ -69,6 +69,7 @@ import kotlin.time.Duration.Companion.seconds
*/
@Composable
fun ExpandablePlayButtons(
title: String,
resumePosition: Duration,
watched: Boolean,
favorite: Boolean,
@ -79,7 +80,7 @@ fun ExpandablePlayButtons(
favoriteOnClick: () -> Unit,
moreOnClick: () -> Unit,
trailerOnClick: (Trailer) -> Unit,
deleteOnClick: () -> Unit,
onConfirmDelete: () -> Unit,
buttonOnFocusChanged: (FocusState) -> Unit,
modifier: Modifier = Modifier,
) {
@ -163,7 +164,8 @@ fun ExpandablePlayButtons(
if (canDelete) {
item("delete") {
DeleteButton(
onClick = deleteOnClick,
title = title,
onConfirmDelete = onConfirmDelete,
modifier =
Modifier
.onFocusChanged(buttonOnFocusChanged),
@ -381,10 +383,12 @@ fun TrailerButton(
@Composable
fun DeleteButton(
onClick: () -> Unit,
onConfirmDelete: () -> Unit,
title: String,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
var showDeleteDialog by remember { mutableStateOf(false) }
val focused by interactionSource.collectIsFocusedAsState()
val iconTint by
animateColorAsState(
@ -415,10 +419,20 @@ fun DeleteButton(
.size(24.dp),
)
},
onClick = { onClick.invoke() },
onClick = { showDeleteDialog = true },
interactionSource = interactionSource,
modifier = modifier,
)
if (showDeleteDialog) {
ConfirmDeleteDialog(
itemTitle = title,
onCancel = { showDeleteDialog = false },
onConfirm = {
onConfirmDelete.invoke()
showDeleteDialog = false
},
)
}
}
@PreviewTvSpec
@ -426,6 +440,7 @@ fun DeleteButton(
private fun ExpandablePlayButtonsPreview() {
WholphinTheme(true) {
ExpandablePlayButtons(
title = "Movie",
resumePosition = 10.seconds,
watched = false,
favorite = false,
@ -437,7 +452,7 @@ private fun ExpandablePlayButtonsPreview() {
trailers = listOf(),
trailerOnClick = {},
canDelete = true,
deleteOnClick = {},
onConfirmDelete = {},
modifier = Modifier,
)
}
@ -482,7 +497,8 @@ private fun ViewOptionsPreview() {
modifier = Modifier,
)
DeleteButton(
onClick = {},
onConfirmDelete = {},
title = "Movie",
)
SortByButton(
sortOptions = listOf(),
@ -491,7 +507,8 @@ private fun ViewOptionsPreview() {
)
}
DeleteButton(
onClick = {},
onConfirmDelete = {},
title = "Movie",
interactionSource = source,
)
}

View file

@ -10,7 +10,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
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
@ -18,6 +17,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
import com.github.damontecres.wholphin.preferences.AppPreferences
@ -31,11 +31,11 @@ import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.deleteItem
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.detail.music.addToQueue
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO
@ -60,6 +60,7 @@ import java.util.UUID
abstract class RecommendedViewModel(
@param:ApplicationContext val context: Context,
val api: ApiClient,
val serverRepository: ServerRepository,
val navigationManager: NavigationManager,
val favoriteWatchManager: FavoriteWatchManager,
val mediaReportService: MediaReportService,
@ -167,10 +168,9 @@ fun RecommendedContent(
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
onFocusPosition: ((RowColumn) -> Unit)? = null,
) {
val context = LocalContext.current
var moreDialog by remember { mutableStateOf<Optional<RowColumnItem>>(Optional.absent()) }
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
var showDeleteDialog by remember { mutableStateOf<RowColumnItem?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
OneTimeLaunchedEffect {
@ -192,6 +192,35 @@ fun RecommendedContent(
LoadingState.Success -> {
var position by rememberPosition()
val contextActions =
remember {
ContextMenuActions(
navigateTo = viewModel.navigationManager::navigateTo,
onClickWatch = { itemId, watched ->
viewModel.setWatched(position, itemId, watched)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(position, itemId, favorite)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onDeleteItem = { viewModel.deleteItem(position, it) },
onShowOverview = { overviewDialog = ItemDetailsDialogInfo(it) },
onChooseVersion = { _, _ ->
// Not supported on this page
},
onChooseTracks = { result ->
// Not supported on this page
},
onClearChosenStreams = {
// Not supported on this page
},
)
}
HomePageContent(
homeRows = rows,
position = position,
@ -199,7 +228,18 @@ fun RecommendedContent(
viewModel.navigationManager.navigateTo(item.destination())
},
onLongClickItem = { position, item ->
moreDialog.makePresent(RowColumnItem(position, item))
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = true,
item = item,
chosenStreams = null,
showGoTo = true,
showStreamChoices = false,
canDelete = viewModel.canDelete(item, preferences.appPreferences),
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = contextActions,
)
},
onClickPlay = { _, item ->
viewModel.navigationManager.navigateTo(Destination.Playback(item))
@ -226,42 +266,22 @@ fun RecommendedContent(
)
}
}
moreDialog.compose { (position, item) ->
DialogPopup(
showDialog = true,
title = item.title ?: "",
dialogItems =
buildMoreDialogItemsForHome(
context = context,
item = item,
seriesId = null,
playbackPosition = item.playbackPosition,
watched = item.played,
favorite = item.favorite,
canDelete = viewModel.canDelete(item, preferences.appPreferences),
actions =
MoreDialogActions(
navigateTo = { viewModel.navigationManager.navigateTo(it) },
onClickWatch = { itemId, watched ->
viewModel.setWatched(position, itemId, watched)
},
onClickFavorite = { itemId, watched ->
viewModel.setFavorite(position, itemId, watched)
},
onClickAddPlaylist = {
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(it)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onClickDelete = { showDeleteDialog = RowColumnItem(position, item) },
onClickAddToQueue = {
viewModel.addToQueue(it, 0)
},
),
),
onDismissRequest = { moreDialog.makeAbsent() },
dismissOnClick = true,
waitToLoad = true,
overviewDialog?.let { info ->
ItemDetailsDialog(
info = info,
showFilePath =
viewModel.serverRepository.currentUserDto.value
?.policy
?.isAdministrator == true,
onDismissRequest = { overviewDialog = null },
)
}
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = null,
contextMenu = contextMenu,
preferredSubtitleLanguage = null,
)
}
showPlaylistDialog.compose { itemId ->
@ -281,16 +301,6 @@ fun RecommendedContent(
elevation = 3.dp,
)
}
showDeleteDialog?.let { (position, item) ->
ConfirmDeleteDialog(
itemTitle = listOfNotNull(item.title, item.subtitle).joinToString(" - "),
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(position, item)
showDeleteDialog = null
},
)
}
}
data class RowColumnItem(

View file

@ -58,7 +58,7 @@ class RecommendedMovieViewModel
@ApplicationContext context: Context,
api: ApiClient,
musicService: MusicService,
private val serverRepository: ServerRepository,
serverRepository: ServerRepository,
private val preferencesDataStore: DataStore<AppPreferences>,
private val suggestionService: SuggestionService,
@Assisted val parentId: UUID,
@ -70,6 +70,7 @@ class RecommendedMovieViewModel
) : RecommendedViewModel(
context,
api,
serverRepository,
navigationManager,
favoriteWatchManager,
mediaReportService,

View file

@ -57,7 +57,7 @@ class RecommendedMusicViewModel
@ApplicationContext context: Context,
api: ApiClient,
musicService: MusicService,
private val serverRepository: ServerRepository,
serverRepository: ServerRepository,
private val preferencesDataStore: DataStore<AppPreferences>,
private val suggestionService: SuggestionService,
@Assisted val parentId: UUID,
@ -69,6 +69,7 @@ class RecommendedMusicViewModel
) : RecommendedViewModel(
context,
api,
serverRepository,
navigationManager,
favoriteWatchManager,
mediaReportService,

View file

@ -61,7 +61,7 @@ class RecommendedTvShowViewModel
@ApplicationContext context: Context,
api: ApiClient,
musicService: MusicService,
private val serverRepository: ServerRepository,
serverRepository: ServerRepository,
private val preferencesDataStore: DataStore<AppPreferences>,
private val lastestNextUpService: LatestNextUpService,
private val suggestionService: SuggestionService,
@ -74,6 +74,7 @@ class RecommendedTvShowViewModel
) : RecommendedViewModel(
context,
api,
serverRepository,
navigationManager,
favoriteWatchManager,
mediaReportService,

View file

@ -6,21 +6,16 @@ import androidx.compose.material.icons.automirrored.filled.ArrowForward
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.Info
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.ui.graphics.Color
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.ui.components.DialogItem
import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.util.supportedPlayableTypes
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@ -36,234 +31,6 @@ data class MoreDialogActions(
val onClickAddToQueue: (BaseItem) -> Unit = {},
)
enum class ClearChosenStreams {
NONE,
ITEM_AND_SERIES,
SERIES,
}
/**
* Build the [DialogItem]s when clicking "More"
*
* If there are multiple versions, adds an option to pick one
*
* If there is more than one (ie two or more) audio track, adds an option to pick one
*
* If there are any (ie one or more) subtitle tracks, adds an option to disable or pick one
*
* @param item the media item to build for, typically an Episode or Movie
* @param seriesId the item's series or null if not a TV episode; a non-null value will include a "Go to Series" option
* @param sourceId the item's media source UUID
* @param onChooseVersion callback to pick a version of the item
* @param onChooseTracks callback to pick a track for the given type of the item
* @param onShowOverview callback to show overview dialog with media information
*/
fun buildMoreDialogItems(
context: Context,
item: BaseItem,
seriesId: UUID?,
sourceId: UUID?,
watched: Boolean,
favorite: Boolean,
canClearChosenStreams: Boolean,
canDelete: Boolean,
actions: MoreDialogActions,
onChooseVersion: () -> Unit,
onChooseTracks: (MediaStreamType) -> Unit,
onShowOverview: () -> Unit,
onClearChosenStreams: () -> Unit,
): List<DialogItem> =
buildList {
add(
DialogItem(
context.getString(R.string.play),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
actions.navigateTo(
Destination.Playback(
item.id,
item.resumeMs ?: 0L,
),
)
},
)
item.data.mediaSources?.letNotEmpty { sources ->
val source =
sourceId?.let { sources.firstOrNull { it.id?.toUUIDOrNull() == sourceId } }
?: sources.firstOrNull()
source?.mediaStreams?.letNotEmpty { streams ->
val audioCount = streams.count { it.type == MediaStreamType.AUDIO }
val subtitleCount = streams.count { it.type == MediaStreamType.SUBTITLE }
if (audioCount > 1) {
add(
DialogItem(
context.getString(
R.string.choose_stream,
context.getString(R.string.audio),
),
R.string.fa_volume_low,
) {
onChooseTracks.invoke(MediaStreamType.AUDIO)
},
)
}
if (subtitleCount > 0) {
add(
DialogItem(
context.getString(
R.string.choose_stream,
context.getString(R.string.subtitles),
),
R.string.fa_closed_captioning,
) {
onChooseTracks.invoke(MediaStreamType.SUBTITLE)
},
)
}
}
if (sources.size > 1) {
add(
DialogItem(
context.getString(
R.string.choose_stream,
context.getString(R.string.version),
),
R.string.fa_file_video,
) {
onChooseVersion.invoke()
},
)
}
}
add(
DialogItem(
text = R.string.add_to_playlist,
iconStringRes = R.string.fa_list_ul,
) {
actions.onClickAddPlaylist.invoke(item.id)
},
)
if (canDelete) {
add(
DialogItem(
context.getString(R.string.delete),
Icons.Default.Delete,
iconColor = Color.Red.copy(alpha = .8f),
) {
actions.onClickDelete.invoke(item)
},
)
}
add(
DialogItem(
text = if (watched) R.string.mark_unwatched else R.string.mark_watched,
iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash,
) {
actions.onClickWatch.invoke(item.id, !watched)
},
)
add(
DialogItem(
text = if (favorite) R.string.remove_favorite else R.string.add_favorite,
iconStringRes = R.string.fa_heart,
iconColor = if (favorite) Color.Red else Color.Unspecified,
) {
actions.onClickFavorite.invoke(item.id, !favorite)
},
)
item.data.albumId?.let { albumId ->
add(
DialogItem(
context.getString(R.string.go_to_album),
R.string.fa_compact_disc,
) {
actions.navigateTo(
Destination.MediaItem(
albumId,
BaseItemKind.MUSIC_ALBUM,
null,
),
)
},
)
}
item.data.artistItems?.firstOrNull()?.id?.let { artistId ->
add(
DialogItem(
context.getString(R.string.go_to_artist),
R.string.fa_user,
) {
actions.navigateTo(
Destination.MediaItem(
artistId,
BaseItemKind.MUSIC_ARTIST,
null,
),
)
},
)
}
seriesId?.let {
add(
DialogItem(
context.getString(R.string.go_to_series),
Icons.AutoMirrored.Filled.ArrowForward,
) {
actions.navigateTo(
Destination.MediaItem(
seriesId,
BaseItemKind.SERIES,
null,
),
)
},
)
}
if (item.data.mediaSources?.isNotEmpty() == true) {
add(
DialogItem(
context.getString(R.string.media_information),
Icons.Default.Info,
) {
onShowOverview.invoke()
},
)
}
if (canClearChosenStreams) {
add(
DialogItem(
context.getString(R.string.clear_track_choices),
Icons.Default.Delete,
) {
onClearChosenStreams()
},
)
}
add(
DialogItem(
context.getString(R.string.play_with_transcoding),
Icons.Default.PlayArrow,
) {
actions.navigateTo(
Destination.Playback(
item.id,
item.resumeMs ?: 0L,
forceTranscoding = true,
),
)
},
)
add(
DialogItem(
text = R.string.send_media_info_log_to_server,
iconStringRes = R.string.fa_file_video,
) {
actions.onSendMediaInfo.invoke(item.id)
},
)
}
fun buildMoreDialogItemsForHome(
context: Context,
item: BaseItem,
@ -424,29 +191,3 @@ fun buildMoreDialogItemsForHome(
},
)
}
fun buildMoreDialogItemsForPerson(
context: Context,
person: Person,
actions: MoreDialogActions,
): List<DialogItem> =
buildList {
val itemId = person.id
add(
DialogItem(
context.getString(R.string.go_to),
Icons.Default.ArrowForward,
) {
actions.navigateTo(Destination.MediaItem(itemId, BaseItemKind.PERSON, null))
},
)
add(
DialogItem(
text = if (person.favorite) R.string.remove_favorite else R.string.add_favorite,
iconStringRes = R.string.fa_heart,
iconColor = if (person.favorite) Color.Red else Color.Unspecified,
) {
actions.onClickFavorite.invoke(itemId, !person.favorite)
},
)
}

View file

@ -72,14 +72,15 @@ import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.cards.ItemCardImage
import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ContextMenu
import com.github.damontecres.wholphin.ui.components.ContextMenuActions
import com.github.damontecres.wholphin.ui.components.ContextMenuDialog
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
import com.github.damontecres.wholphin.ui.components.FilterByButton
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.components.OverviewText
import com.github.damontecres.wholphin.ui.components.SortByButton
@ -87,10 +88,8 @@ import com.github.damontecres.wholphin.ui.components.TextButton
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.BoxSetSortOptions
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.music.MusicMoreDialogActions
import com.github.damontecres.wholphin.ui.detail.music.MusicQueueMarker
import com.github.damontecres.wholphin.ui.detail.music.MusicViewModel
import com.github.damontecres.wholphin.ui.detail.music.buildMoreDialogForMusic
import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.equalsNotNull
import com.github.damontecres.wholphin.ui.formatDateTime
@ -371,7 +370,7 @@ fun PlaylistDetails(
val state by viewModel.state.collectAsState()
val musicState by viewModel.musicState.collectAsState()
var longClickDialog by remember { mutableStateOf<DialogParams?>(null) }
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var showConfirmTypeDialog by remember { mutableStateOf<Triple<Int, BaseItem, Boolean>?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
val addPlaylistState by addToPlaylistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
@ -404,23 +403,23 @@ fun PlaylistDetails(
}
}
}
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
val musicMoreActions =
MusicMoreDialogActions(
onNavigate = { viewModel.navigationManager.navigateTo(it) },
val musicContextActions =
MusicContextActions(
navigateTo = { viewModel.navigationManager.navigateTo(it) },
onClickPlay = { index, item -> play(index, item, false, MediaType.AUDIO) },
onClickPlayNext = { index, item -> viewModel.playNext(item) },
onClickAddToQueue = { index, item -> viewModel.addToQueue(item, Int.MAX_VALUE) },
onClickAddToQueue = { item -> viewModel.addToQueue(item, Int.MAX_VALUE) },
onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) },
onClickAddPlaylist = { itemId ->
addToPlaylistViewModel.loadPlaylists(MediaType.AUDIO)
showPlaylistDialog.makePresent(itemId)
},
onClickRemoveFromQueue = {},
onClickDelete = { showDeleteDialog = it },
onClickRemoveFromQueue = { _, _ -> },
onDeleteItem = viewModel::deleteItem,
)
val moreActions =
MoreDialogActions(
val contextActions =
remember {
ContextMenuActions(
navigateTo = { viewModel.navigationManager.navigateTo(it) },
onClickWatch = { id, watched -> viewModel.setWatched(id, watched) },
onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) },
@ -429,9 +428,15 @@ fun PlaylistDetails(
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel::sendMediaReport,
onClickDelete = { showDeleteDialog = it },
onDeleteItem = viewModel::deleteItem,
onClickAddToQueue = { viewModel.addToQueue(it, 0) },
onShowOverview = {},
onChooseVersion = { _, _ -> },
onChooseTracks = {},
onClearChosenStreams = {},
onClickRemoveFromNextUp = {},
)
}
PlaylistDetailsContent(
loadingState = state.loading,
@ -448,43 +453,41 @@ fun PlaylistDetails(
}
},
onLongClickIndex = { index, item ->
longClickDialog =
DialogParams(
fromLongClick = true,
title = item.name ?: "",
items =
showContextMenu =
if (item.type == BaseItemKind.AUDIO) {
buildMoreDialogForMusic(
context = context,
actions = musicMoreActions,
ContextMenu.ForMusic(
fromLongClick = true,
item = item,
index = index,
canRemove = false,
canDelete = viewModel.canDelete(item, preferences.appPreferences),
canRemoveFromQueue = false,
actions = musicContextActions,
)
} else {
buildMoreDialogItemsForHome(
context = context,
ContextMenu.ForBaseItem(
fromLongClick = true,
item = item,
seriesId = item.data.seriesId,
playbackPosition = item.playbackPosition,
watched = item.played,
favorite = item.favorite,
chosenStreams = null,
showGoTo = true,
showStreamChoices = false,
canDelete = viewModel.canDelete(item, preferences.appPreferences),
actions = moreActions,
)
},
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = contextActions,
)
}
},
filterAndSort = state.filterAndSort,
onFilterAndSortChange = viewModel::loadItems,
getPossibleFilterValues = viewModel::getFilterOptionValues,
modifier = modifier,
)
longClickDialog?.let { params ->
DialogPopup(
params = params,
onDismissRequest = { longClickDialog = null },
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = null,
contextMenu = contextMenu,
preferredSubtitleLanguage = null,
)
}
showConfirmTypeDialog?.let { (index, item, shuffle) ->
@ -510,16 +513,6 @@ fun PlaylistDetails(
elevation = 3.dp,
)
}
showDeleteDialog?.let { item ->
ConfirmDeleteDialog(
itemTitle = item.title ?: "",
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(item)
showDeleteDialog = null
},
)
}
}
@Composable

View file

@ -40,7 +40,7 @@ fun CollectionButtons(
getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List<FilterValueOption>,
onClickViewOptions: () -> Unit,
favoriteOnClick: () -> Unit,
deleteOnClick: () -> Unit,
onConfirmDelete: () -> Unit,
canDelete: Boolean,
moreOnClick: () -> Unit,
modifier: Modifier = Modifier,
@ -91,9 +91,9 @@ fun CollectionButtons(
if (canDelete) {
item("delete") {
DeleteButton(
onClick = deleteOnClick,
modifier =
Modifier,
title = state.collection?.title ?: "",
onConfirmDelete = onConfirmDelete,
modifier = Modifier,
)
}
}

View file

@ -1,6 +1,5 @@
package com.github.damontecres.wholphin.ui.detail.collection
import android.content.Context
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn
@ -15,9 +14,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@ -32,7 +28,6 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -43,10 +38,9 @@ import com.github.damontecres.wholphin.data.filter.ItemFilterBy
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
import com.github.damontecres.wholphin.ui.components.DialogItem
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ContextMenu
import com.github.damontecres.wholphin.ui.components.ContextMenuActions
import com.github.damontecres.wholphin.ui.components.ContextMenuDialog
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.HeaderUtils
import com.github.damontecres.wholphin.ui.components.HiddenFocusBox
@ -57,10 +51,8 @@ import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.main.HomePageHeader
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.tryRequestFocus
@ -85,33 +77,15 @@ fun CollectionDetails(
val state by viewModel.state.collectAsState()
// Dialogs
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
var showDeleteDialog by remember { mutableStateOf<Pair<RowColumn?, BaseItem>?>(null) }
var showViewOptionsDialog by remember { mutableStateOf(false) }
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
// Actions
val onClickItem =
remember {
{ _: RowColumn, item: BaseItem -> viewModel.navigate(item.destination()) }
}
val onLongClickItem =
remember {
{ position: RowColumn, item: BaseItem ->
val dialogItems =
buildMoreDialogItemsForHome(
context = context,
item = item,
seriesId = item.data.seriesId,
playbackPosition = item.playbackPosition,
watched = item.played,
favorite = item.favorite,
canDelete = viewModel.canDelete(item, preferences.appPreferences),
actions =
MoreDialogActions(
navigateTo = viewModel::navigate,
fun contextActionsFor(position: RowColumn?) =
ContextMenuActions(
navigateTo = viewModel::navigateTo,
onClickWatch = { itemId, watched ->
viewModel.setWatched(itemId, watched, position)
},
@ -123,15 +97,38 @@ fun CollectionDetails(
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onClickDelete = { item -> showDeleteDialog = Pair(position, item) },
onClickAddToQueue = { viewModel.addToQueue(it, 0) },
),
onDeleteItem = { viewModel.deleteItem(it, position) },
onShowOverview = { overviewDialog = ItemDetailsDialogInfo(it) },
onChooseVersion = { _, _ ->
// Not supported on this page
},
onChooseTracks = { result ->
// Not supported on this page
},
onClearChosenStreams = {
// Not supported on this page
},
)
moreDialog =
DialogParams(
// Actions
val onClickItem =
remember {
{ _: RowColumn, item: BaseItem -> viewModel.navigateTo(item.destination()) }
}
val onLongClickItem =
remember {
{ position: RowColumn, item: BaseItem ->
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = true,
title = item.title ?: "",
items = dialogItems,
item = item,
chosenStreams = null,
showGoTo = true,
showStreamChoices = false,
canDelete = viewModel.canDelete(item, preferences.appPreferences),
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = contextActionsFor(position),
)
}
}
@ -144,7 +141,7 @@ fun CollectionDetails(
{ filter: GetItemsFilter -> viewModel.changeFilter(filter) }
}
val onClickPlay = { _: RowColumn, item: BaseItem ->
viewModel.navigate(Destination.Playback(item = item))
viewModel.navigateTo(Destination.Playback(item = item))
}
val onClickPlayAll =
remember {
@ -158,7 +155,7 @@ fun CollectionDetails(
sortAndDirection = state.sortAndDirection,
filter = state.itemFilter,
)
viewModel.navigate(dest)
viewModel.navigateTo(dest)
}
}
val onClickViewOptions = remember { { showViewOptionsDialog = true } }
@ -201,7 +198,7 @@ fun CollectionDetails(
}
}
},
deleteOnClick =
onConfirmDelete =
remember {
{
state.collection?.let {
@ -216,37 +213,21 @@ fun CollectionDetails(
} ?: false
},
moreOnClick = {
val collection = state.collection!!
val items =
buildMoreDialogItemsForCollection(
context = context,
item = collection,
favorite = collection.favorite,
canDelete = viewModel.canDelete(collection, preferences.appPreferences),
onClickPlayAll = onClickPlayAll,
actions =
MoreDialogActions(
navigateTo = viewModel::navigate,
onClickWatch = { itemId, watched ->
viewModel.setWatched(itemId, watched, null)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(itemId, favorite, null)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onClickDelete = { item -> showDeleteDialog = Pair(null, item) },
onClickAddToQueue = { viewModel.addToQueue(it, 0) },
),
)
moreDialog =
DialogParams(
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = false,
title = collection.title ?: "",
items = items,
item = state.collection!!,
chosenStreams = null,
showGoTo = false,
showStreamChoices = false,
canDelete =
viewModel.canDelete(
state.collection!!,
preferences.appPreferences,
),
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = contextActionsFor(null),
)
},
)
@ -259,14 +240,22 @@ fun CollectionDetails(
onViewOptionsChange = viewModel::changeViewOptions,
)
}
moreDialog?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { moreDialog = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
overviewDialog?.let { info ->
ItemDetailsDialog(
info = info,
showFilePath =
viewModel.serverRepository.currentUserDto.value
?.policy
?.isAdministrator == true,
onDismissRequest = { overviewDialog = null },
)
}
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = null,
contextMenu = contextMenu,
preferredSubtitleLanguage = null,
)
}
showPlaylistDialog.compose { itemId ->
@ -286,26 +275,6 @@ fun CollectionDetails(
elevation = 3.dp,
)
}
showDeleteDialog?.let { (position, item) ->
ConfirmDeleteDialog(
itemTitle = item.title ?: "",
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(item, position)
showDeleteDialog = null
},
)
}
overviewDialog?.let { info ->
ItemDetailsDialog(
info = info,
showFilePath =
viewModel.serverRepository.currentUserDto.value
?.policy
?.isAdministrator == true,
onDismissRequest = { overviewDialog = null },
)
}
}
@Composable
@ -324,7 +293,7 @@ fun CollectionDetailsContent(
onClickViewOptions: () -> Unit,
overviewOnClick: () -> Unit,
favoriteOnClick: () -> Unit,
deleteOnClick: () -> Unit,
onConfirmDelete: () -> Unit,
canDelete: Boolean,
moreOnClick: () -> Unit,
modifier: Modifier,
@ -423,7 +392,7 @@ fun CollectionDetailsContent(
getPossibleFilterValues = getPossibleFilterValues,
onClickViewOptions = onClickViewOptions,
favoriteOnClick = favoriteOnClick,
deleteOnClick = deleteOnClick,
onConfirmDelete = onConfirmDelete,
canDelete = canDelete,
moreOnClick = moreOnClick,
modifier =
@ -485,60 +454,3 @@ fun CollectionDetailsContent(
}
}
}
fun buildMoreDialogItemsForCollection(
context: Context,
item: BaseItem,
favorite: Boolean,
canDelete: Boolean,
onClickPlayAll: (shuffle: Boolean) -> Unit,
actions: MoreDialogActions,
): List<DialogItem> =
buildList {
add(
DialogItem(
context.getString(R.string.play),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
onClickPlayAll.invoke(false)
},
)
add(
DialogItem(
context.getString(R.string.shuffle),
R.string.fa_shuffle,
) {
onClickPlayAll.invoke(true)
},
)
add(
DialogItem(
text = R.string.add_to_playlist,
iconStringRes = R.string.fa_list_ul,
) {
actions.onClickAddPlaylist.invoke(item.id)
},
)
if (canDelete) {
add(
DialogItem(
context.getString(R.string.delete),
Icons.Default.Delete,
iconColor = Color.Red.copy(alpha = .8f),
) {
actions.onClickDelete.invoke(item)
},
)
}
add(
DialogItem(
text = if (favorite) R.string.remove_favorite else R.string.add_favorite,
iconStringRes = R.string.fa_heart,
iconColor = if (favorite) Color.Red else Color.Unspecified,
) {
actions.onClickFavorite.invoke(item.id, !favorite)
},
)
}

View file

@ -382,7 +382,7 @@ class CollectionViewModel
result.totalRecordCount
}
fun navigate(destination: Destination) {
fun navigateTo(destination: Destination) {
release()
navigationManager.navigateTo(destination)
}
@ -393,7 +393,9 @@ class CollectionViewModel
position: RowColumn?,
) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
favoriteWatchManager.setWatched(itemId, played)
if (position != null) {
if (itemId == state.value.collection?.id) {
refreshCollection()
} else if (position != null) {
refreshItem(itemId, position, false)
}
}
@ -404,11 +406,24 @@ class CollectionViewModel
position: RowColumn?,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setFavorite(itemId, favorite)
if (position != null) {
if (itemId == state.value.collection?.id) {
refreshCollection()
} else if (position != null) {
refreshItem(itemId, position, false)
}
}
private fun refreshCollection() {
viewModelScope.launchDefault {
val collection =
api.userLibraryApi
.getItem(itemId)
.content
.let { BaseItem(it, false) }
_state.update { it.copy(collection = collection) }
}
}
fun canDelete(
item: BaseItem,
appPreferences: AppPreferences,

View file

@ -31,33 +31,27 @@ import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ContextMenu
import com.github.damontecres.wholphin.ui.components.ContextMenuActions
import com.github.damontecres.wholphin.ui.components.ContextMenuDialog
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
import com.github.damontecres.wholphin.ui.components.HeaderUtils
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.Optional
import com.github.damontecres.wholphin.ui.components.chooseStream
import com.github.damontecres.wholphin.ui.components.chooseVersionParams
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.serializer.toUUID
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import java.util.UUID
import kotlin.time.Duration
@ -82,10 +76,8 @@ fun EpisodeDetails(
val chosenStreams by viewModel.chosenStreams.observeAsState(null)
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
val canDelete by viewModel.canDelete.collectAsState()
@ -96,22 +88,36 @@ fun EpisodeDetails(
?.configuration
?.subtitleLanguagePreference
val moreActions =
MoreDialogActions(
val contextActions =
remember {
ContextMenuActions(
navigateTo = viewModel::navigateTo,
onClickWatch = { itemId, watched ->
viewModel.setWatched(itemId, watched)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(itemId, favorite)
},
onClickWatch = viewModel::setWatched,
onClickFavorite = viewModel::setFavorite,
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onClickDelete = { showDeleteDialog = it },
onDeleteItem = viewModel::deleteItem,
onShowOverview = { overviewDialog = ItemDetailsDialogInfo(it) },
onChooseVersion = { item, source ->
viewModel.savePlayVersion(
item,
source.id!!.toUUID(),
)
},
onChooseTracks = { result ->
viewModel.saveTrackSelection(
result.item,
result.itemPlayback,
result.trackIndex,
result.streamType,
)
},
onClearChosenStreams = { viewModel.clearChosenStreams(it) },
)
}
when (val state = loading) {
is LoadingState.Error -> {
@ -152,75 +158,17 @@ fun EpisodeDetails(
ItemDetailsDialogInfo(ep)
},
moreOnClick = {
moreDialog =
DialogParams(
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = false,
title = ep.name + " (${ep.data.productionYear ?: ""})",
items =
buildMoreDialogItems(
context = context,
item = ep,
watched = ep.data.userData?.played ?: false,
favorite = ep.data.userData?.isFavorite ?: false,
seriesId = ep.data.seriesId,
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
canClearChosenStreams = chosenStreams?.itemPlayback != null || chosenStreams?.plc != null,
actions = moreActions,
onChooseVersion = {
chooseVersion =
chooseVersionParams(
context,
ep.data.mediaSources!!,
chosenStreams?.source?.id?.toUUIDOrNull(),
) { idx ->
val source = ep.data.mediaSources!![idx]
viewModel.savePlayVersion(
ep,
source.id!!.toUUID(),
)
}
moreDialog = null
},
onChooseTracks = { type ->
viewModel.streamChoiceService
.chooseSource(
ep.data,
chosenStreams?.itemPlayback,
)?.let { source ->
chooseVersion =
chooseStream(
context = context,
streams = source.mediaStreams.orEmpty(),
currentIndex =
if (type == MediaStreamType.AUDIO) {
chosenStreams?.audioStream?.index
} else {
chosenStreams?.subtitleStream?.index
},
type = type,
onClick = { trackIndex ->
viewModel.saveTrackSelection(
ep,
chosenStreams?.itemPlayback,
trackIndex,
type,
)
},
preferredSubtitleLanguage = preferredSubtitleLanguage,
)
}
},
onShowOverview = {
val source = chosenStreams?.source ?: ep.data.mediaSources?.firstOrNull()
if (source != null) {
overviewDialog = ItemDetailsDialogInfo(ep)
}
},
onClearChosenStreams = {
viewModel.clearChosenStreams(chosenStreams)
},
chosenStreams = chosenStreams,
showGoTo = false,
showStreamChoices = true,
canDelete = canDelete,
),
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = contextActions,
)
},
watchOnClick = {
@ -230,7 +178,7 @@ fun EpisodeDetails(
viewModel.setFavorite(ep.id, !ep.favorite)
},
canDelete = canDelete,
deleteOnClick = { showDeleteDialog = ep },
onConfirmDelete = { viewModel.deleteItem(ep) },
modifier = modifier,
)
}
@ -246,24 +194,12 @@ fun EpisodeDetails(
onDismissRequest = { overviewDialog = null },
)
}
moreDialog?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { moreDialog = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
)
}
chooseVersion?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { chooseVersion = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = viewModel.streamChoiceService::chooseSource,
contextMenu = contextMenu,
preferredSubtitleLanguage = preferredSubtitleLanguage,
)
}
showPlaylistDialog.compose { itemId ->
@ -283,16 +219,6 @@ fun EpisodeDetails(
elevation = 3.dp,
)
}
showDeleteDialog?.let { item ->
ConfirmDeleteDialog(
itemTitle = listOfNotNull(item.title, item.subtitle).joinToString(" - "),
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(item)
showDeleteDialog = null
},
)
}
}
private const val HEADER_ROW = 0
@ -308,7 +234,7 @@ fun EpisodeDetailsContent(
favoriteOnClick: () -> Unit,
moreOnClick: () -> Unit,
canDelete: Boolean,
deleteOnClick: () -> Unit,
onConfirmDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
@ -346,6 +272,7 @@ fun EpisodeDetailsContent(
.padding(top = HeaderUtils.topPadding, bottom = 16.dp),
)
ExpandablePlayButtons(
title = ep.title ?: "",
resumePosition = resumePosition,
watched = dto.userData?.played ?: false,
favorite = dto.userData?.isFavorite ?: false,
@ -367,7 +294,7 @@ fun EpisodeDetailsContent(
trailers = null,
trailerOnClick = {},
canDelete = canDelete,
deleteOnClick = deleteOnClick,
onConfirmDelete = onConfirmDelete,
modifier =
Modifier
.fillMaxWidth()

View file

@ -45,25 +45,20 @@ import com.github.damontecres.wholphin.ui.cards.ExtrasRow
import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.cards.PersonRow
import com.github.damontecres.wholphin.ui.cards.SeasonCard
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ContextMenu
import com.github.damontecres.wholphin.ui.components.ContextMenuActions
import com.github.damontecres.wholphin.ui.components.ContextMenuDialog
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
import com.github.damontecres.wholphin.ui.components.HeaderUtils
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.Optional
import com.github.damontecres.wholphin.ui.components.chooseStream
import com.github.damontecres.wholphin.ui.components.chooseVersionParams
import com.github.damontecres.wholphin.ui.components.PersonContextActions
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
import com.github.damontecres.wholphin.ui.discover.DiscoverRow
import com.github.damontecres.wholphin.ui.discover.DiscoverRowData
import com.github.damontecres.wholphin.ui.letNotEmpty
@ -74,11 +69,9 @@ import com.github.damontecres.wholphin.util.DiscoverRequestType
import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.serializer.toUUID
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import java.util.UUID
import kotlin.time.Duration
@ -101,11 +94,9 @@ fun MovieDetails(
val state by viewModel.state.collectAsState()
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
val preferredSubtitleLanguage =
viewModel.serverRepository.currentUserDto
@ -114,22 +105,36 @@ fun MovieDetails(
?.configuration
?.subtitleLanguagePreference
val moreActions =
MoreDialogActions(
val contextActions =
remember {
ContextMenuActions(
navigateTo = viewModel::navigateTo,
onClickWatch = { itemId, watched ->
viewModel.setWatched(itemId, watched)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(itemId, favorite)
},
onClickWatch = viewModel::setWatched,
onClickFavorite = viewModel::setFavorite,
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onClickDelete = { showDeleteDialog = it },
onDeleteItem = viewModel::deleteItem,
onChooseVersion = { item, source ->
viewModel.savePlayVersion(
item,
source.id!!.toUUID(),
)
},
onChooseTracks = { result ->
viewModel.saveTrackSelection(
result.item,
result.itemPlayback,
result.trackIndex,
result.streamType,
)
},
onShowOverview = { overviewDialog = ItemDetailsDialogInfo(it) },
onClearChosenStreams = { viewModel.clearChosenStreams(it) },
)
}
when (val s = state.loading) {
is DataLoadingState.Error -> {
@ -183,72 +188,17 @@ fun MovieDetails(
ItemDetailsDialogInfo(movie)
},
moreOnClick = {
moreDialog =
DialogParams(
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = false,
title = movie.name + " (${movie.data.productionYear ?: ""})",
items =
buildMoreDialogItems(
context = context,
item = movie,
watched = movie.data.userData?.played ?: false,
favorite = movie.data.userData?.isFavorite ?: false,
seriesId = null,
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
canClearChosenStreams = chosenStreams.let { it?.itemPlayback != null || it?.plc != null },
chosenStreams = chosenStreams,
showGoTo = false,
showStreamChoices = true,
canDelete = state.canDelete,
actions = moreActions,
onChooseVersion = {
chooseVersion =
chooseVersionParams(
context,
movie.data.mediaSources!!,
chosenStreams?.source?.id?.toUUIDOrNull(),
) { idx ->
val source = movie.data.mediaSources!![idx]
viewModel.savePlayVersion(
movie,
source.id!!.toUUID(),
)
}
moreDialog = null
},
onChooseTracks = { type ->
viewModel.streamChoiceService
.chooseSource(
movie.data,
chosenStreams?.itemPlayback,
)?.let { source ->
chooseVersion =
chooseStream(
context = context,
streams = source.mediaStreams.orEmpty(),
type = type,
currentIndex =
if (type == MediaStreamType.AUDIO) {
chosenStreams?.audioStream?.index
} else {
chosenStreams?.subtitleStream?.index
},
onClick = { trackIndex ->
viewModel.saveTrackSelection(
movie,
chosenStreams?.itemPlayback,
trackIndex,
type,
)
},
preferredSubtitleLanguage = preferredSubtitleLanguage,
)
}
},
onShowOverview = {
overviewDialog = ItemDetailsDialogInfo(movie)
},
onClearChosenStreams = {
viewModel.clearChosenStreams(chosenStreams)
},
),
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = contextActions,
)
},
watchOnClick = {
@ -258,36 +208,29 @@ fun MovieDetails(
viewModel.setFavorite(movie.id, !movie.favorite)
},
onLongClickPerson = { index, person ->
val items =
buildMoreDialogItemsForPerson(
context = context,
person = person,
actions = moreActions,
)
moreDialog =
DialogParams(
showContextMenu =
ContextMenu.ForPerson(
fromLongClick = true,
title = person.name ?: "",
items = items,
person = person,
actions =
PersonContextActions(
navigateTo = viewModel::navigateTo,
onClickFavorite = viewModel::setFavorite,
),
)
},
onLongClickSimilar = { index, similar ->
val items =
buildMoreDialogItemsForHome(
context = context,
item = similar,
seriesId = null,
playbackPosition = similar.playbackPosition,
watched = similar.played,
favorite = similar.favorite,
canDelete = false,
actions = moreActions,
)
moreDialog =
DialogParams(
onLongClickSimilar = { _, similar ->
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = true,
title = similar.title ?: "",
items = items,
item = similar,
chosenStreams = null,
showGoTo = true,
showStreamChoices = false,
canDelete = false,
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = contextActions,
)
},
trailerOnClick = {
@ -300,11 +243,19 @@ fun MovieDetails(
viewModel.navigateTo(item.destination)
},
canDelete = state.canDelete,
deleteOnClick = { showDeleteDialog = state.movie },
onConfirmDelete = { state.movie?.let { viewModel.deleteItem(it) } },
modifier = modifier,
)
}
}
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = viewModel.streamChoiceService::chooseSource,
contextMenu = contextMenu,
preferredSubtitleLanguage = preferredSubtitleLanguage,
)
}
overviewDialog?.let { info ->
ItemDetailsDialog(
info = info,
@ -315,26 +266,6 @@ fun MovieDetails(
onDismissRequest = { overviewDialog = null },
)
}
moreDialog?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { moreDialog = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
)
}
chooseVersion?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { chooseVersion = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
)
}
showPlaylistDialog.compose { itemId ->
PlaylistDialog(
title = stringResource(R.string.add_to_playlist),
@ -352,16 +283,6 @@ fun MovieDetails(
elevation = 3.dp,
)
}
showDeleteDialog?.let { item ->
ConfirmDeleteDialog(
itemTitle = item.title ?: "",
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(item)
showDeleteDialog = null
},
)
}
}
private const val HEADER_ROW = 0
@ -390,7 +311,7 @@ fun MovieDetailsContent(
onClickExtra: (Int, ExtrasItem) -> Unit,
onClickDiscover: (Int, DiscoverItem) -> Unit,
canDelete: Boolean,
deleteOnClick: () -> Unit,
onConfirmDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
@ -430,6 +351,7 @@ fun MovieDetailsContent(
.padding(top = HeaderUtils.topPadding, bottom = 16.dp),
)
ExpandablePlayButtons(
title = movie.title ?: "",
resumePosition = resumePosition,
watched = dto.userData?.played ?: false,
favorite = dto.userData?.isFavorite ?: false,
@ -454,7 +376,7 @@ fun MovieDetailsContent(
trailerOnClick.invoke(it)
},
canDelete = canDelete,
deleteOnClick = deleteOnClick,
onConfirmDelete = onConfirmDelete,
modifier =
Modifier
.fillMaxWidth()

View file

@ -61,12 +61,12 @@ import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.cards.BannerCardWithTitle
import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
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.GenreText
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.components.OverviewText
import com.github.damontecres.wholphin.ui.components.QuickDetails
@ -308,22 +308,21 @@ fun AlbumDetailsPage(
val focusManager = LocalFocusManager.current
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
val moreDialogActions =
remember {
MusicMoreDialogActions(
onNavigate = { viewModel.navigationManager.navigateTo(it) },
MusicContextActions(
navigateTo = { viewModel.navigationManager.navigateTo(it) },
onClickPlay = { index, _ -> viewModel.play(false, index) },
onClickPlayNext = { _, song -> viewModel.playNext(song) },
onClickAddToQueue = { index, item -> viewModel.addToQueue(item, index) },
onClickAddToQueue = { item -> viewModel.addToQueue(item, -1) },
onClickFavorite = { itemId, favorite -> viewModel.setFavorite(itemId, favorite) },
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.AUDIO)
showPlaylistDialog.makePresent(itemId)
},
onClickRemoveFromQueue = {},
onClickDelete = { showDeleteDialog = it },
onClickRemoveFromQueue = { _, _ -> },
onDeleteItem = viewModel::deleteItem,
)
}
when (val loading = state.loading) {
@ -402,23 +401,18 @@ fun AlbumDetailsPage(
)
},
onClickMore = {
moreDialog =
DialogParams(
showContextMenu =
ContextMenu.ForMusic(
fromLongClick = false,
title = album.name + " (${album.data.productionYear ?: ""})",
items =
buildMoreDialogForMusic(
context = context,
actions = moreDialogActions,
item = album,
index = 0,
canRemove = false,
canDelete =
viewModel.canDelete(
album,
preferences.appPreferences,
),
),
canRemoveFromQueue = false,
actions = moreDialogActions,
)
},
)
@ -459,45 +453,35 @@ fun AlbumDetailsPage(
},
onLongClick = {
if (song != null) {
moreDialog =
DialogParams(
showContextMenu =
ContextMenu.ForMusic(
fromLongClick = true,
title = song.name ?: "",
items =
buildMoreDialogForMusic(
context = context,
actions = moreDialogActions,
item = song,
index = index,
canRemove = false,
canDelete =
viewModel.canDelete(
song,
preferences.appPreferences,
),
),
canRemoveFromQueue = false,
actions = moreDialogActions,
)
}
},
onClickMore = {
if (song != null) {
moreDialog =
DialogParams(
showContextMenu =
ContextMenu.ForMusic(
fromLongClick = false,
title = song.name ?: "",
items =
buildMoreDialogForMusic(
context = context,
actions = moreDialogActions,
item = song,
index = index,
canRemove = false,
canDelete =
viewModel.canDelete(
song,
preferences.appPreferences,
),
),
canRemoveFromQueue = false,
actions = moreDialogActions,
)
}
},
@ -559,23 +543,18 @@ fun AlbumDetailsPage(
viewModel.navigationManager.navigateTo(item.destination())
},
onLongClickItem = { index, item ->
moreDialog =
DialogParams(
showContextMenu =
ContextMenu.ForMusic(
fromLongClick = true,
title = item.name ?: "",
items =
buildMoreDialogForMusic(
context = context,
actions = moreDialogActions,
item = item,
index = index,
canRemove = false,
canDelete =
viewModel.canDelete(
item,
preferences.appPreferences,
),
),
canRemoveFromQueue = false,
actions = moreDialogActions,
)
},
cardContent = { index: Int, item: BaseItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
@ -597,14 +576,12 @@ fun AlbumDetailsPage(
}
}
}
moreDialog?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { moreDialog = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = null,
contextMenu = contextMenu,
preferredSubtitleLanguage = null,
)
}
showPlaylistDialog.compose { itemId ->
@ -624,17 +601,6 @@ fun AlbumDetailsPage(
elevation = 3.dp,
)
}
showDeleteDialog?.let { item ->
ConfirmDeleteDialog(
itemTitle = item.title ?: "",
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(item)
focusRequesters.getOrNull(position.row)?.tryRequestFocus()
showDeleteDialog = null
},
)
}
}
@Composable

View file

@ -59,12 +59,12 @@ import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.cards.BannerCardWithTitle
import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
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.GenreText
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.components.OverviewText
import com.github.damontecres.wholphin.ui.components.QuickDetails
@ -312,22 +312,21 @@ fun ArtistDetailsPage(
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
val moreDialogActions =
remember {
MusicMoreDialogActions(
onNavigate = { viewModel.navigationManager.navigateTo(it) },
MusicContextActions(
navigateTo = { viewModel.navigationManager.navigateTo(it) },
onClickPlay = { index, item -> viewModel.play(item) },
onClickPlayNext = { _, item -> viewModel.playNext(item) },
onClickAddToQueue = { index, item -> viewModel.addToQueue(item, index) },
onClickAddToQueue = { item -> viewModel.addToQueue(item, -1) },
onClickFavorite = { itemId, favorite -> viewModel.setFavorite(itemId, favorite) },
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.AUDIO)
showPlaylistDialog.makePresent(itemId)
},
onClickRemoveFromQueue = {},
onClickDelete = { showDeleteDialog = it },
onClickRemoveFromQueue = { _, _ -> },
onDeleteItem = viewModel::deleteItem,
)
}
@ -393,23 +392,18 @@ fun ArtistDetailsPage(
)
},
onClickMore = {
moreDialog =
DialogParams(
showContextMenu =
ContextMenu.ForMusic(
fromLongClick = false,
title = artist.name ?: "",
items =
buildMoreDialogForMusic(
context = context,
actions = moreDialogActions,
item = artist,
index = 0,
canRemove = false,
canDelete =
viewModel.canDelete(
artist,
preferences.appPreferences,
),
),
canRemoveFromQueue = false,
actions = moreDialogActions,
)
},
)
@ -451,45 +445,35 @@ fun ArtistDetailsPage(
},
onLongClick = {
if (song != null) {
moreDialog =
DialogParams(
showContextMenu =
ContextMenu.ForMusic(
fromLongClick = true,
title = song.name ?: "",
items =
buildMoreDialogForMusic(
context = context,
actions = moreDialogActions,
item = song,
index = index,
canRemove = false,
canDelete =
viewModel.canDelete(
song,
preferences.appPreferences,
),
),
canRemoveFromQueue = false,
actions = moreDialogActions,
)
}
},
onClickMore = {
if (song != null) {
moreDialog =
DialogParams(
showContextMenu =
ContextMenu.ForMusic(
fromLongClick = false,
title = song.name ?: "",
items =
buildMoreDialogForMusic(
context = context,
actions = moreDialogActions,
item = song,
index = index,
canRemove = false,
canDelete =
viewModel.canDelete(
song,
preferences.appPreferences,
),
),
canRemoveFromQueue = false,
actions = moreDialogActions,
)
}
},
@ -516,23 +500,18 @@ fun ArtistDetailsPage(
viewModel.navigationManager.navigateTo(album.destination())
},
onLongClickItem = { index, album ->
moreDialog =
DialogParams(
showContextMenu =
ContextMenu.ForMusic(
fromLongClick = true,
title = album.name ?: "",
items =
buildMoreDialogForMusic(
context = context,
actions = moreDialogActions,
item = album,
index = index,
canRemove = false,
canDelete =
viewModel.canDelete(
album,
preferences.appPreferences,
),
),
canRemoveFromQueue = false,
actions = moreDialogActions,
)
},
cardContent = { index: Int, album: BaseItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
@ -588,23 +567,18 @@ fun ArtistDetailsPage(
viewModel.navigationManager.navigateTo(item.destination())
},
onLongClickItem = { index, item ->
moreDialog =
DialogParams(
showContextMenu =
ContextMenu.ForMusic(
fromLongClick = true,
title = item.name ?: "",
items =
buildMoreDialogForMusic(
context = context,
actions = moreDialogActions,
item = item,
index = index,
canRemove = false,
canDelete =
viewModel.canDelete(
item,
preferences.appPreferences,
),
),
canRemoveFromQueue = false,
actions = moreDialogActions,
)
},
cardContent = { index: Int, item: BaseItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
@ -626,14 +600,12 @@ fun ArtistDetailsPage(
}
}
}
moreDialog?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { moreDialog = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = null,
contextMenu = contextMenu,
preferredSubtitleLanguage = null,
)
}
showPlaylistDialog.compose { itemId ->
@ -653,17 +625,6 @@ fun ArtistDetailsPage(
elevation = 3.dp,
)
}
showDeleteDialog?.let { item ->
ConfirmDeleteDialog(
itemTitle = item.title ?: "",
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(item)
focusRequesters.getOrNull(position.row)?.tryRequestFocus()
showDeleteDialog = null
},
)
}
}
@Composable

View file

@ -60,9 +60,10 @@ import com.github.damontecres.wholphin.preferences.BackdropStyle
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.rememberQueue
import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ContextMenu
import com.github.damontecres.wholphin.ui.components.ContextMenuDialog
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.QueueContextActions
import com.github.damontecres.wholphin.ui.findActivity
import com.github.damontecres.wholphin.ui.nav.Backdrop
import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler
@ -129,18 +130,9 @@ 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 showViewOptionsDialog by remember { mutableStateOf(false) }
var itemMoreDialog by remember { mutableStateOf<DialogParams?>(null) }
var showContextMenu by remember { mutableStateOf<ContextMenu.ForQueue?>(null) }
var lyricsHaveFocus by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
@ -209,6 +201,7 @@ fun NowPlayingPage(
remember {
shrinkHorizontally(shrinkTowards = Alignment.Start) + fadeOut()
}
//noinspection UnnecessaryFullyQualifiedName
androidx.compose.animation.AnimatedVisibility(
visible = musicPrefs.showAlbumArt,
enter = enter,
@ -250,6 +243,7 @@ fun NowPlayingPage(
}
}
}
//noinspection UnnecessaryFullyQualifiedName
androidx.compose.animation.AnimatedVisibility(
visible = musicPrefs.showVisualizer,
enter = enter,
@ -308,17 +302,21 @@ fun NowPlayingPage(
val showContextForItem =
remember {
{ fromLongClick: Boolean, index: Int, song: AudioItem ->
itemMoreDialog =
DialogParams(
title = song.title ?: "",
showContextMenu =
ContextMenu.ForQueue(
fromLongClick = fromLongClick,
items =
buildMoreDialogForMusicQueue(
context = context,
actions = actions,
item = song,
index = index,
canRemove = true,
actions =
QueueContextActions(
onNavigate = { viewModel.navigationManager.navigateTo(it) },
onClickPlay = { index, _ -> viewModel.play(index) },
onClickPlayNext = { index, _ -> viewModel.playNext(index) },
onClickRemoveFromQueue = { index, _ ->
viewModel.removeFromQueue(
index,
)
},
),
)
}
@ -368,14 +366,12 @@ 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,
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = null,
contextMenu = contextMenu,
preferredSubtitleLanguage = null,
)
}
if (showViewOptionsDialog) {

View file

@ -1,20 +1,8 @@
package com.github.damontecres.wholphin.ui.detail.music
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
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import org.jellyfin.sdk.api.client.ApiClient
@ -23,193 +11,6 @@ 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, 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 = {
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) -> Unit,
val onClickDelete: (BaseItem) -> Unit,
)
fun buildMoreDialogForMusic(
context: Context,
actions: MusicMoreDialogActions,
item: BaseItem,
index: Int,
canRemove: Boolean,
canDelete: 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)
},
)
}
add(
DialogItem(
context.getString(R.string.add_to_queue),
Icons.Default.Add,
) {
actions.onClickAddToQueue(index, item)
},
)
add(
DialogItem(
text = R.string.add_to_playlist,
iconStringRes = R.string.fa_list_ul,
) {
actions.onClickAddPlaylist.invoke(item.id)
},
)
if (canDelete) {
add(
DialogItem(
context.getString(R.string.delete),
Icons.Default.Delete,
iconColor = Color.Red.copy(alpha = .8f),
) {
actions.onClickDelete.invoke(item)
},
)
}
add(
DialogItem(
text = if (item.favorite) R.string.remove_favorite else R.string.add_favorite,
iconStringRes = R.string.fa_heart,
iconColor = if (item.favorite) Color.Red else Color.Unspecified,
) {
actions.onClickFavorite.invoke(item.id, !item.favorite)
},
)
if (item.type == BaseItemKind.AUDIO && item.data.albumId != null) {
add(
DialogItem(
context.getString(R.string.go_to_album),
R.string.fa_compact_disc,
) {
actions.onClickGoToAlbum.invoke(item.data.albumId!!)
},
)
}
if ((item.type == BaseItemKind.AUDIO || item.type == BaseItemKind.MUSIC_ALBUM) && item.data.artistItems?.isNotEmpty() == true) {
add(
DialogItem(
context.getString(R.string.go_to_artist),
R.string.fa_user,
) {
actions.onClickGoToArtist.invoke(
item.data.artistItems!!
.first()
.id,
)
},
)
}
}
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

@ -26,7 +26,7 @@ fun FocusedEpisodeFooter(
watchOnClick: () -> Unit,
favoriteOnClick: () -> Unit,
canDelete: Boolean,
deleteOnClick: () -> Unit,
onConfirmDelete: () -> Unit,
modifier: Modifier = Modifier,
buttonOnFocusChanged: (FocusState) -> Unit = {},
) {
@ -39,6 +39,7 @@ fun FocusedEpisodeFooter(
modifier = modifier,
) {
ExpandablePlayButtons(
title = ep.title ?: "",
resumePosition = resumePosition,
watched = dto.userData?.played ?: false,
favorite = dto.userData?.isFavorite ?: false,
@ -50,7 +51,7 @@ fun FocusedEpisodeFooter(
trailers = null,
trailerOnClick = {},
canDelete = canDelete,
deleteOnClick = deleteOnClick,
onConfirmDelete = onConfirmDelete,
modifier = Modifier.fillMaxWidth(),
)
}

View file

@ -28,7 +28,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
@ -57,12 +56,13 @@ import com.github.damontecres.wholphin.ui.cards.ExtrasRow
import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.cards.PersonRow
import com.github.damontecres.wholphin.ui.cards.SeasonCard
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
import com.github.damontecres.wholphin.ui.components.ContextMenu
import com.github.damontecres.wholphin.ui.components.ContextMenuActions
import com.github.damontecres.wholphin.ui.components.ContextMenuDialog
import com.github.damontecres.wholphin.ui.components.DeleteButton
import com.github.damontecres.wholphin.ui.components.DialogItem
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.ExpandableFaButton
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
@ -71,20 +71,17 @@ import com.github.damontecres.wholphin.ui.components.HeaderUtils
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.PersonContextActions
import com.github.damontecres.wholphin.ui.components.QuickDetails
import com.github.damontecres.wholphin.ui.components.TitleOrLogo
import com.github.damontecres.wholphin.ui.components.TrailerButton
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
import com.github.damontecres.wholphin.ui.discover.DiscoverRow
import com.github.damontecres.wholphin.ui.discover.DiscoverRowData
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.rememberInt
@ -95,6 +92,7 @@ import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.serializer.toUUID
import java.util.UUID
import kotlin.time.Duration
@ -128,10 +126,50 @@ fun SeriesDetails(
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var showWatchConfirmation by remember { mutableStateOf(false) }
var seasonDialog by remember { mutableStateOf<DialogParams?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
val contextActions =
remember {
ContextMenuActions(
navigateTo = viewModel::navigateTo,
onClickWatch = { itemId, watched ->
if (itemId == destination.itemId) {
// Confirm if marking whole series
showWatchConfirmation = true
} else {
viewModel.setWatched(itemId, watched, null)
}
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(itemId, favorite, null)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = Optional.present(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onDeleteItem = viewModel::deleteItem,
onChooseVersion = { item, source ->
viewModel.savePlayVersion(
item,
source.id!!.toUUID(),
)
},
onChooseTracks = { result ->
viewModel.saveTrackSelection(
result.item,
result.itemPlayback,
result.trackIndex,
result.streamType,
)
},
onShowOverview = { overviewDialog = ItemDetailsDialogInfo(it) },
onClearChosenStreams = {},
)
}
LifecycleResumeEffect(destination.itemId) {
viewModel.refresh()
@ -185,30 +223,19 @@ fun SeriesDetails(
)
},
onLongClickItem = { index, season ->
scope.launchDefault {
seasonDialog =
buildDialogForSeason(
context = context,
s = season,
canDelete = viewModel.canDelete(season),
onClickItem = { viewModel.navigateTo(it.destination()) },
markPlayed = { played ->
viewModel.setSeasonWatched(season.id, played)
},
onClickPlay = { shuffle ->
viewModel.navigateTo(
Destination.PlaybackList(
itemId = season.id,
shuffle = shuffle,
),
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = true,
item = season,
chosenStreams = null,
showGoTo = true,
showStreamChoices = false,
canDelete = viewModel.canDelete(season, preferences.appPreferences),
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = contextActions,
)
},
onClickDelete = {
showDeleteDialog = it
},
)
}
},
overviewOnClick = {
overviewDialog = ItemDetailsDialogInfo(item)
},
@ -245,24 +272,10 @@ fun SeriesDetails(
onClickDiscover = { index, item ->
viewModel.navigateTo(item.destination)
},
moreActions =
MoreDialogActions(
navigateTo = { viewModel.navigateTo(it) },
onClickWatch = { itemId, played ->
viewModel.setWatched(itemId, played, null)
onShowContextMenu = {
showContextMenu = it
},
onClickFavorite = { itemId, played ->
viewModel.setFavorite(itemId, played, null)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onClickDelete = {
showDeleteDialog = it
},
),
actions = contextActions,
)
if (showWatchConfirmation) {
ConfirmDialog(
@ -281,6 +294,14 @@ fun SeriesDetails(
}
}
}
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = viewModel.streamChoiceService::chooseSource,
contextMenu = contextMenu,
preferredSubtitleLanguage = null,
)
}
overviewDialog?.let { info ->
ItemDetailsDialog(
info = info,
@ -288,15 +309,6 @@ fun SeriesDetails(
onDismissRequest = { overviewDialog = null },
)
}
seasonDialog?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
waitToLoad = params.fromLongClick,
onDismissRequest = { seasonDialog = null },
)
}
showPlaylistDialog.compose { itemId ->
PlaylistDialog(
title = stringResource(R.string.add_to_playlist),
@ -314,19 +326,6 @@ fun SeriesDetails(
elevation = 3.dp,
)
}
showDeleteDialog?.let { item ->
ConfirmDeleteDialog(
itemTitle = item.title ?: "",
onCancel = { showDeleteDialog = null },
onConfirm = {
if (seasons?.lastOrNull()?.id == item.id) {
focusManager.moveFocus(FocusDirection.Previous)
}
viewModel.deleteItem(item)
showDeleteDialog = null
},
)
}
}
private const val HEADER_ROW = 0
@ -359,7 +358,8 @@ fun SeriesDetailsContent(
favoriteOnClick: () -> Unit,
trailerOnClick: (Trailer) -> Unit,
onClickExtra: (Int, ExtrasItem) -> Unit,
moreActions: MoreDialogActions,
onShowContextMenu: (ContextMenu) -> Unit,
actions: ContextMenuActions,
onClickDiscover: (Int, DiscoverItem) -> Unit,
discoverSeries: DiscoverItem?,
onClickDiscoverSeries: () -> Unit,
@ -373,7 +373,6 @@ fun SeriesDetailsContent(
val focusRequesters = remember { List(DISCOVER_ROW + 1) { FocusRequester() } }
val playFocusRequester = remember { FocusRequester() }
RequestOrRestoreFocus(focusRequesters.getOrNull(position))
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
Box(
modifier = modifier,
@ -486,9 +485,10 @@ fun SeriesDetailsContent(
)
if (canDelete) {
DeleteButton(
onClick = {
title = series.title ?: "",
onConfirmDelete = {
position = HEADER_ROW
moreActions.onClickDelete.invoke(series)
actions.onDeleteItem.invoke(series)
},
modifier =
Modifier
@ -561,17 +561,16 @@ fun SeriesDetailsContent(
},
onLongClick = { index, person ->
position = PEOPLE_ROW
val items =
buildMoreDialogItemsForPerson(
context = context,
person = person,
actions = moreActions,
)
moreDialog =
DialogParams(
onShowContextMenu.invoke(
ContextMenu.ForPerson(
fromLongClick = true,
title = person.name ?: "",
items = items,
person = person,
actions =
PersonContextActions(
navigateTo = actions.navigateTo,
onClickFavorite = actions.onClickFavorite,
),
),
)
},
modifier =
@ -608,22 +607,18 @@ fun SeriesDetailsContent(
},
onLongClickItem = { index, item ->
position = SIMILAR_ROW
val items =
buildMoreDialogItemsForHome(
context = context,
item = item,
seriesId = null,
playbackPosition = item.playbackPosition,
watched = item.played,
favorite = item.favorite,
actions = moreActions,
canDelete = false,
)
moreDialog =
DialogParams(
onShowContextMenu.invoke(
ContextMenu.ForBaseItem(
fromLongClick = true,
title = item.name ?: "",
items = items,
item = item,
chosenStreams = null,
showGoTo = true,
showStreamChoices = false,
canDelete = false,
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = actions,
),
)
},
cardContent = { index, item, mod, onClick, onLongClick ->
@ -666,16 +661,6 @@ fun SeriesDetailsContent(
}
}
}
moreDialog?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { moreDialog = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
)
}
}
@Composable

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@ -20,41 +21,30 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.map
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ContextMenu
import com.github.damontecres.wholphin.ui.components.ContextMenuActions
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.chooseStream
import com.github.damontecres.wholphin.ui.components.chooseVersionParams
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.seasonEpisode
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.flow.update
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.api.PersonKind
import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.serializer.UUIDSerializer
import org.jellyfin.sdk.model.serializer.toUUID
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import java.util.UUID
import kotlin.time.Duration
@ -110,6 +100,7 @@ fun SeriesOverview(
val episodeList = (episodes as? EpisodeList.Success)?.episodes
val position by viewModel.position.collectAsState(SeriesOverviewPosition(0, 0))
val currentPosition by rememberUpdatedState(position)
LaunchedEffect(Unit) {
if (seasons.isNotEmpty()) {
seasons.getOrNull(position.seasonTabIndex)?.let {
@ -119,13 +110,54 @@ fun SeriesOverview(
}
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
var rowFocused by rememberInt()
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
val contextActions =
remember {
ContextMenuActions(
navigateTo = viewModel::navigateTo,
onClickWatch = { itemId, watched ->
viewModel.setWatched(itemId, watched, currentPosition.episodeRowIndex)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(itemId, favorite, currentPosition.episodeRowIndex)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = itemId
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onDeleteItem = viewModel::deleteItem,
onChooseVersion = { item, source ->
viewModel.savePlayVersion(
item,
source.id!!.toUUID(),
)
},
onChooseTracks = { result ->
viewModel.saveTrackSelection(
result.item,
result.itemPlayback,
result.trackIndex,
result.streamType,
)
},
onShowOverview = { overviewDialog = ItemDetailsDialogInfo(it) },
onClearChosenStreams = {
val focusedEpisode =
(episodes as? EpisodeList.Success)
?.episodes
?.getOrNull(currentPosition.episodeRowIndex)
if (focusedEpisode != null) {
viewModel.clearChosenStreams(focusedEpisode, it)
}
},
)
}
LaunchedEffect(position, episodes) {
val focusedEpisode =
@ -179,103 +211,6 @@ fun SeriesOverview(
}
}
suspend fun buildMoreForEpisode(
ep: BaseItem,
chosenStreams: ChosenStreams?,
fromLongClick: Boolean,
): DialogParams =
DialogParams(
fromLongClick = fromLongClick,
title = series.name + " - " + ep.data.seasonEpisode,
items =
buildMoreDialogItems(
context = context,
item = ep,
watched = ep.data.userData?.played ?: false,
favorite = ep.data.userData?.isFavorite ?: false,
seriesId = series.id,
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
canClearChosenStreams = chosenStreams?.itemPlayback != null || chosenStreams?.plc != null,
canDelete = viewModel.canDelete(ep),
actions =
MoreDialogActions(
navigateTo = viewModel::navigateTo,
onClickWatch = { itemId, watched ->
viewModel.setWatched(
itemId,
watched,
position.episodeRowIndex,
)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(
itemId,
favorite,
position.episodeRowIndex,
)
},
onClickAddPlaylist = {
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = it
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onClickDelete = {
showDeleteDialog = it
},
),
onChooseVersion = {
chooseVersion =
chooseVersionParams(
context,
ep.data.mediaSources!!,
chosenStreams?.source?.id?.toUUIDOrNull(),
) { idx ->
val source = ep.data.mediaSources!![idx]
viewModel.savePlayVersion(
ep,
source.id!!.toUUID(),
)
}
moreDialog = null
},
onChooseTracks = { type ->
viewModel.streamChoiceService
.chooseSource(
ep.data,
chosenStreams?.itemPlayback,
)?.let { source ->
chooseVersion =
chooseStream(
context = context,
streams = source.mediaStreams.orEmpty(),
type = type,
currentIndex =
if (type == MediaStreamType.AUDIO) {
chosenStreams?.audioStream?.index
} else {
chosenStreams?.subtitleStream?.index
},
onClick = { trackIndex ->
viewModel.saveTrackSelection(
ep,
chosenStreams?.itemPlayback,
trackIndex,
type,
)
},
preferredSubtitleLanguage = preferredSubtitleLanguage,
)
}
},
onShowOverview = {
overviewDialog = ItemDetailsDialogInfo(ep)
},
onClearChosenStreams = {
viewModel.clearChosenStreams(ep, chosenStreams)
},
),
)
SeriesOverviewContent(
preferences = preferences,
series = series,
@ -319,9 +254,18 @@ fun SeriesOverview(
)
},
onLongClick = { ep ->
scope.launchDefault {
moreDialog = buildMoreForEpisode(ep, chosenStreams, true)
}
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = true,
item = ep,
chosenStreams = chosenStreams,
showGoTo = false,
showStreamChoices = true,
canDelete = viewModel.canDelete(ep, preferences.appPreferences),
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = contextActions,
)
},
playOnClick = { resume ->
rowFocused = EPISODE_ROW
@ -349,9 +293,18 @@ fun SeriesOverview(
},
moreOnClick = {
episodeList?.getOrNull(position.episodeRowIndex)?.let { ep ->
scope.launchDefault {
moreDialog = buildMoreForEpisode(ep, chosenStreams, false)
}
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = false,
item = ep,
chosenStreams = chosenStreams,
showGoTo = false,
showStreamChoices = true,
canDelete = viewModel.canDelete(ep, preferences.appPreferences),
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = contextActions,
)
}
},
overviewOnClick = {
@ -374,13 +327,20 @@ fun SeriesOverview(
viewModel.navigateTo(extra.destination)
},
canDelete = { viewModel.canDelete(it, preferences.appPreferences) },
deleteOnClick = { showDeleteDialog = it },
onConfirmDelete = viewModel::deleteItem,
modifier = modifier,
)
}
}
}
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = viewModel.streamChoiceService::chooseSource,
contextMenu = contextMenu,
preferredSubtitleLanguage = preferredSubtitleLanguage,
)
}
overviewDialog?.let { info ->
ItemDetailsDialog(
info = info,
@ -391,26 +351,6 @@ fun SeriesOverview(
onDismissRequest = { overviewDialog = null },
)
}
moreDialog?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { moreDialog = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
)
}
chooseVersion?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { chooseVersion = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
)
}
showPlaylistDialog?.let { itemId ->
PlaylistDialog(
title = stringResource(R.string.add_to_playlist),
@ -428,17 +368,6 @@ fun SeriesOverview(
elevation = 3.dp,
)
}
showDeleteDialog?.let { item ->
ConfirmDeleteDialog(
itemTitle = item.subtitle ?: "",
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(item)
episodeRowFocusRequester.tryRequestFocus()
showDeleteDialog = null
},
)
}
}
private const val EPISODE_ROW = 0

View file

@ -95,7 +95,7 @@ fun SeriesOverviewContent(
overviewOnClick: () -> Unit,
personOnClick: (Person) -> Unit,
canDelete: (BaseItem) -> Boolean,
deleteOnClick: (BaseItem) -> Unit,
onConfirmDelete: (BaseItem) -> Unit,
onClickExtra: (Int, ExtrasItem) -> Unit,
modifier: Modifier = Modifier,
) {
@ -322,7 +322,7 @@ fun SeriesOverviewContent(
}
},
canDelete = canDelete.invoke(ep),
deleteOnClick = { deleteOnClick.invoke(ep) },
onConfirmDelete = { onConfirmDelete.invoke(ep) },
modifier =
Modifier
.padding(top = 4.dp)

View file

@ -34,6 +34,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
@ -56,24 +57,23 @@ import com.github.damontecres.wholphin.ui.cards.GenreCard
import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.cards.StudioCard
import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ContextMenu
import com.github.damontecres.wholphin.ui.components.ContextMenuActions
import com.github.damontecres.wholphin.ui.components.ContextMenuDialog
import com.github.damontecres.wholphin.ui.components.EpisodeName
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.FocusableItemRow
import com.github.damontecres.wholphin.ui.components.HeaderUtils
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.QuickDetails
import com.github.damontecres.wholphin.ui.components.RowColumnItem
import com.github.damontecres.wholphin.ui.components.TitleOrLogo
import com.github.damontecres.wholphin.ui.components.rememberLogoUrl
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.nav.Destination
@ -120,9 +120,10 @@ fun HomePage(
}
LoadingState.Success -> {
var dialog by remember { mutableStateOf<DialogParams?>(null) }
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
var showDeleteDialog by remember { mutableStateOf<RowColumnItem?>(null) }
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
var position by rememberPosition()
@ -144,43 +145,45 @@ fun HomePage(
row?.rowType is HomeRowConfig.ContinueWatching || row?.rowType is HomeRowConfig.ContinueWatchingCombined
val canRemoveNextUp =
row?.rowType is HomeRowConfig.NextUp || row?.rowType is HomeRowConfig.ContinueWatchingCombined
val dialogItems =
buildMoreDialogItemsForHome(
context = context,
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = true,
item = item,
seriesId = item.data.seriesId,
playbackPosition = item.playbackPosition,
watched = item.played,
favorite = item.favorite,
canDelete = viewModel.canDelete(item, preferences.appPreferences),
canRemoveNextUp = canRemoveNextUp,
chosenStreams = null,
showGoTo = true,
showStreamChoices = false,
canDelete =
viewModel.canDelete(
item,
preferences.appPreferences,
),
canRemoveContinueWatching = canRemoveContinueWatching,
canRemoveNextUp = canRemoveNextUp,
actions =
MoreDialogActions(
ContextMenuActions(
navigateTo = viewModel.navigationManager::navigateTo,
onClickWatch = { itemId, played ->
viewModel.setWatched(itemId, played)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(itemId, favorite)
},
onClickWatch = viewModel::setWatched,
onClickFavorite = viewModel::setFavorite,
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = itemId
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onClickDelete = {
showDeleteDialog = RowColumnItem(position, item)
onDeleteItem = {
viewModel.deleteItem(position, it)
},
onClickRemoveFromNextUp = viewModel::removeFromNextUp,
onChooseVersion = { _, _ ->
// Not supported on this page
},
onChooseTracks = {
// Not supported on this page
},
onShowOverview = {
overviewDialog = ItemDetailsDialogInfo(it)
},
onClearChosenStreams = {},
),
)
dialog =
DialogParams(
title = item.title ?: "",
fromLongClick = true,
items = dialogItems,
)
}
}
val onClickPlay =
@ -203,10 +206,19 @@ fun HomePage(
showLogo = preferences.appPreferences.interfacePreferences.showLogos,
modifier = modifier,
)
dialog?.let { params ->
DialogPopup(
params = params,
onDismissRequest = { dialog = null },
overviewDialog?.let { info ->
ItemDetailsDialog(
info = info,
showFilePath = false,
onDismissRequest = { overviewDialog = null },
)
}
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = null,
contextMenu = contextMenu,
preferredSubtitleLanguage = null,
)
}
showPlaylistDialog?.let { itemId ->
@ -226,20 +238,8 @@ fun HomePage(
elevation = 3.dp,
)
}
showDeleteDialog?.let { (position, item) ->
ConfirmDeleteDialog(
itemTitle = listOfNotNull(item.title, item.subtitle).joinToString(" - "),
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(position, item)
showDeleteDialog = null
},
)
}
}
else -> {}
}
}
@OptIn(ExperimentalFoundationApi::class)
@ -406,7 +406,7 @@ fun HomePageContent(
}
val onKey =
remember(item) {
{ event: androidx.compose.ui.input.key.KeyEvent ->
{ event: KeyEvent ->
if (isPlayKeyUp(event) && item?.type?.playable == true) {
Timber.v("Clicked play on ${item.id}")
currentOnClickPlay(

View file

@ -9,6 +9,7 @@ import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
import com.github.damontecres.wholphin.data.model.DiscoverItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.preferences.PlayerBackend
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
@ -80,6 +81,7 @@ sealed class Destination(
val positionMs: Long,
val itemPlayback: ItemPlayback? = null,
val forceTranscoding: Boolean = false,
val backend: PlayerBackend? = null,
) : Destination(true) {
constructor(item: BaseItem) : this(item.id, item.resumeMs)
}