mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
refactor: update package structure and imports for consistency across UI components
This commit is contained in:
@@ -9,16 +9,13 @@ import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadRequest
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import hu.bbara.purefin.core.data.InMemoryMediaRepository
|
||||
import hu.bbara.purefin.core.data.OfflineCatalogStore
|
||||
import hu.bbara.purefin.core.data.SmartDownloadStore
|
||||
import hu.bbara.purefin.core.data.client.playbackCustomCacheKey
|
||||
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.core.data.download.DownloadState
|
||||
import hu.bbara.purefin.core.data.download.MediaDownloadController
|
||||
import hu.bbara.purefin.core.data.room.dao.MovieDao
|
||||
import hu.bbara.purefin.core.data.room.dao.SmartDownloadDao
|
||||
import hu.bbara.purefin.core.data.room.entity.SmartDownloadEntity
|
||||
import hu.bbara.purefin.core.data.room.offline.OfflineRoomMediaLocalDataSource
|
||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
@@ -36,7 +33,7 @@ import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import java.util.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
@@ -51,11 +48,9 @@ class MediaDownloadManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val downloadManager: DownloadManager,
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
private val offlineDataSource: OfflineRoomMediaLocalDataSource,
|
||||
private val movieDao: MovieDao,
|
||||
private val smartDownloadDao: SmartDownloadDao,
|
||||
private val offlineCatalogStore: OfflineCatalogStore,
|
||||
private val smartDownloadStore: SmartDownloadStore,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
private val inMemoryMediaRepository: InMemoryMediaRepository,
|
||||
) : MediaDownloadController {
|
||||
|
||||
private val stateFlows = ConcurrentHashMap<String, MutableStateFlow<DownloadState>>()
|
||||
@@ -171,7 +166,7 @@ class MediaDownloadManager @Inject constructor(
|
||||
cast = emptyList()
|
||||
)
|
||||
|
||||
offlineDataSource.saveMovies(listOf(movie))
|
||||
offlineCatalogStore.saveMovies(listOf(movie))
|
||||
|
||||
Log.d(TAG, "Starting download for '${movie.title}' from: $url")
|
||||
val request = buildDownloadRequest(
|
||||
@@ -193,7 +188,7 @@ class MediaDownloadManager @Inject constructor(
|
||||
withContext(Dispatchers.IO) {
|
||||
PurefinDownloadService.sendRemoveDownload(context, movieId.toString())
|
||||
try {
|
||||
movieDao.deleteById(movieId)
|
||||
offlineCatalogStore.deleteMovie(movieId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to remove movie from offline DB", e)
|
||||
}
|
||||
@@ -231,15 +226,15 @@ class MediaDownloadManager @Inject constructor(
|
||||
return@withContext
|
||||
}
|
||||
|
||||
if (offlineDataSource.getSeriesBasic(series.id) == null) {
|
||||
offlineDataSource.saveSeries(listOf(series))
|
||||
if (offlineCatalogStore.getSeriesBasic(series.id) == null) {
|
||||
offlineCatalogStore.saveSeries(listOf(series))
|
||||
}
|
||||
|
||||
if (offlineDataSource.getSeason(series.id, season.id) == null) {
|
||||
offlineDataSource.saveSeason(season)
|
||||
if (offlineCatalogStore.getSeason(season.id) == null) {
|
||||
offlineCatalogStore.saveSeason(season)
|
||||
}
|
||||
|
||||
offlineDataSource.saveEpisode(episode)
|
||||
offlineCatalogStore.saveEpisode(episode)
|
||||
|
||||
Log.d(TAG, "Starting download for episode '${episode.title}' from: $url")
|
||||
val request = buildDownloadRequest(
|
||||
@@ -269,7 +264,7 @@ class MediaDownloadManager @Inject constructor(
|
||||
withContext(Dispatchers.IO) {
|
||||
PurefinDownloadService.sendRemoveDownload(context, episodeId.toString())
|
||||
try {
|
||||
offlineDataSource.deleteEpisodeAndCleanup(episodeId)
|
||||
offlineCatalogStore.deleteEpisodeAndCleanup(episodeId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to remove episode from offline DB", e)
|
||||
}
|
||||
@@ -279,24 +274,24 @@ class MediaDownloadManager @Inject constructor(
|
||||
// ── Smart Download ──────────────────────────────────────────────────
|
||||
|
||||
override suspend fun enableSmartDownload(seriesId: UUID) {
|
||||
smartDownloadDao.insert(SmartDownloadEntity(seriesId))
|
||||
smartDownloadStore.enable(seriesId)
|
||||
syncSmartDownloadsForSeries(seriesId)
|
||||
}
|
||||
|
||||
suspend fun disableSmartDownload(seriesId: UUID) {
|
||||
smartDownloadDao.delete(seriesId)
|
||||
smartDownloadStore.disable(seriesId)
|
||||
}
|
||||
|
||||
fun isSmartDownloadEnabled(seriesId: UUID): Flow<Boolean> = smartDownloadDao.observe(seriesId)
|
||||
fun isSmartDownloadEnabled(seriesId: UUID): Flow<Boolean> = smartDownloadStore.observe(seriesId)
|
||||
|
||||
override suspend fun syncSmartDownloads() {
|
||||
withContext(Dispatchers.IO) {
|
||||
val enabled = smartDownloadDao.getAll()
|
||||
for (entry in enabled) {
|
||||
val enabledSeriesIds = smartDownloadStore.getEnabledSeriesIds()
|
||||
for (seriesId in enabledSeriesIds) {
|
||||
try {
|
||||
syncSmartDownloadsForSeries(entry.seriesId)
|
||||
syncSmartDownloadsForSeries(seriesId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Smart download sync failed for series ${entry.seriesId}", e)
|
||||
Log.e(TAG, "Smart download sync failed for series $seriesId", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,7 +302,7 @@ class MediaDownloadManager @Inject constructor(
|
||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
||||
|
||||
// 1. Get currently downloaded episodes for this series
|
||||
val downloadedEpisodes = offlineDataSource.getEpisodesBySeries(seriesId)
|
||||
val downloadedEpisodes = offlineCatalogStore.getEpisodesBySeries(seriesId)
|
||||
|
||||
// 2. Check watched status from server and delete watched downloads
|
||||
val unwatchedDownloaded = mutableListOf<UUID>()
|
||||
|
||||
@@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
@@ -33,5 +34,9 @@ dependencies {
|
||||
implementation(libs.hilt)
|
||||
ksp(libs.hilt.compiler)
|
||||
implementation(libs.jellyfin.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@ 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.AppContentRepository
|
||||
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.MediaCatalogReader
|
||||
import hu.bbara.purefin.core.data.download.DownloadState
|
||||
import hu.bbara.purefin.core.data.download.MediaDownloadController
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
|
||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||
import hu.bbara.purefin.feature.shared.navigation.SeriesDto
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -17,12 +18,11 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
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 appContentRepository: AppContentRepository,
|
||||
private val mediaCatalogReader: MediaCatalogReader,
|
||||
private val navigationManager: NavigationManager,
|
||||
private val mediaDownloadManager: MediaDownloadController,
|
||||
): ViewModel() {
|
||||
@@ -32,14 +32,14 @@ class EpisodeScreenViewModel @Inject constructor(
|
||||
|
||||
val episode: StateFlow<Episode?> = combine(
|
||||
_episodeId,
|
||||
appContentRepository.episodes
|
||||
mediaCatalogReader.episodes
|
||||
) { id, episodesMap ->
|
||||
id?.let { episodesMap[it] }
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||
|
||||
val seriesTitle: StateFlow<String?> = combine(
|
||||
_seriesId,
|
||||
appContentRepository.series
|
||||
mediaCatalogReader.series
|
||||
) { seriesId, seriesMap ->
|
||||
seriesId?.let { id -> seriesMap[id]?.name }
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||
|
||||
@@ -3,12 +3,13 @@ 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.data.MediaCatalogReader
|
||||
import hu.bbara.purefin.core.data.download.DownloadState
|
||||
import hu.bbara.purefin.core.data.download.MediaDownloadController
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
|
||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -16,21 +17,20 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
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 MovieScreenViewModel @Inject constructor(
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val mediaCatalogReader: MediaCatalogReader,
|
||||
private val navigationManager: NavigationManager,
|
||||
private val mediaDownloadManager: MediaDownloadController
|
||||
private val mediaDownloadManager: MediaDownloadController,
|
||||
): ViewModel() {
|
||||
|
||||
private val _movieId = MutableStateFlow<UUID?>(null)
|
||||
|
||||
val movie: StateFlow<Movie?> = combine(
|
||||
_movieId,
|
||||
mediaRepository.movies
|
||||
mediaCatalogReader.movies
|
||||
) { movieId, movies ->
|
||||
movieId?.let { movies[it] }
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||
|
||||
@@ -3,14 +3,15 @@ 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.data.MediaCatalogReader
|
||||
import hu.bbara.purefin.core.data.download.DownloadState
|
||||
import hu.bbara.purefin.core.data.download.MediaDownloadController
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto
|
||||
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
|
||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -20,12 +21,11 @@ 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 mediaCatalogReader: MediaCatalogReader,
|
||||
private val navigationManager: NavigationManager,
|
||||
private val mediaDownloadManager: MediaDownloadController,
|
||||
) : ViewModel() {
|
||||
@@ -35,7 +35,7 @@ class SeriesViewModel @Inject constructor(
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val series: StateFlow<Series?> = _seriesId
|
||||
.flatMapLatest { id ->
|
||||
if (id != null) mediaRepository.observeSeriesWithContent(id) else flowOf(null)
|
||||
if (id != null) mediaCatalogReader.observeSeriesWithContent(id) else flowOf(null)
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||
|
||||
|
||||
@@ -3,28 +3,28 @@ package hu.bbara.purefin.feature.shared.download
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.core.data.OfflineMediaRepository
|
||||
import hu.bbara.purefin.core.data.OfflineCatalogReader
|
||||
import hu.bbara.purefin.core.data.download.MediaDownloadController
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
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.model.MediaKind
|
||||
import hu.bbara.purefin.feature.shared.navigation.MovieDto
|
||||
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
|
||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||
import hu.bbara.purefin.feature.shared.navigation.SeriesDto
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
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.ImageType
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class DownloadsViewModel @Inject constructor(
|
||||
private val offlineMediaRepository: OfflineMediaRepository,
|
||||
private val offlineCatalogReader: OfflineCatalogReader,
|
||||
private val navigationManager: NavigationManager,
|
||||
private val downloadManager: MediaDownloadController
|
||||
private val downloadManager: MediaDownloadController,
|
||||
) : ViewModel() {
|
||||
|
||||
fun onMovieSelected(movieId: UUID) {
|
||||
@@ -47,22 +47,22 @@ class DownloadsViewModel @Inject constructor(
|
||||
|
||||
/** Items that are fully downloaded and not currently in progress. */
|
||||
val downloads = combine(
|
||||
offlineMediaRepository.movies,
|
||||
offlineMediaRepository.series,
|
||||
offlineCatalogReader.movies,
|
||||
offlineCatalogReader.series,
|
||||
activeDownloadsMap
|
||||
) { movies, series, inProgress ->
|
||||
movies.values
|
||||
.filter { it.id.toString() !in inProgress }
|
||||
.map { PosterItem(type = BaseItemKind.MOVIE, movie = it) } +
|
||||
series.values.map { PosterItem(type = BaseItemKind.SERIES, series = it) }
|
||||
.map { PosterItem(type = MediaKind.MOVIE, movie = it) } +
|
||||
series.values.map { PosterItem(type = MediaKind.SERIES, series = it) }
|
||||
}
|
||||
|
||||
/** Items currently being downloaded with their progress. */
|
||||
val activeDownloads = combine(
|
||||
activeDownloadsMap,
|
||||
offlineMediaRepository.movies,
|
||||
offlineMediaRepository.episodes,
|
||||
offlineMediaRepository.series
|
||||
offlineCatalogReader.movies,
|
||||
offlineCatalogReader.episodes,
|
||||
offlineCatalogReader.series
|
||||
) { inProgress, movies, episodes, seriesMap ->
|
||||
inProgress.mapNotNull { (contentId, progress) ->
|
||||
val id = try { UUID.fromString(contentId) } catch (e: Exception) { return@mapNotNull null }
|
||||
@@ -97,7 +97,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
} catch (e: Exception) {
|
||||
return@launch
|
||||
}
|
||||
if (offlineMediaRepository.episodes.value.containsKey(id)) {
|
||||
if (offlineCatalogReader.episodes.value.containsKey(id)) {
|
||||
downloadManager.cancelEpisodeDownload(id)
|
||||
} else {
|
||||
downloadManager.cancelDownload(id)
|
||||
|
||||
@@ -3,17 +3,21 @@ 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.AppContentRepository
|
||||
import hu.bbara.purefin.core.data.HomeRepository
|
||||
import hu.bbara.purefin.core.data.MediaCatalogReader
|
||||
import hu.bbara.purefin.core.data.domain.usecase.RefreshHomeDataUseCase
|
||||
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 hu.bbara.purefin.core.data.download.MediaDownloadController
|
||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||
import hu.bbara.purefin.core.model.LibraryKind
|
||||
import hu.bbara.purefin.core.model.Media
|
||||
import hu.bbara.purefin.core.model.MediaKind
|
||||
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto
|
||||
import hu.bbara.purefin.feature.shared.navigation.LibraryDto
|
||||
import hu.bbara.purefin.feature.shared.navigation.MovieDto
|
||||
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
|
||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||
import hu.bbara.purefin.feature.shared.navigation.SeriesDto
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -22,14 +26,12 @@ 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 javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AppViewModel @Inject constructor(
|
||||
private val appContentRepository: AppContentRepository,
|
||||
private val homeRepository: HomeRepository,
|
||||
private val mediaCatalogReader: MediaCatalogReader,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
private val navigationManager: NavigationManager,
|
||||
private val refreshHomeDataUseCase: RefreshHomeDataUseCase,
|
||||
@@ -46,7 +48,7 @@ class AppViewModel @Inject constructor(
|
||||
""
|
||||
)
|
||||
|
||||
val libraries = appContentRepository.libraries.map { libraries ->
|
||||
val libraries = homeRepository.libraries.map { libraries ->
|
||||
libraries.map {
|
||||
LibraryItem(
|
||||
id = it.id,
|
||||
@@ -54,19 +56,18 @@ class AppViewModel @Inject constructor(
|
||||
type = it.type,
|
||||
posterUrl = it.posterUrl,
|
||||
isEmpty = when(it.type) {
|
||||
CollectionType.MOVIES -> appContentRepository.movies.value.isEmpty()
|
||||
CollectionType.TVSHOWS -> appContentRepository.series.value.isEmpty()
|
||||
else -> true
|
||||
LibraryKind.MOVIES -> mediaCatalogReader.movies.value.isEmpty()
|
||||
LibraryKind.SERIES -> mediaCatalogReader.series.value.isEmpty()
|
||||
}
|
||||
)
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||
|
||||
val suggestions = combine(
|
||||
appContentRepository.suggestions,
|
||||
appContentRepository.movies,
|
||||
appContentRepository.series,
|
||||
appContentRepository.episodes
|
||||
homeRepository.suggestions,
|
||||
mediaCatalogReader.movies,
|
||||
mediaCatalogReader.series,
|
||||
mediaCatalogReader.episodes
|
||||
) { list, moviesMap, seriesMap, episodesMap ->
|
||||
list.mapNotNull { media ->
|
||||
when (media) {
|
||||
@@ -89,17 +90,17 @@ class AppViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
val continueWatching = combine(
|
||||
appContentRepository.continueWatching,
|
||||
appContentRepository.movies,
|
||||
appContentRepository.episodes
|
||||
homeRepository.continueWatching,
|
||||
mediaCatalogReader.movies,
|
||||
mediaCatalogReader.episodes
|
||||
) { list, moviesMap, episodesMap ->
|
||||
list.mapNotNull { media ->
|
||||
when (media) {
|
||||
is Media.MovieMedia -> moviesMap[media.movieId]?.let {
|
||||
ContinueWatchingItem(type = BaseItemKind.MOVIE, movie = it)
|
||||
ContinueWatchingItem(type = MediaKind.MOVIE, movie = it)
|
||||
}
|
||||
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
||||
ContinueWatchingItem(type = BaseItemKind.EPISODE, episode = it)
|
||||
ContinueWatchingItem(type = MediaKind.EPISODE, episode = it)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
@@ -111,8 +112,8 @@ class AppViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
val nextUp = combine(
|
||||
appContentRepository.nextUp,
|
||||
appContentRepository.episodes
|
||||
homeRepository.nextUp,
|
||||
mediaCatalogReader.episodes
|
||||
) { list, episodesMap ->
|
||||
list.mapNotNull { media ->
|
||||
when (media) {
|
||||
@@ -129,27 +130,26 @@ class AppViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
val latestLibraryContent = combine(
|
||||
appContentRepository.latestLibraryContent,
|
||||
appContentRepository.movies,
|
||||
appContentRepository.series,
|
||||
appContentRepository.episodes
|
||||
homeRepository.latestLibraryContent,
|
||||
mediaCatalogReader.movies,
|
||||
mediaCatalogReader.series,
|
||||
mediaCatalogReader.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)
|
||||
PosterItem(type = MediaKind.MOVIE, movie = it)
|
||||
}
|
||||
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
||||
PosterItem(type = BaseItemKind.EPISODE, episode = it)
|
||||
PosterItem(type = MediaKind.EPISODE, episode = it)
|
||||
}
|
||||
is Media.SeriesMedia -> seriesMap[media.seriesId]?.let {
|
||||
PosterItem(type = BaseItemKind.SERIES, series = it)
|
||||
PosterItem(type = MediaKind.SERIES, series = it)
|
||||
}
|
||||
is Media.SeasonMedia -> seriesMap[media.seriesId]?.let {
|
||||
PosterItem(type = BaseItemKind.SERIES, series = it)
|
||||
PosterItem(type = MediaKind.SERIES, series = it)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}.distinctBy { it.id }
|
||||
}
|
||||
@@ -242,5 +242,4 @@ class AppViewModel @Inject constructor(
|
||||
userSessionRepository.setLoggedIn(false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
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
|
||||
import java.util.UUID
|
||||
import hu.bbara.purefin.core.model.MediaKind
|
||||
import hu.bbara.purefin.core.model.LibraryKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ sealed interface SuggestedItem {
|
||||
val imageUrl: String
|
||||
val ctaLabel: String
|
||||
val progress: Float?
|
||||
val type: BaseItemKind
|
||||
val type: MediaKind
|
||||
}
|
||||
|
||||
data class SuggestedEpisode (
|
||||
@@ -34,7 +34,7 @@ data class SuggestedEpisode (
|
||||
.filter { it.isNotBlank() },
|
||||
override val ctaLabel: String = "Open",
|
||||
override val progress: Float? = episode.progress?.toFloat(),
|
||||
override val type: BaseItemKind = BaseItemKind.EPISODE,
|
||||
override val type: MediaKind = MediaKind.EPISODE,
|
||||
override val id: UUID = episode.id,
|
||||
override val title: String = episode.title,
|
||||
override val description: String = episode.synopsis,
|
||||
@@ -57,7 +57,7 @@ data class SuggestedSeries (
|
||||
listOf(series.year, "${series.seasonCount} seasons").filter { it.isNotBlank() },
|
||||
override val ctaLabel: String = "Open",
|
||||
override val progress: Float? = null,
|
||||
override val type: BaseItemKind = BaseItemKind.SERIES,
|
||||
override val type: MediaKind = MediaKind.SERIES,
|
||||
override val id: UUID = series.id,
|
||||
override val title: String = series.name,
|
||||
override val description: String = series.synopsis,
|
||||
@@ -78,7 +78,7 @@ data class SuggestedMovie (
|
||||
.filter { it.isNotBlank() },
|
||||
override val ctaLabel: String = "Open",
|
||||
override val progress: Float? = movie.progress?.toFloat(),
|
||||
override val type: BaseItemKind = BaseItemKind.MOVIE,
|
||||
override val type: MediaKind = MediaKind.MOVIE,
|
||||
override val id: UUID = movie.id,
|
||||
override val title: String = movie.title,
|
||||
override val description: String = movie.synopsis,
|
||||
@@ -94,48 +94,48 @@ sealed interface FocusableItem {
|
||||
val secondaryText: String
|
||||
val description: String
|
||||
val id: UUID
|
||||
val type: BaseItemKind
|
||||
val type: MediaKind
|
||||
}
|
||||
|
||||
data class ContinueWatchingItem(
|
||||
override val type: BaseItemKind,
|
||||
override val type: MediaKind,
|
||||
val movie: Movie? = null,
|
||||
val episode: Episode? = null,
|
||||
) : FocusableItem {
|
||||
override val id: UUID = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.id
|
||||
BaseItemKind.EPISODE -> episode!!.id
|
||||
MediaKind.MOVIE -> movie!!.id
|
||||
MediaKind.EPISODE -> episode!!.id
|
||||
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
||||
}
|
||||
override val primaryText: String = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.title
|
||||
BaseItemKind.EPISODE -> episode!!.title
|
||||
MediaKind.MOVIE -> movie!!.title
|
||||
MediaKind.EPISODE -> episode!!.title
|
||||
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
||||
}
|
||||
override val secondaryText: String = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.year
|
||||
BaseItemKind.EPISODE -> episode!!.releaseDate
|
||||
MediaKind.MOVIE -> movie!!.year
|
||||
MediaKind.EPISODE -> episode!!.releaseDate
|
||||
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
||||
}
|
||||
override val imageUrl: String = when (type) {
|
||||
BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
||||
MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
||||
prefixImageUrl = movie!!.imageUrlPrefix,
|
||||
imageType = ImageType.PRIMARY
|
||||
)
|
||||
BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
||||
MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
||||
prefixImageUrl = episode!!.imageUrlPrefix,
|
||||
imageType = ImageType.PRIMARY
|
||||
)
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
override val description: String = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.synopsis
|
||||
BaseItemKind.EPISODE -> episode!!.synopsis
|
||||
MediaKind.MOVIE -> movie!!.synopsis
|
||||
MediaKind.EPISODE -> episode!!.synopsis
|
||||
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
||||
}
|
||||
val progress: Double = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.progress ?: 0.0
|
||||
BaseItemKind.EPISODE -> episode!!.progress ?: 0.0
|
||||
MediaKind.MOVIE -> movie!!.progress ?: 0.0
|
||||
MediaKind.EPISODE -> episode!!.progress ?: 0.0
|
||||
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
||||
}
|
||||
}
|
||||
@@ -144,8 +144,8 @@ data class NextUpItem(
|
||||
val episode: Episode
|
||||
) : FocusableItem {
|
||||
override val id: UUID = episode.id
|
||||
override val type: BaseItemKind
|
||||
get() = BaseItemKind.EPISODE
|
||||
override val type: MediaKind
|
||||
get() = MediaKind.EPISODE
|
||||
override val imageUrl: String
|
||||
get() = JellyfinImageHelper.finishImageUrl(
|
||||
prefixImageUrl = episode.imageUrlPrefix,
|
||||
@@ -160,39 +160,39 @@ data class NextUpItem(
|
||||
data class LibraryItem(
|
||||
val id: UUID,
|
||||
val name: String,
|
||||
val type: CollectionType,
|
||||
val type: LibraryKind,
|
||||
val posterUrl: String,
|
||||
val isEmpty: Boolean
|
||||
)
|
||||
|
||||
data class PosterItem(
|
||||
override val type: BaseItemKind,
|
||||
override val type: MediaKind,
|
||||
val movie: Movie? = null,
|
||||
val series: Series? = null,
|
||||
val episode: Episode? = null
|
||||
) : FocusableItem {
|
||||
override val id: UUID = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.id
|
||||
BaseItemKind.EPISODE -> episode!!.id
|
||||
BaseItemKind.SERIES -> series!!.id
|
||||
MediaKind.MOVIE -> movie!!.id
|
||||
MediaKind.EPISODE -> episode!!.id
|
||||
MediaKind.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
|
||||
MediaKind.MOVIE -> movie!!.title
|
||||
MediaKind.EPISODE -> episode!!.title
|
||||
MediaKind.SERIES -> series!!.name
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
override val imageUrl: String = when (type) {
|
||||
BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
||||
MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
||||
prefixImageUrl = movie!!.imageUrlPrefix,
|
||||
imageType = ImageType.PRIMARY
|
||||
)
|
||||
BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
||||
MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
||||
prefixImageUrl = episode!!.imageUrlPrefix,
|
||||
imageType = ImageType.PRIMARY
|
||||
)
|
||||
BaseItemKind.SERIES -> JellyfinImageHelper.finishImageUrl(
|
||||
MediaKind.SERIES -> JellyfinImageHelper.finishImageUrl(
|
||||
prefixImageUrl = series!!.imageUrlPrefix,
|
||||
imageType = ImageType.PRIMARY
|
||||
)
|
||||
@@ -200,29 +200,29 @@ data class PosterItem(
|
||||
}
|
||||
override val primaryText: String
|
||||
get() = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.title
|
||||
BaseItemKind.EPISODE -> episode!!.title
|
||||
BaseItemKind.SERIES -> series!!.name
|
||||
MediaKind.MOVIE -> movie!!.title
|
||||
MediaKind.EPISODE -> episode!!.title
|
||||
MediaKind.SERIES -> series!!.name
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
override val secondaryText: String
|
||||
get() = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.year
|
||||
BaseItemKind.EPISODE -> episode!!.releaseDate
|
||||
BaseItemKind.SERIES -> series!!.year
|
||||
MediaKind.MOVIE -> movie!!.year
|
||||
MediaKind.EPISODE -> episode!!.releaseDate
|
||||
MediaKind.SERIES -> series!!.year
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
override val description: String
|
||||
get() = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.synopsis
|
||||
BaseItemKind.EPISODE -> episode!!.synopsis
|
||||
BaseItemKind.SERIES -> series!!.synopsis
|
||||
MediaKind.MOVIE -> movie!!.synopsis
|
||||
MediaKind.EPISODE -> episode!!.synopsis
|
||||
MediaKind.SERIES -> series!!.synopsis
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
|
||||
fun watched() = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.watched
|
||||
BaseItemKind.EPISODE -> episode!!.watched
|
||||
MediaKind.MOVIE -> movie!!.watched
|
||||
MediaKind.EPISODE -> episode!!.watched
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,50 +3,49 @@ 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.core.data.HomeRepository
|
||||
import hu.bbara.purefin.core.model.LibraryKind
|
||||
import hu.bbara.purefin.core.model.MediaKind
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.core.data.AppContentRepository
|
||||
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.feature.shared.navigation.MovieDto
|
||||
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
|
||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||
import hu.bbara.purefin.feature.shared.navigation.SeriesDto
|
||||
import java.util.UUID
|
||||
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 appContentRepository: AppContentRepository,
|
||||
private val navigationManager: NavigationManager
|
||||
private val homeRepository: HomeRepository,
|
||||
private val navigationManager: NavigationManager,
|
||||
) : ViewModel() {
|
||||
|
||||
private val selectedLibrary = MutableStateFlow<UUID?>(null)
|
||||
|
||||
val contents: StateFlow<List<PosterItem>> = combine(selectedLibrary, appContentRepository.libraries) {
|
||||
val contents: StateFlow<List<PosterItem>> = combine(selectedLibrary, homeRepository.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)
|
||||
LibraryKind.SERIES -> library.series!!.map { series ->
|
||||
PosterItem(type = MediaKind.SERIES, series = series)
|
||||
}
|
||||
CollectionType.MOVIES -> library.movies!!.map { movie ->
|
||||
PosterItem(type = BaseItemKind.MOVIE, movie = movie)
|
||||
LibraryKind.MOVIES -> library.movies!!.map { movie ->
|
||||
PosterItem(type = MediaKind.MOVIE, movie = movie)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||
|
||||
init {
|
||||
viewModelScope.launch { appContentRepository.ensureReady() }
|
||||
viewModelScope.launch { homeRepository.ensureReady() }
|
||||
}
|
||||
|
||||
fun onMovieSelected(movieId: UUID) {
|
||||
@@ -72,8 +71,6 @@ class LibraryViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun selectLibrary(libraryId: UUID) {
|
||||
viewModelScope.launch {
|
||||
selectedLibrary.value = libraryId
|
||||
}
|
||||
selectedLibrary.value = libraryId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package hu.bbara.purefin.feature.shared.navigation
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
|
||||
@Serializable
|
||||
data class EpisodeDto(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
val id: UUID,
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
val seasonId: UUID,
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
val seriesId: UUID,
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package hu.bbara.purefin.feature.shared.navigation
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
|
||||
@Serializable
|
||||
data class LibraryDto(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
val id: UUID,
|
||||
val name: String,
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package hu.bbara.purefin.feature.shared.navigation
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
|
||||
@Serializable
|
||||
data class MovieDto(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
val id: UUID,
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
package hu.bbara.purefin.feature.shared.navigation
|
||||
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
||||
sealed interface NavigationCommand {
|
||||
data class Navigate(val route: Route) : NavigationCommand
|
||||
data class ReplaceAll(val route: Route) : NavigationCommand
|
||||
data object Pop : NavigationCommand
|
||||
}
|
||||
|
||||
interface NavigationManager {
|
||||
val commands: SharedFlow<NavigationCommand>
|
||||
fun navigate(route: Route)
|
||||
fun replaceAll(route: Route)
|
||||
fun pop()
|
||||
}
|
||||
|
||||
class DefaultNavigationManager : NavigationManager {
|
||||
private val commandsFlow = MutableSharedFlow<NavigationCommand>(
|
||||
extraBufferCapacity = 64,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
override val commands: SharedFlow<NavigationCommand> = commandsFlow.asSharedFlow()
|
||||
|
||||
override fun navigate(route: Route) {
|
||||
commandsFlow.tryEmit(NavigationCommand.Navigate(route))
|
||||
}
|
||||
|
||||
override fun replaceAll(route: Route) {
|
||||
commandsFlow.tryEmit(NavigationCommand.ReplaceAll(route))
|
||||
}
|
||||
|
||||
override fun pop() {
|
||||
commandsFlow.tryEmit(NavigationCommand.Pop)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package hu.bbara.purefin.feature.shared.navigation
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NavigationManagerModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNavigationManager(): NavigationManager = DefaultNavigationManager()
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package hu.bbara.purefin.feature.shared.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed interface Route : NavKey {
|
||||
@Serializable
|
||||
data object Home : Route
|
||||
|
||||
@Serializable
|
||||
data class MovieRoute(val item: MovieDto) : Route
|
||||
|
||||
@Serializable
|
||||
data class SeriesRoute(val item: SeriesDto) : Route
|
||||
|
||||
@Serializable
|
||||
data class EpisodeRoute(val item: EpisodeDto) : Route
|
||||
|
||||
@Serializable
|
||||
data class LibraryRoute(val library: LibraryDto) : Route
|
||||
|
||||
@Serializable
|
||||
data object LoginRoute : Route
|
||||
|
||||
@Serializable
|
||||
data class PlayerRoute(val mediaId: String) : Route
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package hu.bbara.purefin.feature.shared.navigation
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
|
||||
@Serializable
|
||||
data class SeriesDto(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
val id: UUID,
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
package hu.bbara.purefin.feature.shared.search
|
||||
|
||||
import hu.bbara.purefin.core.model.MediaKind
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import java.util.UUID
|
||||
|
||||
data class SearchResult(
|
||||
val id: UUID,
|
||||
val title: String,
|
||||
val posterUrl: String,
|
||||
val type: MediaKind,
|
||||
) {
|
||||
companion object {
|
||||
fun create(movie: Movie, imageUrl: String): SearchResult {
|
||||
return SearchResult(
|
||||
id = movie.id,
|
||||
title = movie.title,
|
||||
posterUrl = imageUrl,
|
||||
type = MediaKind.MOVIE,
|
||||
)
|
||||
}
|
||||
|
||||
fun create(series: Series, imageUrl: String): SearchResult {
|
||||
return SearchResult(
|
||||
id = series.id,
|
||||
title = series.name,
|
||||
posterUrl = imageUrl,
|
||||
type = MediaKind.SERIES,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@ package hu.bbara.purefin.feature.shared.search
|
||||
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.MediaCatalogReader
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||
import hu.bbara.purefin.core.model.SearchResult
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -16,14 +16,13 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@OptIn(FlowPreview::class)
|
||||
class SearchViewModel @Inject constructor(
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val userSessionRepository: UserSessionRepository
|
||||
private val mediaCatalogReader: MediaCatalogReader,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _searchResult = MutableStateFlow<List<SearchResult>>(emptyList())
|
||||
@@ -34,8 +33,8 @@ class SearchViewModel @Inject constructor(
|
||||
init {
|
||||
combine(
|
||||
query.debounce(300).distinctUntilChanged(),
|
||||
mediaRepository.movies,
|
||||
mediaRepository.series
|
||||
mediaCatalogReader.movies,
|
||||
mediaCatalogReader.series
|
||||
) { currentQuery, movies, series ->
|
||||
val filteredMovies = movies.filter {
|
||||
it.value.title.contains(currentQuery, ignoreCase = true)
|
||||
@@ -59,4 +58,4 @@ class SearchViewModel @Inject constructor(
|
||||
return JellyfinImageHelper.toImageUrl(userSessionRepository.serverUrl.first(), id,
|
||||
ImageType.PRIMARY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package hu.bbara.purefin.feature.shared.navigation
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class RouteSerializationTest {
|
||||
|
||||
private val json = Json
|
||||
|
||||
@Test
|
||||
fun `episode route round trips with nested dto ids`() {
|
||||
val route: Route = Route.EpisodeRoute(
|
||||
item = EpisodeDto(
|
||||
id = UUID.randomUUID(),
|
||||
seasonId = UUID.randomUUID(),
|
||||
seriesId = UUID.randomUUID(),
|
||||
)
|
||||
)
|
||||
|
||||
assertRoundTrip(route)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `library route round trips`() {
|
||||
val route: Route = Route.LibraryRoute(
|
||||
library = LibraryDto(
|
||||
id = UUID.randomUUID(),
|
||||
name = "Shows",
|
||||
)
|
||||
)
|
||||
|
||||
assertRoundTrip(route)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `movie and series routes round trip`() {
|
||||
assertRoundTrip(
|
||||
Route.MovieRoute(
|
||||
item = MovieDto(id = UUID.randomUUID())
|
||||
)
|
||||
)
|
||||
assertRoundTrip(
|
||||
Route.SeriesRoute(
|
||||
item = SeriesDto(id = UUID.randomUUID())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `singleton routes round trip`() {
|
||||
assertRoundTrip(Route.Home)
|
||||
assertRoundTrip(Route.LoginRoute)
|
||||
assertRoundTrip(Route.PlayerRoute(mediaId = UUID.randomUUID().toString()))
|
||||
}
|
||||
|
||||
private fun assertRoundTrip(route: Route) {
|
||||
val encoded = json.encodeToString(route)
|
||||
val decoded = json.decodeFromString<Route>(encoded)
|
||||
|
||||
assertEquals(route, decoded)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user