Fix API contract: Response<ResponseBody> for endpoints returning JSON objects

- getBook, updateReadingProgress, updatePlaybackProgress: Response<String> → Response<ResponseBody>
- Fixes kotlinx-serialization converter crash on JSON object responses
- health(): Response<Unit> (already fixed)
- Fix hasUriScheme(): matches() → containsMatchIn()
- Full Russian translation of strings.xml (583 lines)
- Russian added as default language in CoreData
This commit is contained in:
Atte149 2026-06-19 21:05:29 +03:00
parent efbd2aff66
commit a7a9f65391
7 changed files with 468 additions and 446 deletions

View file

@ -10,8 +10,9 @@ import kotlinx.collections.immutable.toPersistentList
import org.dueattendant149.bookshelf.core.language.Language
object CoreData {
val defaultLanguage = Language.fromLanguageTag(languageTag = "en") // English
val defaultLanguage = Language.fromLanguageTag(languageTag = "ru") // Russian
val languages = listOf(
Language.fromLanguageTag(languageTag = "ru"), // Russian
Language.fromLanguageTag(languageTag = "en"), // English
Language.fromLanguageTag(languageTag = "uk"), // Ukrainian
Language.fromLanguageTag(languageTag = "de"), // German

View file

@ -35,28 +35,31 @@ interface BookshelfApiService {
// Health
@GET("health")
suspend fun health(): Response<String>
suspend fun health(): Response<Unit>
// Libraries / books
@GET("api/v1/books/libraries")
suspend fun getLibraries(): Response<List<LibraryResponse>>
@Streaming
@GET("api/v1/books/{itemId}")
suspend fun getBook(
@Path("itemId") itemId: String,
): Response<String>
): Response<ResponseBody>
@Streaming
@POST("api/v1/books/{itemId}/progress")
suspend fun updateReadingProgress(
@Path("itemId") itemId: String,
@Body request: ProgressUpdateRequest,
): Response<String>
): Response<ResponseBody>
@Streaming
@POST("api/v1/books/{itemId}/playback-progress")
suspend fun updatePlaybackProgress(
@Path("itemId") itemId: String,
@Body request: PlaybackProgressUpdateRequest,
): Response<String>
): Response<ResponseBody>
@GET("api/v1/books/library/{libraryId}/search")
suspend fun searchLibrary(

View file

@ -8,6 +8,7 @@ package org.dueattendant149.bookshelf.data.settings
import androidx.datastore.preferences.core.stringPreferencesKey
import org.dueattendant149.bookshelf.data.local.data_store.DataStore
import org.dueattendant149.bookshelf.domain.util.ensureUriScheme
import org.dueattendant149.bookshelf.domain.util.isValidUri
import org.dueattendant149.bookshelf.domain.util.normalizeUri
import javax.inject.Inject
@ -47,11 +48,12 @@ class ServerSettings
* or null when all credentials are present.
*/
suspend fun getMissingCredentialsError(): String? {
val bookshelfUrl = dataStore.getNullableData(bookshelfUrlKey)
val bookshelfUrl = dataStore.getNullableData(bookshelfUrlKey)?.trim()
if (bookshelfUrl.isNullOrBlank()) {
return "Bookshelf API URL is missing"
}
if (!bookshelfUrl.isValidUri()) {
val normalizedBookshelfUrl = bookshelfUrl.ensureUriScheme()
if (!normalizedBookshelfUrl.isValidUri()) {
return "Bookshelf API URL is invalid: $bookshelfUrl"
}
if (dataStore.getNullableData(absUrlKey).isNullOrBlank()) {
@ -67,8 +69,8 @@ class ServerSettings
val url = dataStore.getNullableData(bookshelfUrlKey)?.trim()
return when {
url.isNullOrBlank() -> Result.failure(IllegalArgumentException("Bookshelf URL is empty"))
!url.isValidUri() -> Result.failure(IllegalArgumentException("Bookshelf URL is invalid: $url"))
else -> Result.success(url.normalizeUri())
!url.ensureUriScheme().isValidUri() -> Result.failure(IllegalArgumentException("Bookshelf URL is invalid: $url"))
else -> Result.success(url.ensureUriScheme().normalizeUri())
}
}
@ -80,6 +82,8 @@ class ServerSettings
private fun String.normalizeIfValid(): String? {
val trimmed = trim()
return if (trimmed.isValidUri()) trimmed.normalizeUri() else null
if (trimmed.isBlank()) return null
val withScheme = trimmed.ensureUriScheme()
return if (withScheme.isValidUri()) withScheme.normalizeUri() else null
}
}

View file

@ -20,7 +20,7 @@ fun String.isValidUri(): Boolean {
/**
* Returns true if the string starts with http:// or https:// (case-insensitive).
*/
fun String.hasUriScheme(): Boolean = matches(URL_SCHEME_REGEX)
fun String.hasUriScheme(): Boolean = URL_SCHEME_REGEX.containsMatchIn(this)
/**
* Adds https:// if the string has no http/https scheme.

View file

@ -18,6 +18,8 @@ import org.dueattendant149.bookshelf.domain.model.remote.RemoteLibrary
import org.dueattendant149.bookshelf.domain.use_case.remote.CheckHealthUseCase
import org.dueattendant149.bookshelf.domain.use_case.remote.FetchLibrariesUseCase
import org.dueattendant149.bookshelf.domain.util.deriveAbsUrl
import org.dueattendant149.bookshelf.domain.util.ensureUriScheme
import org.dueattendant149.bookshelf.domain.util.normalizeUri
import javax.inject.Inject
data class BookshelfSettingsState(
@ -108,8 +110,20 @@ class BookshelfSettingsModel
fun save() {
viewModelScope.launch {
val current = _state.value
serverSettings.setBookshelfUrl(current.bookshelfUrl)
serverSettings.setAbsUrl(current.absUrl)
val normalizedBookshelfUrl = current.bookshelfUrl.trim().let {
if (it.isNotBlank()) it.ensureUriScheme().normalizeUri() else it
}
val normalizedAbsUrl = current.absUrl.trim().let {
if (it.isNotBlank()) it.ensureUriScheme().normalizeUri() else it
}
_state.update {
it.copy(
bookshelfUrl = normalizedBookshelfUrl,
absUrl = normalizedAbsUrl,
)
}
serverSettings.setBookshelfUrl(normalizedBookshelfUrl)
serverSettings.setAbsUrl(normalizedAbsUrl)
serverSettings.setAbsToken(current.absToken)
}
}

View file

@ -73,7 +73,7 @@ fun BookshelfSettingsContent(
value = state.bookshelfUrl,
onValueChange = model::updateBookshelfUrl,
label = { Text(stringResource(R.string.bookshelf_url_option)) },
placeholder = { Text("http://bookshelf-api:8073") },
placeholder = { Text("https://tts.dueattendant149.org") },
supportingText = { Text(stringResource(R.string.bookshelf_url_option_desc)) },
modifier = Modifier.fillMaxWidth()
)
@ -83,7 +83,7 @@ fun BookshelfSettingsContent(
value = state.absUrl,
onValueChange = model::updateAbsUrl,
label = { Text(stringResource(R.string.abs_url_option)) },
placeholder = { Text("http://192.168.1.119:13378") },
placeholder = { Text("https://books.dueattendant149.org") },
supportingText = { Text(stringResource(R.string.abs_url_option_desc)) },
modifier = Modifier.fillMaxWidth()
)

File diff suppressed because it is too large Load diff