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:
parent
efbd2aff66
commit
a7a9f65391
7 changed files with 468 additions and 446 deletions
|
|
@ -10,8 +10,9 @@ import kotlinx.collections.immutable.toPersistentList
|
||||||
import org.dueattendant149.bookshelf.core.language.Language
|
import org.dueattendant149.bookshelf.core.language.Language
|
||||||
|
|
||||||
object CoreData {
|
object CoreData {
|
||||||
val defaultLanguage = Language.fromLanguageTag(languageTag = "en") // English
|
val defaultLanguage = Language.fromLanguageTag(languageTag = "ru") // Russian
|
||||||
val languages = listOf(
|
val languages = listOf(
|
||||||
|
Language.fromLanguageTag(languageTag = "ru"), // Russian
|
||||||
Language.fromLanguageTag(languageTag = "en"), // English
|
Language.fromLanguageTag(languageTag = "en"), // English
|
||||||
Language.fromLanguageTag(languageTag = "uk"), // Ukrainian
|
Language.fromLanguageTag(languageTag = "uk"), // Ukrainian
|
||||||
Language.fromLanguageTag(languageTag = "de"), // German
|
Language.fromLanguageTag(languageTag = "de"), // German
|
||||||
|
|
|
||||||
|
|
@ -35,28 +35,31 @@ interface BookshelfApiService {
|
||||||
|
|
||||||
// Health
|
// Health
|
||||||
@GET("health")
|
@GET("health")
|
||||||
suspend fun health(): Response<String>
|
suspend fun health(): Response<Unit>
|
||||||
|
|
||||||
// Libraries / books
|
// Libraries / books
|
||||||
@GET("api/v1/books/libraries")
|
@GET("api/v1/books/libraries")
|
||||||
suspend fun getLibraries(): Response<List<LibraryResponse>>
|
suspend fun getLibraries(): Response<List<LibraryResponse>>
|
||||||
|
|
||||||
|
@Streaming
|
||||||
@GET("api/v1/books/{itemId}")
|
@GET("api/v1/books/{itemId}")
|
||||||
suspend fun getBook(
|
suspend fun getBook(
|
||||||
@Path("itemId") itemId: String,
|
@Path("itemId") itemId: String,
|
||||||
): Response<String>
|
): Response<ResponseBody>
|
||||||
|
|
||||||
|
@Streaming
|
||||||
@POST("api/v1/books/{itemId}/progress")
|
@POST("api/v1/books/{itemId}/progress")
|
||||||
suspend fun updateReadingProgress(
|
suspend fun updateReadingProgress(
|
||||||
@Path("itemId") itemId: String,
|
@Path("itemId") itemId: String,
|
||||||
@Body request: ProgressUpdateRequest,
|
@Body request: ProgressUpdateRequest,
|
||||||
): Response<String>
|
): Response<ResponseBody>
|
||||||
|
|
||||||
|
@Streaming
|
||||||
@POST("api/v1/books/{itemId}/playback-progress")
|
@POST("api/v1/books/{itemId}/playback-progress")
|
||||||
suspend fun updatePlaybackProgress(
|
suspend fun updatePlaybackProgress(
|
||||||
@Path("itemId") itemId: String,
|
@Path("itemId") itemId: String,
|
||||||
@Body request: PlaybackProgressUpdateRequest,
|
@Body request: PlaybackProgressUpdateRequest,
|
||||||
): Response<String>
|
): Response<ResponseBody>
|
||||||
|
|
||||||
@GET("api/v1/books/library/{libraryId}/search")
|
@GET("api/v1/books/library/{libraryId}/search")
|
||||||
suspend fun searchLibrary(
|
suspend fun searchLibrary(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ package org.dueattendant149.bookshelf.data.settings
|
||||||
|
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import org.dueattendant149.bookshelf.data.local.data_store.DataStore
|
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.isValidUri
|
||||||
import org.dueattendant149.bookshelf.domain.util.normalizeUri
|
import org.dueattendant149.bookshelf.domain.util.normalizeUri
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -47,11 +48,12 @@ class ServerSettings
|
||||||
* or null when all credentials are present.
|
* or null when all credentials are present.
|
||||||
*/
|
*/
|
||||||
suspend fun getMissingCredentialsError(): String? {
|
suspend fun getMissingCredentialsError(): String? {
|
||||||
val bookshelfUrl = dataStore.getNullableData(bookshelfUrlKey)
|
val bookshelfUrl = dataStore.getNullableData(bookshelfUrlKey)?.trim()
|
||||||
if (bookshelfUrl.isNullOrBlank()) {
|
if (bookshelfUrl.isNullOrBlank()) {
|
||||||
return "Bookshelf API URL is missing"
|
return "Bookshelf API URL is missing"
|
||||||
}
|
}
|
||||||
if (!bookshelfUrl.isValidUri()) {
|
val normalizedBookshelfUrl = bookshelfUrl.ensureUriScheme()
|
||||||
|
if (!normalizedBookshelfUrl.isValidUri()) {
|
||||||
return "Bookshelf API URL is invalid: $bookshelfUrl"
|
return "Bookshelf API URL is invalid: $bookshelfUrl"
|
||||||
}
|
}
|
||||||
if (dataStore.getNullableData(absUrlKey).isNullOrBlank()) {
|
if (dataStore.getNullableData(absUrlKey).isNullOrBlank()) {
|
||||||
|
|
@ -67,8 +69,8 @@ class ServerSettings
|
||||||
val url = dataStore.getNullableData(bookshelfUrlKey)?.trim()
|
val url = dataStore.getNullableData(bookshelfUrlKey)?.trim()
|
||||||
return when {
|
return when {
|
||||||
url.isNullOrBlank() -> Result.failure(IllegalArgumentException("Bookshelf URL is empty"))
|
url.isNullOrBlank() -> Result.failure(IllegalArgumentException("Bookshelf URL is empty"))
|
||||||
!url.isValidUri() -> Result.failure(IllegalArgumentException("Bookshelf URL is invalid: $url"))
|
!url.ensureUriScheme().isValidUri() -> Result.failure(IllegalArgumentException("Bookshelf URL is invalid: $url"))
|
||||||
else -> Result.success(url.normalizeUri())
|
else -> Result.success(url.ensureUriScheme().normalizeUri())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,6 +82,8 @@ class ServerSettings
|
||||||
|
|
||||||
private fun String.normalizeIfValid(): String? {
|
private fun String.normalizeIfValid(): String? {
|
||||||
val trimmed = trim()
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ fun String.isValidUri(): Boolean {
|
||||||
/**
|
/**
|
||||||
* Returns true if the string starts with http:// or https:// (case-insensitive).
|
* 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.
|
* Adds https:// if the string has no http/https scheme.
|
||||||
|
|
|
||||||
|
|
@ -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.CheckHealthUseCase
|
||||||
import org.dueattendant149.bookshelf.domain.use_case.remote.FetchLibrariesUseCase
|
import org.dueattendant149.bookshelf.domain.use_case.remote.FetchLibrariesUseCase
|
||||||
import org.dueattendant149.bookshelf.domain.util.deriveAbsUrl
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class BookshelfSettingsState(
|
data class BookshelfSettingsState(
|
||||||
|
|
@ -108,8 +110,20 @@ class BookshelfSettingsModel
|
||||||
fun save() {
|
fun save() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val current = _state.value
|
val current = _state.value
|
||||||
serverSettings.setBookshelfUrl(current.bookshelfUrl)
|
val normalizedBookshelfUrl = current.bookshelfUrl.trim().let {
|
||||||
serverSettings.setAbsUrl(current.absUrl)
|
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)
|
serverSettings.setAbsToken(current.absToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ fun BookshelfSettingsContent(
|
||||||
value = state.bookshelfUrl,
|
value = state.bookshelfUrl,
|
||||||
onValueChange = model::updateBookshelfUrl,
|
onValueChange = model::updateBookshelfUrl,
|
||||||
label = { Text(stringResource(R.string.bookshelf_url_option)) },
|
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)) },
|
supportingText = { Text(stringResource(R.string.bookshelf_url_option_desc)) },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
@ -83,7 +83,7 @@ fun BookshelfSettingsContent(
|
||||||
value = state.absUrl,
|
value = state.absUrl,
|
||||||
onValueChange = model::updateAbsUrl,
|
onValueChange = model::updateAbsUrl,
|
||||||
label = { Text(stringResource(R.string.abs_url_option)) },
|
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)) },
|
supportingText = { Text(stringResource(R.string.abs_url_option_desc)) },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue