From c718d903a4ca1abc46c73387e786995d3b41da3c Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:58:54 -0400 Subject: [PATCH] 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 --- .../ui/components/CollectionFolderGrid.kt | 121 +-- .../wholphin/ui/components/ContextMenu.kt | 861 ++++++++++++++++++ .../wholphin/ui/components/Dialogs.kt | 21 +- .../wholphin/ui/components/PlayButtons.kt | 31 +- .../ui/components/RecommendedContent.kt | 116 +-- .../ui/components/RecommendedMovie.kt | 3 +- .../ui/components/RecommendedMusic.kt | 3 +- .../ui/components/RecommendedTvShow.kt | 3 +- .../wholphin/ui/detail/DetailUtils.kt | 259 ------ .../wholphin/ui/detail/PlaylistDetails.kt | 127 ++- .../ui/detail/collection/CollectionButtons.kt | 8 +- .../ui/detail/collection/CollectionDetails.kt | 242 ++--- .../detail/collection/CollectionViewModel.kt | 21 +- .../ui/detail/episode/EpisodeDetails.kt | 181 ++-- .../wholphin/ui/detail/movie/MovieDetails.kt | 230 ++--- .../ui/detail/music/AlbumDetailsPage.kt | 144 ++- .../ui/detail/music/ArtistDetailsPage.kt | 169 ++-- .../ui/detail/music/NowPlayingPage.kt | 58 +- .../wholphin/ui/detail/music/Utils.kt | 199 ---- .../ui/detail/series/FocusedEpisodeFooter.kt | 5 +- .../ui/detail/series/SeriesDetails.kt | 211 ++--- .../ui/detail/series/SeriesOverview.kt | 235 ++--- .../ui/detail/series/SeriesOverviewContent.kt | 4 +- .../damontecres/wholphin/ui/main/HomePage.kt | 100 +- .../wholphin/ui/nav/Destination.kt | 2 + 25 files changed, 1707 insertions(+), 1647 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index 848288d5..c7a8fecf 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -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.absent()) } + var showContextMenu by remember { mutableStateOf(null) } + var overviewDialog by remember { mutableStateOf(null) } var showPlaylistDialog by remember { mutableStateOf>(Optional.absent()) } - var showDeleteDialog by remember { mutableStateOf(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) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt new file mode 100644 index 00000000..22fd640f --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt @@ -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(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 = + 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 = + 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 = + 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) + }, + ) + } + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt index 538f623b..58bcde20 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt index 75a3147c..ca4c11b8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt @@ -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, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt index 1e2d3e61..ad7010cf 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt @@ -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.absent()) } + var showContextMenu by remember { mutableStateOf(null) } + var overviewDialog by remember { mutableStateOf(null) } var showPlaylistDialog by remember { mutableStateOf>(Optional.absent()) } - var showDeleteDialog by remember { mutableStateOf(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( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt index 3228046b..71b762f0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt @@ -58,7 +58,7 @@ class RecommendedMovieViewModel @ApplicationContext context: Context, api: ApiClient, musicService: MusicService, - private val serverRepository: ServerRepository, + serverRepository: ServerRepository, private val preferencesDataStore: DataStore, private val suggestionService: SuggestionService, @Assisted val parentId: UUID, @@ -70,6 +70,7 @@ class RecommendedMovieViewModel ) : RecommendedViewModel( context, api, + serverRepository, navigationManager, favoriteWatchManager, mediaReportService, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMusic.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMusic.kt index e659347b..30576e7b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMusic.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMusic.kt @@ -57,7 +57,7 @@ class RecommendedMusicViewModel @ApplicationContext context: Context, api: ApiClient, musicService: MusicService, - private val serverRepository: ServerRepository, + serverRepository: ServerRepository, private val preferencesDataStore: DataStore, private val suggestionService: SuggestionService, @Assisted val parentId: UUID, @@ -69,6 +69,7 @@ class RecommendedMusicViewModel ) : RecommendedViewModel( context, api, + serverRepository, navigationManager, favoriteWatchManager, mediaReportService, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt index a48af22d..ce50249e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt @@ -61,7 +61,7 @@ class RecommendedTvShowViewModel @ApplicationContext context: Context, api: ApiClient, musicService: MusicService, - private val serverRepository: ServerRepository, + serverRepository: ServerRepository, private val preferencesDataStore: DataStore, private val lastestNextUpService: LatestNextUpService, private val suggestionService: SuggestionService, @@ -74,6 +74,7 @@ class RecommendedTvShowViewModel ) : RecommendedViewModel( context, api, + serverRepository, navigationManager, favoriteWatchManager, mediaReportService, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt index 049569d9..20172832 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt @@ -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 = - 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 = - 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) - }, - ) - } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt index ff5809fb..f498a497 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt @@ -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(null) } + var showContextMenu by remember { mutableStateOf(null) } var showConfirmTypeDialog by remember { mutableStateOf?>(null) } var showPlaylistDialog by remember { mutableStateOf>(Optional.absent()) } val addPlaylistState by addToPlaylistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) @@ -404,34 +403,40 @@ fun PlaylistDetails( } } } - var showDeleteDialog by remember { mutableStateOf(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 }, - ) - val moreActions = - MoreDialogActions( - navigateTo = { viewModel.navigationManager.navigateTo(it) }, - onClickWatch = { id, watched -> viewModel.setWatched(id, watched) }, - onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) }, - onClickAddPlaylist = { itemId -> - addToPlaylistViewModel.loadPlaylists(MediaType.VIDEO) - showPlaylistDialog.makePresent(itemId) - }, - onSendMediaInfo = viewModel::sendMediaReport, - onClickDelete = { showDeleteDialog = it }, - onClickAddToQueue = { viewModel.addToQueue(it, 0) }, + onClickRemoveFromQueue = { _, _ -> }, + onDeleteItem = viewModel::deleteItem, ) + val contextActions = + remember { + ContextMenuActions( + navigateTo = { viewModel.navigationManager.navigateTo(it) }, + onClickWatch = { id, watched -> viewModel.setWatched(id, watched) }, + onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) }, + onClickAddPlaylist = { itemId -> + addToPlaylistViewModel.loadPlaylists(MediaType.VIDEO) + showPlaylistDialog.makePresent(itemId) + }, + onSendMediaInfo = viewModel::sendMediaReport, + 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 = - if (item.type == BaseItemKind.AUDIO) { - buildMoreDialogForMusic( - context = context, - actions = musicMoreActions, - item = item, - index = index, - canRemove = false, - canDelete = viewModel.canDelete(item, preferences.appPreferences), - ) - } else { - 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 = moreActions, - ) - }, - ) + showContextMenu = + if (item.type == BaseItemKind.AUDIO) { + ContextMenu.ForMusic( + fromLongClick = true, + item = item, + index = index, + canDelete = viewModel.canDelete(item, preferences.appPreferences), + canRemoveFromQueue = false, + actions = musicContextActions, + ) + } else { + ContextMenu.ForBaseItem( + fromLongClick = true, + item = item, + chosenStreams = null, + showGoTo = true, + showStreamChoices = false, + canDelete = viewModel.canDelete(item, preferences.appPreferences), + 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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionButtons.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionButtons.kt index b10e9a17..759da02e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionButtons.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionButtons.kt @@ -40,7 +40,7 @@ fun CollectionButtons( getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List, 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, ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetails.kt index 2f61fa4f..332e2a3f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetails.kt @@ -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,53 +77,58 @@ fun CollectionDetails( val state by viewModel.state.collectAsState() // Dialogs - var moreDialog by remember { mutableStateOf(null) } + var showContextMenu by remember { mutableStateOf(null) } var showPlaylistDialog by remember { mutableStateOf>(Optional.absent()) } val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) - var showDeleteDialog by remember { mutableStateOf?>(null) } var showViewOptionsDialog by remember { mutableStateOf(false) } var overviewDialog by remember { mutableStateOf(null) } + fun contextActionsFor(position: RowColumn?) = + ContextMenuActions( + navigateTo = viewModel::navigateTo, + onClickWatch = { itemId, watched -> + viewModel.setWatched(itemId, watched, position) + }, + onClickFavorite = { itemId, favorite -> + viewModel.setFavorite(itemId, favorite, position) + }, + onClickAddPlaylist = { itemId -> + playlistViewModel.loadPlaylists(MediaType.VIDEO) + showPlaylistDialog.makePresent(itemId) + }, + onSendMediaInfo = viewModel.mediaReportService::sendReportFor, + 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 + }, + ) + // Actions val onClickItem = remember { - { _: RowColumn, item: BaseItem -> viewModel.navigate(item.destination()) } + { _: RowColumn, item: BaseItem -> viewModel.navigateTo(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, - onClickWatch = { itemId, watched -> - viewModel.setWatched(itemId, watched, position) - }, - onClickFavorite = { itemId, favorite -> - viewModel.setFavorite(itemId, favorite, position) - }, - onClickAddPlaylist = { itemId -> - playlistViewModel.loadPlaylists(MediaType.VIDEO) - showPlaylistDialog.makePresent(itemId) - }, - onSendMediaInfo = viewModel.mediaReportService::sendReportFor, - onClickDelete = { item -> showDeleteDialog = Pair(position, item) }, - onClickAddToQueue = { viewModel.addToQueue(it, 0) }, - ), - ) - moreDialog = - DialogParams( + 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 = - 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) - }, - ) - } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt index b0a6c606..915471d7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt index 79637ee0..fa39f919 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt @@ -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(null) } - var moreDialog by remember { mutableStateOf(null) } - var chooseVersion by remember { mutableStateOf(null) } + var showContextMenu by remember { mutableStateOf(null) } var showPlaylistDialog by remember { mutableStateOf>(Optional.absent()) } - var showDeleteDialog by remember { mutableStateOf(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( - navigateTo = viewModel::navigateTo, - onClickWatch = { itemId, watched -> - viewModel.setWatched(itemId, watched) - }, - onClickFavorite = { itemId, favorite -> - viewModel.setFavorite(itemId, favorite) - }, - onClickAddPlaylist = { itemId -> - playlistViewModel.loadPlaylists(MediaType.VIDEO) - showPlaylistDialog.makePresent(itemId) - }, - onSendMediaInfo = viewModel.mediaReportService::sendReportFor, - onClickDelete = { showDeleteDialog = it }, - ) + val contextActions = + remember { + ContextMenuActions( + navigateTo = viewModel::navigateTo, + onClickWatch = viewModel::setWatched, + onClickFavorite = viewModel::setFavorite, + onClickAddPlaylist = { itemId -> + playlistViewModel.loadPlaylists(MediaType.VIDEO) + showPlaylistDialog.makePresent(itemId) + }, + onSendMediaInfo = viewModel.mediaReportService::sendReportFor, + 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) - }, - canDelete = canDelete, - ), + item = ep, + 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() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index 64bf2f1c..e198689d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -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(null) } - var moreDialog by remember { mutableStateOf(null) } - var chooseVersion by remember { mutableStateOf(null) } + var showContextMenu by remember { mutableStateOf(null) } var showPlaylistDialog by remember { mutableStateOf>(Optional.absent()) } val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) - var showDeleteDialog by remember { mutableStateOf(null) } val preferredSubtitleLanguage = viewModel.serverRepository.currentUserDto @@ -114,22 +105,36 @@ fun MovieDetails( ?.configuration ?.subtitleLanguagePreference - val moreActions = - MoreDialogActions( - navigateTo = viewModel::navigateTo, - onClickWatch = { itemId, watched -> - viewModel.setWatched(itemId, watched) - }, - onClickFavorite = { itemId, favorite -> - viewModel.setFavorite(itemId, favorite) - }, - onClickAddPlaylist = { itemId -> - playlistViewModel.loadPlaylists(MediaType.VIDEO) - showPlaylistDialog.makePresent(itemId) - }, - onSendMediaInfo = viewModel.mediaReportService::sendReportFor, - onClickDelete = { showDeleteDialog = it }, - ) + val contextActions = + remember { + ContextMenuActions( + navigateTo = viewModel::navigateTo, + onClickWatch = viewModel::setWatched, + onClickFavorite = viewModel::setFavorite, + onClickAddPlaylist = { itemId -> + playlistViewModel.loadPlaylists(MediaType.VIDEO) + showPlaylistDialog.makePresent(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 = { 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 }, - 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) - }, - ), + item = movie, + chosenStreams = chosenStreams, + showGoTo = false, + showStreamChoices = true, + canDelete = state.canDelete, + 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() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt index 53cdb444..173ba49b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt @@ -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.absent()) } val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) - var moreDialog by remember { mutableStateOf(null) } - var showDeleteDialog by remember { mutableStateOf(null) } + var showContextMenu by remember { mutableStateOf(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, - ), + item = album, + index = 0, + 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, - ), + item = song, + index = index, + 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, - ), + item = song, + index = index, + 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, - ), + item = item, + index = index, + 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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt index 4a338d03..77f2e063 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt @@ -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.absent()) } val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) - var moreDialog by remember { mutableStateOf(null) } - var showDeleteDialog by remember { mutableStateOf(null) } + var showContextMenu by remember { mutableStateOf(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, - ), + item = artist, + index = 0, + 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, - ), + item = song, + index = index, + 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, - ), + item = song, + index = index, + 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, - ), + item = album, + index = index, + 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, - ), + item = item, + index = index, + 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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt index d7da35ed..a9d43a27 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt @@ -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(null) } + var showContextMenu by remember { mutableStateOf(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, + item = song, + index = index, + 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) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/Utils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/Utils.kt index 5fae787c..8e170706 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/Utils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/Utils.kt @@ -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 = - 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 = - 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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt index 2aeac04e..83987524 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt @@ -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(), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index 3c6b7d5c..18a539c4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -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(null) } + var showContextMenu by remember { mutableStateOf(null) } + var showWatchConfirmation by remember { mutableStateOf(false) } - var seasonDialog by remember { mutableStateOf(null) } var showPlaylistDialog by remember { mutableStateOf>(Optional.absent()) } - var showDeleteDialog by remember { mutableStateOf(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,29 +223,18 @@ 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, - ), - ) - }, - onClickDelete = { - showDeleteDialog = it - }, - ) - } + 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, + ) }, 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) - }, - onClickFavorite = { itemId, played -> - viewModel.setFavorite(itemId, played, null) - }, - onClickAddPlaylist = { itemId -> - playlistViewModel.loadPlaylists(MediaType.VIDEO) - showPlaylistDialog.makePresent(itemId) - }, - onSendMediaInfo = viewModel.mediaReportService::sendReportFor, - onClickDelete = { - showDeleteDialog = it - }, - ), + onShowContextMenu = { + showContextMenu = 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(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,18 +561,17 @@ 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 = Modifier @@ -608,23 +607,19 @@ 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 -> SeasonCard( @@ -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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt index 5762e55b..6d99e156 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt @@ -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(null) } - var moreDialog by remember { mutableStateOf(null) } - var chooseVersion by remember { mutableStateOf(null) } + var showContextMenu by remember { mutableStateOf(null) } var showPlaylistDialog by remember { mutableStateOf(null) } val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) var rowFocused by rememberInt() - var showDeleteDialog by remember { mutableStateOf(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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index d87d339c..999ed7c4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -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) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index ef0fdbad..47a86748 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -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(null) } + var showContextMenu by remember { mutableStateOf(null) } var showPlaylistDialog by remember { mutableStateOf(null) } - var showDeleteDialog by remember { mutableStateOf(null) } + var overviewDialog by remember { mutableStateOf(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,19 +238,7 @@ 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 -> {} } } @@ -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( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt index a7559914..9c0596f3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt @@ -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) }