mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Show similar items on movie & tv show pages (#52)
Adds a row of similar items to the Movie & TV Show details pages
This commit is contained in:
parent
177e009f8c
commit
298016b131
4 changed files with 96 additions and 1 deletions
|
|
@ -33,6 +33,7 @@ import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
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.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
|
@ -100,6 +101,7 @@ fun SeriesDetails(
|
||||||
val item by viewModel.item.observeAsState()
|
val item by viewModel.item.observeAsState()
|
||||||
val seasons by viewModel.seasons.observeAsState(ItemListAndMapping.empty())
|
val seasons by viewModel.seasons.observeAsState(ItemListAndMapping.empty())
|
||||||
val people by viewModel.people.observeAsState(listOf())
|
val people by viewModel.people.observeAsState(listOf())
|
||||||
|
val similar by viewModel.similar.observeAsState(listOf())
|
||||||
|
|
||||||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||||
var showWatchConfirmation by remember { mutableStateOf(false) }
|
var showWatchConfirmation by remember { mutableStateOf(false) }
|
||||||
|
|
@ -118,6 +120,7 @@ fun SeriesDetails(
|
||||||
series = item,
|
series = item,
|
||||||
seasons = seasons,
|
seasons = seasons,
|
||||||
people = people,
|
people = people,
|
||||||
|
similar = similar,
|
||||||
played = played,
|
played = played,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onClickItem = { viewModel.navigateTo(it.destination()) },
|
onClickItem = { viewModel.navigateTo(it.destination()) },
|
||||||
|
|
@ -180,6 +183,7 @@ fun SeriesDetailsContent(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
series: BaseItem,
|
series: BaseItem,
|
||||||
seasons: ItemListAndMapping,
|
seasons: ItemListAndMapping,
|
||||||
|
similar: List<BaseItem>,
|
||||||
people: List<Person>,
|
people: List<Person>,
|
||||||
played: Boolean,
|
played: Boolean,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
|
|
@ -302,6 +306,28 @@ fun SeriesDetailsContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (similar.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
ItemRow(
|
||||||
|
title = stringResource(R.string.more_like_this),
|
||||||
|
items = similar,
|
||||||
|
onClickItem = onClickItem,
|
||||||
|
onLongClickItem = {},
|
||||||
|
cardContent = { index, item, mod, onClick, onLongClick ->
|
||||||
|
SeasonCard(
|
||||||
|
item = item,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
modifier = mod,
|
||||||
|
showImageOverlay = true,
|
||||||
|
imageHeight = Cards.height2x3,
|
||||||
|
imageWidth = Dp.Unspecified,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,13 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
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.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
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.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.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
|
@ -47,6 +49,7 @@ import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
import org.jellyfin.sdk.model.api.SortOrder
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
|
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -57,6 +60,7 @@ class SeriesViewModel
|
||||||
constructor(
|
constructor(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
@param:ApplicationContext val context: Context,
|
@param:ApplicationContext val context: Context,
|
||||||
|
private val serverRepository: ServerRepository,
|
||||||
private val navigationManager: NavigationManager,
|
private val navigationManager: NavigationManager,
|
||||||
private val itemPlaybackRepository: ItemPlaybackRepository,
|
private val itemPlaybackRepository: ItemPlaybackRepository,
|
||||||
private val themeSongPlayer: ThemeSongPlayer,
|
private val themeSongPlayer: ThemeSongPlayer,
|
||||||
|
|
@ -67,6 +71,7 @@ class SeriesViewModel
|
||||||
val seasons = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty())
|
val seasons = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty())
|
||||||
val episodes = MutableLiveData<EpisodeList>(EpisodeList.Loading)
|
val episodes = MutableLiveData<EpisodeList>(EpisodeList.Loading)
|
||||||
val people = MutableLiveData<List<Person>>(listOf())
|
val people = MutableLiveData<List<Person>>(listOf())
|
||||||
|
val similar = MutableLiveData<List<BaseItem>>(listOf())
|
||||||
|
|
||||||
fun init(
|
fun init(
|
||||||
prefs: UserPreferences,
|
prefs: UserPreferences,
|
||||||
|
|
@ -102,6 +107,18 @@ class SeriesViewModel
|
||||||
people.map { Person.fromDto(it, api) }
|
people.map { Person.fromDto(it, api) }
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
}
|
}
|
||||||
|
val similar =
|
||||||
|
api.libraryApi
|
||||||
|
.getSimilarItems(
|
||||||
|
GetSimilarItemsRequest(
|
||||||
|
userId = serverRepository.currentUser?.id,
|
||||||
|
itemId = itemId,
|
||||||
|
fields = SlimItemFields,
|
||||||
|
limit = 25,
|
||||||
|
),
|
||||||
|
).content.items
|
||||||
|
.map { BaseItem.from(it, api, true) }
|
||||||
|
this@SeriesViewModel.similar.setValueOnMain(similar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ 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.ItemPlaybackRepository
|
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
||||||
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
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.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
|
|
@ -54,7 +55,9 @@ import com.github.damontecres.wholphin.data.model.Person
|
||||||
import com.github.damontecres.wholphin.data.model.chooseSource
|
import com.github.damontecres.wholphin.data.model.chooseSource
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.Cards
|
import com.github.damontecres.wholphin.ui.Cards
|
||||||
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.cards.ChapterRow
|
import com.github.damontecres.wholphin.ui.cards.ChapterRow
|
||||||
|
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
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||||
|
|
@ -76,6 +79,7 @@ import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
@ -85,9 +89,11 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
|
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
@ -100,13 +106,16 @@ class MovieViewModel
|
||||||
constructor(
|
constructor(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
|
val serverRepository: ServerRepository,
|
||||||
val itemPlaybackRepository: ItemPlaybackRepository,
|
val itemPlaybackRepository: ItemPlaybackRepository,
|
||||||
) : LoadingItemViewModel(api) {
|
) : LoadingItemViewModel(api) {
|
||||||
private lateinit var itemId: UUID
|
private lateinit var itemId: UUID
|
||||||
|
|
||||||
|
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 similar = MutableLiveData<List<BaseItem>>(listOf())
|
||||||
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
||||||
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
|
||||||
|
|
||||||
override fun init(
|
override fun init(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
|
|
@ -156,6 +165,19 @@ class MovieViewModel
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
chapters.value = Chapter.fromDto(item.data, api)
|
chapters.value = Chapter.fromDto(item.data, api)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val similar =
|
||||||
|
api.libraryApi
|
||||||
|
.getSimilarItems(
|
||||||
|
GetSimilarItemsRequest(
|
||||||
|
userId = serverRepository.currentUser?.id,
|
||||||
|
itemId = itemId,
|
||||||
|
fields = SlimItemFields,
|
||||||
|
limit = 25,
|
||||||
|
),
|
||||||
|
).content.items
|
||||||
|
.map { BaseItem.from(it, api) }
|
||||||
|
this@MovieViewModel.similar.setValueOnMain(similar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -226,6 +248,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 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)
|
||||||
|
|
||||||
|
|
@ -247,6 +270,10 @@ fun MovieDetails(
|
||||||
people = people,
|
people = people,
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
trailers = trailers,
|
trailers = trailers,
|
||||||
|
similar = similar,
|
||||||
|
onClickItem = {
|
||||||
|
viewModel.navigationManager.navigateTo(it.destination())
|
||||||
|
},
|
||||||
playOnClick = {
|
playOnClick = {
|
||||||
viewModel.navigationManager.navigateTo(
|
viewModel.navigationManager.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
|
|
@ -375,11 +402,13 @@ fun MovieDetailsContent(
|
||||||
people: List<Person>,
|
people: List<Person>,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
trailers: List<Trailer>,
|
trailers: List<Trailer>,
|
||||||
|
similar: List<BaseItem>,
|
||||||
playOnClick: (Duration) -> Unit,
|
playOnClick: (Duration) -> Unit,
|
||||||
trailerOnClick: (Trailer) -> Unit,
|
trailerOnClick: (Trailer) -> Unit,
|
||||||
overviewOnClick: () -> Unit,
|
overviewOnClick: () -> Unit,
|
||||||
watchOnClick: () -> Unit,
|
watchOnClick: () -> Unit,
|
||||||
moreOnClick: () -> Unit,
|
moreOnClick: () -> Unit,
|
||||||
|
onClickItem: (BaseItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
@ -492,6 +521,28 @@ fun MovieDetailsContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (similar.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
ItemRow(
|
||||||
|
title = stringResource(R.string.more_like_this),
|
||||||
|
items = similar,
|
||||||
|
onClickItem = onClickItem,
|
||||||
|
onLongClickItem = {},
|
||||||
|
cardContent = { index, item, mod, onClick, onLongClick ->
|
||||||
|
SeasonCard(
|
||||||
|
item = item,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
modifier = mod,
|
||||||
|
showImageOverlay = true,
|
||||||
|
imageHeight = Cards.height2x3,
|
||||||
|
imageWidth = Dp.Unspecified,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,5 +84,6 @@
|
||||||
<string name="direct_play_pgs">Direct play PGS subtitles</string>
|
<string name="direct_play_pgs">Direct play PGS subtitles</string>
|
||||||
<string name="trailers">Trailers</string>
|
<string name="trailers">Trailers</string>
|
||||||
<string name="pass_out_protection">Passout Protection</string>
|
<string name="pass_out_protection">Passout Protection</string>
|
||||||
|
<string name="more_like_this">More like this</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue