mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Show extras for Movies & TV Shows (#224)
## Description Shows local extras (clips, interviews, etc) for movies and tv shows Extras are grouped by type and clicking on the type will go to a grid page listing all of the items. However, if there is just one of that type, it is shown in-line. Trailers still have on their own row. And theme songs & theme videos are not shown. ## Issues Closes #123
This commit is contained in:
parent
2f56a3a114
commit
93d82a263d
14 changed files with 542 additions and 36 deletions
|
|
@ -0,0 +1,78 @@
|
||||||
|
package com.github.damontecres.wholphin.data
|
||||||
|
|
||||||
|
import androidx.annotation.PluralsRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
import org.jellyfin.sdk.model.api.ExtraType
|
||||||
|
|
||||||
|
sealed interface ExtrasItem {
|
||||||
|
val parentId: UUID
|
||||||
|
val type: ExtraType
|
||||||
|
val destination: Destination
|
||||||
|
val title: String?
|
||||||
|
val imageUrl: String?
|
||||||
|
|
||||||
|
data class Group(
|
||||||
|
override val parentId: UUID,
|
||||||
|
override val type: ExtraType,
|
||||||
|
val items: List<BaseItem>,
|
||||||
|
) : ExtrasItem {
|
||||||
|
override val destination: Destination =
|
||||||
|
Destination.ItemGrid(null, type.stringRes, items.map { it.id })
|
||||||
|
|
||||||
|
override val title: String? = null
|
||||||
|
override val imageUrl: String? = items.random().imageUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Single(
|
||||||
|
override val parentId: UUID,
|
||||||
|
override val type: ExtraType,
|
||||||
|
val item: BaseItem,
|
||||||
|
) : ExtrasItem {
|
||||||
|
override val destination: Destination =
|
||||||
|
Destination.Playback(
|
||||||
|
item = item,
|
||||||
|
)
|
||||||
|
override val title: String? get() = item.title
|
||||||
|
override val imageUrl: String? get() = item.imageUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:StringRes
|
||||||
|
val ExtraType.stringRes: Int
|
||||||
|
get() =
|
||||||
|
when (this) {
|
||||||
|
ExtraType.UNKNOWN -> R.string.other_extras
|
||||||
|
ExtraType.CLIP -> R.string.clips
|
||||||
|
ExtraType.TRAILER -> R.string.trailers
|
||||||
|
ExtraType.BEHIND_THE_SCENES -> R.string.behind_the_scenes
|
||||||
|
ExtraType.DELETED_SCENE -> R.string.deleted_scenes
|
||||||
|
ExtraType.INTERVIEW -> R.string.interviews
|
||||||
|
ExtraType.SCENE -> R.string.scenes
|
||||||
|
ExtraType.SAMPLE -> R.string.samples
|
||||||
|
ExtraType.THEME_SONG -> R.string.theme_songs
|
||||||
|
ExtraType.THEME_VIDEO -> R.string.theme_videos
|
||||||
|
ExtraType.FEATURETTE -> R.string.featurettes
|
||||||
|
ExtraType.SHORT -> R.string.shorts
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:PluralsRes
|
||||||
|
val ExtraType.pluralRes: Int
|
||||||
|
get() =
|
||||||
|
when (this) {
|
||||||
|
ExtraType.UNKNOWN -> R.plurals.other_extras
|
||||||
|
ExtraType.CLIP -> R.plurals.clips
|
||||||
|
ExtraType.TRAILER -> R.plurals.trailers
|
||||||
|
ExtraType.BEHIND_THE_SCENES -> R.plurals.behind_the_scenes
|
||||||
|
ExtraType.DELETED_SCENE -> R.plurals.deleted_scenes
|
||||||
|
ExtraType.INTERVIEW -> R.plurals.interviews
|
||||||
|
ExtraType.SCENE -> R.plurals.scenes
|
||||||
|
ExtraType.SAMPLE -> R.plurals.samples
|
||||||
|
ExtraType.THEME_SONG -> R.plurals.theme_songs
|
||||||
|
ExtraType.THEME_VIDEO -> R.plurals.theme_videos
|
||||||
|
ExtraType.FEATURETTE -> R.plurals.featurettes
|
||||||
|
ExtraType.SHORT -> R.plurals.shorts
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
|
import com.github.damontecres.wholphin.data.ExtrasItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
|
import org.jellyfin.sdk.model.api.ExtraType
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ExtrasService
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val api: ApiClient,
|
||||||
|
) {
|
||||||
|
suspend fun getExtras(itemId: UUID): List<ExtrasItem> {
|
||||||
|
val extrasMap =
|
||||||
|
api.userLibraryApi
|
||||||
|
.getSpecialFeatures(itemId)
|
||||||
|
.content
|
||||||
|
.filterNot {
|
||||||
|
it.extraType == ExtraType.THEME_SONG ||
|
||||||
|
it.extraType == ExtraType.THEME_VIDEO ||
|
||||||
|
it.extraType == ExtraType.TRAILER
|
||||||
|
}.map { BaseItem.from(it, api) }
|
||||||
|
.groupBy { it.data.extraType ?: ExtraType.UNKNOWN }
|
||||||
|
|
||||||
|
val result =
|
||||||
|
extrasMap
|
||||||
|
.mapNotNull { (type, items) ->
|
||||||
|
if (items.size == 1) {
|
||||||
|
ExtrasItem.Single(itemId, type, items.first())
|
||||||
|
} else if (items.size > 1) {
|
||||||
|
ExtrasItem.Group(itemId, type, items)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.sortedBy { it.type.sortOrder }
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val ExtraType.sortOrder: Int
|
||||||
|
get() =
|
||||||
|
when (this) {
|
||||||
|
ExtraType.TRAILER -> 0
|
||||||
|
ExtraType.FEATURETTE -> 1
|
||||||
|
ExtraType.SHORT -> 2
|
||||||
|
ExtraType.CLIP -> 3
|
||||||
|
ExtraType.SCENE -> 4
|
||||||
|
ExtraType.SAMPLE -> 5
|
||||||
|
ExtraType.DELETED_SCENE -> 6
|
||||||
|
ExtraType.INTERVIEW -> 7
|
||||||
|
ExtraType.BEHIND_THE_SCENES -> 8
|
||||||
|
ExtraType.THEME_SONG -> 9
|
||||||
|
ExtraType.THEME_VIDEO -> 10
|
||||||
|
ExtraType.UNKNOWN -> 11
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.cards
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.data.ExtrasItem
|
||||||
|
import com.github.damontecres.wholphin.data.pluralRes
|
||||||
|
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||||
|
import com.github.damontecres.wholphin.ui.Cards
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExtrasRow(
|
||||||
|
extras: List<ExtrasItem>,
|
||||||
|
onClickItem: (Int, ExtrasItem) -> Unit,
|
||||||
|
onLongClickItem: (Int, ExtrasItem) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val resources = LocalResources.current
|
||||||
|
ItemRow(
|
||||||
|
title = stringResource(R.string.extras),
|
||||||
|
items = extras,
|
||||||
|
onClickItem = onClickItem,
|
||||||
|
onLongClickItem = onLongClickItem,
|
||||||
|
cardContent = { index, item, mod, onClick, onLongClick ->
|
||||||
|
SeasonCard(
|
||||||
|
title =
|
||||||
|
when (item) {
|
||||||
|
is ExtrasItem.Group -> item.type.pluralRes.let { resources.getQuantityString(it, item.items.size) }
|
||||||
|
is ExtrasItem.Single -> item.title ?: item.type.pluralRes.let { resources.getQuantityString(it, 1) }
|
||||||
|
null -> null
|
||||||
|
},
|
||||||
|
name = null,
|
||||||
|
subtitle =
|
||||||
|
if (item is ExtrasItem.Single && item.title != null) {
|
||||||
|
item.type.pluralRes.let { resources.getQuantityString(it, 1) }
|
||||||
|
} else if (item is ExtrasItem.Group) {
|
||||||
|
resources.getQuantityString(R.plurals.items, item.items.size, item.items.size)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
modifier = mod,
|
||||||
|
showImageOverlay = true,
|
||||||
|
imageHeight = Cards.height2x3 * .75f,
|
||||||
|
imageWidth = Dp.Unspecified,
|
||||||
|
imageUrl = item?.imageUrl,
|
||||||
|
isFavorite = false,
|
||||||
|
isPlayed = false,
|
||||||
|
unplayedItemCount = -1,
|
||||||
|
playedPercentage = -1.0,
|
||||||
|
aspectRatio = AspectRatios.FOUR_THREE, // TODO
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -26,14 +26,14 @@ import com.github.damontecres.wholphin.ui.AspectRatios
|
||||||
import com.github.damontecres.wholphin.util.FocusPair
|
import com.github.damontecres.wholphin.util.FocusPair
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ItemRow(
|
fun <T> ItemRow(
|
||||||
title: String,
|
title: String,
|
||||||
items: List<BaseItem?>,
|
items: List<T?>,
|
||||||
onClickItem: (Int, BaseItem) -> Unit,
|
onClickItem: (Int, T) -> Unit,
|
||||||
onLongClickItem: (Int, BaseItem) -> Unit,
|
onLongClickItem: (Int, T) -> Unit,
|
||||||
cardContent: @Composable (
|
cardContent: @Composable (
|
||||||
index: Int,
|
index: Int,
|
||||||
item: BaseItem?,
|
item: T?,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
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 androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
|
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||||
|
import com.github.damontecres.wholphin.ui.cards.GridCard
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.CardGrid
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
|
|
||||||
|
@HiltViewModel(assistedFactory = ItemGridViewModel.Factory::class)
|
||||||
|
class ItemGridViewModel
|
||||||
|
@AssistedInject
|
||||||
|
constructor(
|
||||||
|
private val api: ApiClient,
|
||||||
|
private val navigationManager: NavigationManager,
|
||||||
|
@Assisted private val destination: Destination.ItemGrid,
|
||||||
|
) : ViewModel() {
|
||||||
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
|
val items = MutableLiveData<List<BaseItem?>>(listOf())
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(destination: Destination.ItemGrid): ItemGridViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launchIO(LoadingExceptionHandler(loading, "Error fetching items")) {
|
||||||
|
val request =
|
||||||
|
GetItemsRequest(
|
||||||
|
ids = destination.itemIds,
|
||||||
|
sortBy = listOf(ItemSortBy.SORT_NAME),
|
||||||
|
sortOrder = listOf(SortOrder.ASCENDING),
|
||||||
|
)
|
||||||
|
val pager = ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope).init()
|
||||||
|
if (pager.isNotEmpty()) {
|
||||||
|
pager.getBlocking(0)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
this@ItemGridViewModel.items.value = pager
|
||||||
|
this@ItemGridViewModel.loading.value = LoadingState.Success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateTo(destination: Destination) {
|
||||||
|
navigationManager.navigateTo(destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ItemGrid(
|
||||||
|
destination: Destination.ItemGrid,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: ItemGridViewModel =
|
||||||
|
hiltViewModel<ItemGridViewModel, ItemGridViewModel.Factory>(
|
||||||
|
creationCallback = { it.create(destination) },
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
|
val items by viewModel.items.observeAsState(listOf())
|
||||||
|
when (val state = loading) {
|
||||||
|
is LoadingState.Error -> ErrorMessage(state)
|
||||||
|
LoadingState.Loading,
|
||||||
|
LoadingState.Pending,
|
||||||
|
-> LoadingPage()
|
||||||
|
|
||||||
|
LoadingState.Success -> {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequester.tryRequestFocus()
|
||||||
|
}
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Text(
|
||||||
|
text = destination.title ?: destination.titleRes?.let { stringResource(it) } ?: "",
|
||||||
|
style = MaterialTheme.typography.displayMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
CardGrid(
|
||||||
|
pager = items,
|
||||||
|
onClickItem = { index: Int, item: BaseItem ->
|
||||||
|
// TODO handle more types
|
||||||
|
viewModel.navigateTo(Destination.Playback(item.id, 0))
|
||||||
|
},
|
||||||
|
onLongClickItem = { index: Int, item: BaseItem -> },
|
||||||
|
letterPosition = { c: Char -> 0 },
|
||||||
|
gridFocusRequester = focusRequester,
|
||||||
|
showJumpButtons = false,
|
||||||
|
showLetterButtons = false,
|
||||||
|
spacing = 24.dp,
|
||||||
|
cardContent = @Composable { item, onClick, onLongClick, mod ->
|
||||||
|
GridCard(
|
||||||
|
item = item,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
modifier = mod,
|
||||||
|
imageAspectRatio = AspectRatios.WIDE, // TODO
|
||||||
|
)
|
||||||
|
},
|
||||||
|
columns = 3,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,6 +44,7 @@ import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.input.key.type
|
import androidx.compose.ui.input.key.type
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.LocalContentColor
|
import androidx.tv.material3.LocalContentColor
|
||||||
|
|
@ -93,9 +94,10 @@ fun CardGrid(
|
||||||
modifier = mod,
|
modifier = mod,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
columns: Int = 6,
|
||||||
|
spacing: Dp = 16.dp,
|
||||||
) {
|
) {
|
||||||
val startPosition = initialPosition.coerceIn(0, (pager.size - 1).coerceAtLeast(0))
|
val startPosition = initialPosition.coerceIn(0, (pager.size - 1).coerceAtLeast(0))
|
||||||
val columns = 6
|
|
||||||
|
|
||||||
val fractionCacheWindow = LazyLayoutCacheWindow(aheadFraction = 1f, behindFraction = 0.5f)
|
val fractionCacheWindow = LazyLayoutCacheWindow(aheadFraction = 1f, behindFraction = 0.5f)
|
||||||
val gridState = rememberLazyGridState(cacheWindow = fractionCacheWindow)
|
val gridState = rememberLazyGridState(cacheWindow = fractionCacheWindow)
|
||||||
|
|
@ -232,8 +234,8 @@ fun CardGrid(
|
||||||
) {
|
) {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Fixed(columns),
|
columns = GridCells.Fixed(columns),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(spacing),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(spacing),
|
||||||
state = gridState,
|
state = gridState,
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
modifier =
|
modifier =
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
|
import com.github.damontecres.wholphin.data.ExtrasItem
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.Chapter
|
import com.github.damontecres.wholphin.data.model.Chapter
|
||||||
import com.github.damontecres.wholphin.data.model.LocalTrailer
|
import com.github.damontecres.wholphin.data.model.LocalTrailer
|
||||||
|
|
@ -58,6 +59,7 @@ import com.github.damontecres.wholphin.services.TrailerService
|
||||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||||
import com.github.damontecres.wholphin.ui.Cards
|
import com.github.damontecres.wholphin.ui.Cards
|
||||||
import com.github.damontecres.wholphin.ui.cards.ChapterRow
|
import com.github.damontecres.wholphin.ui.cards.ChapterRow
|
||||||
|
import com.github.damontecres.wholphin.ui.cards.ExtrasRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
||||||
|
|
@ -112,6 +114,7 @@ fun MovieDetails(
|
||||||
val people by viewModel.people.observeAsState(listOf())
|
val people by viewModel.people.observeAsState(listOf())
|
||||||
val chapters by viewModel.chapters.observeAsState(listOf())
|
val chapters by viewModel.chapters.observeAsState(listOf())
|
||||||
val trailers by viewModel.trailers.observeAsState(listOf())
|
val trailers by viewModel.trailers.observeAsState(listOf())
|
||||||
|
val extras by viewModel.extras.observeAsState(listOf())
|
||||||
val similar by viewModel.similar.observeAsState(listOf())
|
val similar by viewModel.similar.observeAsState(listOf())
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
val chosenStreams by viewModel.chosenStreams.observeAsState(null)
|
val chosenStreams by viewModel.chosenStreams.observeAsState(null)
|
||||||
|
|
@ -159,6 +162,7 @@ fun MovieDetails(
|
||||||
chosenStreams = chosenStreams,
|
chosenStreams = chosenStreams,
|
||||||
people = people,
|
people = people,
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
|
extras = extras,
|
||||||
trailers = trailers,
|
trailers = trailers,
|
||||||
similar = similar,
|
similar = similar,
|
||||||
onClickItem = { index, item ->
|
onClickItem = { index, item ->
|
||||||
|
|
@ -282,6 +286,9 @@ fun MovieDetails(
|
||||||
trailerOnClick = {
|
trailerOnClick = {
|
||||||
TrailerService.onClick(context, it, viewModel::navigateTo)
|
TrailerService.onClick(context, it, viewModel::navigateTo)
|
||||||
},
|
},
|
||||||
|
onClickExtra = { index, extra ->
|
||||||
|
viewModel.navigateTo(extra.destination)
|
||||||
|
},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -340,7 +347,8 @@ private const val HEADER_ROW = 0
|
||||||
private const val PEOPLE_ROW = HEADER_ROW + 1
|
private const val PEOPLE_ROW = HEADER_ROW + 1
|
||||||
private const val TRAILER_ROW = PEOPLE_ROW + 1
|
private const val TRAILER_ROW = PEOPLE_ROW + 1
|
||||||
private const val CHAPTER_ROW = TRAILER_ROW + 1
|
private const val CHAPTER_ROW = TRAILER_ROW + 1
|
||||||
private const val SIMILAR_ROW = CHAPTER_ROW + 1
|
private const val EXTRAS_ROW = CHAPTER_ROW + 1
|
||||||
|
private const val SIMILAR_ROW = EXTRAS_ROW + 1
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MovieDetailsContent(
|
fun MovieDetailsContent(
|
||||||
|
|
@ -350,6 +358,7 @@ fun MovieDetailsContent(
|
||||||
people: List<Person>,
|
people: List<Person>,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
trailers: List<Trailer>,
|
trailers: List<Trailer>,
|
||||||
|
extras: List<ExtrasItem>,
|
||||||
similar: List<BaseItem>,
|
similar: List<BaseItem>,
|
||||||
playOnClick: (Duration) -> Unit,
|
playOnClick: (Duration) -> Unit,
|
||||||
trailerOnClick: (Trailer) -> Unit,
|
trailerOnClick: (Trailer) -> Unit,
|
||||||
|
|
@ -361,8 +370,10 @@ fun MovieDetailsContent(
|
||||||
onClickPerson: (Person) -> Unit,
|
onClickPerson: (Person) -> Unit,
|
||||||
onLongClickPerson: (Int, Person) -> Unit,
|
onLongClickPerson: (Int, Person) -> Unit,
|
||||||
onLongClickSimilar: (Int, BaseItem) -> Unit,
|
onLongClickSimilar: (Int, BaseItem) -> Unit,
|
||||||
|
onClickExtra: (Int, ExtrasItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var position by rememberInt(0)
|
var position by rememberInt(0)
|
||||||
val focusRequesters = remember { List(SIMILAR_ROW + 1) { FocusRequester() } }
|
val focusRequesters = remember { List(SIMILAR_ROW + 1) { FocusRequester() } }
|
||||||
|
|
@ -501,6 +512,22 @@ fun MovieDetailsContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (extras.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
ExtrasRow(
|
||||||
|
extras = extras,
|
||||||
|
onClickItem = { index, item ->
|
||||||
|
position = EXTRAS_ROW
|
||||||
|
onClickExtra.invoke(index, item)
|
||||||
|
},
|
||||||
|
onLongClickItem = { _, _ -> },
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequesters[EXTRAS_ROW]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (similar.isNotEmpty()) {
|
if (similar.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
ItemRow(
|
ItemRow(
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
|
import com.github.damontecres.wholphin.data.ExtrasItem
|
||||||
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
|
@ -13,6 +14,7 @@ import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
import com.github.damontecres.wholphin.data.model.Person
|
import com.github.damontecres.wholphin.data.model.Person
|
||||||
import com.github.damontecres.wholphin.data.model.Trailer
|
import com.github.damontecres.wholphin.data.model.Trailer
|
||||||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||||
|
import com.github.damontecres.wholphin.services.ExtrasService
|
||||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.PeopleFavorites
|
import com.github.damontecres.wholphin.services.PeopleFavorites
|
||||||
|
|
@ -56,6 +58,7 @@ class MovieViewModel
|
||||||
private val favoriteWatchManager: FavoriteWatchManager,
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
private val peopleFavorites: PeopleFavorites,
|
private val peopleFavorites: PeopleFavorites,
|
||||||
private val trailerService: TrailerService,
|
private val trailerService: TrailerService,
|
||||||
|
private val extrasService: ExtrasService,
|
||||||
@Assisted val itemId: UUID,
|
@Assisted val itemId: UUID,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -68,6 +71,7 @@ class MovieViewModel
|
||||||
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
||||||
val people = MutableLiveData<List<Person>>(listOf())
|
val people = MutableLiveData<List<Person>>(listOf())
|
||||||
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
||||||
|
val extras = MutableLiveData<List<ExtrasItem>>(listOf())
|
||||||
val similar = MutableLiveData<List<BaseItem>>()
|
val similar = MutableLiveData<List<BaseItem>>()
|
||||||
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
||||||
|
|
||||||
|
|
@ -116,6 +120,11 @@ class MovieViewModel
|
||||||
val people = peopleFavorites.getPeopleFor(item)
|
val people = peopleFavorites.getPeopleFor(item)
|
||||||
this@MovieViewModel.people.setValueOnMain(people)
|
this@MovieViewModel.people.setValueOnMain(people)
|
||||||
}
|
}
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val extras = extrasService.getExtras(item.id)
|
||||||
|
this@MovieViewModel.extras.setValueOnMain(extras)
|
||||||
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
chapters.value = Chapter.fromDto(item.data, api)
|
chapters.value = Chapter.fromDto(item.data, api)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,14 @@ import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.data.ExtrasItem
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.Person
|
import com.github.damontecres.wholphin.data.model.Person
|
||||||
import com.github.damontecres.wholphin.data.model.Trailer
|
import com.github.damontecres.wholphin.data.model.Trailer
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.services.TrailerService
|
import com.github.damontecres.wholphin.services.TrailerService
|
||||||
import com.github.damontecres.wholphin.ui.Cards
|
import com.github.damontecres.wholphin.ui.Cards
|
||||||
|
import com.github.damontecres.wholphin.ui.cards.ExtrasRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
||||||
|
|
@ -99,13 +101,14 @@ fun SeriesDetails(
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.init(preferences, destination.itemId, destination.item, null)
|
viewModel.init(preferences, destination.itemId, null, true)
|
||||||
}
|
}
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
|
|
||||||
val item by viewModel.item.observeAsState()
|
val item by viewModel.item.observeAsState()
|
||||||
val seasons by viewModel.seasons.observeAsState(listOf())
|
val seasons by viewModel.seasons.observeAsState(listOf())
|
||||||
val trailers by viewModel.trailers.observeAsState(listOf())
|
val trailers by viewModel.trailers.observeAsState(listOf())
|
||||||
|
val extras by viewModel.extras.observeAsState(listOf())
|
||||||
val people by viewModel.people.observeAsState(listOf())
|
val people by viewModel.people.observeAsState(listOf())
|
||||||
val similar by viewModel.similar.observeAsState(listOf())
|
val similar by viewModel.similar.observeAsState(listOf())
|
||||||
|
|
||||||
|
|
@ -138,6 +141,7 @@ fun SeriesDetails(
|
||||||
series = item,
|
series = item,
|
||||||
seasons = seasons,
|
seasons = seasons,
|
||||||
trailers = trailers,
|
trailers = trailers,
|
||||||
|
extras = extras,
|
||||||
people = people,
|
people = people,
|
||||||
similar = similar,
|
similar = similar,
|
||||||
played = played,
|
played = played,
|
||||||
|
|
@ -182,6 +186,9 @@ fun SeriesDetails(
|
||||||
trailerOnClick = {
|
trailerOnClick = {
|
||||||
TrailerService.onClick(context, it, viewModel::navigateTo)
|
TrailerService.onClick(context, it, viewModel::navigateTo)
|
||||||
},
|
},
|
||||||
|
onClickExtra = { _, extra ->
|
||||||
|
viewModel.navigateTo(extra.destination)
|
||||||
|
},
|
||||||
moreActions =
|
moreActions =
|
||||||
MoreDialogActions(
|
MoreDialogActions(
|
||||||
navigateTo = { viewModel.navigateTo(it) },
|
navigateTo = { viewModel.navigateTo(it) },
|
||||||
|
|
@ -253,7 +260,8 @@ private const val HEADER_ROW = 0
|
||||||
private const val SEASONS_ROW = HEADER_ROW + 1
|
private const val SEASONS_ROW = HEADER_ROW + 1
|
||||||
private const val PEOPLE_ROW = SEASONS_ROW + 1
|
private const val PEOPLE_ROW = SEASONS_ROW + 1
|
||||||
private const val TRAILER_ROW = PEOPLE_ROW + 1
|
private const val TRAILER_ROW = PEOPLE_ROW + 1
|
||||||
private const val SIMILAR_ROW = TRAILER_ROW + 1
|
private const val EXTRAS_ROW = TRAILER_ROW + 1
|
||||||
|
private const val SIMILAR_ROW = EXTRAS_ROW + 1
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SeriesDetailsContent(
|
fun SeriesDetailsContent(
|
||||||
|
|
@ -262,6 +270,7 @@ fun SeriesDetailsContent(
|
||||||
seasons: List<BaseItem>,
|
seasons: List<BaseItem>,
|
||||||
similar: List<BaseItem>,
|
similar: List<BaseItem>,
|
||||||
trailers: List<Trailer>,
|
trailers: List<Trailer>,
|
||||||
|
extras: List<ExtrasItem>,
|
||||||
people: List<Person>,
|
people: List<Person>,
|
||||||
played: Boolean,
|
played: Boolean,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
|
|
@ -273,6 +282,7 @@ fun SeriesDetailsContent(
|
||||||
watchOnClick: () -> Unit,
|
watchOnClick: () -> Unit,
|
||||||
favoriteOnClick: () -> Unit,
|
favoriteOnClick: () -> Unit,
|
||||||
trailerOnClick: (Trailer) -> Unit,
|
trailerOnClick: (Trailer) -> Unit,
|
||||||
|
onClickExtra: (Int, ExtrasItem) -> Unit,
|
||||||
moreActions: MoreDialogActions,
|
moreActions: MoreDialogActions,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -478,6 +488,22 @@ fun SeriesDetailsContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (extras.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
ExtrasRow(
|
||||||
|
extras = extras,
|
||||||
|
onClickItem = { index, item ->
|
||||||
|
position = EXTRAS_ROW
|
||||||
|
onClickExtra.invoke(index, item)
|
||||||
|
},
|
||||||
|
onLongClickItem = { _, _ -> },
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequesters[EXTRAS_ROW]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (similar.isNotEmpty()) {
|
if (similar.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
ItemRow(
|
ItemRow(
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,8 @@ fun SeriesOverview(
|
||||||
viewModel.init(
|
viewModel.init(
|
||||||
preferences,
|
preferences,
|
||||||
destination.itemId,
|
destination.itemId,
|
||||||
destination.item,
|
|
||||||
initialSeasonEpisode,
|
initialSeasonEpisode,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
initialLoadDone = true
|
initialLoadDone = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import android.widget.Toast
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
|
import com.github.damontecres.wholphin.data.ExtrasItem
|
||||||
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
|
@ -13,6 +14,7 @@ import com.github.damontecres.wholphin.data.model.Person
|
||||||
import com.github.damontecres.wholphin.data.model.Trailer
|
import com.github.damontecres.wholphin.data.model.Trailer
|
||||||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.services.ExtrasService
|
||||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.PeopleFavorites
|
import com.github.damontecres.wholphin.services.PeopleFavorites
|
||||||
|
|
@ -65,21 +67,24 @@ class SeriesViewModel
|
||||||
private val favoriteWatchManager: FavoriteWatchManager,
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
private val peopleFavorites: PeopleFavorites,
|
private val peopleFavorites: PeopleFavorites,
|
||||||
private val trailerService: TrailerService,
|
private val trailerService: TrailerService,
|
||||||
|
private val extrasService: ExtrasService,
|
||||||
) : ItemViewModel(api) {
|
) : ItemViewModel(api) {
|
||||||
private lateinit var seriesId: UUID
|
private lateinit var seriesId: UUID
|
||||||
private lateinit var prefs: UserPreferences
|
private lateinit var prefs: UserPreferences
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
val seasons = MutableLiveData<List<BaseItem>>(listOf())
|
val seasons = MutableLiveData<List<BaseItem>>(listOf())
|
||||||
val episodes = MutableLiveData<EpisodeList>(EpisodeList.Loading)
|
val episodes = MutableLiveData<EpisodeList>(EpisodeList.Loading)
|
||||||
|
|
||||||
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
||||||
|
val extras = MutableLiveData<List<ExtrasItem>>(listOf())
|
||||||
val people = MutableLiveData<List<Person>>(listOf())
|
val people = MutableLiveData<List<Person>>(listOf())
|
||||||
val similar = MutableLiveData<List<BaseItem>>()
|
val similar = MutableLiveData<List<BaseItem>>()
|
||||||
|
|
||||||
fun init(
|
fun init(
|
||||||
prefs: UserPreferences,
|
prefs: UserPreferences,
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
potential: BaseItem?,
|
|
||||||
seasonEpisodeIds: SeasonEpisodeIds?,
|
seasonEpisodeIds: SeasonEpisodeIds?,
|
||||||
|
loadAdditionalDetails: Boolean,
|
||||||
) {
|
) {
|
||||||
this.seriesId = itemId
|
this.seriesId = itemId
|
||||||
this.prefs = prefs
|
this.prefs = prefs
|
||||||
|
|
@ -115,30 +120,36 @@ class SeriesViewModel
|
||||||
episodes.value = episodeInfo
|
episodes.value = episodeInfo
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
}
|
}
|
||||||
viewModelScope.launchIO {
|
if (loadAdditionalDetails) {
|
||||||
val trailers = trailerService.getTrailers(item)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
this@SeriesViewModel.trailers.value = trailers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModelScope.launchIO {
|
|
||||||
val people = peopleFavorites.getPeopleFor(item)
|
|
||||||
this@SeriesViewModel.people.setValueOnMain(people)
|
|
||||||
}
|
|
||||||
if (!similar.isInitialized) {
|
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val similar =
|
val trailers = trailerService.getTrailers(item)
|
||||||
api.libraryApi
|
withContext(Dispatchers.Main) {
|
||||||
.getSimilarItems(
|
this@SeriesViewModel.trailers.value = trailers
|
||||||
GetSimilarItemsRequest(
|
}
|
||||||
userId = serverRepository.currentUser.value?.id,
|
}
|
||||||
itemId = itemId,
|
viewModelScope.launchIO {
|
||||||
fields = SlimItemFields,
|
val people = peopleFavorites.getPeopleFor(item)
|
||||||
limit = 25,
|
this@SeriesViewModel.people.setValueOnMain(people)
|
||||||
),
|
}
|
||||||
).content.items
|
viewModelScope.launchIO {
|
||||||
.map { BaseItem.from(it, api, true) }
|
val extras = extrasService.getExtras(item.id)
|
||||||
this@SeriesViewModel.similar.setValueOnMain(similar)
|
this@SeriesViewModel.extras.setValueOnMain(extras)
|
||||||
|
}
|
||||||
|
if (!similar.isInitialized) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val similar =
|
||||||
|
api.libraryApi
|
||||||
|
.getSimilarItems(
|
||||||
|
GetSimilarItemsRequest(
|
||||||
|
userId = serverRepository.currentUser.value?.id,
|
||||||
|
itemId = itemId,
|
||||||
|
fields = SlimItemFields,
|
||||||
|
limit = 25,
|
||||||
|
),
|
||||||
|
).content.items
|
||||||
|
.map { BaseItem.from(it, api, true) }
|
||||||
|
this@SeriesViewModel.similar.setValueOnMain(similar)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
package com.github.damontecres.wholphin.ui.nav
|
package com.github.damontecres.wholphin.ui.nav
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
|
|
@ -90,6 +91,13 @@ sealed class Destination(
|
||||||
val recursive: Boolean,
|
val recursive: Boolean,
|
||||||
) : Destination(false)
|
) : Destination(false)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ItemGrid(
|
||||||
|
val title: String?,
|
||||||
|
@param:StringRes val titleRes: Int?,
|
||||||
|
val itemIds: List<UUID>,
|
||||||
|
) : Destination(false)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Favorites : Destination(false)
|
data object Favorites : Destination(false)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ItemGrid
|
||||||
import com.github.damontecres.wholphin.ui.components.LicenseInfo
|
import com.github.damontecres.wholphin.ui.components.LicenseInfo
|
||||||
import com.github.damontecres.wholphin.ui.detail.CollectionFolderGeneric
|
import com.github.damontecres.wholphin.ui.detail.CollectionFolderGeneric
|
||||||
import com.github.damontecres.wholphin.ui.detail.CollectionFolderLiveTv
|
import com.github.damontecres.wholphin.ui.detail.CollectionFolderLiveTv
|
||||||
|
|
@ -211,6 +212,12 @@ fun DestinationContent(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is Destination.ItemGrid ->
|
||||||
|
ItemGrid(
|
||||||
|
destination,
|
||||||
|
modifier,
|
||||||
|
)
|
||||||
|
|
||||||
Destination.Favorites ->
|
Destination.Favorites ->
|
||||||
FavoritesPage(
|
FavoritesPage(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,80 @@
|
||||||
<string name="official_rating">Parental Rating</string>
|
<string name="official_rating">Parental Rating</string>
|
||||||
<string name="runtime_sort">Runtime</string>
|
<string name="runtime_sort">Runtime</string>
|
||||||
|
|
||||||
|
<string name="extras">Extras</string>
|
||||||
|
<string name="other_extras">Other</string>
|
||||||
|
<string name="behind_the_scenes">Behind the Scenes</string>
|
||||||
|
<string name="theme_songs">Theme Songs</string>
|
||||||
|
<string name="theme_videos">Theme Videos</string>
|
||||||
|
<string name="clips">Clips</string>
|
||||||
|
<string name="deleted_scenes">Deleted Scenes</string>
|
||||||
|
<string name="interviews">Interviews</string>
|
||||||
|
<string name="scenes">Scenes</string>
|
||||||
|
<string name="samples">Samples</string>
|
||||||
|
<string name="featurettes">Featurettes</string>
|
||||||
|
<string name="shorts">Shorts</string>
|
||||||
|
|
||||||
|
<plurals name="trailers">
|
||||||
|
<item quantity="zero">Trailers</item>
|
||||||
|
<item quantity="one">Trailer</item>
|
||||||
|
<item quantity="other">Trailers</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="other_extras">
|
||||||
|
<item quantity="zero">Other</item>
|
||||||
|
<item quantity="one">Other</item>
|
||||||
|
<item quantity="other">Other</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="behind_the_scenes">
|
||||||
|
<item quantity="zero">Behind the scenes</item>
|
||||||
|
<item quantity="one">Behind the scenes</item>
|
||||||
|
<item quantity="other">Behind the scenes</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="theme_songs">
|
||||||
|
<item quantity="zero">Theme songs</item>
|
||||||
|
<item quantity="one">Theme song</item>
|
||||||
|
<item quantity="other">Theme songs</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="theme_videos">
|
||||||
|
<item quantity="zero">Theme videos</item>
|
||||||
|
<item quantity="one">Theme video</item>
|
||||||
|
<item quantity="other">Theme videos</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="clips">
|
||||||
|
<item quantity="zero">Clips</item>
|
||||||
|
<item quantity="one">Clip</item>
|
||||||
|
<item quantity="other">Clips</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="deleted_scenes">
|
||||||
|
<item quantity="zero">Deleted scenes</item>
|
||||||
|
<item quantity="one">Deleted scene</item>
|
||||||
|
<item quantity="other">Deleted scenes</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="interviews">
|
||||||
|
<item quantity="zero">Interviews</item>
|
||||||
|
<item quantity="one">Interview</item>
|
||||||
|
<item quantity="other">Interviews</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="scenes">
|
||||||
|
<item quantity="zero">Scenes</item>
|
||||||
|
<item quantity="one">Scene</item>
|
||||||
|
<item quantity="other">Scenes</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="samples">
|
||||||
|
<item quantity="zero">Samples</item>
|
||||||
|
<item quantity="one">Sample</item>
|
||||||
|
<item quantity="other">Samples</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="featurettes">
|
||||||
|
<item quantity="zero">Featurettes</item>
|
||||||
|
<item quantity="one">Featurette</item>
|
||||||
|
<item quantity="other">Featurettes</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="shorts">
|
||||||
|
<item quantity="zero">Shorts</item>
|
||||||
|
<item quantity="one">Short</item>
|
||||||
|
<item quantity="other">Shorts</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
<plurals name="downloads">
|
<plurals name="downloads">
|
||||||
<item quantity="zero">%s downloads</item>
|
<item quantity="zero">%s downloads</item>
|
||||||
<item quantity="one">%s download</item>
|
<item quantity="one">%s download</item>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue