implement latest library content loading and refactor Home screen state

- Implement `loadLatestLibraryContent` in `InMemoryMediaRepository` to fetch and categorize latest media from Jellyfin libraries.
- Update `HomePageViewModel` to use `mapLatest` and `stateIn` for asynchronous, thread-safe loading of "Continue Watching" and "Latest" content.
- Refactor `HomePage` and its UI components (`HomeContent`, `HomeDrawer`, `HomeSections`) to pass data and callbacks as parameters instead of using `hiltViewModel()` internally.
- Enhance `PosterCard` and `ContinueWatchingCard` with explicit click listeners and image request optimization using `Coil`.
- Add `SeasonMedia` type to the `Media` sealed class to support more granular library item tracking.
- Standardize `UUID` usage for media selection callbacks across `LibraryViewModel` and common UI components.
- Improve UI styling by replacing shadows with subtle borders and consistent corner radii on media cards.
This commit is contained in:
2026-02-01 07:56:46 +01:00
parent b643988ed4
commit 88b34c4780
10 changed files with 252 additions and 175 deletions

View File

@@ -35,7 +35,8 @@ fun HomePage(
val drawerState = rememberDrawerState(DrawerValue.Closed)
val coroutineScope = rememberCoroutineScope()
val libraries = viewModel.libraries.collectAsState().value.map {
val libraries = viewModel.libraries.collectAsState().value
val libraryNavItems = libraries.map {
HomeNavItem(
id = it.id,
label = it.name,
@@ -47,6 +48,7 @@ fun HomePage(
)
}
val continueWatching = viewModel.continueWatching.collectAsState()
val latestLibraryContent = viewModel.latestLibraryContent.collectAsState()
ModalNavigationDrawer(
drawerState = drawerState,
@@ -61,9 +63,11 @@ fun HomePage(
HomeDrawerContent(
title = "Jellyfin",
subtitle = "Library Dashboard",
primaryNavItems = libraries,
primaryNavItems = libraryNavItems,
secondaryNavItems = HomeMockData.secondaryNavItems,
user = HomeMockData.user,
onLibrarySelected = viewModel::onLibrarySelected,
onLogout = viewModel::logout
)
}
}
@@ -79,7 +83,12 @@ fun HomePage(
}
) { innerPadding ->
HomeContent(
libraries = libraries,
libraryContent = latestLibraryContent.value,
continueWatching = continueWatching.value,
onMovieSelected = viewModel::onMovieSelected,
onSeriesSelected = viewModel::onSeriesSelected,
onEpisodeSelected = viewModel::onEpisodeSelected,
modifier = Modifier.padding(innerPadding)
)
}

View File

@@ -18,14 +18,16 @@ import hu.bbara.purefin.navigation.NavigationManager
import hu.bbara.purefin.navigation.Route
import hu.bbara.purefin.navigation.SeriesDto
import hu.bbara.purefin.session.UserSessionRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
@@ -49,42 +51,106 @@ class HomePageViewModel @Inject constructor(
private val _libraries = MutableStateFlow<List<LibraryItem>>(emptyList())
val libraries = _libraries.asStateFlow()
val continueWatching = mediaRepository.continueWatching.map { list ->
list.map {
when ( it ) {
is Media.MovieMedia -> {
val movie = mediaRepository.getMovie(it.movieId)
ContinueWatchingItem(
type = BaseItemKind.MOVIE,
movie = movie
)
}
is Media.EpisodeMedia -> {
val episode = mediaRepository.getEpisode(
seriesId = it.seriesId,
episodeId = it.episodeId
)
ContinueWatchingItem(
type = BaseItemKind.EPISODE,
episode = episode
)
}
else -> throw UnsupportedOperationException("Unsupported item type: $it")
init {
viewModelScope.launch {
loadLibraries()
}
}
val continueWatching = mediaRepository.continueWatching
.mapLatest { list ->
withContext(Dispatchers.IO) {
list.map { media ->
when (media) {
is Media.MovieMedia -> {
val movie = mediaRepository.getMovie(media.movieId)
ContinueWatchingItem(
type = BaseItemKind.MOVIE,
movie = movie
)
}
is Media.EpisodeMedia -> {
val episode = mediaRepository.getEpisode(
seriesId = media.seriesId,
episodeId = media.episodeId
)
ContinueWatchingItem(
type = BaseItemKind.EPISODE,
episode = episode
)
}
else -> throw UnsupportedOperationException("Unsupported item type: $media")
}
}.distinctBy { it.id }
}
}
}.distinctUntilChanged()
.distinctUntilChanged()
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList()
)
private val _latestLibraryContent = MutableStateFlow<Map<UUID, List<PosterItem>>>(emptyMap())
val latestLibraryContent = _latestLibraryContent.asStateFlow()
val latestLibraryContent = mediaRepository.latestLibraryContent
.mapLatest { libraryMap ->
withContext(Dispatchers.IO) {
libraryMap.mapValues { (_, items) ->
items.map { media ->
when (media) {
is Media.MovieMedia -> {
val movie = mediaRepository.getMovie(media.movieId)
PosterItem(
type = BaseItemKind.MOVIE,
movie = movie
)
}
is Media.EpisodeMedia -> {
val episode = mediaRepository.getEpisode(
seriesId = media.seriesId,
episodeId = media.episodeId
)
PosterItem(
type = BaseItemKind.EPISODE,
episode = episode
)
}
is Media.SeriesMedia -> {
val series = mediaRepository.getSeries(media.id)
PosterItem(
type = BaseItemKind.SERIES,
series = series
)
}
is Media.SeasonMedia -> {
val series = mediaRepository.getSeries(media.seriesId)
PosterItem(
type = BaseItemKind.SERIES,
series = series
)
}
else -> throw UnsupportedOperationException("Unsupported item type: $media")
}
}.distinctBy { it.id }
}
}
}
.distinctUntilChanged()
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyMap()
)
init {
viewModelScope.launch { mediaRepository.ensureReady() }
loadHomePageData()
}
fun onLibrarySelected(library : HomeNavItem) {
@@ -132,19 +198,7 @@ class HomePageViewModel @Inject constructor(
navigationManager.replaceAll(Route.Home)
}
fun loadContinueWatching() {
viewModelScope.launch {
// mediaRepository.loadContinueWatching()
}
}
fun loadLibraries() {
viewModelScope.launch {
// mediaRepository.loadLibraries()
}
}
private suspend fun loadLibrariesInternal() {
private suspend fun loadLibraries() {
val libraries: List<BaseItemDto> = jellyfinApiClient.getLibraries()
val mappedLibraries = libraries.map {
LibraryItem(
@@ -157,72 +211,6 @@ class HomePageViewModel @Inject constructor(
_libraries.value = mappedLibraries
}
fun loadAllShownLibraryItems() {
viewModelScope.launch {
if (_libraries.value.isEmpty()) {
loadLibrariesInternal()
}
_libraries.value.forEach { library ->
loadLatestLibraryItems(library.id)
}
}
}
fun loadLatestLibraryItems(libraryId: UUID) {
viewModelScope.launch {
val latestLibraryItems = jellyfinApiClient.getLatestFromLibrary(libraryId)
val latestLibraryPosterItem = latestLibraryItems.map {
when (it.type) {
BaseItemKind.MOVIE -> {
val movie = mediaRepository.getMovie(it.id)
PosterItem(
type = BaseItemKind.MOVIE,
movie = movie
)
}
BaseItemKind.EPISODE -> {
val episode = mediaRepository.getEpisode(
it.seriesId!!,
it.parentId!!,
it.id
)
PosterItem(
type = BaseItemKind.EPISODE,
episode = episode
)
}
BaseItemKind.SEASON -> {
val series = mediaRepository.getSeries(
seriesId = it.seriesId!!
)
PosterItem(
type = BaseItemKind.SERIES,
series = series
)
}
BaseItemKind.SERIES -> {
val series = mediaRepository.getSeries(
seriesId = it.id
)
PosterItem(
type = BaseItemKind.SERIES,
series = series
)
}
else -> throw UnsupportedOperationException("Unsupported item type: ${it.type}")
}
}.distinctBy { it.id }
_latestLibraryContent.update { currentMap ->
currentMap + (libraryId to latestLibraryPosterItem)
}
}
}
fun loadHomePageData() {
loadContinueWatching()
loadAllShownLibraryItems()
}
fun getImageUrl(itemId: UUID, type: ImageType): String {
return JellyfinImageHelper.toImageUrl(
url = _url.value,

View File

@@ -8,23 +8,20 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.app.home.HomePageViewModel
import org.jellyfin.sdk.model.UUID
@Composable
fun HomeContent(
viewModel: HomePageViewModel = hiltViewModel(),
libraries: List<LibraryItem>,
libraryContent: Map<UUID, List<PosterItem>>,
continueWatching: List<ContinueWatchingItem>,
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier
) {
val libraries by viewModel.libraries.collectAsState()
val libraryContent by viewModel.latestLibraryContent.collectAsState()
LazyColumn(
modifier = modifier
.fillMaxSize()
@@ -35,7 +32,9 @@ fun HomeContent(
}
item {
ContinueWatchingSection(
items = continueWatching
items = continueWatching,
onMovieSelected = onMovieSelected,
onEpisodeSelected = onEpisodeSelected
)
}
items(
@@ -46,6 +45,9 @@ fun HomeContent(
title = item.name,
items = libraryContent[item.id] ?: emptyList(),
action = "See All",
onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected
)
}
item {

View File

@@ -26,8 +26,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.app.home.HomePageViewModel
@Composable
fun HomeDrawerContent(
@@ -36,6 +34,8 @@ fun HomeDrawerContent(
primaryNavItems: List<HomeNavItem>,
secondaryNavItems: List<HomeNavItem>,
user: HomeUser,
onLibrarySelected: (HomeNavItem) -> Unit,
onLogout: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.fillMaxSize()) {
@@ -45,10 +45,11 @@ fun HomeDrawerContent(
)
HomeDrawerNav(
primaryItems = primaryNavItems,
secondaryItems = secondaryNavItems
secondaryItems = secondaryNavItems,
onLibrarySelected = onLibrarySelected
)
Spacer(modifier = Modifier.weight(1f))
HomeDrawerFooter(user = user)
HomeDrawerFooter(user = user, onLogout = onLogout)
}
}
@@ -100,6 +101,7 @@ fun HomeDrawerHeader(
fun HomeDrawerNav(
primaryItems: List<HomeNavItem>,
secondaryItems: List<HomeNavItem>,
onLibrarySelected: (HomeNavItem) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@@ -108,7 +110,7 @@ fun HomeDrawerNav(
.padding(vertical = 16.dp)
) {
primaryItems.forEach { item ->
HomeDrawerNavItem(item = item)
HomeDrawerNavItem(item = item, onLibrarySelected = onLibrarySelected)
}
if (secondaryItems.isNotEmpty()) {
HorizontalDivider(
@@ -117,7 +119,7 @@ fun HomeDrawerNav(
color = MaterialTheme.colorScheme.outlineVariant
)
secondaryItems.forEach { item ->
HomeDrawerNavItem(item = item)
HomeDrawerNavItem(item = item, onLibrarySelected = onLibrarySelected)
}
}
}
@@ -127,7 +129,7 @@ fun HomeDrawerNav(
fun HomeDrawerNavItem(
item: HomeNavItem,
modifier: Modifier = Modifier,
viewModel: HomePageViewModel = hiltViewModel(),
onLibrarySelected: (HomeNavItem) -> Unit
) {
val scheme = MaterialTheme.colorScheme
val background = if (item.selected) scheme.primary.copy(alpha = 0.12f) else Color.Transparent
@@ -137,7 +139,7 @@ fun HomeDrawerNavItem(
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
.background(background, RoundedCornerShape(12.dp))
.clickable { viewModel.onLibrarySelected(item) }
.clickable { onLibrarySelected(item) }
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
@@ -158,8 +160,8 @@ fun HomeDrawerNavItem(
@Composable
fun HomeDrawerFooter (
viewModel: HomePageViewModel = hiltViewModel(),
user: HomeUser,
onLogout: () -> Unit,
modifier: Modifier = Modifier,
) {
val scheme = MaterialTheme.colorScheme
@@ -181,7 +183,7 @@ fun HomeDrawerFooter (
iconTint = scheme.onBackground
)
Column(modifier = Modifier.padding(start = 12.dp)
.clickable {viewModel.logout()}) {
.clickable { onLogout() }) {
Text(
text = user.name,
color = scheme.onBackground,

View File

@@ -2,6 +2,7 @@ package hu.bbara.purefin.app.home.ui
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -32,25 +33,26 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.app.home.HomePageViewModel
import coil3.request.ImageRequest
import hu.bbara.purefin.common.ui.PosterCard
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.player.PlayerActivity
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
import kotlin.math.nextUp
@Composable
fun ContinueWatchingSection(
items: List<ContinueWatchingItem>,
onMovieSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier
) {
SectionHeader(
@@ -65,7 +67,9 @@ fun ContinueWatchingSection(
items(
items = items, key = { it.id }) { item ->
ContinueWatchingCard(
item = item
item = item,
onMovieSelected = onMovieSelected,
onEpisodeSelected = onEpisodeSelected
)
}
}
@@ -75,42 +79,55 @@ fun ContinueWatchingSection(
fun ContinueWatchingCard(
item: ContinueWatchingItem,
modifier: Modifier = Modifier,
viewModel: HomePageViewModel = hiltViewModel()
onMovieSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
) {
val scheme = MaterialTheme.colorScheme
val context = LocalContext.current
val density = LocalDensity.current
val imageUrl = when (item.type) {
BaseItemKind.MOVIE -> item.movie?.heroImageUrl
BaseItemKind.EPISODE -> item.episode?.heroImageUrl
else -> null
}
val cardWidth = 280.dp
val cardHeight = cardWidth * 9 / 16
fun openItem(item: ContinueWatchingItem) {
when (item.type) {
BaseItemKind.MOVIE -> viewModel.onMovieSelected(item.movie!!.id)
BaseItemKind.MOVIE -> onMovieSelected(item.movie!!.id)
BaseItemKind.EPISODE -> {
val episode = item.episode!!
viewModel.onEpisodeSelected(
seriesId = episode.seriesId,
seasonId = episode.seasonId,
episodeId = episode.id
)
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
}
else -> {}
}
}
val imageRequest = ImageRequest.Builder(context)
.data(imageUrl)
.size(with(density) { cardWidth.roundToPx() }, with(density) { cardHeight.roundToPx() })
.build()
Column(
modifier = modifier
.width(280.dp)
.width(cardWidth)
.wrapContentHeight()
) {
Box(
modifier = Modifier
.width(cardWidth)
.aspectRatio(16f / 9f)
.shadow(12.dp, RoundedCornerShape(16.dp))
.clip(RoundedCornerShape(16.dp))
.border(1.dp, scheme.outlineVariant.copy(alpha = 0.3f), RoundedCornerShape(16.dp))
.background(scheme.surfaceVariant)
) {
PurefinAsyncImage(
model = viewModel.getImageUrl(itemId = item.id, type = ImageType.PRIMARY),
model = imageRequest,
contentDescription = null,
modifier = Modifier
.fillMaxSize()
@@ -187,7 +204,9 @@ fun LibraryPosterSection(
items: List<PosterItem>,
action: String?,
modifier: Modifier = Modifier,
viewModel: HomePageViewModel = hiltViewModel()
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
) {
SectionHeader(
title = title,
@@ -202,15 +221,9 @@ fun LibraryPosterSection(
items = items, key = { it.id }) { item ->
PosterCard(
item = item,
onMovieSelected = { viewModel.onMovieSelected(item.movie!!.id) },
onSeriesSelected = { viewModel.onSeriesSelected(item.series!!.id) },
onEpisodeSelected = {
viewModel.onEpisodeSelected(
seriesId = item.episode!!.seriesId,
seasonId = item.episode.seasonId,
episodeId = item.episode.id
)
}
onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected
)
}
}

View File

@@ -42,19 +42,19 @@ class LibraryViewModel @Inject constructor(
viewModelScope.launch { mediaRepository.ensureReady() }
}
fun onMovieSelected(movieId: String) {
fun onMovieSelected(movieId: UUID) {
navigationManager.navigate(Route.MovieRoute(
MovieDto(
id = UUID.fromString(movieId),
id = movieId,
)
))
}
fun onSeriesSelected(seriesId: String) {
fun onSeriesSelected(seriesId: UUID) {
viewModelScope.launch {
navigationManager.navigate(Route.SeriesRoute(
SeriesDto(
id = UUID.fromString(seriesId),
id = seriesId,
)
))
}

View File

@@ -86,10 +86,9 @@ internal fun LibraryPosterGrid(
items(libraryItems) { item ->
PosterCard(
item = item,
onMovieSelected = { viewModel.onMovieSelected(item.id.toString()) },
onSeriesSelected = { viewModel.onSeriesSelected(item.id.toString()) },
onEpisodeSelected = {
}
onMovieSelected = viewModel::onMovieSelected,
onSeriesSelected = viewModel::onSeriesSelected,
onEpisodeSelected = { _, _, _ -> }
)
}
}

View File

@@ -1,6 +1,7 @@
package hu.bbara.purefin.common.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
@@ -12,45 +13,62 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.request.ImageRequest
import hu.bbara.purefin.app.home.ui.PosterItem
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind
@Composable
fun PosterCard(
item: PosterItem,
modifier: Modifier = Modifier,
onMovieSelected: (String) -> Unit,
onSeriesSelected: (String) -> Unit,
onEpisodeSelected: (String) -> Unit,
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
) {
val scheme = MaterialTheme.colorScheme
val context = LocalContext.current
val density = LocalDensity.current
val posterWidth = 144.dp
val posterHeight = posterWidth * 3 / 2
fun openItem(posterItem: PosterItem) {
when (posterItem.type) {
BaseItemKind.MOVIE -> onMovieSelected(posterItem.id.toString())
BaseItemKind.SERIES -> onSeriesSelected(posterItem.id.toString())
BaseItemKind.EPISODE -> onEpisodeSelected(posterItem.id.toString())
BaseItemKind.MOVIE -> onMovieSelected(posterItem.id)
BaseItemKind.SERIES -> onSeriesSelected(posterItem.id)
BaseItemKind.EPISODE -> onEpisodeSelected(
posterItem.episode!!.seriesId,
posterItem.episode.seasonId,
posterItem.episode.id
)
else -> {}
}
}
val imageRequest = ImageRequest.Builder(context)
.data(item.imageUrl)
.size(with(density) { posterWidth.roundToPx() }, with(density) { posterHeight.roundToPx() })
.build()
Column(
modifier = Modifier
.width(144.dp)
.width(posterWidth)
) {
PurefinAsyncImage(
model = item.imageUrl,
model = imageRequest,
contentDescription = null,
modifier = Modifier
.aspectRatio(2f / 3f)
.shadow(10.dp, RoundedCornerShape(14.dp))
.clip(RoundedCornerShape(14.dp))
.border(1.dp, scheme.outlineVariant.copy(alpha = 0.3f), RoundedCornerShape(14.dp))
.background(scheme.surfaceVariant)
.clickable(onClick = { openItem(item) }),
contentScale = ContentScale.Crop

View File

@@ -16,6 +16,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto
@@ -40,16 +41,19 @@ class InMemoryMediaRepository @Inject constructor(
private val ready = CompletableDeferred<Unit>()
private val _state: MutableStateFlow<MediaRepositoryState> = MutableStateFlow(MediaRepositoryState.Loading)
override val state: StateFlow<MediaRepositoryState> = _state
override val state: StateFlow<MediaRepositoryState> = _state.asStateFlow()
private val _movies : MutableStateFlow<Map<UUID, Movie>> = MutableStateFlow(emptyMap())
override val movies: StateFlow<Map<UUID, Movie>> = _movies
override val movies: StateFlow<Map<UUID, Movie>> = _movies.asStateFlow()
private val _series : MutableStateFlow<Map<UUID, Series>> = MutableStateFlow(emptyMap())
override val series: StateFlow<Map<UUID, Series>> = _series
override val series: StateFlow<Map<UUID, Series>> = _series.asStateFlow()
private val _continueWatching: MutableStateFlow<List<Media>> = MutableStateFlow(emptyList())
val continueWatching: StateFlow<List<Media>> = _continueWatching
val continueWatching: StateFlow<List<Media>> = _continueWatching.asStateFlow()
private val _latestLibraryContent: MutableStateFlow<Map<UUID, List<Media>>> = MutableStateFlow(emptyMap())
val latestLibraryContent: StateFlow<Map<UUID, List<Media>>> = _latestLibraryContent.asStateFlow()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@@ -65,6 +69,7 @@ class InMemoryMediaRepository @Inject constructor(
try {
loadLibraries()
loadContinueWatching()
loadLatestLibraryContent()
_state.value = MediaRepositoryState.Ready
ready.complete(Unit)
} catch (t: Throwable) {
@@ -157,6 +162,46 @@ class InMemoryMediaRepository @Inject constructor(
}
}
suspend fun loadLatestLibraryContent() {
// TODO Make libraries accessible in a field or something that is not this ugly.
val librariesItem = jellyfinApiClient.getLibraries()
val filterLibraries =
librariesItem.filter { it.collectionType == CollectionType.MOVIES || it.collectionType == CollectionType.TVSHOWS }
val latestLibraryContents = filterLibraries.associate { library ->
val latestFromLibrary = jellyfinApiClient.getLatestFromLibrary(library.id)
library.id to when (library.collectionType) {
CollectionType.MOVIES -> {
latestFromLibrary.map {
val movie = it.toMovie(serverUrl(), library.id)
Media.MovieMedia(movieId = movie.id)
}
}
CollectionType.TVSHOWS -> {
latestFromLibrary.map {
when (it.type) {
BaseItemKind.SERIES -> {
val series = it.toSeries(serverUrl(), library.id)
Media.SeriesMedia(seriesId = series.id)
}
BaseItemKind.SEASON -> {
val season = it.toSeason(serverUrl())
Media.SeasonMedia(seasonId = season.id, seriesId = season.seriesId)
}
BaseItemKind.EPISODE -> {
val episode = it.toEpisode(serverUrl())
Media.EpisodeMedia(episodeId = episode.id, seriesId = episode.seriesId)
} else -> throw UnsupportedOperationException("Unsupported item type: ${it.type}")
}
}
}
else -> throw UnsupportedOperationException("Unsupported library type: ${library.collectionType}")
}
}
_latestLibraryContent.value = latestLibraryContents
//TODO Load seasons and episodes, other types are already loaded at this point.
}
override suspend fun getMovie(movieId: UUID): Movie {
awaitReady()
localDataSource.getMovie(movieId)?.let {

View File

@@ -9,5 +9,6 @@ sealed class Media(
) {
class MovieMedia(val movieId: UUID) : Media(movieId, BaseItemKind.MOVIE)
class SeriesMedia(val seriesId: UUID) : Media(seriesId, BaseItemKind.SERIES)
class SeasonMedia(val seasonId: UUID, val seriesId: UUID) : Media(seasonId, BaseItemKind.SEASON)
class EpisodeMedia(val episodeId: UUID, val seriesId: UUID) : Media(episodeId, BaseItemKind.EPISODE)
}