diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedMovie.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedMovie.kt new file mode 100644 index 00000000..9e2c0369 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedMovie.kt @@ -0,0 +1,152 @@ +package com.github.damontecres.dolphin.ui.components + +import androidx.compose.runtime.Composable +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.dolphin.preferences.UserPreferences +import com.github.damontecres.dolphin.ui.DefaultItemFields +import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect +import com.github.damontecres.dolphin.ui.main.HomePageContent +import com.github.damontecres.dolphin.ui.main.HomeRow +import com.github.damontecres.dolphin.ui.main.HomeSection +import com.github.damontecres.dolphin.ui.nav.NavigationManager +import com.github.damontecres.dolphin.util.ApiRequestPager +import com.github.damontecres.dolphin.util.ExceptionHandler +import com.github.damontecres.dolphin.util.GetItemsRequestHandler +import com.github.damontecres.dolphin.util.GetResumeItemsRequestHandler +import com.github.damontecres.dolphin.util.GetSuggestionsRequestHandler +import com.github.damontecres.dolphin.util.LoadingState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ItemSortBy +import org.jellyfin.sdk.model.api.SortOrder +import org.jellyfin.sdk.model.api.request.GetItemsRequest +import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest +import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest +import java.util.UUID +import javax.inject.Inject + +@HiltViewModel +class RecommendedMovieViewModel + @Inject + constructor( + private val api: ApiClient, + ) : ViewModel() { + val loading = MutableLiveData(LoadingState.Loading) + val rows = MutableLiveData>() + + fun init( + preferences: UserPreferences, + parentId: UUID, + ) { + viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) { + val resumeItemsRequest = + GetResumeItemsRequest( + parentId = parentId, + fields = DefaultItemFields, + includeItemTypes = listOf(BaseItemKind.MOVIE), + enableUserData = true, + ) + val resumeItems = + ApiRequestPager(api, resumeItemsRequest, GetResumeItemsRequestHandler, viewModelScope) + + val recentlyReleasedRequest = + GetItemsRequest( + parentId = parentId, + fields = DefaultItemFields, + includeItemTypes = listOf(BaseItemKind.MOVIE), + recursive = true, + enableUserData = true, + sortBy = listOf(ItemSortBy.PREMIERE_DATE), + sortOrder = listOf(SortOrder.DESCENDING), + ) + val recentlyReleasedItems = + ApiRequestPager(api, recentlyReleasedRequest, GetItemsRequestHandler, viewModelScope) + + val recentlyAddedRequest = + GetItemsRequest( + parentId = parentId, + fields = DefaultItemFields, + includeItemTypes = listOf(BaseItemKind.MOVIE), + recursive = true, + enableUserData = true, + sortBy = listOf(ItemSortBy.DATE_CREATED), + sortOrder = listOf(SortOrder.DESCENDING), + ) + val recentlyAddedItems = + ApiRequestPager(api, recentlyAddedRequest, GetItemsRequestHandler, viewModelScope) + + val suggestionsRequest = + GetSuggestionsRequest( + type = listOf(BaseItemKind.MOVIE), + ) + val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope) + + val unwatchedTopRatedRequest = + GetItemsRequest( + parentId = parentId, + fields = DefaultItemFields, + includeItemTypes = listOf(BaseItemKind.MOVIE), + recursive = true, + enableUserData = true, + isPlayed = false, + sortBy = listOf(ItemSortBy.COMMUNITY_RATING), + sortOrder = listOf(SortOrder.DESCENDING), + ) + val unwatchedTopRatedItems = + ApiRequestPager(api, unwatchedTopRatedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true) + + val rows = listOf(resumeItems, recentlyReleasedItems, recentlyAddedItems, suggestedItems, unwatchedTopRatedItems) + rows.forEach { it.init() } + val homeRows = + listOf( + HomeRow(HomeSection.RESUME, resumeItems, "Continue Watching"), + HomeRow(HomeSection.LATEST_MEDIA, recentlyReleasedItems, "Recently Released"), + HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"), + HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"), + HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"), + ) + withContext(Dispatchers.Main) { + this@RecommendedMovieViewModel.rows.value = homeRows + loading.value = LoadingState.Success + } + } + } + } + +@Composable +fun RecommendedMovie( + preferences: UserPreferences, + navigationManager: NavigationManager, + parentId: UUID, + modifier: Modifier, + viewModel: RecommendedMovieViewModel = hiltViewModel(), +) { + OneTimeLaunchedEffect { + viewModel.init(preferences, parentId) + } + val loading by viewModel.loading.observeAsState(LoadingState.Loading) + val rows by viewModel.rows.observeAsState(listOf()) + + when (val state = loading) { + is LoadingState.Error -> ErrorMessage(state) + + LoadingState.Loading -> LoadingPage() + + LoadingState.Success -> + HomePageContent( + navigationManager = navigationManager, + homeRows = rows, + modifier = modifier, + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedTvShow.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedTvShow.kt new file mode 100644 index 00000000..e837e6db --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedTvShow.kt @@ -0,0 +1,164 @@ +package com.github.damontecres.dolphin.ui.components + +import androidx.compose.runtime.Composable +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.dolphin.preferences.UserPreferences +import com.github.damontecres.dolphin.ui.DefaultItemFields +import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect +import com.github.damontecres.dolphin.ui.main.HomePageContent +import com.github.damontecres.dolphin.ui.main.HomeRow +import com.github.damontecres.dolphin.ui.main.HomeSection +import com.github.damontecres.dolphin.ui.nav.NavigationManager +import com.github.damontecres.dolphin.util.ApiRequestPager +import com.github.damontecres.dolphin.util.ExceptionHandler +import com.github.damontecres.dolphin.util.GetItemsRequestHandler +import com.github.damontecres.dolphin.util.GetNextUpRequestHandler +import com.github.damontecres.dolphin.util.GetResumeItemsRequestHandler +import com.github.damontecres.dolphin.util.GetSuggestionsRequestHandler +import com.github.damontecres.dolphin.util.LoadingState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ItemSortBy +import org.jellyfin.sdk.model.api.SortOrder +import org.jellyfin.sdk.model.api.request.GetItemsRequest +import org.jellyfin.sdk.model.api.request.GetNextUpRequest +import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest +import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest +import java.util.UUID +import javax.inject.Inject + +@HiltViewModel +class RecommendedTvShowViewModel + @Inject + constructor( + private val api: ApiClient, + ) : ViewModel() { + val loading = MutableLiveData(LoadingState.Loading) + val rows = MutableLiveData>() + + fun init( + preferences: UserPreferences, + parentId: UUID, + ) { + viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) { + val resumeItemsRequest = + GetResumeItemsRequest( + parentId = parentId, + fields = DefaultItemFields, + includeItemTypes = listOf(BaseItemKind.EPISODE), + enableUserData = true, + ) + val resumeItems = + ApiRequestPager(api, resumeItemsRequest, GetResumeItemsRequestHandler, viewModelScope, useSeriesForPrimary = true) + + val nextUpRequest = + GetNextUpRequest( + parentId = parentId, + fields = DefaultItemFields, + enableUserData = true, + ) + val nextUpItems = ApiRequestPager(api, nextUpRequest, GetNextUpRequestHandler, viewModelScope, useSeriesForPrimary = true) + + val recentlyReleasedRequest = + GetItemsRequest( + parentId = parentId, + fields = DefaultItemFields, + includeItemTypes = listOf(BaseItemKind.EPISODE), + recursive = true, + enableUserData = true, + sortBy = listOf(ItemSortBy.PREMIERE_DATE), + sortOrder = listOf(SortOrder.DESCENDING), + ) + val recentlyReleasedItems = + ApiRequestPager(api, recentlyReleasedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true) + + val recentlyAddedRequest = + GetItemsRequest( + parentId = parentId, + fields = DefaultItemFields, + includeItemTypes = listOf(BaseItemKind.EPISODE), + recursive = true, + enableUserData = true, + sortBy = listOf(ItemSortBy.DATE_CREATED), + sortOrder = listOf(SortOrder.DESCENDING), + ) + val recentlyAddedItems = + ApiRequestPager(api, recentlyAddedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true) + + val suggestionsRequest = + GetSuggestionsRequest( + type = listOf(BaseItemKind.SERIES), + ) + val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope) + + val unwatchedTopRatedRequest = + GetItemsRequest( + parentId = parentId, + fields = DefaultItemFields, + includeItemTypes = listOf(BaseItemKind.SERIES), + recursive = true, + enableUserData = true, + isPlayed = false, + sortBy = listOf(ItemSortBy.COMMUNITY_RATING), + sortOrder = listOf(SortOrder.DESCENDING), + ) + val unwatchedTopRatedItems = + ApiRequestPager(api, unwatchedTopRatedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true) + + val rows = + listOf(resumeItems, nextUpItems, recentlyReleasedItems, recentlyAddedItems, suggestedItems, unwatchedTopRatedItems) + rows.forEach { it.init() } + val homeRows = + listOf( + HomeRow(HomeSection.RESUME, resumeItems, "Continue Watching"), + HomeRow(HomeSection.NEXT_UP, nextUpItems, "Next Up"), + HomeRow(HomeSection.LATEST_MEDIA, recentlyReleasedItems, "Recently Released"), + HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"), + HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"), + HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"), + ) + withContext(Dispatchers.Main) { + this@RecommendedTvShowViewModel.rows.value = homeRows + loading.value = LoadingState.Success + } + } + } + } + +@Composable +fun RecommendedTvShow( + preferences: UserPreferences, + navigationManager: NavigationManager, + parentId: UUID, + modifier: Modifier, + viewModel: RecommendedTvShowViewModel = hiltViewModel(), +) { + OneTimeLaunchedEffect { + viewModel.init(preferences, parentId) + } + val loading by viewModel.loading.observeAsState(LoadingState.Loading) + val rows by viewModel.rows.observeAsState(listOf()) + + when (val state = loading) { + is LoadingState.Error -> ErrorMessage(state) + + LoadingState.Loading -> LoadingPage() + + LoadingState.Success -> + HomePageContent( + navigationManager = navigationManager, + homeRows = rows, + modifier = modifier, + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt index 55816c11..bdd1661b 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt @@ -135,6 +135,7 @@ fun CollectionFolderDetails( ItemSortBy.SORT_NAME, SortOrder.ASCENDING, ), + showTitle: Boolean = true, ) { OneTimeLaunchedEffect { viewModel.init(destination.itemId, destination.item, initialSortAndDirection) @@ -161,6 +162,7 @@ fun CollectionFolderDetails( onSortChange = { viewModel.loadResults(it) }, + showTitle = showTitle, ) } } @@ -177,6 +179,7 @@ fun CollectionDetails( sortAndDirection: SortAndDirection, onSortChange: (SortAndDirection) -> Unit, modifier: Modifier = Modifier, + showTitle: Boolean = true, ) { val title = library.name ?: item.data.name ?: item.data.collectionType?.name ?: "Collection" val sortOptions = @@ -193,13 +196,15 @@ fun CollectionDetails( verticalArrangement = Arrangement.spacedBy(0.dp), modifier = modifier, ) { - Text( - text = title, - style = MaterialTheme.typography.displayMedium, - color = MaterialTheme.colorScheme.onBackground, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - ) + if (showTitle) { + Text( + text = title, + style = MaterialTheme.typography.displayMedium, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + } SortByButton( sortOptions = sortOptions, current = sortAndDirection, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderMovie.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderMovie.kt new file mode 100644 index 00000000..36b140ba --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderMovie.kt @@ -0,0 +1,100 @@ +package com.github.damontecres.dolphin.ui.detail + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +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.focus.focusRequester +import androidx.compose.ui.focus.focusRestorer +import androidx.compose.ui.unit.dp +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Tab +import androidx.tv.material3.TabRow +import androidx.tv.material3.Text +import com.github.damontecres.dolphin.preferences.UserPreferences +import com.github.damontecres.dolphin.ui.components.ErrorMessage +import com.github.damontecres.dolphin.ui.components.RecommendedMovie +import com.github.damontecres.dolphin.ui.ifElse +import com.github.damontecres.dolphin.ui.nav.Destination +import com.github.damontecres.dolphin.ui.nav.NavigationManager +import com.github.damontecres.dolphin.ui.tryRequestFocus + +@Composable +fun CollectionFolderMovie( + preferences: UserPreferences, + navigationManager: NavigationManager, + destination: Destination.MediaItem, + modifier: Modifier = Modifier, +) { + val tabs = listOf("Recommended", "Library") + var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } + val focusRequester = remember { FocusRequester() } + val firstTabFocusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + Column( + modifier = modifier, + ) { + TabRow( + selectedTabIndex = selectedTabIndex, + modifier = + Modifier + .padding(start = 32.dp, top = 16.dp) + .focusRestorer(firstTabFocusRequester), + tabs = { + tabs.forEachIndexed { index, title -> + Tab( + selected = selectedTabIndex == index, + onClick = { selectedTabIndex = index }, + onFocus = {}, + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + modifier = + Modifier + .padding(8.dp) + .ifElse(index == 0, Modifier.focusRequester(firstTabFocusRequester)), + ) + } + } + }, + ) + when (selectedTabIndex) { + 0 -> { + RecommendedMovie( + preferences = preferences, + navigationManager = navigationManager, + parentId = destination.itemId, + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxSize() + .focusRequester(focusRequester), + ) + } + 1 -> { + CollectionFolderDetails( + preferences = preferences, + navigationManager = navigationManager, + destination = destination, + showTitle = false, + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxSize() + .focusRequester(focusRequester), + ) + } + else -> ErrorMessage("Invalid tab index $selectedTabIndex", null) + } + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderTv.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderTv.kt new file mode 100644 index 00000000..fb8df728 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderTv.kt @@ -0,0 +1,100 @@ +package com.github.damontecres.dolphin.ui.detail + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +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.focus.focusRequester +import androidx.compose.ui.focus.focusRestorer +import androidx.compose.ui.unit.dp +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Tab +import androidx.tv.material3.TabRow +import androidx.tv.material3.Text +import com.github.damontecres.dolphin.preferences.UserPreferences +import com.github.damontecres.dolphin.ui.components.ErrorMessage +import com.github.damontecres.dolphin.ui.components.RecommendedTvShow +import com.github.damontecres.dolphin.ui.ifElse +import com.github.damontecres.dolphin.ui.nav.Destination +import com.github.damontecres.dolphin.ui.nav.NavigationManager +import com.github.damontecres.dolphin.ui.tryRequestFocus + +@Composable +fun CollectionFolderTv( + preferences: UserPreferences, + navigationManager: NavigationManager, + destination: Destination.MediaItem, + modifier: Modifier = Modifier, +) { + val tabs = listOf("Recommended", "Library") + var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } + val focusRequester = remember { FocusRequester() } + val firstTabFocusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + Column( + modifier = modifier, + ) { + TabRow( + selectedTabIndex = selectedTabIndex, + modifier = + Modifier + .padding(start = 32.dp, top = 16.dp) + .focusRestorer(firstTabFocusRequester), + tabs = { + tabs.forEachIndexed { index, title -> + Tab( + selected = selectedTabIndex == index, + onClick = { selectedTabIndex = index }, + onFocus = {}, + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + modifier = + Modifier + .padding(8.dp) + .ifElse(index == 0, Modifier.focusRequester(firstTabFocusRequester)), + ) + } + } + }, + ) + when (selectedTabIndex) { + 0 -> { + RecommendedTvShow( + preferences = preferences, + navigationManager = navigationManager, + parentId = destination.itemId, + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxSize() + .focusRequester(focusRequester), + ) + } + 1 -> { + CollectionFolderDetails( + preferences = preferences, + navigationManager = navigationManager, + destination = destination, + showTitle = false, + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxSize() + .focusRequester(focusRequester), + ) + } + else -> ErrorMessage("Invalid tab index $selectedTabIndex", null) + } + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt index bcc3c92f..0f540605 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt @@ -163,44 +163,46 @@ fun HomePageContent( modifier = Modifier, ) { itemsIndexed(homeRows) { rowIndex, row -> - ItemRow( - title = row.title ?: stringResource(row.section.nameRes), - items = row.items, - onClickItem = { - navigationManager.navigateTo(it.destination()) - }, - cardOnFocus = { isFocused, index -> - if (isFocused) { - focusedItem = row.items.getOrNull(index) - position = RowColumn(rowIndex, index) - } - }, - onLongClickItem = {}, - modifier = Modifier.fillMaxWidth(), - cardContent = { index, item, modifier, onClick, onLongClick -> - // TODO better aspect ration handling? - BannerCard( - imageUrl = item?.imageUrl, - aspectRatio = (2f / 3f), - cornerText = item?.data?.indexNumber?.let { "E$it" }, - played = item?.data?.userData?.played ?: false, - playPercent = item?.data?.userData?.playedPercentage ?: 0.0, - onClick = onClick, - onLongClick = onLongClick, - modifier = - modifier - .ifElse( - focusedItem == item, - Modifier.focusRequester(focusRequester), - ).ifElse( - RowColumn(rowIndex, index) == position, - Modifier.focusRequester(positionFocusRequester), - ), - interactionSource = null, - cardHeight = 200.dp, - ) - }, - ) + if (row.items.isNotEmpty()) { + ItemRow( + title = row.title ?: stringResource(row.section.nameRes), + items = row.items, + onClickItem = { + navigationManager.navigateTo(it.destination()) + }, + cardOnFocus = { isFocused, index -> + if (isFocused) { + focusedItem = row.items.getOrNull(index) + position = RowColumn(rowIndex, index) + } + }, + onLongClickItem = {}, + modifier = Modifier.fillMaxWidth(), + cardContent = { index, item, modifier, onClick, onLongClick -> + // TODO better aspect ration handling? + BannerCard( + imageUrl = item?.imageUrl, + aspectRatio = (2f / 3f), + cornerText = item?.data?.indexNumber?.let { "E$it" }, + played = item?.data?.userData?.played ?: false, + playPercent = item?.data?.userData?.playedPercentage ?: 0.0, + onClick = onClick, + onLongClick = onLongClick, + modifier = + modifier + .ifElse( + focusedItem == item, + Modifier.focusRequester(focusRequester), + ).ifElse( + RowColumn(rowIndex, index) == position, + Modifier.focusRequester(positionFocusRequester), + ), + interactionSource = null, + cardHeight = 200.dp, + ) + }, + ) + } } } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/Destination.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/Destination.kt index 51fb9117..432ed775 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/Destination.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/Destination.kt @@ -53,7 +53,8 @@ sealed class Destination( @Transient val item: BaseItem? = null, val seasonEpisode: SeasonEpisode? = null, ) : Destination() { - override fun toString(): String = "MediaItem(itemId=$itemId, type=$type, seasonEpisode=$seasonEpisode)" + override fun toString(): String = + "MediaItem(itemId=$itemId, type=$type, seasonEpisode=$seasonEpisode, collectionType=${item?.data?.collectionType})" } @Serializable diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/DestinationContent.kt index c56b3df6..6bc6744d 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/DestinationContent.kt @@ -6,6 +6,8 @@ import androidx.tv.material3.Text import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.ui.components.LicenseInfo import com.github.damontecres.dolphin.ui.detail.CollectionFolderDetails +import com.github.damontecres.dolphin.ui.detail.CollectionFolderMovie +import com.github.damontecres.dolphin.ui.detail.CollectionFolderTv import com.github.damontecres.dolphin.ui.detail.EpisodeDetails import com.github.damontecres.dolphin.ui.detail.SeasonDetails import com.github.damontecres.dolphin.ui.detail.SeriesDetails @@ -20,6 +22,7 @@ import com.github.damontecres.dolphin.ui.preferences.PreferencesPage import com.github.damontecres.dolphin.ui.setup.SwitchServerContent import com.github.damontecres.dolphin.ui.setup.SwitchUserContent import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.DeviceProfile @Composable @@ -110,12 +113,31 @@ fun DestinationContent( ) BaseItemKind.COLLECTION_FOLDER -> { - CollectionFolderDetails( - preferences, - navigationManager, - destination, - modifier, - ) + when (destination.item?.data?.collectionType) { + CollectionType.TVSHOWS -> + CollectionFolderTv( + preferences, + navigationManager, + destination, + modifier, + ) + + CollectionType.MOVIES -> + CollectionFolderMovie( + preferences, + navigationManager, + destination, + modifier, + ) + + else -> + CollectionFolderDetails( + preferences, + navigationManager, + destination, + modifier, + ) + } } else -> { diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavDrawer.kt index 4b3828b9..07ea9395 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavDrawer.kt @@ -53,7 +53,7 @@ import com.github.damontecres.dolphin.R import com.github.damontecres.dolphin.data.JellyfinServer import com.github.damontecres.dolphin.data.JellyfinUser import com.github.damontecres.dolphin.data.ServerRepository -import com.github.damontecres.dolphin.data.model.Library +import com.github.damontecres.dolphin.data.model.BaseItem import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.ui.FontAwesome import com.github.damontecres.dolphin.util.ExceptionHandler @@ -72,7 +72,7 @@ class NavDrawerViewModel val serverRepository: ServerRepository, val api: ApiClient, ) : ViewModel() { - val libraries = MutableLiveData>(listOf()) + val libraries = MutableLiveData>(listOf()) init { viewModelScope.launch(ExceptionHandler()) { @@ -81,7 +81,7 @@ class NavDrawerViewModel .getUserViews() .content.items // Timber.v("userViews: $userViews") - libraries.value = userViews.map { Library.fromDto(it, api) } + libraries.value = userViews.map { BaseItem.from(it, api) } } } } @@ -164,12 +164,7 @@ fun NavDrawer( LibraryNavItem( library = it, onClick = { - navigationManager.navigateTo( - Destination.MediaItem( - it.id, - it.type, - ), - ) + navigationManager.navigateTo(it.destination()) }, ) } @@ -234,13 +229,13 @@ fun NavigationDrawerScope.IconNavItem( @Composable fun NavigationDrawerScope.LibraryNavItem( - library: Library, + library: BaseItem, onClick: () -> Unit, modifier: Modifier = Modifier, ) { // TODO val icon = - when (library.collectionType) { + when (library.data.collectionType) { CollectionType.MOVIES -> R.string.fa_film CollectionType.TVSHOWS -> R.string.fa_tv CollectionType.HOMEVIDEOS -> R.string.fa_video diff --git a/app/src/main/java/com/github/damontecres/dolphin/util/ApiRequestPager.kt b/app/src/main/java/com/github/damontecres/dolphin/util/ApiRequestPager.kt index a4272517..788ecbef 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/util/ApiRequestPager.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/util/ApiRequestPager.kt @@ -144,6 +144,7 @@ class ApiRequestPager( private val scope: CoroutineScope, private val pageSize: Int = DEFAULT_PAGE_SIZE, cacheSize: Long = 8, + private val useSeriesForPrimary: Boolean = false, ) : AbstractList(), BlockingList { private var items by mutableStateOf(ItemList(0, pageSize, mapOf())) @@ -217,7 +218,7 @@ class ApiRequestPager( false, ) val result = requestHandler.execute(api, newRequest).content - val data = result.items.map { BaseItem.from(it, api) } + val data = result.items.map { BaseItem.from(it, api, useSeriesForPrimary) } cachedPages.put(pageNumber, data) items = ItemList(totalCount, pageSize, cachedPages.asMap()) }