diff --git a/app/src/main/java/com/pinakes/app/data/model/Models.kt b/app/src/main/java/com/pinakes/app/data/model/Models.kt index bad397f..f1063c6 100644 --- a/app/src/main/java/com/pinakes/app/data/model/Models.kt +++ b/app/src/main/java/com/pinakes/app/data/model/Models.kt @@ -248,6 +248,17 @@ data class GenreNode( val children: List = emptyList(), ) +/** + * A distinct language value present in the catalogue, from GET /catalog/languages + * (#282). `libri.lingua` is free text, so the app must offer these real values — + * not a hardcoded ISO-code list that never matched the stored strings. + */ +@Serializable +data class LanguageValue( + val language: String = "", + val count: Int = 0, +) + // ---------- Loans / reservations ---------- @Serializable data class LoansData( diff --git a/app/src/main/java/com/pinakes/app/data/network/PinakesApi.kt b/app/src/main/java/com/pinakes/app/data/network/PinakesApi.kt index dbcf9da..64d1694 100644 --- a/app/src/main/java/com/pinakes/app/data/network/PinakesApi.kt +++ b/app/src/main/java/com/pinakes/app/data/network/PinakesApi.kt @@ -10,6 +10,7 @@ import com.pinakes.app.data.model.Envelope import com.pinakes.app.data.model.ForgotRequest import com.pinakes.app.data.model.GenreNode import com.pinakes.app.data.model.HealthPayload +import com.pinakes.app.data.model.LanguageValue import com.pinakes.app.data.model.LoanItem import com.pinakes.app.data.model.LoansData import com.pinakes.app.data.model.LoginRequest @@ -113,6 +114,10 @@ interface PinakesApi { @GET("catalog/genres") suspend fun genres(): Envelope> + // #282: real catalogue language values for the language filter. + @GET("catalog/languages") + suspend fun languages(): Envelope> + // ---- Reviews ---- /** Aggregate rating + the user's own review + a page of other users' reviews for a book. */ @GET("catalog/books/{id}/reviews") diff --git a/app/src/main/java/com/pinakes/app/data/repository/CatalogRepository.kt b/app/src/main/java/com/pinakes/app/data/repository/CatalogRepository.kt index 969461e..274c25c 100644 --- a/app/src/main/java/com/pinakes/app/data/repository/CatalogRepository.kt +++ b/app/src/main/java/com/pinakes/app/data/repository/CatalogRepository.kt @@ -7,6 +7,7 @@ import com.pinakes.app.data.model.AvailabilityCalendar import com.pinakes.app.data.model.BookDetail import com.pinakes.app.data.model.BookSummary import com.pinakes.app.data.model.GenreNode +import com.pinakes.app.data.model.LanguageValue import com.pinakes.app.data.network.ApiResult import com.pinakes.app.data.network.ErrorCodes import com.pinakes.app.data.network.NetworkModule @@ -147,6 +148,13 @@ class CatalogRepository( return apiCall { api.genres() } } + // #282: real catalogue language values, so the language filter offers what + // the collection actually holds instead of a hardcoded ISO-code list. + suspend fun languages(): ApiResult> { + val api = network.api() + return apiCall { api.languages() } + } + /** * Per-day availability for [id] (~180 days from today) used to drive the colored * loan-request calendar. Mirrors the website's availability view. diff --git a/app/src/main/java/com/pinakes/app/ui/screens/search/SearchFilterSheet.kt b/app/src/main/java/com/pinakes/app/ui/screens/search/SearchFilterSheet.kt index a7ccd44..055ccb1 100644 --- a/app/src/main/java/com/pinakes/app/ui/screens/search/SearchFilterSheet.kt +++ b/app/src/main/java/com/pinakes/app/ui/screens/search/SearchFilterSheet.kt @@ -195,12 +195,14 @@ fun SearchFilterSheet( label = { Text(stringResource(R.string.filters_all_languages)) }, colors = chipColors, ) - SearchLanguageOptions.forEach { opt -> - val selected = state.language == opt.code + // #282: real catalogue language values (free text), loaded from + // GET /catalog/languages, instead of a hardcoded ISO-code list. + state.languages.forEach { lang -> + val selected = state.language == lang.language FilterChip( selected = selected, - onClick = { onLanguageChange(if (selected) null else opt.code) }, - label = { Text(stringResource(opt.labelRes)) }, + onClick = { onLanguageChange(if (selected) null else lang.language) }, + label = { Text(lang.language) }, leadingIcon = if (selected) { { Icon(Icons.Outlined.Check, contentDescription = null, modifier = Modifier.height(18.dp)) } } else null, diff --git a/app/src/main/java/com/pinakes/app/ui/screens/search/SearchViewModel.kt b/app/src/main/java/com/pinakes/app/ui/screens/search/SearchViewModel.kt index 081472c..058613c 100644 --- a/app/src/main/java/com/pinakes/app/ui/screens/search/SearchViewModel.kt +++ b/app/src/main/java/com/pinakes/app/ui/screens/search/SearchViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import com.pinakes.app.R import com.pinakes.app.data.model.BookSummary import com.pinakes.app.data.model.GenreNode +import com.pinakes.app.data.model.LanguageValue import com.pinakes.app.data.network.ApiResult import com.pinakes.app.data.repository.CatalogRepository import com.pinakes.app.data.repository.SearchFilters @@ -40,6 +41,7 @@ data class SearchUiState( val language: String? = null, val sort: BookSort = BookSort.NEWEST, val genres: List = emptyList(), + val languages: List = emptyList(), val items: List = emptyList(), val nextCursor: String? = null, val totalCount: Int? = null, @@ -66,18 +68,6 @@ data class SearchUiState( get() = query.isBlank() && !hasActiveFilters && items.isEmpty() && !loading && error == null } -/** Languages offered in the filter sheet. `code` is sent to the API; `labelRes` is shown. */ -data class LanguageOption(val code: String, @StringRes val labelRes: Int) - -val SearchLanguageOptions: List = listOf( - LanguageOption("ita", R.string.lang_italian), - LanguageOption("eng", R.string.lang_english), - LanguageOption("fra", R.string.lang_french), - LanguageOption("deu", R.string.lang_german), - LanguageOption("spa", R.string.lang_spanish), - LanguageOption("lat", R.string.lang_latin), -) - @HiltViewModel class SearchViewModel @Inject constructor(private val catalog: CatalogRepository) : ViewModel() { @@ -97,6 +87,7 @@ class SearchViewModel @Inject constructor(private val catalog: CatalogRepository init { loadGenres() + loadLanguages() // Catalog lands on the full listing — browse-all (empty query, no filters). runSearch(reset = true) } @@ -110,6 +101,17 @@ class SearchViewModel @Inject constructor(private val catalog: CatalogRepository } } + // #282: populate the language filter from the real catalogue values instead + // of a hardcoded ISO-code list that never matched the free-text libri.lingua. + private fun loadLanguages() { + viewModelScope.launch { + when (val res = catalog.languages()) { + is ApiResult.Success -> _state.update { it.copy(languages = res.data) } + is ApiResult.Failure -> { /* non-fatal: filter simply has no language chips */ } + } + } + } + fun onQueryChange(value: String) { _state.update { it.copy(query = value) } // Debounced auto-search as the user types.