mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +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,
|
ItemSortBy.SORT_NAME,
|
||||||
SortOrder.ASCENDING,
|
SortOrder.ASCENDING,
|
||||||
),
|
),
|
||||||
|
showTitle: Boolean = true,
|
||||||
) {
|
) {
|
||||||
OneTimeLaunchedEffect {
|
OneTimeLaunchedEffect {
|
||||||
viewModel.init(destination.itemId, destination.item, initialSortAndDirection)
|
viewModel.init(destination.itemId, destination.item, initialSortAndDirection)
|
||||||
|
|
@ -161,6 +162,7 @@ fun CollectionFolderDetails(
|
||||||
onSortChange = {
|
onSortChange = {
|
||||||
viewModel.loadResults(it)
|
viewModel.loadResults(it)
|
||||||
},
|
},
|
||||||
|
showTitle = showTitle,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -177,6 +179,7 @@ fun CollectionDetails(
|
||||||
sortAndDirection: SortAndDirection,
|
sortAndDirection: SortAndDirection,
|
||||||
onSortChange: (SortAndDirection) -> Unit,
|
onSortChange: (SortAndDirection) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
showTitle: Boolean = true,
|
||||||
) {
|
) {
|
||||||
val title = library.name ?: item.data.name ?: item.data.collectionType?.name ?: "Collection"
|
val title = library.name ?: item.data.name ?: item.data.collectionType?.name ?: "Collection"
|
||||||
val sortOptions =
|
val sortOptions =
|
||||||
|
|
@ -193,6 +196,7 @@ fun CollectionDetails(
|
||||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
|
if (showTitle) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.displayMedium,
|
style = MaterialTheme.typography.displayMedium,
|
||||||
|
|
@ -200,6 +204,7 @@ fun CollectionDetails(
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
SortByButton(
|
SortByButton(
|
||||||
sortOptions = sortOptions,
|
sortOptions = sortOptions,
|
||||||
current = sortAndDirection,
|
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,6 +163,7 @@ fun HomePageContent(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
itemsIndexed(homeRows) { rowIndex, row ->
|
itemsIndexed(homeRows) { rowIndex, row ->
|
||||||
|
if (row.items.isNotEmpty()) {
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = row.title ?: stringResource(row.section.nameRes),
|
title = row.title ?: stringResource(row.section.nameRes),
|
||||||
items = row.items,
|
items = row.items,
|
||||||
|
|
@ -206,6 +207,7 @@ fun HomePageContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainPageHeader(
|
fun MainPageHeader(
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,8 @@ sealed class Destination(
|
||||||
@Transient val item: BaseItem? = null,
|
@Transient val item: BaseItem? = null,
|
||||||
val seasonEpisode: SeasonEpisode? = null,
|
val seasonEpisode: SeasonEpisode? = null,
|
||||||
) : Destination() {
|
) : 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
|
@Serializable
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.components.LicenseInfo
|
import com.github.damontecres.dolphin.ui.components.LicenseInfo
|
||||||
import com.github.damontecres.dolphin.ui.detail.CollectionFolderDetails
|
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.EpisodeDetails
|
||||||
import com.github.damontecres.dolphin.ui.detail.SeasonDetails
|
import com.github.damontecres.dolphin.ui.detail.SeasonDetails
|
||||||
import com.github.damontecres.dolphin.ui.detail.SeriesDetails
|
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.SwitchServerContent
|
||||||
import com.github.damontecres.dolphin.ui.setup.SwitchUserContent
|
import com.github.damontecres.dolphin.ui.setup.SwitchUserContent
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -110,6 +113,24 @@ fun DestinationContent(
|
||||||
)
|
)
|
||||||
|
|
||||||
BaseItemKind.COLLECTION_FOLDER -> {
|
BaseItemKind.COLLECTION_FOLDER -> {
|
||||||
|
when (destination.item?.data?.collectionType) {
|
||||||
|
CollectionType.TVSHOWS ->
|
||||||
|
CollectionFolderTv(
|
||||||
|
preferences,
|
||||||
|
navigationManager,
|
||||||
|
destination,
|
||||||
|
modifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
CollectionType.MOVIES ->
|
||||||
|
CollectionFolderMovie(
|
||||||
|
preferences,
|
||||||
|
navigationManager,
|
||||||
|
destination,
|
||||||
|
modifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
else ->
|
||||||
CollectionFolderDetails(
|
CollectionFolderDetails(
|
||||||
preferences,
|
preferences,
|
||||||
navigationManager,
|
navigationManager,
|
||||||
|
|
@ -117,6 +138,7 @@ fun DestinationContent(
|
||||||
modifier,
|
modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Text("Unsupported item type: ${destination.type}")
|
Text("Unsupported item type: ${destination.type}")
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ import com.github.damontecres.dolphin.R
|
||||||
import com.github.damontecres.dolphin.data.JellyfinServer
|
import com.github.damontecres.dolphin.data.JellyfinServer
|
||||||
import com.github.damontecres.dolphin.data.JellyfinUser
|
import com.github.damontecres.dolphin.data.JellyfinUser
|
||||||
import com.github.damontecres.dolphin.data.ServerRepository
|
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.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.FontAwesome
|
import com.github.damontecres.dolphin.ui.FontAwesome
|
||||||
import com.github.damontecres.dolphin.util.ExceptionHandler
|
import com.github.damontecres.dolphin.util.ExceptionHandler
|
||||||
|
|
@ -72,7 +72,7 @@ class NavDrawerViewModel
|
||||||
val serverRepository: ServerRepository,
|
val serverRepository: ServerRepository,
|
||||||
val api: ApiClient,
|
val api: ApiClient,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val libraries = MutableLiveData<List<Library>>(listOf())
|
val libraries = MutableLiveData<List<BaseItem>>(listOf())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(ExceptionHandler()) {
|
viewModelScope.launch(ExceptionHandler()) {
|
||||||
|
|
@ -81,7 +81,7 @@ class NavDrawerViewModel
|
||||||
.getUserViews()
|
.getUserViews()
|
||||||
.content.items
|
.content.items
|
||||||
// Timber.v("userViews: $userViews")
|
// 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(
|
LibraryNavItem(
|
||||||
library = it,
|
library = it,
|
||||||
onClick = {
|
onClick = {
|
||||||
navigationManager.navigateTo(
|
navigationManager.navigateTo(it.destination())
|
||||||
Destination.MediaItem(
|
|
||||||
it.id,
|
|
||||||
it.type,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -234,13 +229,13 @@ fun NavigationDrawerScope.IconNavItem(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationDrawerScope.LibraryNavItem(
|
fun NavigationDrawerScope.LibraryNavItem(
|
||||||
library: Library,
|
library: BaseItem,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
// TODO
|
// TODO
|
||||||
val icon =
|
val icon =
|
||||||
when (library.collectionType) {
|
when (library.data.collectionType) {
|
||||||
CollectionType.MOVIES -> R.string.fa_film
|
CollectionType.MOVIES -> R.string.fa_film
|
||||||
CollectionType.TVSHOWS -> R.string.fa_tv
|
CollectionType.TVSHOWS -> R.string.fa_tv
|
||||||
CollectionType.HOMEVIDEOS -> R.string.fa_video
|
CollectionType.HOMEVIDEOS -> R.string.fa_video
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,7 @@ class ApiRequestPager<T>(
|
||||||
private val scope: CoroutineScope,
|
private val scope: CoroutineScope,
|
||||||
private val pageSize: Int = DEFAULT_PAGE_SIZE,
|
private val pageSize: Int = DEFAULT_PAGE_SIZE,
|
||||||
cacheSize: Long = 8,
|
cacheSize: Long = 8,
|
||||||
|
private val useSeriesForPrimary: Boolean = false,
|
||||||
) : AbstractList<BaseItem?>(),
|
) : AbstractList<BaseItem?>(),
|
||||||
BlockingList<BaseItem?> {
|
BlockingList<BaseItem?> {
|
||||||
private var items by mutableStateOf(ItemList<BaseItem>(0, pageSize, mapOf()))
|
private var items by mutableStateOf(ItemList<BaseItem>(0, pageSize, mapOf()))
|
||||||
|
|
@ -217,7 +218,7 @@ class ApiRequestPager<T>(
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
val result = requestHandler.execute(api, newRequest).content
|
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)
|
cachedPages.put(pageNumber, data)
|
||||||
items = ItemList(totalCount, pageSize, cachedPages.asMap())
|
items = ItemList(totalCount, pageSize, cachedPages.asMap())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue