refactor: Do not use JellyfinApiClient in viewModels. Use MediaRepository for consistency

This commit is contained in:
2026-02-18 18:37:57 +01:00
parent 1a46247da0
commit 9a9cb9c2e7
3 changed files with 27 additions and 88 deletions

View File

@@ -3,30 +3,23 @@ package hu.bbara.purefin.app.content.movie
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.client.JellyfinApiClient import hu.bbara.purefin.data.MediaRepository
import hu.bbara.purefin.data.model.Movie
import hu.bbara.purefin.download.DownloadState import hu.bbara.purefin.download.DownloadState
import hu.bbara.purefin.download.MediaDownloadManager import hu.bbara.purefin.download.MediaDownloadManager
import hu.bbara.purefin.image.JellyfinImageHelper
import hu.bbara.purefin.navigation.NavigationManager import hu.bbara.purefin.navigation.NavigationManager
import hu.bbara.purefin.navigation.Route import hu.bbara.purefin.navigation.Route
import hu.bbara.purefin.session.UserSessionRepository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemPerson
import org.jellyfin.sdk.model.api.ImageType
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MovieScreenViewModel @Inject constructor( class MovieScreenViewModel @Inject constructor(
private val jellyfinApiClient: JellyfinApiClient, private val mediaRepository: MediaRepository,
private val navigationManager: NavigationManager, private val navigationManager: NavigationManager,
private val userSessionRepository: UserSessionRepository,
private val mediaDownloadManager: MediaDownloadManager private val mediaDownloadManager: MediaDownloadManager
): ViewModel() { ): ViewModel() {
@@ -47,15 +40,12 @@ class MovieScreenViewModel @Inject constructor(
fun selectMovie(movieId: UUID) { fun selectMovie(movieId: UUID) {
viewModelScope.launch { viewModelScope.launch {
val movieInfo = jellyfinApiClient.getItemInfo(movieId) val movieData = mediaRepository.movies.value[movieId]
if (movieInfo == null) { if (movieData == null) {
_movie.value = null _movie.value = null
return@launch return@launch
} }
val serverUrl = userSessionRepository.serverUrl.first().trim().ifBlank { _movie.value = movieData.toUiModel()
"https://jellyfin.bbara.hu"
}
_movie.value = movieInfo.toUiModel(serverUrl)
launch { launch {
mediaDownloadManager.observeDownloadState(movieId.toString()).collect { mediaDownloadManager.observeDownloadState(movieId.toString()).collect {
@@ -82,54 +72,20 @@ class MovieScreenViewModel @Inject constructor(
} }
} }
private fun BaseItemDto.toUiModel(serverUrl: String): MovieUiModel { private fun Movie.toUiModel(): MovieUiModel {
val year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty()
val rating = officialRating ?: "NR"
val runtime = formatRuntime(runTimeTicks)
val format = container?.uppercase() ?: "VIDEO"
val synopsis = overview ?: "No synopsis available."
val heroImageUrl = id?.let { itemId ->
JellyfinImageHelper.toImageUrl(
url = serverUrl,
itemId = itemId,
type = ImageType.BACKDROP
)
} ?: ""
val cast = people.orEmpty().map { it.toCastMember() }
return MovieUiModel( return MovieUiModel(
id = id, id = id,
title = title,
title = name ?: "Unknown title",
year = year, year = year,
rating = rating, rating = rating,
runtime = runtime, runtime = runtime,
format = format, format = format,
synopsis = synopsis, synopsis = synopsis,
heroImageUrl = heroImageUrl, heroImageUrl = heroImageUrl,
audioTrack = "Default", audioTrack = audioTrack,
subtitles = "Unknown", subtitles = subtitles,
cast = cast cast = cast.map { CastMember(name = it.name, role = it.role, imageUrl = it.imageUrl) }
) )
} }
private fun BaseItemPerson.toCastMember(): CastMember {
return CastMember(
name = name ?: "Unknown",
role = role ?: "",
imageUrl = null
)
}
private fun formatRuntime(ticks: Long?): String {
if (ticks == null || ticks <= 0) return ""
val totalSeconds = ticks / 10_000_000
val hours = TimeUnit.SECONDS.toHours(totalSeconds)
val minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) % 60
return if (hours > 0) {
"${hours}h ${minutes}m"
} else {
"${minutes}m"
}
}
} }

View File

@@ -8,7 +8,6 @@ import hu.bbara.purefin.app.home.ui.HomeNavItem
import hu.bbara.purefin.app.home.ui.LibraryItem import hu.bbara.purefin.app.home.ui.LibraryItem
import hu.bbara.purefin.app.home.ui.NextUpItem import hu.bbara.purefin.app.home.ui.NextUpItem
import hu.bbara.purefin.app.home.ui.PosterItem import hu.bbara.purefin.app.home.ui.PosterItem
import hu.bbara.purefin.client.JellyfinApiClient
import hu.bbara.purefin.data.MediaRepository import hu.bbara.purefin.data.MediaRepository
import hu.bbara.purefin.data.model.Media import hu.bbara.purefin.data.model.Media
import hu.bbara.purefin.domain.usecase.RefreshHomeDataUseCase import hu.bbara.purefin.domain.usecase.RefreshHomeDataUseCase
@@ -20,15 +19,14 @@ import hu.bbara.purefin.navigation.NavigationManager
import hu.bbara.purefin.navigation.Route import hu.bbara.purefin.navigation.Route
import hu.bbara.purefin.navigation.SeriesDto import hu.bbara.purefin.navigation.SeriesDto
import hu.bbara.purefin.session.UserSessionRepository import hu.bbara.purefin.session.UserSessionRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ImageType
import javax.inject.Inject import javax.inject.Inject
@@ -37,7 +35,6 @@ class HomePageViewModel @Inject constructor(
private val mediaRepository: MediaRepository, private val mediaRepository: MediaRepository,
private val userSessionRepository: UserSessionRepository, private val userSessionRepository: UserSessionRepository,
private val navigationManager: NavigationManager, private val navigationManager: NavigationManager,
private val jellyfinApiClient: JellyfinApiClient,
private val refreshHomeDataUseCase: RefreshHomeDataUseCase private val refreshHomeDataUseCase: RefreshHomeDataUseCase
) : ViewModel() { ) : ViewModel() {
@@ -47,8 +44,20 @@ class HomePageViewModel @Inject constructor(
initialValue = "" initialValue = ""
) )
private val _libraries = MutableStateFlow<List<LibraryItem>>(emptyList()) val libraries = mediaRepository.libraries.map { libraries ->
val libraries = _libraries.asStateFlow() libraries.map {
LibraryItem(
id = it.id,
name = it.name,
type = it.type,
isEmpty = when(it.type) {
CollectionType.MOVIES -> mediaRepository.movies.value.isEmpty()
CollectionType.TVSHOWS -> mediaRepository.series.value.isEmpty()
else -> true
}
)
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
val isOfflineMode = userSessionRepository.isOfflineMode.stateIn( val isOfflineMode = userSessionRepository.isOfflineMode.stateIn(
scope = viewModelScope, scope = viewModelScope,
@@ -56,12 +65,6 @@ class HomePageViewModel @Inject constructor(
initialValue = false initialValue = false
) )
init {
viewModelScope.launch {
loadLibraries()
}
}
val continueWatching = combine( val continueWatching = combine(
mediaRepository.continueWatching, mediaRepository.continueWatching,
mediaRepository.movies, mediaRepository.movies,
@@ -182,19 +185,6 @@ class HomePageViewModel @Inject constructor(
navigationManager.replaceAll(Route.Home) navigationManager.replaceAll(Route.Home)
} }
private suspend fun loadLibraries() {
val libraries: List<BaseItemDto> = jellyfinApiClient.getLibraries()
val mappedLibraries = libraries.map {
LibraryItem(
name = it.name!!,
id = it.id,
isEmpty = it.childCount!! == 0,
type = it.collectionType!!
)
}
_libraries.value = mappedLibraries
}
fun getImageUrl(itemId: UUID, type: ImageType): String { fun getImageUrl(itemId: UUID, type: ImageType): String {
return JellyfinImageHelper.toImageUrl( return JellyfinImageHelper.toImageUrl(
url = _url.value, url = _url.value,

View File

@@ -4,28 +4,21 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.app.home.ui.PosterItem import hu.bbara.purefin.app.home.ui.PosterItem
import hu.bbara.purefin.client.JellyfinApiClient
import hu.bbara.purefin.data.MediaRepository import hu.bbara.purefin.data.MediaRepository
import hu.bbara.purefin.data.model.Media
import hu.bbara.purefin.image.JellyfinImageHelper
import hu.bbara.purefin.navigation.MovieDto import hu.bbara.purefin.navigation.MovieDto
import hu.bbara.purefin.navigation.NavigationManager import hu.bbara.purefin.navigation.NavigationManager
import hu.bbara.purefin.navigation.Route import hu.bbara.purefin.navigation.Route
import hu.bbara.purefin.navigation.SeriesDto import hu.bbara.purefin.navigation.SeriesDto
import hu.bbara.purefin.session.UserSessionRepository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.ImageType
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.emptyList
@HiltViewModel @HiltViewModel
class LibraryViewModel @Inject constructor( class LibraryViewModel @Inject constructor(