feat(search): wire full-screen search with genre browsing

This commit is contained in:
2026-05-02 15:15:20 +02:00
parent c6fe84654f
commit 672d8e1d64
9 changed files with 301 additions and 234 deletions

View File

@@ -6,6 +6,7 @@ import hu.bbara.purefin.data.HomeRepository
import hu.bbara.purefin.data.NetworkMonitor
import hu.bbara.purefin.data.UserSessionRepository
import hu.bbara.purefin.data.converter.toEpisode
import hu.bbara.purefin.data.converter.toGenre
import hu.bbara.purefin.data.converter.toLibrary
import hu.bbara.purefin.data.converter.toMovie
import hu.bbara.purefin.data.converter.toSeason
@@ -103,6 +104,7 @@ class InMemoryAppContentRepository @Inject constructor(
loadContinueWatching()
loadNextUp()
loadLatestLibraryContent()
loadGenres()
Log.d(TAG, "Home refresh successful")
persistHomeCache()
}.onFailure { error ->
@@ -199,7 +201,7 @@ class InMemoryAppContentRepository @Inject constructor(
)
}
suspend fun loadLibraries() {
private suspend fun loadLibraries() {
val librariesItem = runCatching { jellyfinApiClient.getLibraries() }
.getOrElse { error ->
Log.w(TAG, "Unable to load libraries", error)
@@ -221,7 +223,7 @@ class InMemoryAppContentRepository @Inject constructor(
onlineMediaRepository.upsertSeries(series)
}
suspend fun loadLibrary(library: Library): Library {
private suspend fun loadLibrary(library: Library): Library {
val contentItem = runCatching { jellyfinApiClient.getLibraryContent(library.id) }
.getOrElse { error ->
Log.w(TAG, "Unable to load library ${library.id}", error)
@@ -237,7 +239,7 @@ class InMemoryAppContentRepository @Inject constructor(
}
}
suspend fun loadSuggestions() {
private suspend fun loadSuggestions() {
val suggestionsItems = runCatching { jellyfinApiClient.getSuggestions() }
.getOrElse { error ->
Log.w(TAG, "Unable to load suggestions", error)
@@ -258,7 +260,7 @@ class InMemoryAppContentRepository @Inject constructor(
}
}
suspend fun loadContinueWatching() {
private suspend fun loadContinueWatching() {
val continueWatchingItems = runCatching { jellyfinApiClient.getContinueWatching() }
.getOrElse { error ->
Log.w(TAG, "Unable to load continue watching", error)
@@ -279,7 +281,7 @@ class InMemoryAppContentRepository @Inject constructor(
}
}
suspend fun loadNextUp() {
private suspend fun loadNextUp() {
val nextUpItems = runCatching { jellyfinApiClient.getNextUpEpisodes() }
.getOrElse { error ->
Log.w(TAG, "Unable to load next up", error)
@@ -294,7 +296,7 @@ class InMemoryAppContentRepository @Inject constructor(
}
}
suspend fun loadLatestLibraryContent() {
private suspend fun loadLatestLibraryContent() {
val librariesItem = runCatching { jellyfinApiClient.getLibraries() }
.getOrElse { error ->
Log.w(TAG, "Unable to load latest library content", error)
@@ -337,6 +339,12 @@ class InMemoryAppContentRepository @Inject constructor(
latestLibraryContentState.value = latestLibraryContents
}
private suspend fun loadGenres() {
val baseItemDtos = jellyfinApiClient.getGenres()
val genres = baseItemDtos.map { it.toGenre() }
onlineMediaRepository.upsertGenres(genres.toSet())
}
private suspend fun serverUrl(): String {
return userSessionRepository.serverUrl.first()
}

View File

@@ -9,6 +9,7 @@ import hu.bbara.purefin.data.converter.toSeason
import hu.bbara.purefin.data.converter.toSeries
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
import hu.bbara.purefin.model.Episode
import hu.bbara.purefin.model.Genre
import hu.bbara.purefin.model.Movie
import hu.bbara.purefin.model.Series
import kotlinx.coroutines.CancellationException
@@ -48,6 +49,9 @@ class InMemoryMediaRepository @Inject constructor(
private val episodesState = MutableStateFlow<Map<UUID, Episode>>(emptyMap())
override val episodes: StateFlow<Map<UUID, Episode>> = episodesState.asStateFlow()
private val genresState = MutableStateFlow<Set<Genre>>(emptySet())
override var genres: StateFlow<Set<Genre>> = genresState.asStateFlow()
override suspend fun getMovie(id: UUID): Flow<Movie?> {
if (!moviesState.value.containsKey(id)) {
jellyfinApiClient.getItemInfo(id)?.let { item ->
@@ -92,6 +96,10 @@ class InMemoryMediaRepository @Inject constructor(
return episodesState.map { it[id] }
}
fun upsertGenres(genres: Set<Genre>) {
genresState.update { current -> current + genres }
}
fun upsertMovies(movies: List<Movie>) {
moviesState.update { current -> current + movies.associateBy { it.id } }
}

View File

@@ -3,12 +3,14 @@ package hu.bbara.purefin.data.catalog
import hu.bbara.purefin.data.MediaRepository
import hu.bbara.purefin.data.offline.room.offline.OfflineRoomMediaLocalDataSource
import hu.bbara.purefin.model.Episode
import hu.bbara.purefin.model.Genre
import hu.bbara.purefin.model.Movie
import hu.bbara.purefin.model.Series
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
@@ -32,6 +34,8 @@ class OfflineMediaRepository @Inject constructor(
override val episodes: StateFlow<Map<UUID, Episode>> = localDataSource.episodesFlow
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
override var genres: StateFlow<Set<Genre>> = MutableStateFlow(emptySet())
override suspend fun getMovie(id: UUID): Flow<Movie?> {
return movies.map { it[id] }
}

View File

@@ -3,6 +3,7 @@ package hu.bbara.purefin.data.converter
import hu.bbara.purefin.image.ArtworkKind
import hu.bbara.purefin.image.ImageUrlBuilder
import hu.bbara.purefin.model.Episode
import hu.bbara.purefin.model.Genre
import hu.bbara.purefin.model.Library
import hu.bbara.purefin.model.LibraryKind
import hu.bbara.purefin.model.Movie
@@ -112,6 +113,12 @@ fun BaseItemDto.toEpisode(serverUrl: String): Episode {
)
}
fun BaseItemDto.toGenre() : Genre {
return Genre(
name = name ?: "Unknown"
)
}
fun formatReleaseDate(date: LocalDateTime?, fallbackYear: Int?): String {
if (date == null) {
return fallbackYear?.toString() ?: ""