From 2e3e4e3f5fffab0e91389daeecb3be9c550e7802 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Wed, 12 Nov 2025 15:36:36 -0500 Subject: [PATCH] Add long clicking context menus everywhere (#202) Adds long click context menus to just about every card throughout the app. Previously only a few pages had this. Closes #196 --- .../wholphin/data/model/BaseItem.kt | 31 +-- .../damontecres/wholphin/hilt/AppModule.kt | 7 + .../wholphin/ui/cards/BannerCard.kt | 18 ++ .../damontecres/wholphin/ui/cards/ItemRow.kt | 12 +- .../wholphin/ui/cards/PersonRow.kt | 4 +- .../ui/components/CollectionFolderGrid.kt | 102 +++++++++- .../wholphin/ui/components/GenreCardGrid.kt | 4 +- .../wholphin/ui/components/LoadingRow.kt | 4 +- .../wholphin/ui/components/Optional.kt | 31 +++ .../ui/components/RecommendedContent.kt | 178 ++++++++++++++++++ .../ui/components/RecommendedMovie.kt | 50 ++--- .../ui/components/RecommendedTvShow.kt | 51 ++--- .../wholphin/ui/detail/CardGrid.kt | 8 +- .../ui/detail/CollectionFolderGeneric.kt | 2 +- .../ui/detail/CollectionFolderLiveTv.kt | 2 +- .../ui/detail/CollectionFolderMovie.kt | 14 +- .../wholphin/ui/detail/CollectionFolderTv.kt | 5 +- .../wholphin/ui/detail/DetailUtils.kt | 74 +++++--- .../wholphin/ui/detail/FavoritesPage.kt | 6 +- .../wholphin/ui/detail/PersonPage.kt | 9 +- .../wholphin/ui/detail/movie/MovieDetails.kt | 100 +++++++--- .../ui/detail/movie/MovieViewModel.kt | 35 ++-- .../ui/detail/series/SeriesDetails.kt | 115 +++++++++-- .../ui/detail/series/SeriesOverview.kt | 36 ++-- .../ui/detail/series/SeriesViewModel.kt | 22 +-- .../damontecres/wholphin/ui/main/HomePage.kt | 62 +++--- .../wholphin/ui/main/HomeViewModel.kt | 15 +- .../wholphin/ui/main/SearchPage.kt | 15 +- .../wholphin/util/FavoriteWatchManager.kt | 43 +++++ 29 files changed, 766 insertions(+), 289 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/components/Optional.kt create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt create mode 100644 app/src/main/java/com/github/damontecres/wholphin/util/FavoriteWatchManager.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt index ed468a90..e3ffbcbd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt @@ -11,6 +11,7 @@ import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.extensions.ticks +import kotlin.time.Duration @Serializable data class BaseItem( @@ -18,29 +19,29 @@ data class BaseItem( val imageUrl: String?, val backdropImageUrl: String? = null, ) { - @Transient val id = data.id + val id get() = data.id - @Transient val type = data.type + val type get() = data.type - @Transient val name = data.name + val name get() = data.name - @Transient - val title = if (type == BaseItemKind.EPISODE) data.seriesName else name + val title get() = if (type == BaseItemKind.EPISODE) data.seriesName else name - @Transient - val subtitle = - if (type == BaseItemKind.EPISODE) data.seasonEpisode + " - " + name else data.productionYear?.toString() - - @Transient - val resumeMs = - data.userData - ?.playbackPositionTicks - ?.ticks - ?.inWholeMilliseconds + val subtitle + get() = + if (type == BaseItemKind.EPISODE) data.seasonEpisode + " - " + name else data.productionYear?.toString() @Transient val indexNumber = data.indexNumber ?: dateAsIndex() + val playbackPosition get() = data.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO + + val resumeMs get() = playbackPosition.inWholeMilliseconds + + val played get() = data.userData?.played ?: false + + val favorite get() = data.userData?.isFavorite ?: false + private fun dateAsIndex(): Int? = data.premiereDate ?.let { diff --git a/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt b/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt index 34219c58..98eb1f81 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt @@ -11,6 +11,8 @@ import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.updateInterfacePreferences import com.github.damontecres.wholphin.util.ExceptionHandler +import com.github.damontecres.wholphin.util.FavoriteWatchManager +import com.github.damontecres.wholphin.util.FavoriteWatchManagerImpl import com.github.damontecres.wholphin.util.RememberTabManager import dagger.Module import dagger.Provides @@ -23,6 +25,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import okhttp3.OkHttpClient import org.jellyfin.sdk.Jellyfin +import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.util.AuthorizationHeaderBuilder import org.jellyfin.sdk.api.okhttp.OkHttpFactory import org.jellyfin.sdk.createJellyfin @@ -182,4 +185,8 @@ object AppModule { @Singleton @IoCoroutineScope fun ioCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + @Provides + @Singleton + fun favoriteWatchManager(api: ApiClient): FavoriteWatchManager = FavoriteWatchManagerImpl(api) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt index 1b2e90a0..06f994f5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt @@ -21,16 +21,21 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.tv.material3.Card import androidx.tv.material3.CardDefaults import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import coil3.compose.AsyncImage +import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.Cards +import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.isNotNullOrBlank /** @@ -45,6 +50,7 @@ fun BannerCard( modifier: Modifier = Modifier, cornerText: String? = null, played: Boolean = false, + favorite: Boolean = false, playPercent: Double = 0.0, cardHeight: Dp = 140.dp * .85f, aspectRatio: Float = 16f / 9, @@ -118,6 +124,18 @@ fun BannerCard( } } } + if (favorite) { + Text( + modifier = + Modifier + .align(Alignment.TopStart) + .padding(8.dp), + color = colorResource(android.R.color.holo_red_light), + text = stringResource(R.string.fa_heart), + fontSize = 16.sp, + fontFamily = FontAwesome, + ) + } if (playPercent > 0 && playPercent < 100) { Box( modifier = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt index 46d38594..71df426b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt @@ -28,8 +28,8 @@ import com.github.damontecres.wholphin.util.FocusPair fun ItemRow( title: String, items: List, - onClickItem: (BaseItem) -> Unit, - onLongClickItem: (BaseItem) -> Unit, + onClickItem: (Int, BaseItem) -> Unit, + onLongClickItem: (Int, BaseItem) -> Unit, cardContent: @Composable ( index: Int, item: BaseItem?, @@ -83,8 +83,8 @@ fun ItemRow( index, item, cardModifier, - { if (item != null) onClickItem.invoke(item) }, - { if (item != null) onLongClickItem.invoke(item) }, + { if (item != null) onClickItem.invoke(index, item) }, + { if (item != null) onLongClickItem.invoke(index, item) }, ) } } @@ -95,8 +95,8 @@ fun ItemRow( fun BannerItemRow( title: String, items: List, - onClickItem: (BaseItem) -> Unit, - onLongClickItem: (BaseItem) -> Unit, + onClickItem: (Int, BaseItem) -> Unit, + onLongClickItem: (Int, BaseItem) -> Unit, modifier: Modifier = Modifier, focusPair: FocusPair? = null, cardOnFocus: ((isFocused: Boolean, index: Int) -> Unit)? = null, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonRow.kt index 63730ca7..6504a13f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonRow.kt @@ -28,7 +28,7 @@ fun PersonRow( people: List, onClick: (Person) -> Unit, modifier: Modifier = Modifier, - onLongClick: ((Person) -> Unit)? = null, + onLongClick: ((Int, Person) -> Unit)? = null, ) { val firstFocus = remember { FocusRequester() } Column( @@ -54,7 +54,7 @@ fun PersonRow( PersonCard( item = item, onClick = { onClick.invoke(item) }, - onLongClick = { onLongClick?.invoke(item) }, + onLongClick = { onLongClick?.invoke(index, item) }, modifier = Modifier .width(120.dp) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index fa65dc3e..1622904d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -37,12 +38,20 @@ import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.SlimItemFields +import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.detail.CardGrid import com.github.damontecres.wholphin.ui.detail.ItemViewModel +import com.github.damontecres.wholphin.ui.detail.MoreDialogActions +import com.github.damontecres.wholphin.ui.detail.PlaylistDialog +import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState +import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome +import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ApiRequestPager +import com.github.damontecres.wholphin.util.ExceptionHandler +import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState @@ -57,6 +66,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ItemSortBy +import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.serializer.toUUIDOrNull @@ -71,6 +81,8 @@ class CollectionFolderViewModel @param:ApplicationContext private val context: Context, private val serverRepository: ServerRepository, private val libraryDisplayInfoDao: LibraryDisplayInfoDao, + private val favoriteWatchManager: FavoriteWatchManager, + val navigationManager: NavigationManager, ) : ItemViewModel(api) { val loading = MutableLiveData(LoadingState.Loading) val pager = MutableLiveData>(listOf()) @@ -228,6 +240,24 @@ class CollectionFolderViewModel result.totalRecordCount } } + + fun setWatched( + position: Int, + itemId: UUID, + played: Boolean, + ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { + favoriteWatchManager.setWatched(itemId, played) + (pager.value as? ApiRequestPager<*>)?.refreshItem(position, itemId) + } + + fun setFavorite( + position: Int, + itemId: UUID, + favorite: Boolean, + ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { + favoriteWatchManager.setFavorite(itemId, favorite) + (pager.value as? ApiRequestPager<*>)?.refreshItem(position, itemId) + } } /** @@ -241,7 +271,7 @@ fun CollectionFolderGrid( itemId: UUID, initialFilter: GetItemsFilter, recursive: Boolean, - onClickItem: (BaseItem) -> Unit, + onClickItem: (Int, BaseItem) -> Unit, sortOptions: List, modifier: Modifier = Modifier, initialSortAndDirection: SortAndDirection? = null, @@ -266,14 +296,16 @@ fun CollectionFolderGrid( itemId: String, initialFilter: GetItemsFilter, recursive: Boolean, - onClickItem: (BaseItem) -> Unit, + onClickItem: (Int, BaseItem) -> Unit, sortOptions: List, modifier: Modifier = Modifier, viewModel: CollectionFolderViewModel = hiltViewModel(), + playlistViewModel: AddPlaylistViewModel = hiltViewModel(), initialSortAndDirection: SortAndDirection? = null, showTitle: Boolean = true, positionCallback: ((columns: Int, position: Int) -> Unit)? = null, ) { + val context = LocalContext.current OneTimeLaunchedEffect { viewModel.init(itemId, initialSortAndDirection, recursive, initialFilter) } @@ -283,6 +315,10 @@ fun CollectionFolderGrid( val item by viewModel.item.observeAsState() val pager by viewModel.pager.observeAsState() + var moreDialog by remember { mutableStateOf>(Optional.absent()) } + var showPlaylistDialog by remember { mutableStateOf>(Optional.absent()) } + val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) + when (val state = loading) { is LoadingState.Error -> ErrorMessage(state) LoadingState.Loading, @@ -297,6 +333,9 @@ fun CollectionFolderGrid( sortAndDirection = sortAndDirection!!, modifier = modifier, onClickItem = onClickItem, + onLongClickItem = { position, item -> + moreDialog.makePresent(PositionItem(position, item)) + }, onSortChange = { viewModel.onSortChange(it, recursive, filter) }, @@ -308,6 +347,55 @@ fun CollectionFolderGrid( } } } + moreDialog.compose { (position, item) -> + DialogPopup( + showDialog = true, + title = item.title ?: "", + dialogItems = + buildMoreDialogItemsForHome( + context = context, + item = item, + seriesId = null, + playbackPosition = item.playbackPosition, + watched = item.played, + favorite = item.favorite, + actions = + MoreDialogActions( + navigateTo = { viewModel.navigationManager.navigateTo(it) }, + onClickWatch = { itemId, watched -> + viewModel.setWatched(position, itemId, watched) + }, + onClickFavorite = { itemId, watched -> + viewModel.setFavorite(position, itemId, watched) + }, + onClickAddPlaylist = { + playlistViewModel.loadPlaylists(MediaType.VIDEO) + showPlaylistDialog.makePresent(it) + }, + ), + ), + onDismissRequest = { moreDialog.makeAbsent() }, + dismissOnClick = true, + waitToLoad = true, + ) + } + showPlaylistDialog.compose { itemId -> + PlaylistDialog( + title = stringResource(R.string.add_to_playlist), + state = playlistState, + onDismissRequest = { showPlaylistDialog.makeAbsent() }, + onClick = { + playlistViewModel.addToPlaylist(it.id, itemId) + showPlaylistDialog.makeAbsent() + }, + createEnabled = true, + onCreatePlaylist = { + playlistViewModel.createPlaylistAndAddItem(it, itemId) + showPlaylistDialog.makeAbsent() + }, + elevation = 3.dp, + ) + } } @Composable @@ -316,7 +404,8 @@ fun CollectionFolderGridContent( item: BaseItem?, pager: List, sortAndDirection: SortAndDirection, - onClickItem: (BaseItem) -> Unit, + onClickItem: (Int, BaseItem) -> Unit, + onLongClickItem: (Int, BaseItem) -> Unit, onSortChange: (SortAndDirection) -> Unit, letterPosition: suspend (Char) -> Int, sortOptions: List, @@ -358,7 +447,7 @@ fun CollectionFolderGridContent( CardGrid( pager = pager, onClickItem = onClickItem, - onLongClickItem = {}, + onLongClickItem = onLongClickItem, letterPosition = letterPosition, gridFocusRequester = gridFocusRequester, showJumpButtons = false, // TODO add preference @@ -372,3 +461,8 @@ fun CollectionFolderGridContent( ) } } + +data class PositionItem( + val position: Int, + val item: BaseItem, +) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt index e2ce1b79..2949428d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt @@ -103,7 +103,7 @@ fun GenreCardGrid( LaunchedEffect(Unit) { gridFocusRequester.tryRequestFocus() } CardGrid( pager = genres, - onClickItem = { genre -> + onClickItem = { _, genre -> viewModel.navigationManager.navigateTo( Destination.FilteredCollection( itemId = itemId, @@ -112,7 +112,7 @@ fun GenreCardGrid( ), ) }, - onLongClickItem = {}, + onLongClickItem = { _, _ -> }, letterPosition = { viewModel.positionOfLetter(it) }, gridFocusRequester = gridFocusRequester, showJumpButtons = false, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/LoadingRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/LoadingRow.kt index 593f2db3..b915fdc1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/LoadingRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/LoadingRow.kt @@ -29,7 +29,7 @@ fun LoadingRow( rowIndex: Int, position: RowColumn, focusRequester: FocusRequester, - onClickItem: (BaseItem) -> Unit, + onClickItem: (Int, BaseItem) -> Unit, onClickPosition: (RowColumn) -> Unit, modifier: Modifier = Modifier, showIfEmpty: Boolean = true, @@ -82,7 +82,7 @@ fun LoadingRow( title = title, items = r.items, onClickItem = onClickItem, - onLongClickItem = {}, + onLongClickItem = { _, _ -> }, modifier = modifier, cardContent = cardContent, horizontalPadding = horizontalPadding, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Optional.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Optional.kt new file mode 100644 index 00000000..5d670918 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Optional.kt @@ -0,0 +1,31 @@ +package com.github.damontecres.wholphin.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue + +class Optional private constructor( + initialValue: T?, +) { + private var value by mutableStateOf(initialValue) + + @Composable + fun compose(run: @Composable (T) -> Unit) { + value?.let { run.invoke(it) } + } + + fun makeAbsent() { + value = null + } + + fun makePresent(value: T) { + this.value = value + } + + companion object { + fun absent() = Optional(null) + + fun present(value: T) = Optional(value) + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt new file mode 100644 index 00000000..55e37c73 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt @@ -0,0 +1,178 @@ +package com.github.damontecres.wholphin.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect +import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel +import com.github.damontecres.wholphin.ui.data.RowColumn +import com.github.damontecres.wholphin.ui.detail.MoreDialogActions +import com.github.damontecres.wholphin.ui.detail.PlaylistDialog +import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState +import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome +import com.github.damontecres.wholphin.ui.launchIO +import com.github.damontecres.wholphin.ui.main.HomePageContent +import com.github.damontecres.wholphin.ui.nav.NavigationManager +import com.github.damontecres.wholphin.util.ApiRequestPager +import com.github.damontecres.wholphin.util.FavoriteWatchManager +import com.github.damontecres.wholphin.util.HomeRowLoadingState +import com.github.damontecres.wholphin.util.LoadingState +import kotlinx.coroutines.flow.MutableStateFlow +import org.jellyfin.sdk.model.api.MediaType +import java.util.UUID + +abstract class RecommendedViewModel( + val navigationManager: NavigationManager, + val favoriteWatchManager: FavoriteWatchManager, +) : ViewModel() { + abstract fun init() + + abstract val rows: MutableStateFlow> + + val loading = MutableLiveData(LoadingState.Loading) + + fun refreshItem( + position: RowColumn, + itemId: UUID, + ) { + viewModelScope.launchIO { + val row = rows.value.getOrNull(position.row) + if (row is HomeRowLoadingState.Success) { + (row.items as? ApiRequestPager<*>)?.refreshItem(position.column, itemId) + } + } + } + + fun setWatched( + position: RowColumn, + itemId: UUID, + watched: Boolean, + ) { + viewModelScope.launchIO { + favoriteWatchManager.setWatched(itemId, watched) + refreshItem(position, itemId) + } + } + + fun setFavorite( + position: RowColumn, + itemId: UUID, + watched: Boolean, + ) { + viewModelScope.launchIO { + favoriteWatchManager.setFavorite(itemId, watched) + refreshItem(position, itemId) + } + } +} + +@Composable +fun RecommendedContent( + preferences: UserPreferences, + viewModel: RecommendedViewModel, + modifier: Modifier = Modifier, + playlistViewModel: AddPlaylistViewModel = hiltViewModel(), + onFocusPosition: ((RowColumn) -> Unit)? = null, +) { + val context = LocalContext.current + var moreDialog by remember { mutableStateOf>(Optional.absent()) } + var showPlaylistDialog by remember { mutableStateOf>(Optional.absent()) } + val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) + + OneTimeLaunchedEffect { + viewModel.init() + } + val loading by viewModel.loading.observeAsState(LoadingState.Loading) + val rows by viewModel.rows.collectAsState() + + when (val state = loading) { + is LoadingState.Error -> ErrorMessage(state) + + LoadingState.Loading, + LoadingState.Pending, + -> LoadingPage() + + LoadingState.Success -> + HomePageContent( + homeRows = rows, + onClickItem = { _, item -> + viewModel.navigationManager.navigateTo(item.destination()) + }, + onLongClickItem = { position, item -> + moreDialog.makePresent(RowColumnItem(position, item)) + }, + onFocusPosition = onFocusPosition, + showClock = preferences.appPreferences.interfacePreferences.showClock, + modifier = modifier, + ) + } + moreDialog.compose { (position, item) -> + DialogPopup( + showDialog = true, + title = item.title ?: "", + dialogItems = + buildMoreDialogItemsForHome( + context = context, + item = item, + seriesId = null, + playbackPosition = item.playbackPosition, + watched = item.played, + favorite = item.favorite, + actions = + MoreDialogActions( + navigateTo = { viewModel.navigationManager.navigateTo(it) }, + onClickWatch = { itemId, watched -> + viewModel.setWatched(position, itemId, watched) + }, + onClickFavorite = { itemId, watched -> + viewModel.setFavorite(position, itemId, watched) + }, + onClickAddPlaylist = { + playlistViewModel.loadPlaylists(MediaType.VIDEO) + showPlaylistDialog.makePresent(it) + }, + ), + ), + onDismissRequest = { moreDialog.makeAbsent() }, + dismissOnClick = true, + waitToLoad = true, + ) + } + showPlaylistDialog.compose { itemId -> + PlaylistDialog( + title = stringResource(R.string.add_to_playlist), + state = playlistState, + onDismissRequest = { showPlaylistDialog.makeAbsent() }, + onClick = { + playlistViewModel.addToPlaylist(it.id, itemId) + showPlaylistDialog.makeAbsent() + }, + createEnabled = true, + onCreatePlaylist = { + playlistViewModel.createPlaylistAndAddItem(it, itemId) + showPlaylistDialog.makeAbsent() + }, + elevation = 3.dp, + ) + } +} + +private data class RowColumnItem( + val position: RowColumn, + val item: BaseItem, +) 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 2f9e971e..132df264 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 @@ -2,25 +2,19 @@ package com.github.damontecres.wholphin.ui.components import android.content.Context import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.launchIO -import com.github.damontecres.wholphin.ui.main.HomePageContent +import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler +import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler import com.github.damontecres.wholphin.util.GetSuggestionsRequestHandler @@ -54,15 +48,15 @@ class RecommendedMovieViewModel private val api: ApiClient, private val serverRepository: ServerRepository, @Assisted val parentId: UUID, - ) : ViewModel() { + navigationManager: NavigationManager, + favoriteWatchManager: FavoriteWatchManager, + ) : RecommendedViewModel(navigationManager, favoriteWatchManager) { @AssistedFactory interface Factory { fun create(parentId: UUID): RecommendedMovieViewModel } - val loading = MutableLiveData(LoadingState.Loading) - - val rows = + override val rows = MutableStateFlow>( rowTitles .map { @@ -72,7 +66,7 @@ class RecommendedMovieViewModel }.toMutableList(), ) - fun init() { + override fun init() { viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) { try { val resumeItemsRequest = @@ -211,7 +205,6 @@ class RecommendedMovieViewModel fun RecommendedMovie( preferences: UserPreferences, parentId: UUID, - onClickItem: (BaseItem) -> Unit, onFocusPosition: (RowColumn) -> Unit, modifier: Modifier = Modifier, viewModel: RecommendedMovieViewModel = @@ -219,27 +212,10 @@ fun RecommendedMovie( creationCallback = { it.create(parentId) }, ), ) { - OneTimeLaunchedEffect { - viewModel.init() - } - val loading by viewModel.loading.observeAsState(LoadingState.Loading) - val rows by viewModel.rows.collectAsState() - - when (val state = loading) { - is LoadingState.Error -> ErrorMessage(state) - - LoadingState.Loading, - LoadingState.Pending, - -> LoadingPage() - - LoadingState.Success -> - HomePageContent( - homeRows = rows, - onClickItem = onClickItem, - onLongClickItem = {}, - onFocusPosition = onFocusPosition, - showClock = preferences.appPreferences.interfacePreferences.showClock, - modifier = modifier, - ) - } + RecommendedContent( + preferences = preferences, + viewModel = viewModel, + 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 d19baf0e..18cd7fbe 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 @@ -2,25 +2,19 @@ package com.github.damontecres.wholphin.ui.components import android.content.Context import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.launchIO -import com.github.damontecres.wholphin.ui.main.HomePageContent +import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler +import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetNextUpRequestHandler import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler @@ -56,15 +50,15 @@ class RecommendedTvShowViewModel private val api: ApiClient, private val serverRepository: ServerRepository, @Assisted val parentId: UUID, - ) : ViewModel() { + navigationManager: NavigationManager, + favoriteWatchManager: FavoriteWatchManager, + ) : RecommendedViewModel(navigationManager, favoriteWatchManager) { @AssistedFactory interface Factory { fun create(parentId: UUID): RecommendedTvShowViewModel } - val loading = MutableLiveData(LoadingState.Loading) - - val rows = + override val rows = MutableStateFlow>( rowTitles .map { @@ -74,7 +68,7 @@ class RecommendedTvShowViewModel }.toMutableList(), ) - fun init() { + override fun init() { viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) { try { val resumeItemsRequest = @@ -240,7 +234,6 @@ class RecommendedTvShowViewModel fun RecommendedTvShow( preferences: UserPreferences, parentId: UUID, - onClickItem: (BaseItem) -> Unit, onFocusPosition: (RowColumn) -> Unit, modifier: Modifier = Modifier, viewModel: RecommendedTvShowViewModel = @@ -248,28 +241,10 @@ fun RecommendedTvShow( creationCallback = { it.create(parentId) }, ), ) { - OneTimeLaunchedEffect { - viewModel.init() - } - - val loading by viewModel.loading.observeAsState(LoadingState.Loading) - val rows by viewModel.rows.collectAsState() - - when (val state = loading) { - is LoadingState.Error -> ErrorMessage(state) - - LoadingState.Loading, - LoadingState.Pending, - -> LoadingPage() - - LoadingState.Success -> - HomePageContent( - homeRows = rows, - onClickItem = onClickItem, - onLongClickItem = {}, - onFocusPosition = onFocusPosition, - showClock = preferences.appPreferences.interfacePreferences.showClock, - modifier = modifier, - ) - } + RecommendedContent( + preferences = preferences, + viewModel = viewModel, + onFocusPosition = onFocusPosition, + modifier = modifier, + ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt index 7fa770ba..4eb40eec 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt @@ -71,8 +71,8 @@ private const val DEBUG = false @Composable fun CardGrid( pager: List, - onClickItem: (BaseItem) -> Unit, - onLongClickItem: (BaseItem) -> Unit, + onClickItem: (Int, BaseItem) -> Unit, + onLongClickItem: (Int, BaseItem) -> Unit, letterPosition: suspend (Char) -> Int, gridFocusRequester: FocusRequester, showJumpButtons: Boolean, @@ -270,10 +270,10 @@ fun CardGrid( { if (item != null) { focusedIndex = index - onClickItem.invoke(item) + onClickItem.invoke(index, item) } }, - { if (item != null) onLongClickItem.invoke(item) }, + { if (item != null) onLongClickItem.invoke(index, item) }, mod .ifElse(index == 0, Modifier.focusRequester(zeroFocus)) .onFocusChanged { focusState -> diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt index 06d49e1d..5898f9d5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt @@ -30,7 +30,7 @@ fun CollectionFolderGeneric( var showHeader by remember { mutableStateOf(true) } CollectionFolderGrid( preferences = preferences, - onClickItem = { preferencesViewModel.navigationManager.navigateTo(it.destination()) }, + onClickItem = { _, item -> preferencesViewModel.navigationManager.navigateTo(item.destination()) }, itemId = itemId, initialFilter = filter, showTitle = showHeader, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt index fdf369f2..cf74a205 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt @@ -106,7 +106,7 @@ fun CollectionFolderLiveTv( logTab("livetv", selectedTabIndex) viewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex) } - val onClickItem = { item: BaseItem -> + val onClickItem = { position: Int, item: BaseItem -> viewModel.navigationManager.navigateTo(item.destination()) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt index 5c156e28..f6cd23a9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid @@ -69,10 +68,6 @@ fun CollectionFolderMovie( var showHeader by rememberSaveable { mutableStateOf(true) } - val onClickItem = { item: BaseItem -> - preferencesViewModel.navigationManager.navigateTo(item.destination()) - } - LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } Column( modifier = modifier, @@ -96,7 +91,6 @@ fun CollectionFolderMovie( 0 -> { RecommendedMovie( preferences = preferences, - onClickItem = onClickItem, parentId = destination.itemId, onFocusPosition = { pos -> showHeader = pos.row < 1 @@ -111,7 +105,9 @@ fun CollectionFolderMovie( 1 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = onClickItem, + onClickItem = { _, item -> + preferencesViewModel.navigationManager.navigateTo(item.destination()) + }, itemId = destination.itemId, initialFilter = GetItemsFilter( @@ -133,7 +129,9 @@ fun CollectionFolderMovie( 2 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = onClickItem, + onClickItem = { _, item -> + preferencesViewModel.navigationManager.navigateTo(item.destination()) + }, itemId = destination.itemId, initialFilter = GetItemsFilter( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt index 3170a0af..cddfc04c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt @@ -95,7 +95,6 @@ fun CollectionFolderTv( RecommendedTvShow( preferences = preferences, parentId = destination.itemId, - onClickItem = onClickItem, onFocusPosition = { pos -> showHeader = pos.row < 1 }, @@ -125,7 +124,9 @@ fun CollectionFolderTv( positionCallback = { columns, position -> showHeader = position < columns }, - onClickItem = onClickItem, + onClickItem = { _, item -> + preferencesViewModel.navigationManager.navigateTo(item.destination()) + }, ) } 2 -> { 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 7622436e..5fabe479 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 @@ -9,6 +9,7 @@ import androidx.compose.material.icons.filled.Refresh import androidx.compose.ui.graphics.Color import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.ui.components.DialogItem import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination @@ -20,6 +21,13 @@ import org.jellyfin.sdk.model.serializer.toUUIDOrNull import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds +data class MoreDialogActions( + val navigateTo: (Destination) -> Unit, + var onClickWatch: (UUID, Boolean) -> Unit, + var onClickFavorite: (UUID, Boolean) -> Unit, + var onClickAddPlaylist: (UUID) -> Unit, +) + /** * Build the [DialogItem]s when clicking "More" * @@ -43,12 +51,9 @@ fun buildMoreDialogItems( sourceId: UUID?, watched: Boolean, favorite: Boolean, - navigateTo: (Destination) -> Unit, - onClickWatch: (Boolean) -> Unit, - onClickFavorite: (Boolean) -> Unit, + actions: MoreDialogActions, onChooseVersion: () -> Unit, onChooseTracks: (MediaStreamType) -> Unit, - onClickAddPlaylist: () -> Unit, ): List = buildList { add( @@ -57,7 +62,7 @@ fun buildMoreDialogItems( Icons.Default.PlayArrow, iconColor = Color.Green.copy(alpha = .8f), ) { - navigateTo( + actions.navigateTo( Destination.Playback( item.id, item.resumeMs ?: 0L, @@ -119,7 +124,7 @@ fun buildMoreDialogItems( text = R.string.add_to_playlist, iconStringRes = R.string.fa_list_ul, ) { - onClickAddPlaylist.invoke() + actions.onClickAddPlaylist.invoke(item.id) }, ) add( @@ -127,7 +132,7 @@ fun buildMoreDialogItems( 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(!watched) + actions.onClickWatch.invoke(item.id, !watched) }, ) add( @@ -136,7 +141,7 @@ fun buildMoreDialogItems( iconStringRes = R.string.fa_heart, iconColor = if (favorite) Color.Red else Color.Unspecified, ) { - onClickFavorite.invoke(!favorite) + actions.onClickFavorite.invoke(item.id, !favorite) }, ) series?.let { @@ -145,7 +150,7 @@ fun buildMoreDialogItems( context.getString(R.string.go_to_series), Icons.AutoMirrored.Filled.ArrowForward, ) { - navigateTo( + actions.navigateTo( Destination.MediaItem( series.id, BaseItemKind.SERIES, @@ -160,7 +165,7 @@ fun buildMoreDialogItems( context.getString(R.string.play_with_transcoding), Icons.Default.PlayArrow, ) { - navigateTo( + actions.navigateTo( Destination.Playback( item.id, item.resumeMs ?: 0L, @@ -179,10 +184,7 @@ fun buildMoreDialogItemsForHome( playbackPosition: Duration, watched: Boolean, favorite: Boolean, - navigateTo: (Destination) -> Unit, - onClickWatch: (UUID, Boolean) -> Unit, - onClickFavorite: (UUID, Boolean) -> Unit, - onClickAddPlaylist: (UUID) -> Unit, + actions: MoreDialogActions, ): List = buildList { val itemId = item.id @@ -191,7 +193,7 @@ fun buildMoreDialogItemsForHome( context.getString(R.string.go_to), Icons.Default.ArrowForward, ) { - navigateTo(item.destination()) + actions.navigateTo(item.destination()) }, ) if (item.type in supportedPlayableTypes) { @@ -202,7 +204,7 @@ fun buildMoreDialogItemsForHome( Icons.Default.PlayArrow, iconColor = Color.Green.copy(alpha = .8f), ) { - navigateTo( + actions.navigateTo( Destination.Playback( itemId, playbackPosition.inWholeMilliseconds, @@ -216,7 +218,7 @@ fun buildMoreDialogItemsForHome( Icons.Default.Refresh, // iconColor = Color.Green.copy(alpha = .8f), ) { - navigateTo( + actions.navigateTo( Destination.Playback( itemId, 0L, @@ -231,7 +233,7 @@ fun buildMoreDialogItemsForHome( Icons.Default.PlayArrow, iconColor = Color.Green.copy(alpha = .8f), ) { - navigateTo( + actions.navigateTo( Destination.Playback( itemId, 0L, @@ -246,7 +248,7 @@ fun buildMoreDialogItemsForHome( text = R.string.add_to_playlist, iconStringRes = R.string.fa_list_ul, ) { - onClickAddPlaylist.invoke(itemId) + actions.onClickAddPlaylist.invoke(itemId) }, ) add( @@ -254,7 +256,7 @@ fun buildMoreDialogItemsForHome( 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) + actions.onClickWatch.invoke(itemId, !watched) }, ) add( @@ -263,7 +265,7 @@ fun buildMoreDialogItemsForHome( iconStringRes = R.string.fa_heart, iconColor = if (favorite) Color.Red else Color.Unspecified, ) { - onClickFavorite.invoke(itemId, !favorite) + actions.onClickFavorite.invoke(itemId, !favorite) }, ) seriesId?.let { @@ -272,7 +274,7 @@ fun buildMoreDialogItemsForHome( context.getString(R.string.go_to_series), Icons.AutoMirrored.Filled.ArrowForward, ) { - navigateTo( + actions.navigateTo( Destination.MediaItem( it, BaseItemKind.SERIES, @@ -282,3 +284,31 @@ fun buildMoreDialogItemsForHome( ) } } + +fun buildMoreDialogItemsForPerson( + context: Context, + person: Person, +// favorite: Boolean, + actions: MoreDialogActions, +): List = + buildList { + val itemId = person.id + add( + DialogItem( + context.getString(R.string.go_to), + Icons.Default.ArrowForward, + ) { + actions.navigateTo(Destination.MediaItem(itemId, BaseItemKind.PERSON)) + }, + ) + // TODO need way to get person's favorite status +// 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(itemId, !favorite) +// }, +// ) + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt index bae04bcb..0afa076d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt @@ -99,7 +99,7 @@ fun FavoritesPage( 0 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = onClickItem, + onClickItem = { _, item -> onClickItem.invoke(item) }, itemId = "${NavDrawerItem.Favorites.id}_movies", initialFilter = GetItemsFilter( @@ -122,7 +122,7 @@ fun FavoritesPage( 1 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = onClickItem, + onClickItem = { _, item -> onClickItem.invoke(item) }, itemId = "${NavDrawerItem.Favorites.id}_series", initialFilter = GetItemsFilter( @@ -145,7 +145,7 @@ fun FavoritesPage( 2 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = onClickItem, + onClickItem = { _, item -> onClickItem.invoke(item) }, itemId = "${NavDrawerItem.Favorites.id}_episodes", initialFilter = GetItemsFilter( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt index 3e8a8d12..db2ff356 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt @@ -26,9 +26,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -186,8 +183,8 @@ fun PersonPage( movies = movies, series = series, episodes = episodes, - onClickItem = { - viewModel.navigationManager.navigateTo(it.destination()) + onClickItem = { index, item -> + viewModel.navigationManager.navigateTo(item.destination()) }, overviewOnClick = { showOverviewDialog = true }, modifier = modifier, @@ -226,7 +223,7 @@ fun PersonPageContent( movies: RowLoadingState, series: RowLoadingState, episodes: RowLoadingState, - onClickItem: (BaseItem) -> Unit, + onClickItem: (Int, BaseItem) -> Unit, overviewOnClick: () -> Unit, modifier: Modifier = Modifier, ) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index fe784adb..f4cd91b4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -64,14 +64,18 @@ import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons import com.github.damontecres.wholphin.ui.components.LoadingPage +import com.github.damontecres.wholphin.ui.components.Optional import com.github.damontecres.wholphin.ui.components.chooseStream import com.github.damontecres.wholphin.ui.components.chooseVersionParams import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo +import com.github.damontecres.wholphin.ui.detail.MoreDialogActions import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems +import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome +import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.rememberInt @@ -83,6 +87,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.serializer.toUUID +import java.util.UUID import kotlin.time.Duration @Composable @@ -112,9 +117,24 @@ fun MovieDetails( var overviewDialog by remember { mutableStateOf(null) } var moreDialog by remember { mutableStateOf(null) } var chooseVersion by remember { mutableStateOf(null) } - var showPlaylistDialog by remember { mutableStateOf(false) } + var showPlaylistDialog by remember { mutableStateOf>(Optional.absent()) } val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) + val moreActions = + MoreDialogActions( + navigateTo = viewModel::navigateTo, + onClickWatch = { itemId, watched -> + viewModel.setWatched(itemId, watched) + }, + onClickFavorite = { itemId, favorite -> + viewModel.setFavorite(itemId, favorite) + }, + onClickAddPlaylist = { itemId -> + playlistViewModel.loadPlaylists(MediaType.VIDEO) + showPlaylistDialog.makePresent(itemId) + }, + ) + when (val state = loading) { is LoadingState.Error -> ErrorMessage(state) LoadingState.Loading, @@ -139,8 +159,8 @@ fun MovieDetails( chapters = chapters, trailers = trailers, similar = similar, - onClickItem = { - viewModel.navigateTo(it.destination()) + onClickItem = { index, item -> + viewModel.navigateTo(item.destination()) }, onClickPerson = { viewModel.navigateTo( @@ -180,9 +200,7 @@ fun MovieDetails( favorite = movie.data.userData?.isFavorite ?: false, series = null, sourceId = chosenStreams?.sourceId, - navigateTo = viewModel::navigateTo, - onClickWatch = viewModel::setWatched, - onClickFavorite = viewModel::setFavorite, + actions = moreActions, onChooseVersion = { chooseVersion = chooseVersionParams( @@ -218,18 +236,46 @@ fun MovieDetails( ) } }, - onClickAddPlaylist = { - playlistViewModel.loadPlaylists(MediaType.VIDEO) - showPlaylistDialog = true - }, ), ) }, watchOnClick = { - viewModel.setWatched((movie.data.userData?.played ?: false).not()) + viewModel.setWatched(movie.id, !movie.played) }, favoriteOnClick = { - viewModel.setFavorite((movie.data.userData?.isFavorite ?: false).not()) + viewModel.setFavorite(movie.id, !movie.favorite) + }, + onLongClickPerson = { index, person -> + val items = + buildMoreDialogItemsForPerson( + context = context, + person = person, + actions = moreActions, + ) + moreDialog = + DialogParams( + fromLongClick = true, + title = person.name ?: "", + items = items, + ) + }, + onLongClickSimilar = { index, similar -> + val items = + buildMoreDialogItemsForHome( + context = context, + item = similar, + seriesId = null, + playbackPosition = similar.playbackPosition, + watched = similar.played, + favorite = similar.favorite, + actions = moreActions, + ) + moreDialog = + DialogParams( + fromLongClick = true, + title = similar.title ?: "", + items = items, + ) }, trailerOnClick = { trailer -> when (trailer) { @@ -283,19 +329,19 @@ fun MovieDetails( waitToLoad = params.fromLongClick, ) } - if (showPlaylistDialog) { + showPlaylistDialog.compose { itemId -> PlaylistDialog( title = stringResource(R.string.add_to_playlist), state = playlistState, - onDismissRequest = { showPlaylistDialog = false }, + onDismissRequest = { showPlaylistDialog.makeAbsent() }, onClick = { - playlistViewModel.addToPlaylist(it.id, destination.itemId) - showPlaylistDialog = false + playlistViewModel.addToPlaylist(it.id, itemId) + showPlaylistDialog.makeAbsent() }, createEnabled = true, onCreatePlaylist = { - playlistViewModel.createPlaylistAndAddItem(it, destination.itemId) - showPlaylistDialog = false + playlistViewModel.createPlaylistAndAddItem(it, itemId) + showPlaylistDialog.makeAbsent() }, elevation = 3.dp, ) @@ -323,8 +369,10 @@ fun MovieDetailsContent( watchOnClick: () -> Unit, favoriteOnClick: () -> Unit, moreOnClick: () -> Unit, - onClickItem: (BaseItem) -> Unit, + onClickItem: (Int, BaseItem) -> Unit, onClickPerson: (Person) -> Unit, + onLongClickPerson: (Int, Person) -> Unit, + onLongClickSimilar: (Int, BaseItem) -> Unit, modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() @@ -422,7 +470,10 @@ fun MovieDetailsContent( position = PEOPLE_ROW onClickPerson.invoke(it) }, - onLongClick = {}, + onLongClick = { index, person -> + position = PEOPLE_ROW + onLongClickPerson.invoke(index, person) + }, modifier = Modifier .fillMaxWidth() @@ -466,11 +517,14 @@ fun MovieDetailsContent( ItemRow( title = stringResource(R.string.more_like_this), items = similar, - onClickItem = { + onClickItem = { index, item -> position = SIMILAR_ROW - onClickItem.invoke(it) + onClickItem.invoke(index, item) + }, + onLongClickItem = { index, similar -> + position = SIMILAR_ROW + onLongClickSimilar.invoke(index, similar) }, - onLongClickItem = {}, cardContent = { index, item, mod, onClick, onLongClick -> SeasonCard( item = item, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt index 540affd8..250a8f0b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt @@ -23,6 +23,7 @@ import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.util.ExceptionHandler +import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.ThemeSongPlayer @@ -39,7 +40,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.libraryApi -import org.jellyfin.sdk.api.client.extensions.playStateApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest @@ -55,6 +55,7 @@ class MovieViewModel val serverRepository: ServerRepository, val itemPlaybackRepository: ItemPlaybackRepository, private val themeSongPlayer: ThemeSongPlayer, + private val favoriteWatchManager: FavoriteWatchManager, @Assisted val itemId: UUID, ) : ViewModel() { @AssistedFactory @@ -157,25 +158,21 @@ class MovieViewModel } } - fun setWatched(played: Boolean) = - viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { - if (played) { - api.playStateApi.markPlayedItem(itemId) - } else { - api.playStateApi.markUnplayedItem(itemId) - } - fetchAndSetItem() - } + fun setWatched( + itemId: UUID, + played: Boolean, + ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { + favoriteWatchManager.setWatched(itemId, played) + fetchAndSetItem() + } - fun setFavorite(favorite: Boolean) = - viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { - if (favorite) { - api.userLibraryApi.markFavoriteItem(itemId) - } else { - api.userLibraryApi.unmarkFavoriteItem(itemId) - } - fetchAndSetItem() - } + fun setFavorite( + itemId: UUID, + favorite: Boolean, + ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { + favoriteWatchManager.setFavorite(itemId, favorite) + fetchAndSetItem() + } fun savePlayVersion( item: BaseItem, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index 6186ff80..983e5798 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -60,10 +60,17 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ExpandableFaButton import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton import com.github.damontecres.wholphin.ui.components.LoadingPage +import com.github.damontecres.wholphin.ui.components.Optional import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.components.SimpleStarRating +import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo +import com.github.damontecres.wholphin.ui.detail.MoreDialogActions +import com.github.damontecres.wholphin.ui.detail.PlaylistDialog +import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState +import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome +import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination @@ -74,7 +81,9 @@ import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.extensions.ticks +import java.util.UUID import kotlin.time.Duration @Composable @@ -83,6 +92,7 @@ fun SeriesDetails( destination: Destination.MediaItem, modifier: Modifier = Modifier, viewModel: SeriesViewModel = hiltViewModel(), + playlistViewModel: AddPlaylistViewModel = hiltViewModel(), ) { val context = LocalContext.current LaunchedEffect(Unit) { @@ -98,6 +108,8 @@ fun SeriesDetails( var overviewDialog by remember { mutableStateOf(null) } var showWatchConfirmation by remember { mutableStateOf(false) } var seasonDialog by remember { mutableStateOf(null) } + var showPlaylistDialog by remember { mutableStateOf>(Optional.absent()) } + val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) when (val state = loading) { is LoadingState.Error -> ErrorMessage(state) @@ -126,7 +138,9 @@ fun SeriesDetails( played = played, favorite = item.data.userData?.isFavorite ?: false, modifier = modifier, - onClickItem = { viewModel.navigateTo(it.destination()) }, + onClickItem = { index, item -> + viewModel.navigateTo(item.destination()) + }, onClickPerson = { viewModel.navigateTo( Destination.MediaItem( @@ -135,7 +149,7 @@ fun SeriesDetails( ), ) }, - onLongClickItem = { season -> + onLongClickItem = { index, season -> seasonDialog = buildDialogForSeason( context = context, @@ -160,6 +174,20 @@ fun SeriesDetails( val favorite = item.data.userData?.isFavorite ?: false viewModel.setFavorite(item.id, !favorite, null) }, + moreActions = + MoreDialogActions( + navigateTo = { viewModel.navigateTo(it) }, + onClickWatch = { itemId, played -> + viewModel.setWatched(itemId, played, null) + }, + onClickFavorite = { itemId, played -> + viewModel.setFavorite(itemId, played, null) + }, + onClickAddPlaylist = { itemId -> + playlistViewModel.loadPlaylists(MediaType.VIDEO) + showPlaylistDialog.makePresent(itemId) + }, + ), ) if (showWatchConfirmation) { ConfirmDialog( @@ -194,6 +222,23 @@ fun SeriesDetails( onDismissRequest = { seasonDialog = null }, ) } + showPlaylistDialog.compose { itemId -> + PlaylistDialog( + title = stringResource(R.string.add_to_playlist), + state = playlistState, + onDismissRequest = { showPlaylistDialog.makeAbsent() }, + onClick = { + playlistViewModel.addToPlaylist(it.id, itemId) + showPlaylistDialog.makeAbsent() + }, + createEnabled = true, + onCreatePlaylist = { + playlistViewModel.createPlaylistAndAddItem(it, itemId) + showPlaylistDialog.makeAbsent() + }, + elevation = 3.dp, + ) + } } private const val HEADER_ROW = 0 @@ -210,15 +255,17 @@ fun SeriesDetailsContent( people: List, played: Boolean, favorite: Boolean, - onClickItem: (BaseItem) -> Unit, + onClickItem: (Int, BaseItem) -> Unit, onClickPerson: (Person) -> Unit, - onLongClickItem: (BaseItem) -> Unit, + onLongClickItem: (Int, BaseItem) -> Unit, overviewOnClick: () -> Unit, playOnClick: () -> Unit, watchOnClick: () -> Unit, favoriteOnClick: () -> Unit, + moreActions: MoreDialogActions, modifier: Modifier = Modifier, ) { + val context = LocalContext.current val scope = rememberCoroutineScope() val bringIntoViewRequester = remember { BringIntoViewRequester() } @@ -227,6 +274,7 @@ fun SeriesDetailsContent( LaunchedEffect(Unit) { focusRequesters.getOrNull(position)?.tryRequestFocus() } + var moreDialog by remember { mutableStateOf(null) } Box( modifier = modifier, @@ -349,11 +397,14 @@ fun SeriesDetailsContent( ItemRow( title = stringResource(R.string.tv_seasons), items = seasons, - onClickItem = { + onClickItem = { index, item -> position = SEASONS_ROW - onClickItem.invoke(it) + onClickItem.invoke(index, item) + }, + onLongClickItem = { index, item -> + position = SEASONS_ROW + onLongClickItem.invoke(index, item) }, - onLongClickItem = onLongClickItem, modifier = Modifier .fillMaxWidth() @@ -379,7 +430,21 @@ fun SeriesDetailsContent( position = PEOPLE_ROW onClickPerson.invoke(it) }, - onLongClick = {}, + onLongClick = { index, person -> + position = PEOPLE_ROW + val items = + buildMoreDialogItemsForPerson( + context = context, + person = person, + actions = moreActions, + ) + moreDialog = + DialogParams( + fromLongClick = true, + title = person.name ?: "", + items = items, + ) + }, modifier = Modifier .fillMaxWidth() @@ -392,11 +457,29 @@ fun SeriesDetailsContent( ItemRow( title = stringResource(R.string.more_like_this), items = similar, - onClickItem = { + onClickItem = { index, item -> position = SIMILAR_ROW - onClickItem.invoke(it) + onClickItem.invoke(index, item) + }, + onLongClickItem = { index, item -> + position = SIMILAR_ROW + val items = + buildMoreDialogItemsForHome( + context = context, + item = item, + seriesId = null, + playbackPosition = item.playbackPosition, + watched = item.played, + favorite = item.favorite, + actions = moreActions, + ) + moreDialog = + DialogParams( + fromLongClick = true, + title = item.name ?: "", + items = items, + ) }, - onLongClickItem = {}, cardContent = { index, item, mod, onClick, onLongClick -> SeasonCard( item = item, @@ -418,6 +501,16 @@ fun SeriesDetailsContent( } } } + moreDialog?.let { params -> + DialogPopup( + showDialog = true, + title = params.title, + dialogItems = params.items, + onDismissRequest = { moreDialog = null }, + dismissOnClick = true, + waitToLoad = params.fromLongClick, + ) + } } @Composable diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt index 52d17610..5a77d3d9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt @@ -32,6 +32,7 @@ import com.github.damontecres.wholphin.ui.components.chooseVersionParams import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo +import com.github.damontecres.wholphin.ui.detail.MoreDialogActions import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems @@ -196,29 +197,28 @@ fun SeriesOverview( favorite = ep.data.userData?.isFavorite ?: false, series = series, sourceId = chosenStreams?.sourceId, - navigateTo = viewModel::navigateTo, - onClickWatch = { played -> - episodeList - ?.getOrNull(position.episodeRowIndex) - ?.let { + actions = + MoreDialogActions( + navigateTo = viewModel::navigateTo, + onClickWatch = { itemId, watched -> viewModel.setWatched( - it.id, - played, + itemId, + watched, position.episodeRowIndex, ) - } - }, - onClickFavorite = { favorite -> - episodeList - ?.getOrNull(position.episodeRowIndex) - ?.let { + }, + onClickFavorite = { itemId, favorite -> viewModel.setFavorite( - it.id, + itemId, favorite, position.episodeRowIndex, ) - } - }, + }, + onClickAddPlaylist = { + playlistViewModel.loadPlaylists(MediaType.VIDEO) + showPlaylistDialog = it + }, + ), onChooseVersion = { chooseVersion = chooseVersionParams( @@ -254,10 +254,6 @@ fun SeriesOverview( ) } }, - onClickAddPlaylist = { - playlistViewModel.loadPlaylists(MediaType.VIDEO) - showPlaylistDialog = ep.id - }, ), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index 36b8b124..b5c99411 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -23,6 +23,7 @@ import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler +import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler @@ -36,9 +37,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.libraryApi -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.model.api.BaseItemKind import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemSortBy @@ -61,6 +60,7 @@ class SeriesViewModel private val navigationManager: NavigationManager, private val itemPlaybackRepository: ItemPlaybackRepository, private val themeSongPlayer: ThemeSongPlayer, + private val favoriteWatchManager: FavoriteWatchManager, ) : ItemViewModel(api) { private lateinit var seriesId: UUID private lateinit var prefs: UserPreferences @@ -240,11 +240,7 @@ class SeriesViewModel played: Boolean, listIndex: Int?, ) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) { - if (played) { - api.playStateApi.markPlayedItem(itemId) - } else { - api.playStateApi.markUnplayedItem(itemId) - } + favoriteWatchManager.setWatched(itemId, played) listIndex?.let { refreshEpisode(itemId, listIndex) } @@ -255,11 +251,7 @@ class SeriesViewModel favorite: Boolean, listIndex: Int?, ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { - if (favorite) { - api.userLibraryApi.markFavoriteItem(itemId) - } else { - api.userLibraryApi.unmarkFavoriteItem(itemId) - } + favoriteWatchManager.setFavorite(itemId, favorite) if (listIndex != null) { refreshEpisode(itemId, listIndex) } else { @@ -279,11 +271,7 @@ class SeriesViewModel fun setWatchedSeries(played: Boolean) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { - if (played) { - api.playStateApi.markPlayedItem(seriesId) - } else { - api.playStateApi.markUnplayedItem(seriesId) - } + favoriteWatchManager.setWatched(seriesId, played) val series = fetchItem(seriesId) val seasons = getSeasons(series) this@SeriesViewModel.seasons.setValueOnMain(seasons) 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 ae67b21c..e73f0cb6 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 @@ -58,6 +58,7 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.data.RowColumnSaver +import com.github.damontecres.wholphin.ui.detail.MoreDialogActions import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome @@ -75,7 +76,6 @@ import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.extensions.ticks import java.util.UUID -import kotlin.time.Duration @Composable fun HomePage( @@ -121,37 +121,36 @@ fun HomePage( val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) HomePageContent( watchingRows + latestRows, - onClickItem = { - viewModel.navigationManager.navigateTo(it.destination()) + onClickItem = { position, item -> + viewModel.navigationManager.navigateTo(item.destination()) }, - onLongClickItem = { + onLongClickItem = { position, item -> val dialogItems = buildMoreDialogItemsForHome( context = context, - 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) - }, - onClickAddPlaylist = { itemId -> - playlistViewModel.loadPlaylists(MediaType.VIDEO) - showPlaylistDialog = itemId - }, + item = item, + seriesId = item.data.seriesId, + playbackPosition = item.playbackPosition, + watched = item.played, + favorite = item.favorite, + actions = + MoreDialogActions( + navigateTo = viewModel.navigationManager::navigateTo, + onClickWatch = { itemId, played -> + viewModel.setWatched(itemId, played) + }, + onClickFavorite = { itemId, favorite -> + viewModel.setFavorite(itemId, favorite) + }, + onClickAddPlaylist = { itemId -> + playlistViewModel.loadPlaylists(MediaType.VIDEO) + showPlaylistDialog = itemId + }, + ), ) dialog = DialogParams( - title = it.title ?: "", + title = item.title ?: "", fromLongClick = true, items = dialogItems, ) @@ -190,8 +189,8 @@ fun HomePage( @Composable fun HomePageContent( homeRows: List, - onClickItem: (BaseItem) -> Unit, - onLongClickItem: (BaseItem) -> Unit, + onClickItem: (RowColumn, BaseItem) -> Unit, + onLongClickItem: (RowColumn, BaseItem) -> Unit, showClock: Boolean, modifier: Modifier = Modifier, onFocusPosition: ((RowColumn) -> Unit)? = null, @@ -317,14 +316,18 @@ fun HomePageContent( ItemRow( title = row.title, items = row.items, - onClickItem = onClickItem, + onClickItem = { index, item -> + onClickItem.invoke(RowColumn(rowIndex, index), item) + }, cardOnFocus = { isFocused, index -> if (isFocused) { focusedItem = row.items.getOrNull(index) position = RowColumn(rowIndex, index) } }, - onLongClickItem = onLongClickItem, + onLongClickItem = { index, item -> + onLongClickItem.invoke(RowColumn(rowIndex, index), item) + }, modifier = Modifier .fillMaxWidth() @@ -339,6 +342,7 @@ fun HomePageContent( item?.data?.indexNumber?.let { "E$it" } ?: item?.data?.childCount?.let { if (it > 0) it.toString() else null }, played = item?.data?.userData?.played ?: false, + favorite = item?.favorite ?: false, playPercent = item?.data?.userData?.playedPercentage ?: 0.0, 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 4292909e..839e282e 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 @@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.util.ExceptionHandler +import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState @@ -27,7 +28,6 @@ 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 @@ -50,6 +50,7 @@ class HomeViewModel val navigationManager: NavigationManager, val serverRepository: ServerRepository, val navDrawerItemRepository: NavDrawerItemRepository, + private val favoriteWatchManager: FavoriteWatchManager, ) : ViewModel() { val loadingState = MutableLiveData(LoadingState.Pending) val refreshState = MutableLiveData(LoadingState.Pending) @@ -258,11 +259,7 @@ class HomeViewModel itemId: UUID, played: Boolean, ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { - if (played) { - api.playStateApi.markPlayedItem(itemId) - } else { - api.playStateApi.markUnplayedItem(itemId) - } + favoriteWatchManager.setWatched(itemId, played) withContext(Dispatchers.Main) { init(preferences) } @@ -272,11 +269,7 @@ class HomeViewModel itemId: UUID, favorite: Boolean, ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { - if (favorite) { - api.userLibraryApi.markFavoriteItem(itemId) - } else { - api.userLibraryApi.unmarkFavoriteItem(itemId) - } + favoriteWatchManager.setFavorite(itemId, favorite) withContext(Dispatchers.Main) { init(preferences) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt index 6d464fa7..244edbcc 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt @@ -174,6 +174,9 @@ fun SearchPage( LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + val onClickItem = { index: Int, item: BaseItem -> + viewModel.navigationManager.navigateTo(item.destination()) + } LazyColumn( contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 44.dp), @@ -207,7 +210,7 @@ fun SearchPage( rowIndex = MOVIE_ROW, position = position, focusRequester = focusRequester, - onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) }, + onClickItem = onClickItem, onClickPosition = { position = it }, modifier = Modifier.fillMaxWidth(), ) @@ -217,7 +220,7 @@ fun SearchPage( rowIndex = COLLECTION_ROW, position = position, focusRequester = focusRequester, - onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) }, + onClickItem = onClickItem, onClickPosition = { position = it }, modifier = Modifier.fillMaxWidth(), ) @@ -227,7 +230,7 @@ fun SearchPage( rowIndex = SERIES_ROW, position = position, focusRequester = focusRequester, - onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) }, + onClickItem = onClickItem, onClickPosition = { position = it }, modifier = Modifier.fillMaxWidth(), ) @@ -237,7 +240,7 @@ fun SearchPage( rowIndex = EPISODE_ROW, position = position, focusRequester = focusRequester, - onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) }, + onClickItem = onClickItem, onClickPosition = { position = it }, modifier = Modifier.fillMaxWidth(), cardContent = @Composable { index, item, mod, onClick, onLongClick -> @@ -268,7 +271,7 @@ fun LazyListScope.searchResultRow( rowIndex: Int, position: RowColumn, focusRequester: FocusRequester, - onClickItem: (BaseItem) -> Unit, + onClickItem: (Int, BaseItem) -> Unit, onClickPosition: (RowColumn) -> Unit, modifier: Modifier = Modifier, cardContent: @Composable ( @@ -328,7 +331,7 @@ fun LazyListScope.searchResultRow( title = title, items = r.items, onClickItem = onClickItem, - onLongClickItem = {}, + onLongClickItem = { _, _ -> }, modifier = modifier, cardContent = cardContent, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/FavoriteWatchManager.kt b/app/src/main/java/com/github/damontecres/wholphin/util/FavoriteWatchManager.kt new file mode 100644 index 00000000..72540435 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/FavoriteWatchManager.kt @@ -0,0 +1,43 @@ +package com.github.damontecres.wholphin.util + +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.playStateApi +import org.jellyfin.sdk.api.client.extensions.userLibraryApi +import org.jellyfin.sdk.model.api.UserItemDataDto +import java.util.UUID + +interface FavoriteWatchManager { + suspend fun setWatched( + itemId: UUID, + played: Boolean, + ): UserItemDataDto + + suspend fun setFavorite( + itemId: UUID, + favorite: Boolean, + ): UserItemDataDto +} + +class FavoriteWatchManagerImpl( + private val api: ApiClient, +) : FavoriteWatchManager { + override suspend fun setWatched( + itemId: UUID, + played: Boolean, + ): UserItemDataDto = + if (played) { + api.playStateApi.markPlayedItem(itemId).content + } else { + api.playStateApi.markUnplayedItem(itemId).content + } + + override suspend fun setFavorite( + itemId: UUID, + favorite: Boolean, + ): UserItemDataDto = + if (favorite) { + api.userLibraryApi.markFavoriteItem(itemId).content + } else { + api.userLibraryApi.unmarkFavoriteItem(itemId).content + } +}