mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
refactor: modularize app into multi-module architecture
This commit is contained in:
38
feature/shared/build.gradle.kts
Normal file
38
feature/shared/build.gradle.kts
Normal file
@@ -0,0 +1,38 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "hu.bbara.purefin.feature.shared"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 29
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:model"))
|
||||
implementation(project(":core:data"))
|
||||
implementation(project(":feature:download"))
|
||||
implementation(libs.hilt)
|
||||
ksp(libs.hilt.compiler)
|
||||
implementation(libs.jellyfin.core)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package hu.bbara.purefin.feature.shared.content.episode
|
||||
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
|
||||
data class CastMember(
|
||||
val name: String,
|
||||
val role: String,
|
||||
val imageUrl: String?
|
||||
)
|
||||
|
||||
data class EpisodeUiModel(
|
||||
val id: UUID,
|
||||
val title: String,
|
||||
val seasonNumber: Int,
|
||||
val episodeNumber: Int,
|
||||
val releaseDate: String,
|
||||
val rating: String,
|
||||
val runtime: String,
|
||||
val format: String,
|
||||
val synopsis: String,
|
||||
val heroImageUrl: String,
|
||||
val audioTrack: String,
|
||||
val subtitles: String,
|
||||
val cast: List<CastMember>
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
package hu.bbara.purefin.feature.shared.content.episode
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.core.data.MediaRepository
|
||||
import hu.bbara.purefin.core.data.navigation.NavigationManager
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class EpisodeScreenViewModel @Inject constructor(
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val navigationManager: NavigationManager,
|
||||
): ViewModel() {
|
||||
|
||||
private val _episodeId = MutableStateFlow<UUID?>(null)
|
||||
|
||||
val episode: StateFlow<Episode?> = combine(
|
||||
_episodeId,
|
||||
mediaRepository.episodes
|
||||
) { id, episodesMap ->
|
||||
id?.let { episodesMap[it] }
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||
|
||||
init {
|
||||
viewModelScope.launch { mediaRepository.ensureReady() }
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
navigationManager.pop()
|
||||
}
|
||||
|
||||
fun selectEpisode(seriesId: UUID, seasonId: UUID, episodeId: UUID) {
|
||||
_episodeId.value = episodeId
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package hu.bbara.purefin.feature.shared.content.movie
|
||||
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
|
||||
data class CastMember(
|
||||
val name: String,
|
||||
val role: String,
|
||||
val imageUrl: String?
|
||||
)
|
||||
|
||||
data class MovieUiModel(
|
||||
val id: UUID,
|
||||
val title: String,
|
||||
val year: String,
|
||||
val rating: String,
|
||||
val runtime: String,
|
||||
val format: String,
|
||||
val synopsis: String,
|
||||
val heroImageUrl: String,
|
||||
val audioTrack: String,
|
||||
val subtitles: String,
|
||||
val progress: Double?,
|
||||
val cast: List<CastMember>
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
package hu.bbara.purefin.feature.shared.content.movie
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.core.data.MediaRepository
|
||||
import hu.bbara.purefin.core.data.navigation.NavigationManager
|
||||
import hu.bbara.purefin.core.data.navigation.Route
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.feature.download.DownloadState
|
||||
import hu.bbara.purefin.feature.download.MediaDownloadManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MovieScreenViewModel @Inject constructor(
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val navigationManager: NavigationManager,
|
||||
private val mediaDownloadManager: MediaDownloadManager
|
||||
): ViewModel() {
|
||||
|
||||
private val _movie = MutableStateFlow<MovieUiModel?>(null)
|
||||
val movie = _movie.asStateFlow()
|
||||
|
||||
private val _downloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
|
||||
val downloadState: StateFlow<DownloadState> = _downloadState.asStateFlow()
|
||||
|
||||
fun onBack() {
|
||||
navigationManager.pop()
|
||||
}
|
||||
|
||||
|
||||
fun onGoHome() {
|
||||
navigationManager.replaceAll(Route.Home)
|
||||
}
|
||||
|
||||
fun selectMovie(movieId: UUID) {
|
||||
viewModelScope.launch {
|
||||
val movieData = mediaRepository.movies.value[movieId]
|
||||
if (movieData == null) {
|
||||
_movie.value = null
|
||||
return@launch
|
||||
}
|
||||
_movie.value = movieData.toUiModel()
|
||||
|
||||
launch {
|
||||
mediaDownloadManager.observeDownloadState(movieId.toString()).collect {
|
||||
_downloadState.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onDownloadClick() {
|
||||
val movieId = _movie.value?.id ?: return
|
||||
viewModelScope.launch {
|
||||
when (_downloadState.value) {
|
||||
is DownloadState.NotDownloaded, is DownloadState.Failed -> {
|
||||
mediaDownloadManager.downloadMovie(movieId)
|
||||
}
|
||||
is DownloadState.Downloading -> {
|
||||
mediaDownloadManager.cancelDownload(movieId)
|
||||
}
|
||||
is DownloadState.Downloaded -> {
|
||||
mediaDownloadManager.cancelDownload(movieId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Movie.toUiModel(): MovieUiModel {
|
||||
return MovieUiModel(
|
||||
id = id,
|
||||
title = title,
|
||||
year = year,
|
||||
rating = rating,
|
||||
runtime = runtime,
|
||||
format = format,
|
||||
synopsis = synopsis,
|
||||
heroImageUrl = heroImageUrl,
|
||||
audioTrack = audioTrack,
|
||||
subtitles = subtitles,
|
||||
progress = progress,
|
||||
cast = cast.map { CastMember(name = it.name, role = it.role, imageUrl = it.imageUrl) }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package hu.bbara.purefin.feature.shared.content.series
|
||||
|
||||
data class SeriesEpisodeUiModel(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val seasonNumber: Int,
|
||||
val episodeNumber: Int,
|
||||
val description: String,
|
||||
val duration: String,
|
||||
val imageUrl: String,
|
||||
val watched: Boolean,
|
||||
val progress: Double?
|
||||
)
|
||||
|
||||
data class SeriesSeasonUiModel(
|
||||
val name: String,
|
||||
val episodes: List<SeriesEpisodeUiModel>,
|
||||
val unplayedCount: Int?
|
||||
)
|
||||
|
||||
data class SeriesCastMemberUiModel(
|
||||
val name: String,
|
||||
val role: String,
|
||||
val imageUrl: String?
|
||||
)
|
||||
|
||||
data class SeriesUiModel(
|
||||
val title: String,
|
||||
val year: String,
|
||||
val rating: String,
|
||||
val seasons: String,
|
||||
val format: String,
|
||||
val synopsis: String,
|
||||
val heroImageUrl: String,
|
||||
val seasonTabs: List<SeriesSeasonUiModel>,
|
||||
val cast: List<SeriesCastMemberUiModel>
|
||||
) {
|
||||
fun getNextEpisode(): SeriesEpisodeUiModel {
|
||||
for (season in seasonTabs) {
|
||||
for (episode in season.episodes) {
|
||||
if (!episode.watched) {
|
||||
return episode
|
||||
}
|
||||
}
|
||||
}
|
||||
return seasonTabs.first().episodes.first()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package hu.bbara.purefin.feature.shared.content.series
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.core.data.MediaRepository
|
||||
import hu.bbara.purefin.core.data.navigation.EpisodeDto
|
||||
import hu.bbara.purefin.core.data.navigation.NavigationManager
|
||||
import hu.bbara.purefin.core.data.navigation.Route
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SeriesViewModel @Inject constructor(
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val navigationManager: NavigationManager,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _seriesId = MutableStateFlow<UUID?>(null)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val series: StateFlow<Series?> = _seriesId
|
||||
.flatMapLatest { id ->
|
||||
if (id != null) mediaRepository.observeSeriesWithContent(id) else flowOf(null)
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||
|
||||
init {
|
||||
viewModelScope.launch { mediaRepository.ensureReady() }
|
||||
}
|
||||
|
||||
fun onSelectEpisode(seriesId: UUID, seasonId:UUID, episodeId: UUID) {
|
||||
viewModelScope.launch {
|
||||
navigationManager.navigate(Route.EpisodeRoute(
|
||||
EpisodeDto(
|
||||
id = episodeId,
|
||||
seasonId = seasonId,
|
||||
seriesId = seriesId
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
navigationManager.pop()
|
||||
}
|
||||
|
||||
fun onGoHome() {
|
||||
navigationManager.replaceAll(Route.Home)
|
||||
}
|
||||
|
||||
fun selectSeries(seriesId: UUID) {
|
||||
_seriesId.value = seriesId
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package hu.bbara.purefin.feature.shared.home
|
||||
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
|
||||
data class ContinueWatchingItem(
|
||||
val type: BaseItemKind,
|
||||
val movie: Movie? = null,
|
||||
val episode: Episode? = null
|
||||
) {
|
||||
val id: UUID = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.id
|
||||
BaseItemKind.EPISODE -> episode!!.id
|
||||
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
||||
}
|
||||
val primaryText: String = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.title
|
||||
BaseItemKind.EPISODE -> episode!!.title
|
||||
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
||||
}
|
||||
val secondaryText: String = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.year
|
||||
BaseItemKind.EPISODE -> episode!!.releaseDate
|
||||
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
||||
}
|
||||
val progress: Double = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.progress ?: 0.0
|
||||
BaseItemKind.EPISODE -> episode!!.progress ?: 0.0
|
||||
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
||||
}
|
||||
}
|
||||
|
||||
data class NextUpItem(
|
||||
val episode: Episode
|
||||
) {
|
||||
val id: UUID = episode.id
|
||||
val primaryText: String = episode.title
|
||||
val secondaryText: String = episode.releaseDate
|
||||
}
|
||||
|
||||
data class LibraryItem(
|
||||
val id: UUID,
|
||||
val name: String,
|
||||
val type: CollectionType,
|
||||
val isEmpty: Boolean
|
||||
)
|
||||
|
||||
data class PosterItem(
|
||||
val type: BaseItemKind,
|
||||
val movie: Movie? = null,
|
||||
val series: Series? = null,
|
||||
val episode: Episode? = null
|
||||
) {
|
||||
val id: UUID = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.id
|
||||
BaseItemKind.EPISODE -> episode!!.id
|
||||
BaseItemKind.SERIES -> series!!.id
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
val title: String = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.title
|
||||
BaseItemKind.EPISODE -> episode!!.title
|
||||
BaseItemKind.SERIES -> series!!.name
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
val imageUrl: String = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.heroImageUrl
|
||||
BaseItemKind.EPISODE -> episode!!.heroImageUrl
|
||||
BaseItemKind.SERIES -> series!!.heroImageUrl
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
fun watched() = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.watched
|
||||
BaseItemKind.EPISODE -> episode!!.watched
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package hu.bbara.purefin.feature.shared.home
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.core.data.MediaRepository
|
||||
import hu.bbara.purefin.core.model.Media
|
||||
import hu.bbara.purefin.core.data.domain.usecase.RefreshHomeDataUseCase
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.core.data.navigation.EpisodeDto
|
||||
import hu.bbara.purefin.core.data.navigation.LibraryDto
|
||||
import hu.bbara.purefin.core.data.navigation.MovieDto
|
||||
import hu.bbara.purefin.core.data.navigation.NavigationManager
|
||||
import hu.bbara.purefin.core.data.navigation.Route
|
||||
import hu.bbara.purefin.core.data.navigation.SeriesDto
|
||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomePageViewModel @Inject constructor(
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
private val navigationManager: NavigationManager,
|
||||
private val refreshHomeDataUseCase: RefreshHomeDataUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _url = userSessionRepository.serverUrl.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = ""
|
||||
)
|
||||
|
||||
val libraries = mediaRepository.libraries.map { libraries ->
|
||||
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(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = false
|
||||
)
|
||||
|
||||
val continueWatching = combine(
|
||||
mediaRepository.continueWatching,
|
||||
mediaRepository.movies,
|
||||
mediaRepository.episodes
|
||||
) { list, moviesMap, episodesMap ->
|
||||
list.mapNotNull { media ->
|
||||
when (media) {
|
||||
is Media.MovieMedia -> moviesMap[media.movieId]?.let {
|
||||
ContinueWatchingItem(type = BaseItemKind.MOVIE, movie = it)
|
||||
}
|
||||
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
||||
ContinueWatchingItem(type = BaseItemKind.EPISODE, episode = it)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}.distinctBy { it.id }
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyList()
|
||||
)
|
||||
|
||||
val nextUp = combine(
|
||||
mediaRepository.nextUp,
|
||||
mediaRepository.episodes
|
||||
) { list, episodesMap ->
|
||||
list.mapNotNull { media ->
|
||||
when (media) {
|
||||
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
||||
NextUpItem(episode = it)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}.distinctBy { it.id }
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyList()
|
||||
)
|
||||
|
||||
val latestLibraryContent = combine(
|
||||
mediaRepository.latestLibraryContent,
|
||||
mediaRepository.movies,
|
||||
mediaRepository.series,
|
||||
mediaRepository.episodes
|
||||
) { libraryMap, moviesMap, seriesMap, episodesMap ->
|
||||
libraryMap.mapValues { (_, items) ->
|
||||
items.mapNotNull { media ->
|
||||
when (media) {
|
||||
is Media.MovieMedia -> moviesMap[media.movieId]?.let {
|
||||
PosterItem(type = BaseItemKind.MOVIE, movie = it)
|
||||
}
|
||||
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
||||
PosterItem(type = BaseItemKind.EPISODE, episode = it)
|
||||
}
|
||||
is Media.SeriesMedia -> seriesMap[media.seriesId]?.let {
|
||||
PosterItem(type = BaseItemKind.SERIES, series = it)
|
||||
}
|
||||
is Media.SeasonMedia -> seriesMap[media.seriesId]?.let {
|
||||
PosterItem(type = BaseItemKind.SERIES, series = it)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}.distinctBy { it.id }
|
||||
}
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyMap()
|
||||
)
|
||||
|
||||
init {
|
||||
viewModelScope.launch { mediaRepository.ensureReady() }
|
||||
}
|
||||
|
||||
fun onLibrarySelected(id: UUID, name: String) {
|
||||
viewModelScope.launch {
|
||||
navigationManager.navigate(Route.LibraryRoute(library = LibraryDto(id = id, name = name)))
|
||||
}
|
||||
}
|
||||
|
||||
fun onMovieSelected(movieId: UUID) {
|
||||
navigationManager.navigate(Route.MovieRoute(
|
||||
MovieDto(
|
||||
id = movieId,
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
fun onSeriesSelected(seriesId: UUID) {
|
||||
viewModelScope.launch {
|
||||
navigationManager.navigate(Route.SeriesRoute(
|
||||
SeriesDto(
|
||||
id = seriesId,
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fun onEpisodeSelected(seriesId: UUID, seasonId: UUID, episodeId: UUID) {
|
||||
viewModelScope.launch {
|
||||
navigationManager.navigate(Route.EpisodeRoute(
|
||||
EpisodeDto(
|
||||
id = episodeId,
|
||||
seasonId = seasonId,
|
||||
seriesId = seriesId
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
navigationManager.pop()
|
||||
}
|
||||
|
||||
|
||||
fun onGoHome() {
|
||||
navigationManager.replaceAll(Route.Home)
|
||||
}
|
||||
|
||||
fun getImageUrl(itemId: UUID, type: ImageType): String {
|
||||
return JellyfinImageHelper.toImageUrl(
|
||||
url = _url.value,
|
||||
itemId = itemId,
|
||||
type = type
|
||||
)
|
||||
}
|
||||
|
||||
fun onResumed() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
refreshHomeDataUseCase()
|
||||
} catch (e: Exception) {
|
||||
// Refresh is best-effort; don't crash on failure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
userSessionRepository.setLoggedIn(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleOfflineMode() {
|
||||
viewModelScope.launch {
|
||||
userSessionRepository.setOfflineMode(!isOfflineMode.value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package hu.bbara.purefin.feature.shared.library
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.core.data.MediaRepository
|
||||
import hu.bbara.purefin.core.data.navigation.MovieDto
|
||||
import hu.bbara.purefin.core.data.navigation.NavigationManager
|
||||
import hu.bbara.purefin.core.data.navigation.Route
|
||||
import hu.bbara.purefin.core.data.navigation.SeriesDto
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LibraryViewModel @Inject constructor(
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val navigationManager: NavigationManager
|
||||
) : ViewModel() {
|
||||
|
||||
private val selectedLibrary = MutableStateFlow<UUID?>(null)
|
||||
|
||||
val contents: StateFlow<List<PosterItem>> = combine(selectedLibrary, mediaRepository.libraries) {
|
||||
libraryId, libraries ->
|
||||
if (libraryId == null) {
|
||||
return@combine emptyList()
|
||||
}
|
||||
val library = libraries.find { it.id == libraryId } ?: return@combine emptyList()
|
||||
when (library.type) {
|
||||
CollectionType.TVSHOWS -> library.series!!.map { series ->
|
||||
PosterItem(type = BaseItemKind.SERIES, series = series)
|
||||
}
|
||||
CollectionType.MOVIES -> library.movies!!.map { movie ->
|
||||
PosterItem(type = BaseItemKind.MOVIE, movie = movie)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||
|
||||
init {
|
||||
viewModelScope.launch { mediaRepository.ensureReady() }
|
||||
}
|
||||
|
||||
fun onMovieSelected(movieId: UUID) {
|
||||
navigationManager.navigate(Route.MovieRoute(
|
||||
MovieDto(
|
||||
id = movieId,
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
fun onSeriesSelected(seriesId: UUID) {
|
||||
viewModelScope.launch {
|
||||
navigationManager.navigate(Route.SeriesRoute(
|
||||
SeriesDto(
|
||||
id = seriesId,
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
navigationManager.pop()
|
||||
}
|
||||
|
||||
fun selectLibrary(libraryId: UUID) {
|
||||
viewModelScope.launch {
|
||||
selectedLibrary.value = libraryId
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package hu.bbara.purefin.feature.shared.login
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||
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 javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LoginViewModel @Inject constructor(
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
) : ViewModel() {
|
||||
private val _username = MutableStateFlow("")
|
||||
val username: StateFlow<String> = _username.asStateFlow()
|
||||
private val _password = MutableStateFlow("")
|
||||
val password: StateFlow<String> = _password.asStateFlow()
|
||||
private val _url = MutableStateFlow("")
|
||||
val url: StateFlow<String> = _url.asStateFlow()
|
||||
private val _errorMessage = MutableStateFlow<String?>(null)
|
||||
val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
_url.value = userSessionRepository.serverUrl.first()
|
||||
}
|
||||
}
|
||||
|
||||
fun setUrl(url: String) {
|
||||
_url.value = url
|
||||
}
|
||||
|
||||
fun setUsername(username: String) {
|
||||
_username.value = username
|
||||
}
|
||||
|
||||
fun setPassword(password: String) {
|
||||
_password.value = password
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_errorMessage.value = null
|
||||
}
|
||||
|
||||
suspend fun clearFields() {
|
||||
userSessionRepository.setServerUrl("");
|
||||
_username.value = ""
|
||||
_password.value = ""
|
||||
}
|
||||
|
||||
suspend fun login(): Boolean {
|
||||
_errorMessage.value = null
|
||||
userSessionRepository.setServerUrl(url.value)
|
||||
val success = jellyfinApiClient.login(url.value, username.value, password.value)
|
||||
if (!success) {
|
||||
_errorMessage.value = "Login failed. Check your server URL, username, and password."
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user