Merge branch 'main' into fea/live-tv

This commit is contained in:
Damontecres 2025-10-21 23:30:38 -04:00
commit 0002dece8c
No known key found for this signature in database
4 changed files with 96 additions and 1 deletions

View file

@ -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(),
)
}
}
} }
} }
} }

View file

@ -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)
} }
} }

View file

@ -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(),
)
}
}
} }
} }
} }

View file

@ -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>