diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt index 88ee10c0..ec193b2f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt @@ -156,6 +156,7 @@ fun RecommendedMovie( HomePageContent( homeRows = rows, onClickItem = onClickItem, + onLongClickItem = {}, onFocusPosition = onFocusPosition, modifier = modifier, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt index e1dab51c..589403f2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt @@ -168,6 +168,7 @@ fun RecommendedTvShow( HomePageContent( homeRows = rows, onClickItem = onClickItem, + onLongClickItem = {}, onFocusPosition = onFocusPosition, modifier = modifier, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt index c8c36af9..2f7c0ede 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt @@ -2,17 +2,22 @@ package com.github.damontecres.wholphin.ui.detail import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.filled.ArrowForward import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Refresh import androidx.compose.ui.graphics.Color import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.components.DialogItem import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination +import com.github.damontecres.wholphin.util.supportedPlayableTypes import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.serializer.toUUIDOrNull +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds /** * Build the [DialogItem]s when clicking "More" @@ -131,3 +136,104 @@ fun buildMoreDialogItems( } } } + +fun buildMoreDialogItemsForHome( + item: BaseItem, + seriesId: UUID?, + playbackPosition: Duration, + watched: Boolean, + favorite: Boolean, + navigateTo: (Destination) -> Unit, + onClickWatch: (UUID, Boolean) -> Unit, + onClickFavorite: (UUID, Boolean) -> Unit, +): List = + buildList { + val itemId = item.id + add( + DialogItem( + "Go To", + Icons.Default.ArrowForward, + ) { + navigateTo(item.destination()) + }, + ) + if (item.type in supportedPlayableTypes) { + if (playbackPosition >= 1.seconds) { + add( + DialogItem( + "Resume", + Icons.Default.PlayArrow, + iconColor = Color.Green.copy(alpha = .8f), + ) { + navigateTo( + Destination.Playback( + itemId, + playbackPosition.inWholeMilliseconds, + ), + ) + }, + ) + add( + DialogItem( + "Restart", + Icons.Default.Refresh, +// iconColor = Color.Green.copy(alpha = .8f), + ) { + navigateTo( + Destination.Playback( + itemId, + 0L, + ), + ) + }, + ) + } else { + add( + DialogItem( + "Play", + Icons.Default.PlayArrow, + iconColor = Color.Green.copy(alpha = .8f), + ) { + navigateTo( + Destination.Playback( + itemId, + 0L, + ), + ) + }, + ) + } + } + 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, + ) { + onClickWatch.invoke(itemId, !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, + ) { + onClickFavorite.invoke(itemId, !favorite) + }, + ) + seriesId?.let { + add( + DialogItem( + "Go to series", + Icons.AutoMirrored.Filled.ArrowForward, + ) { + navigateTo( + Destination.MediaItem( + it, + BaseItemKind.SERIES, + ), + ) + }, + ) + } + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index ed749a27..73a1fe0d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -45,11 +45,14 @@ import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.cards.BannerCard import com.github.damontecres.wholphin.ui.cards.ItemRow import com.github.damontecres.wholphin.ui.components.CircularProgress +import com.github.damontecres.wholphin.ui.components.DialogParams +import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.DotSeparatedRow import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.data.RowColumnSaver +import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.roundMinutes @@ -60,6 +63,7 @@ import com.github.damontecres.wholphin.util.formatDateTime import com.github.damontecres.wholphin.util.seasonEpisode import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.extensions.ticks +import kotlin.time.Duration data class HomeRow( val section: HomeSection, @@ -88,14 +92,48 @@ fun HomePage( -> LoadingPage() LoadingState.Success -> { + var dialog by remember { mutableStateOf(null) } HomePageContent( homeRows, onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) }, + onLongClickItem = { + val dialogItems = + buildMoreDialogItemsForHome( + item = it, + seriesId = it.data.seriesId, + playbackPosition = + it.data.userData + ?.playbackPositionTicks + ?.ticks + ?: Duration.ZERO, + watched = it.data.userData?.played ?: false, + favorite = it.data.userData?.isFavorite ?: false, + navigateTo = viewModel.navigationManager::navigateTo, + onClickWatch = { itemId, played -> + viewModel.setWatched(itemId, played) + }, + onClickFavorite = { itemId, favorite -> + viewModel.setFavorite(itemId, favorite) + }, + ) + dialog = + DialogParams( + title = it.title ?: "", + fromLongClick = true, + items = dialogItems, + ) + }, loadingState = loading, modifier = modifier, ) + dialog?.let { params -> + DialogPopup( + params = params, + onDismissRequest = { dialog = null }, + ) + } } } } @@ -104,6 +142,7 @@ fun HomePage( fun HomePageContent( homeRows: List, onClickItem: (BaseItem) -> Unit, + onLongClickItem: (BaseItem) -> Unit, modifier: Modifier = Modifier, onFocusPosition: ((RowColumn) -> Unit)? = null, loadingState: LoadingState? = null, @@ -183,7 +222,7 @@ fun HomePageContent( position = RowColumn(rowIndex, index) } }, - onLongClickItem = {}, + onLongClickItem = onLongClickItem, modifier = Modifier.fillMaxWidth(), cardContent = { index, item, cardModifier, onClick, onLongClick -> // TODO better aspect ration handling? diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt index 930df7ef..080d3771 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt @@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem +import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.supportItemKinds @@ -21,6 +22,7 @@ import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.liveTvApi +import org.jellyfin.sdk.api.client.extensions.playStateApi import org.jellyfin.sdk.api.client.extensions.tvShowsApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.userViewsApi @@ -46,8 +48,11 @@ class HomeViewModel val loadingState = MutableLiveData(LoadingState.Pending) val homeRows = MutableLiveData>() + private lateinit var preferences: UserPreferences + fun init(preferences: UserPreferences): Job { loadingState.value = LoadingState.Loading + this.preferences = preferences val prefs = preferences.appPreferences.homePagePreferences val limit = prefs.maxItemsPerRow return viewModelScope.launch( @@ -171,7 +176,7 @@ class HomeViewModel latestMediaIncludes .mapNotNull { viewId -> views.items.firstOrNull { it.id == viewId } } .filter { it.collectionType in supportedLatestCollectionTypes } - .map { view -> + .mapNotNull { view -> val title = if (view.collectionType == CollectionType.LIVETV) { "Recently Recorded" @@ -189,28 +194,58 @@ class HomeViewModel } else { view.id } - val request = - GetLatestMediaRequest( - fields = SlimItemFields, - imageTypeLimit = 1, - parentId = viewId, - groupItems = true, - limit = limit, - isPlayed = null, // Server will handle user's preference + viewId?.let { + val request = + GetLatestMediaRequest( + fields = SlimItemFields, + imageTypeLimit = 1, + parentId = viewId, + groupItems = true, + limit = limit, + isPlayed = null, // Server will handle user's preference + ) + val latest = + api.userLibraryApi + .getLatestMedia(request) + .content + .map { BaseItem.from(it, api, true) } + HomeRow( + section = HomeSection.LATEST_MEDIA, + items = latest, + title = title, ) - val latest = - api.userLibraryApi - .getLatestMedia(request) - .content - .map { BaseItem.from(it, api, true) } - HomeRow( - section = HomeSection.LATEST_MEDIA, - items = latest, - title = title, - ) + } } return rows } + + fun setWatched( + itemId: UUID, + played: Boolean, + ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { + if (played) { + api.playStateApi.markPlayedItem(itemId) + } else { + api.playStateApi.markUnplayedItem(itemId) + } + withContext(Dispatchers.Main) { + init(preferences) + } + } + + fun setFavorite( + itemId: UUID, + favorite: Boolean, + ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { + if (favorite) { + api.userLibraryApi.markFavoriteItem(itemId) + } else { + api.userLibraryApi.unmarkFavoriteItem(itemId) + } + withContext(Dispatchers.Main) { + init(preferences) + } + } } val supportedLatestCollectionTypes = diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt b/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt index 60304936..c6ec4bb5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt @@ -38,3 +38,13 @@ val supportedCollectionTypes = CollectionType.BOXSETS, CollectionType.LIVETV, ) + +val supportedPlayableTypes = + setOf( + BaseItemKind.MOVIE, + BaseItemKind.EPISODE, + BaseItemKind.VIDEO, + BaseItemKind.TV_CHANNEL, + BaseItemKind.TV_PROGRAM, + BaseItemKind.RECORDING, + )