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.Color
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.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -100,6 +101,7 @@ fun SeriesDetails(
val item by viewModel.item.observeAsState()
val seasons by viewModel.seasons.observeAsState(ItemListAndMapping.empty())
val people by viewModel.people.observeAsState(listOf())
val similar by viewModel.similar.observeAsState(listOf())
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var showWatchConfirmation by remember { mutableStateOf(false) }
@ -118,6 +120,7 @@ fun SeriesDetails(
series = item,
seasons = seasons,
people = people,
similar = similar,
played = played,
modifier = modifier,
onClickItem = { viewModel.navigateTo(it.destination()) },
@ -180,6 +183,7 @@ fun SeriesDetailsContent(
preferences: UserPreferences,
series: BaseItem,
seasons: ItemListAndMapping,
similar: List<BaseItem>,
people: List<Person>,
played: Boolean,
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 com.github.damontecres.wholphin.data.ChosenStreams
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.ItemPlayback
import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
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.letNotEmpty
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.request.GetEpisodesRequest
import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@ -57,6 +60,7 @@ class SeriesViewModel
constructor(
api: ApiClient,
@param:ApplicationContext val context: Context,
private val serverRepository: ServerRepository,
private val navigationManager: NavigationManager,
private val itemPlaybackRepository: ItemPlaybackRepository,
private val themeSongPlayer: ThemeSongPlayer,
@ -67,6 +71,7 @@ class SeriesViewModel
val seasons = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty())
val episodes = MutableLiveData<EpisodeList>(EpisodeList.Loading)
val people = MutableLiveData<List<Person>>(listOf())
val similar = MutableLiveData<List<BaseItem>>(listOf())
fun init(
prefs: UserPreferences,
@ -102,6 +107,18 @@ class SeriesViewModel
people.map { Person.fromDto(it, api) }
}.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.data.ChosenStreams
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.Chapter
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.preferences.UserPreferences
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.ItemRow
import com.github.damontecres.wholphin.ui.cards.PersonRow
import com.github.damontecres.wholphin.ui.cards.SeasonCard
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.nav.Destination
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.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
@ -85,9 +89,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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.userLibraryApi
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.serializer.toUUID
import java.util.UUID
@ -100,13 +106,16 @@ class MovieViewModel
constructor(
api: ApiClient,
val navigationManager: NavigationManager,
val serverRepository: ServerRepository,
val itemPlaybackRepository: ItemPlaybackRepository,
) : LoadingItemViewModel(api) {
private lateinit var itemId: UUID
val trailers = MutableLiveData<List<Trailer>>(listOf())
val people = MutableLiveData<List<Person>>(listOf())
val chapters = MutableLiveData<List<Chapter>>(listOf())
val similar = MutableLiveData<List<BaseItem>>(listOf())
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
val trailers = MutableLiveData<List<Trailer>>(listOf())
override fun init(
itemId: UUID,
@ -156,6 +165,19 @@ class MovieViewModel
}.orEmpty()
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 chapters by viewModel.chapters.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 chosenStreams by viewModel.chosenStreams.observeAsState(null)
@ -247,6 +270,10 @@ fun MovieDetails(
people = people,
chapters = chapters,
trailers = trailers,
similar = similar,
onClickItem = {
viewModel.navigationManager.navigateTo(it.destination())
},
playOnClick = {
viewModel.navigationManager.navigateTo(
Destination.Playback(
@ -375,11 +402,13 @@ fun MovieDetailsContent(
people: List<Person>,
chapters: List<Chapter>,
trailers: List<Trailer>,
similar: List<BaseItem>,
playOnClick: (Duration) -> Unit,
trailerOnClick: (Trailer) -> Unit,
overviewOnClick: () -> Unit,
watchOnClick: () -> Unit,
moreOnClick: () -> Unit,
onClickItem: (BaseItem) -> Unit,
modifier: Modifier = Modifier,
) {
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="trailers">Trailers</string>
<string name="pass_out_protection">Passout Protection</string>
<string name="more_like_this">More like this</string>
</resources>