mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Add tabs for tv/movie libraries w/ recommended
This commit is contained in:
parent
1a4ddabff9
commit
126530ab73
10 changed files with 606 additions and 64 deletions
|
|
@ -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>(LoadingState.Loading)
|
||||
val rows = MutableLiveData<List<HomeRow>>()
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>(LoadingState.Loading)
|
||||
val rows = MutableLiveData<List<HomeRow>>()
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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<List<Library>>(listOf())
|
||||
val libraries = MutableLiveData<List<BaseItem>>(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
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ class ApiRequestPager<T>(
|
|||
private val scope: CoroutineScope,
|
||||
private val pageSize: Int = DEFAULT_PAGE_SIZE,
|
||||
cacheSize: Long = 8,
|
||||
private val useSeriesForPrimary: Boolean = false,
|
||||
) : AbstractList<BaseItem?>(),
|
||||
BlockingList<BaseItem?> {
|
||||
private var items by mutableStateOf(ItemList<BaseItem>(0, pageSize, mapOf()))
|
||||
|
|
@ -217,7 +218,7 @@ class ApiRequestPager<T>(
|
|||
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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue