Refactor context menu code (#1302)

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

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

There's no significant user facing changes.

### Related issues
None

### Testing
Emulator

## Screenshots
N/A

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

View file

@ -66,6 +66,7 @@ import com.github.damontecres.wholphin.services.MediaManagementService
import com.github.damontecres.wholphin.services.MediaReportService import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.MusicService import com.github.damontecres.wholphin.services.MusicService
import com.github.damontecres.wholphin.services.NavigationManager 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.ThemeSongPlayer
import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.services.deleteItem 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.SlimItemFields
import com.github.damontecres.wholphin.ui.cards.GridCard import com.github.damontecres.wholphin.ui.cards.GridCard
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel 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.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.CardGrid import com.github.damontecres.wholphin.ui.detail.CardGrid
import com.github.damontecres.wholphin.ui.detail.ItemViewModel 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.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState 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.detail.music.addToQueue
import com.github.damontecres.wholphin.ui.equalsNotNull import com.github.damontecres.wholphin.ui.equalsNotNull
import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchDefault
@ -133,7 +134,7 @@ class CollectionFolderViewModel
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
api: ApiClient, api: ApiClient,
@param:ApplicationContext private val context: Context, @param:ApplicationContext private val context: Context,
private val serverRepository: ServerRepository, val serverRepository: ServerRepository,
private val libraryDisplayInfoDao: LibraryDisplayInfoDao, private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
private val favoriteWatchManager: FavoriteWatchManager, private val favoriteWatchManager: FavoriteWatchManager,
private val backdropService: BackdropService, private val backdropService: BackdropService,
@ -142,6 +143,7 @@ class CollectionFolderViewModel
private val userPreferencesService: UserPreferencesService, private val userPreferencesService: UserPreferencesService,
private val mediaManagementService: MediaManagementService, private val mediaManagementService: MediaManagementService,
private val musicService: MusicService, private val musicService: MusicService,
val streamChoiceService: StreamChoiceService,
val mediaReportService: MediaReportService, val mediaReportService: MediaReportService,
@Assisted itemId: String, @Assisted itemId: String,
@Assisted initialSortAndDirection: SortAndDirection?, @Assisted initialSortAndDirection: SortAndDirection?,
@ -662,18 +664,58 @@ fun CollectionFolderGrid(
val item by viewModel.item.observeAsState() val item by viewModel.item.observeAsState()
val viewOptions by viewModel.viewOptions.observeAsState(defaultViewOptions) val viewOptions by viewModel.viewOptions.observeAsState(defaultViewOptions)
var moreDialog by remember { mutableStateOf<Optional<PositionItem>>(Optional.absent()) } var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) } var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
var showDeleteDialog by remember { mutableStateOf<PositionItem?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) 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 = val gridActions =
remember(actions) { remember(actions) {
GridClickActions( GridClickActions(
onClickItem = actions.onClickItem, onClickItem = actions.onClickItem,
onLongClickItem = onLongClickItem =
actions.onLongClickItem ?: { position, item -> 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 = onClickPlayAll =
actions.onClickPlayAll ?: { shuffle -> actions.onClickPlayAll ?: { shuffle ->
@ -802,47 +844,22 @@ fun CollectionFolderGrid(
} }
} }
} }
moreDialog.compose { (position, item) -> overviewDialog?.let { info ->
DialogPopup( ItemDetailsDialog(
showDialog = true, info = info,
title = item.title ?: "", showFilePath =
dialogItems = viewModel.serverRepository.currentUserDto.value
buildMoreDialogItemsForHome( ?.policy
context = context, ?.isAdministrator == true,
item = item, onDismissRequest = { overviewDialog = null },
seriesId = null, )
playbackPosition = item.playbackPosition, }
watched = item.played, showContextMenu?.let { contextMenu ->
favorite = item.favorite, ContextMenuDialog(
canDelete = viewModel.canDelete(item, preferences.appPreferences), onDismissRequest = { showContextMenu = null },
actions = getMediaSource = null,
MoreDialogActions( contextMenu = contextMenu,
navigateTo = { viewModel.navigateTo(it) }, preferredSubtitleLanguage = null,
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,
) )
} }
showPlaylistDialog.compose { itemId -> showPlaylistDialog.compose { itemId ->
@ -862,16 +879,6 @@ fun CollectionFolderGrid(
elevation = 3.dp, 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) @OptIn(ExperimentalFoundationApi::class)

View file

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

View file

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

View file

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

View file

@ -10,7 +10,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -18,6 +17,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R 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.BaseItem
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
import com.github.damontecres.wholphin.preferences.AppPreferences 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.services.deleteItem
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel 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.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.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState 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.detail.music.addToQueue
import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
@ -60,6 +60,7 @@ import java.util.UUID
abstract class RecommendedViewModel( abstract class RecommendedViewModel(
@param:ApplicationContext val context: Context, @param:ApplicationContext val context: Context,
val api: ApiClient, val api: ApiClient,
val serverRepository: ServerRepository,
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
val favoriteWatchManager: FavoriteWatchManager, val favoriteWatchManager: FavoriteWatchManager,
val mediaReportService: MediaReportService, val mediaReportService: MediaReportService,
@ -167,10 +168,9 @@ fun RecommendedContent(
playlistViewModel: AddPlaylistViewModel = hiltViewModel(), playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
onFocusPosition: ((RowColumn) -> Unit)? = null, onFocusPosition: ((RowColumn) -> Unit)? = null,
) { ) {
val context = LocalContext.current var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var moreDialog by remember { mutableStateOf<Optional<RowColumnItem>>(Optional.absent()) } var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) } var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
var showDeleteDialog by remember { mutableStateOf<RowColumnItem?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
OneTimeLaunchedEffect { OneTimeLaunchedEffect {
@ -192,6 +192,35 @@ fun RecommendedContent(
LoadingState.Success -> { LoadingState.Success -> {
var position by rememberPosition() 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( HomePageContent(
homeRows = rows, homeRows = rows,
position = position, position = position,
@ -199,7 +228,18 @@ fun RecommendedContent(
viewModel.navigationManager.navigateTo(item.destination()) viewModel.navigationManager.navigateTo(item.destination())
}, },
onLongClickItem = { position, item -> 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 -> onClickPlay = { _, item ->
viewModel.navigationManager.navigateTo(Destination.Playback(item)) viewModel.navigationManager.navigateTo(Destination.Playback(item))
@ -226,42 +266,22 @@ fun RecommendedContent(
) )
} }
} }
moreDialog.compose { (position, item) -> overviewDialog?.let { info ->
DialogPopup( ItemDetailsDialog(
showDialog = true, info = info,
title = item.title ?: "", showFilePath =
dialogItems = viewModel.serverRepository.currentUserDto.value
buildMoreDialogItemsForHome( ?.policy
context = context, ?.isAdministrator == true,
item = item, onDismissRequest = { overviewDialog = null },
seriesId = null, )
playbackPosition = item.playbackPosition, }
watched = item.played, showContextMenu?.let { contextMenu ->
favorite = item.favorite, ContextMenuDialog(
canDelete = viewModel.canDelete(item, preferences.appPreferences), onDismissRequest = { showContextMenu = null },
actions = getMediaSource = null,
MoreDialogActions( contextMenu = contextMenu,
navigateTo = { viewModel.navigationManager.navigateTo(it) }, preferredSubtitleLanguage = null,
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,
) )
} }
showPlaylistDialog.compose { itemId -> showPlaylistDialog.compose { itemId ->
@ -281,16 +301,6 @@ fun RecommendedContent(
elevation = 3.dp, 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( data class RowColumnItem(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
package com.github.damontecres.wholphin.ui.detail.collection package com.github.damontecres.wholphin.ui.detail.collection
import android.content.Context
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.relocation.BringIntoViewRequester 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState 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.focusProperties
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp 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.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog import com.github.damontecres.wholphin.ui.components.ContextMenu
import com.github.damontecres.wholphin.ui.components.DialogItem import com.github.damontecres.wholphin.ui.components.ContextMenuActions
import com.github.damontecres.wholphin.ui.components.DialogParams import com.github.damontecres.wholphin.ui.components.ContextMenuDialog
import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.HeaderUtils
import com.github.damontecres.wholphin.ui.components.HiddenFocusBox 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.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.data.SortAndDirection 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.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState 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.main.HomePageHeader
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
@ -85,53 +77,58 @@ fun CollectionDetails(
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
// Dialogs // Dialogs
var moreDialog by remember { mutableStateOf<DialogParams?>(null) } var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) } var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
var showDeleteDialog by remember { mutableStateOf<Pair<RowColumn?, BaseItem>?>(null) }
var showViewOptionsDialog by remember { mutableStateOf(false) } var showViewOptionsDialog by remember { mutableStateOf(false) }
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) } var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(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 // Actions
val onClickItem = val onClickItem =
remember { remember {
{ _: RowColumn, item: BaseItem -> viewModel.navigate(item.destination()) } { _: RowColumn, item: BaseItem -> viewModel.navigateTo(item.destination()) }
} }
val onLongClickItem = val onLongClickItem =
remember { remember {
{ position: RowColumn, item: BaseItem -> { position: RowColumn, item: BaseItem ->
val dialogItems = showContextMenu =
buildMoreDialogItemsForHome( ContextMenu.ForBaseItem(
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(
fromLongClick = true, fromLongClick = true,
title = item.title ?: "", item = item,
items = dialogItems, 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) } { filter: GetItemsFilter -> viewModel.changeFilter(filter) }
} }
val onClickPlay = { _: RowColumn, item: BaseItem -> val onClickPlay = { _: RowColumn, item: BaseItem ->
viewModel.navigate(Destination.Playback(item = item)) viewModel.navigateTo(Destination.Playback(item = item))
} }
val onClickPlayAll = val onClickPlayAll =
remember { remember {
@ -158,7 +155,7 @@ fun CollectionDetails(
sortAndDirection = state.sortAndDirection, sortAndDirection = state.sortAndDirection,
filter = state.itemFilter, filter = state.itemFilter,
) )
viewModel.navigate(dest) viewModel.navigateTo(dest)
} }
} }
val onClickViewOptions = remember { { showViewOptionsDialog = true } } val onClickViewOptions = remember { { showViewOptionsDialog = true } }
@ -201,7 +198,7 @@ fun CollectionDetails(
} }
} }
}, },
deleteOnClick = onConfirmDelete =
remember { remember {
{ {
state.collection?.let { state.collection?.let {
@ -216,37 +213,21 @@ fun CollectionDetails(
} ?: false } ?: false
}, },
moreOnClick = { moreOnClick = {
val collection = state.collection!! showContextMenu =
val items = ContextMenu.ForBaseItem(
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(
fromLongClick = false, fromLongClick = false,
title = collection.title ?: "", item = state.collection!!,
items = items, 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, onViewOptionsChange = viewModel::changeViewOptions,
) )
} }
moreDialog?.let { params -> overviewDialog?.let { info ->
DialogPopup( ItemDetailsDialog(
showDialog = true, info = info,
title = params.title, showFilePath =
dialogItems = params.items, viewModel.serverRepository.currentUserDto.value
onDismissRequest = { moreDialog = null }, ?.policy
dismissOnClick = true, ?.isAdministrator == true,
waitToLoad = params.fromLongClick, onDismissRequest = { overviewDialog = null },
)
}
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = null,
contextMenu = contextMenu,
preferredSubtitleLanguage = null,
) )
} }
showPlaylistDialog.compose { itemId -> showPlaylistDialog.compose { itemId ->
@ -286,26 +275,6 @@ fun CollectionDetails(
elevation = 3.dp, 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 @Composable
@ -324,7 +293,7 @@ fun CollectionDetailsContent(
onClickViewOptions: () -> Unit, onClickViewOptions: () -> Unit,
overviewOnClick: () -> Unit, overviewOnClick: () -> Unit,
favoriteOnClick: () -> Unit, favoriteOnClick: () -> Unit,
deleteOnClick: () -> Unit, onConfirmDelete: () -> Unit,
canDelete: Boolean, canDelete: Boolean,
moreOnClick: () -> Unit, moreOnClick: () -> Unit,
modifier: Modifier, modifier: Modifier,
@ -423,7 +392,7 @@ fun CollectionDetailsContent(
getPossibleFilterValues = getPossibleFilterValues, getPossibleFilterValues = getPossibleFilterValues,
onClickViewOptions = onClickViewOptions, onClickViewOptions = onClickViewOptions,
favoriteOnClick = favoriteOnClick, favoriteOnClick = favoriteOnClick,
deleteOnClick = deleteOnClick, onConfirmDelete = onConfirmDelete,
canDelete = canDelete, canDelete = canDelete,
moreOnClick = moreOnClick, moreOnClick = moreOnClick,
modifier = modifier =
@ -485,60 +454,3 @@ fun CollectionDetailsContent(
} }
} }
} }
fun buildMoreDialogItemsForCollection(
context: Context,
item: BaseItem,
favorite: Boolean,
canDelete: Boolean,
onClickPlayAll: (shuffle: Boolean) -> Unit,
actions: MoreDialogActions,
): List<DialogItem> =
buildList {
add(
DialogItem(
context.getString(R.string.play),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
onClickPlayAll.invoke(false)
},
)
add(
DialogItem(
context.getString(R.string.shuffle),
R.string.fa_shuffle,
) {
onClickPlayAll.invoke(true)
},
)
add(
DialogItem(
text = R.string.add_to_playlist,
iconStringRes = R.string.fa_list_ul,
) {
actions.onClickAddPlaylist.invoke(item.id)
},
)
if (canDelete) {
add(
DialogItem(
context.getString(R.string.delete),
Icons.Default.Delete,
iconColor = Color.Red.copy(alpha = .8f),
) {
actions.onClickDelete.invoke(item)
},
)
}
add(
DialogItem(
text = if (favorite) R.string.remove_favorite else R.string.add_favorite,
iconStringRes = R.string.fa_heart,
iconColor = if (favorite) Color.Red else Color.Unspecified,
) {
actions.onClickFavorite.invoke(item.id, !favorite)
},
)
}

View file

@ -382,7 +382,7 @@ class CollectionViewModel
result.totalRecordCount result.totalRecordCount
} }
fun navigate(destination: Destination) { fun navigateTo(destination: Destination) {
release() release()
navigationManager.navigateTo(destination) navigationManager.navigateTo(destination)
} }
@ -393,7 +393,9 @@ class CollectionViewModel
position: RowColumn?, position: RowColumn?,
) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) { ) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
favoriteWatchManager.setWatched(itemId, played) favoriteWatchManager.setWatched(itemId, played)
if (position != null) { if (itemId == state.value.collection?.id) {
refreshCollection()
} else if (position != null) {
refreshItem(itemId, position, false) refreshItem(itemId, position, false)
} }
} }
@ -404,11 +406,24 @@ class CollectionViewModel
position: RowColumn?, position: RowColumn?,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setFavorite(itemId, favorite) favoriteWatchManager.setFavorite(itemId, favorite)
if (position != null) { if (itemId == state.value.collection?.id) {
refreshCollection()
} else if (position != null) {
refreshItem(itemId, position, false) 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( fun canDelete(
item: BaseItem, item: BaseItem,
appPreferences: AppPreferences, appPreferences: AppPreferences,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,20 +1,8 @@
package com.github.damontecres.wholphin.ui.detail.music 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.ViewModel
import androidx.lifecycle.viewModelScope 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.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.ApiRequestPager
import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import org.jellyfin.sdk.api.client.ApiClient 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 org.jellyfin.sdk.model.api.request.GetItemsRequest
import java.util.UUID import java.util.UUID
data class MusicMoreDialogActions(
val onNavigate: (Destination) -> Unit,
val onClickPlay: (Int, BaseItem) -> Unit,
val onClickPlayNext: (Int, BaseItem) -> Unit,
val onClickAddToQueue: (Int, BaseItem) -> Unit,
val onClickFavorite: (UUID, Boolean) -> Unit,
val onClickAddPlaylist: (UUID) -> Unit,
val onClickGoToAlbum: (UUID) -> Unit = {
onNavigate.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ALBUM))
},
val onClickGoToArtist: (UUID) -> Unit = {
onNavigate.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ARTIST))
},
val onClickRemoveFromQueue: (Int) -> Unit,
val onClickDelete: (BaseItem) -> Unit,
)
fun buildMoreDialogForMusic(
context: Context,
actions: MusicMoreDialogActions,
item: BaseItem,
index: Int,
canRemove: Boolean,
canDelete: Boolean,
): List<DialogItem> =
buildList {
add(
DialogItem(
context.getString(R.string.play),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
actions.onClickPlay(index, item)
},
)
add(
DialogItem(
context.getString(R.string.play_next),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
actions.onClickPlayNext(index, item)
},
)
if (canRemove) {
add(
DialogItem(
context.getString(R.string.remove_from_queue),
Icons.Default.Delete,
) {
actions.onClickRemoveFromQueue(index)
},
)
}
add(
DialogItem(
context.getString(R.string.add_to_queue),
Icons.Default.Add,
) {
actions.onClickAddToQueue(index, item)
},
)
add(
DialogItem(
text = R.string.add_to_playlist,
iconStringRes = R.string.fa_list_ul,
) {
actions.onClickAddPlaylist.invoke(item.id)
},
)
if (canDelete) {
add(
DialogItem(
context.getString(R.string.delete),
Icons.Default.Delete,
iconColor = Color.Red.copy(alpha = .8f),
) {
actions.onClickDelete.invoke(item)
},
)
}
add(
DialogItem(
text = if (item.favorite) R.string.remove_favorite else R.string.add_favorite,
iconStringRes = R.string.fa_heart,
iconColor = if (item.favorite) Color.Red else Color.Unspecified,
) {
actions.onClickFavorite.invoke(item.id, !item.favorite)
},
)
if (item.type == BaseItemKind.AUDIO && item.data.albumId != null) {
add(
DialogItem(
context.getString(R.string.go_to_album),
R.string.fa_compact_disc,
) {
actions.onClickGoToAlbum.invoke(item.data.albumId!!)
},
)
}
if ((item.type == BaseItemKind.AUDIO || item.type == BaseItemKind.MUSIC_ALBUM) && item.data.artistItems?.isNotEmpty() == true) {
add(
DialogItem(
context.getString(R.string.go_to_artist),
R.string.fa_user,
) {
actions.onClickGoToArtist.invoke(
item.data.artistItems!!
.first()
.id,
)
},
)
}
}
data class MusicQueueDialogActions(
val onNavigate: (Destination) -> Unit,
val onClickPlay: (Int, AudioItem) -> Unit,
val onClickPlayNext: (Int, AudioItem) -> Unit,
val onClickGoToAlbum: (UUID) -> Unit = {
onNavigate.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ALBUM))
},
val onClickGoToArtist: (UUID) -> Unit = {
onNavigate.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ARTIST))
},
val onClickRemoveFromQueue: (Int, AudioItem) -> Unit,
)
fun buildMoreDialogForMusicQueue(
context: Context,
actions: MusicQueueDialogActions,
item: AudioItem,
index: Int,
canRemove: Boolean,
): List<DialogItem> =
buildList {
add(
DialogItem(
context.getString(R.string.play),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
actions.onClickPlay(index, item)
},
)
add(
DialogItem(
context.getString(R.string.play_next),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
actions.onClickPlayNext(index, item)
},
)
if (canRemove) {
add(
DialogItem(
context.getString(R.string.remove_from_queue),
Icons.Default.Delete,
) {
actions.onClickRemoveFromQueue(index, item)
},
)
}
if (item.albumId != null) {
add(
DialogItem(
context.getString(R.string.go_to_album),
Icons.Default.ArrowForward,
) {
actions.onClickGoToAlbum.invoke(item.albumId)
},
)
}
if (item.artistId != null) {
add(
DialogItem(
context.getString(R.string.go_to_artist),
Icons.Default.ArrowForward,
) {
actions.onClickGoToArtist.invoke(item.artistId)
},
)
}
}
suspend fun ViewModel.getPagerForAlbum( suspend fun ViewModel.getPagerForAlbum(
api: ApiClient, api: ApiClient,
albumId: UUID, albumId: UUID,

View file

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

View file

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

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester 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.compose.LifecycleResumeEffect
import androidx.lifecycle.map import androidx.lifecycle.map
import com.github.damontecres.wholphin.R 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.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog import com.github.damontecres.wholphin.ui.components.ContextMenu
import com.github.damontecres.wholphin.ui.components.DialogParams import com.github.damontecres.wholphin.ui.components.ContextMenuActions
import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.ContextMenuDialog
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage 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.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo 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.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState 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.nav.Destination
import com.github.damontecres.wholphin.ui.rememberInt 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 com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers import kotlinx.serialization.UseSerializers
import org.jellyfin.sdk.model.api.BaseItemKind 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.MediaType
import org.jellyfin.sdk.model.api.PersonKind import org.jellyfin.sdk.model.api.PersonKind
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.serializer.UUIDSerializer import org.jellyfin.sdk.model.serializer.UUIDSerializer
import org.jellyfin.sdk.model.serializer.toUUID import org.jellyfin.sdk.model.serializer.toUUID
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import java.util.UUID import java.util.UUID
import kotlin.time.Duration import kotlin.time.Duration
@ -110,6 +100,7 @@ fun SeriesOverview(
val episodeList = (episodes as? EpisodeList.Success)?.episodes val episodeList = (episodes as? EpisodeList.Success)?.episodes
val position by viewModel.position.collectAsState(SeriesOverviewPosition(0, 0)) val position by viewModel.position.collectAsState(SeriesOverviewPosition(0, 0))
val currentPosition by rememberUpdatedState(position)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (seasons.isNotEmpty()) { if (seasons.isNotEmpty()) {
seasons.getOrNull(position.seasonTabIndex)?.let { seasons.getOrNull(position.seasonTabIndex)?.let {
@ -119,13 +110,54 @@ fun SeriesOverview(
} }
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) } var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var moreDialog by remember { mutableStateOf<DialogParams?>(null) } var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) } var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
var rowFocused by rememberInt() var rowFocused by rememberInt()
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
val contextActions =
remember {
ContextMenuActions(
navigateTo = viewModel::navigateTo,
onClickWatch = { itemId, watched ->
viewModel.setWatched(itemId, watched, currentPosition.episodeRowIndex)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(itemId, favorite, currentPosition.episodeRowIndex)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = itemId
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onDeleteItem = viewModel::deleteItem,
onChooseVersion = { item, source ->
viewModel.savePlayVersion(
item,
source.id!!.toUUID(),
)
},
onChooseTracks = { result ->
viewModel.saveTrackSelection(
result.item,
result.itemPlayback,
result.trackIndex,
result.streamType,
)
},
onShowOverview = { overviewDialog = ItemDetailsDialogInfo(it) },
onClearChosenStreams = {
val focusedEpisode =
(episodes as? EpisodeList.Success)
?.episodes
?.getOrNull(currentPosition.episodeRowIndex)
if (focusedEpisode != null) {
viewModel.clearChosenStreams(focusedEpisode, it)
}
},
)
}
LaunchedEffect(position, episodes) { LaunchedEffect(position, episodes) {
val focusedEpisode = 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( SeriesOverviewContent(
preferences = preferences, preferences = preferences,
series = series, series = series,
@ -319,9 +254,18 @@ fun SeriesOverview(
) )
}, },
onLongClick = { ep -> onLongClick = { ep ->
scope.launchDefault { showContextMenu =
moreDialog = buildMoreForEpisode(ep, chosenStreams, true) 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 -> playOnClick = { resume ->
rowFocused = EPISODE_ROW rowFocused = EPISODE_ROW
@ -349,9 +293,18 @@ fun SeriesOverview(
}, },
moreOnClick = { moreOnClick = {
episodeList?.getOrNull(position.episodeRowIndex)?.let { ep -> episodeList?.getOrNull(position.episodeRowIndex)?.let { ep ->
scope.launchDefault { showContextMenu =
moreDialog = buildMoreForEpisode(ep, chosenStreams, false) 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 = { overviewOnClick = {
@ -374,13 +327,20 @@ fun SeriesOverview(
viewModel.navigateTo(extra.destination) viewModel.navigateTo(extra.destination)
}, },
canDelete = { viewModel.canDelete(it, preferences.appPreferences) }, canDelete = { viewModel.canDelete(it, preferences.appPreferences) },
deleteOnClick = { showDeleteDialog = it }, onConfirmDelete = viewModel::deleteItem,
modifier = modifier, modifier = modifier,
) )
} }
} }
} }
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = viewModel.streamChoiceService::chooseSource,
contextMenu = contextMenu,
preferredSubtitleLanguage = preferredSubtitleLanguage,
)
}
overviewDialog?.let { info -> overviewDialog?.let { info ->
ItemDetailsDialog( ItemDetailsDialog(
info = info, info = info,
@ -391,26 +351,6 @@ fun SeriesOverview(
onDismissRequest = { overviewDialog = null }, 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 -> showPlaylistDialog?.let { itemId ->
PlaylistDialog( PlaylistDialog(
title = stringResource(R.string.add_to_playlist), title = stringResource(R.string.add_to_playlist),
@ -428,17 +368,6 @@ fun SeriesOverview(
elevation = 3.dp, 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 private const val EPISODE_ROW = 0

View file

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

View file

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

View file

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