merge: PR #1025 feat(i18n) extract remaining hardcoded strings

Resolved conflicts in values/strings.xml and values-fr/strings.xml
by concatenating both blocks (no duplicate keys).
This commit is contained in:
Stéphane 2026-05-24 09:41:28 +02:00
commit 6ef8c42065
16 changed files with 364 additions and 107 deletions

View file

@ -7,9 +7,15 @@ import android.os.Build
import android.provider.Settings
import androidx.core.content.FileProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.updates_download_failed_http
import nuvio.composeapp.generated.resources.updates_downloaded_file_missing
import nuvio.composeapp.generated.resources.updates_empty_download_body
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jetbrains.compose.resources.getString
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.TimeUnit
@ -63,10 +69,10 @@ object AndroidAppUpdaterPlatform {
httpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
error("Download failed with HTTP ${response.code}")
error(runBlocking { getString(Res.string.updates_download_failed_http, response.code) })
}
val body = response.body ?: error("Empty download body")
val body = response.body ?: error(runBlocking { getString(Res.string.updates_empty_download_body) })
val totalBytes = body.contentLength().takeIf { it > 0L }
body.byteStream().use { input ->
FileOutputStream(destination).use { output ->
@ -115,7 +121,7 @@ object AndroidAppUpdaterPlatform {
fun installDownloadedApk(path: String): Result<Unit> = runCatching {
val context = requireContext()
val apkFile = File(path)
check(apkFile.exists()) { "Downloaded update file is missing." }
check(apkFile.exists()) { runBlocking { getString(Res.string.updates_downloaded_file_missing) } }
val apkUri = FileProvider.getUriForFile(
context,

View file

@ -4,7 +4,12 @@ import android.content.Context
import android.content.SharedPreferences
import com.nuvio.app.core.network.IPv4FirstDns
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.network_empty_response_body
import nuvio.composeapp.generated.resources.network_request_failed_http
import org.jetbrains.compose.resources.getString
import okhttp3.ResponseBody
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
@ -182,10 +187,10 @@ private suspend fun executeTextRequest(
addonHttpClient.newCall(request).execute().use { response ->
val payload = readResponseBody(response.body)
if (!response.isSuccessful) {
error("Request failed with HTTP ${response.code}")
error(runBlocking { getString(Res.string.network_request_failed_http, response.code) })
}
if (payload.isBlank()) {
throw IllegalStateException("Empty response body")
throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) })
}
payload
}

View file

@ -111,7 +111,7 @@ internal actual object DownloadsLiveStatusPlatform {
.setPriority(NotificationCompat.PRIORITY_LOW)
.addAction(
0,
"Pause",
runBlocking { getString(Res.string.compose_action_pause) },
buildActionPendingIntent(
context = context,
action = DownloadsNotificationActionReceiver.actionPause,
@ -178,7 +178,15 @@ internal actual object DownloadsLiveStatusPlatform {
private fun formatBytes(bytes: Long): String {
val safe = bytes.coerceAtLeast(0L).toDouble()
val units = arrayOf("B", "KB", "MB", "GB", "TB")
val units = runBlocking {
arrayOf(
getString(Res.string.unit_bytes_b),
getString(Res.string.unit_bytes_kb),
getString(Res.string.unit_bytes_mb),
getString(Res.string.unit_bytes_gb),
getString(Res.string.unit_bytes_tb),
)
}
var value = safe
var unitIndex = 0
while (value >= 1024.0 && unitIndex < units.lastIndex) {

View file

@ -1459,4 +1459,72 @@
<string name="settings_debrid_formatter_reset_title">Réinitialiser la mise en forme</string>
<string name="settings_debrid_not_set">Non défini</string>
<string name="settings_debrid_prepare_stream_count_warning">Privilégie une valeur basse quand cest possible. Les services Debrid peuvent limiter le nombre de liens résolus dans une période donnée. Ouvrir un film ou un épisode peut compter dans ces limites même sans appuyer sur Lecture, car les liens sont préparés à lavance.</string>
<string name="unit_bytes_tb">To</string>
<string name="collections_editor_tmdb_genres_movie_placeholder">28,12</string>
<string name="collections_editor_tmdb_genres_series_placeholder">18,35</string>
<string name="collections_editor_tmdb_date_from_placeholder">2020-01-01</string>
<string name="collections_editor_tmdb_date_to_placeholder">2024-12-31</string>
<string name="collections_editor_tmdb_rating_min_placeholder">7.0</string>
<string name="collections_editor_tmdb_rating_max_placeholder">10</string>
<string name="collections_editor_tmdb_votes_min_placeholder">100</string>
<string name="collections_editor_tmdb_language_placeholder">en, ko, ja, hi</string>
<string name="collections_editor_tmdb_country_placeholder">US, KR, JP, IN</string>
<string name="collections_editor_tmdb_year_placeholder">2024</string>
<string name="collections_editor_trakt_list_id_format">ID %1$s</string>
<string name="player_addon_subtitle_display_format">%1$s (%2$s)</string>
<string name="trakt_connected_status">Connecté à Trakt</string>
<string name="trakt_disconnected_status">Déconnecté de Trakt</string>
<string name="collections_editor_trakt_enter_name_url_or_id">Saisis un nom, une URL ou un ID de liste Trakt</string>
<string name="collections_editor_trakt_enter_id_or_url">Saisis un ID ou une URL de liste Trakt</string>
<string name="collections_editor_trakt_load_failed">Impossible de charger la liste Trakt</string>
<string name="collections_editor_trakt_fallback_title">Liste Trakt %1$d</string>
<string name="collections_editor_trakt_resolved_subtitle">Liste Trakt résolue</string>
<string name="collections_editor_trakt_no_lists_found">Aucune liste Trakt trouvée</string>
<string name="collections_editor_tmdb_invalid_id_or_url">Saisis un ID ou une URL TMDB valide.</string>
<string name="collections_editor_tmdb_list_title_format">Liste TMDB %1$s</string>
<string name="collections_editor_tmdb_collection_title_format">Collection TMDB %1$s</string>
<string name="collections_editor_tmdb_production_title_format">Production TMDB %1$s</string>
<string name="collections_editor_tmdb_network_title_format">Chaîne TMDB %1$s</string>
<string name="collections_editor_tmdb_person_title_format">Personne TMDB %1$s</string>
<string name="collections_editor_tmdb_director_title_format">Réalisateur TMDB %1$s</string>
<string name="collections_editor_tmdb_discover_title">Découverte TMDB</string>
<string name="collections_editor_tmdb_source_load_failed">Impossible de charger la source TMDB</string>
<string name="collections_folder_trakt_series_list">Liste Trakt de séries</string>
<string name="collections_folder_trakt_movie_list">Liste Trakt de films</string>
<string name="collections_tmdb_api_key_required">Ajoute une clé API TMDB dans les Paramètres pour utiliser les sources TMDB.</string>
<string name="collections_tmdb_list_not_found">Liste TMDB introuvable</string>
<string name="collections_tmdb_collection_not_found">Collection TMDB introuvable</string>
<string name="collections_tmdb_company_not_found">Société TMDB introuvable</string>
<string name="collections_tmdb_network_not_found">Chaîne TMDB introuvable</string>
<string name="collections_tmdb_person_not_found">Personne TMDB introuvable</string>
<string name="collections_tmdb_missing_list_id">ID de liste TMDB manquant</string>
<string name="collections_tmdb_missing_collection_id">ID de collection TMDB manquant</string>
<string name="collections_tmdb_missing_person_id">ID de personne TMDB manquant</string>
<string name="collections_tmdb_person_credits_not_found">Crédits TMDB de la personne introuvables</string>
<string name="collections_tmdb_discover_no_data">Découverte TMDB na retourné aucune donnée</string>
<string name="collections_trakt_missing_list_id">ID de liste Trakt manquant</string>
<string name="collections_trakt_invalid_list_id_or_url">Saisis un ID ou une URL de liste Trakt valide</string>
<string name="collections_trakt_missing_numeric_id">La liste Trakt ne contient pas dID numérique</string>
<string name="collections_trakt_request_failed">Échec de la requête Trakt</string>
<string name="collections_trakt_credentials_missing">Identifiants Trakt manquants.</string>
<string name="collections_trakt_list_items_count">%1$d éléments</string>
<string name="collections_trakt_list_likes_count">%1$d jaime</string>
<string name="collections_trakt_public_list">Liste Trakt publique</string>
<string name="collections_trakt_list_not_found_or_private">Liste Trakt introuvable ou non publique</string>
<string name="collections_trakt_rate_limit_reached">Limite de requêtes Trakt atteinte</string>
<string name="collections_trakt_error_with_code">%1$s (%2$d)</string>
<string name="details_comments_trakt_load_failed_with_code">Échec du chargement des commentaires Trakt (%1$d)</string>
<string name="updates_no_channel_release">Aucune mise à jour na encore été publiée.</string>
<string name="updates_github_api_error">Erreur API releases GitHub : %1$d</string>
<string name="updates_release_missing_title">La release na pas de tag ni de nom</string>
<string name="updates_apk_asset_missing">Aucun fichier APK trouvé dans la release</string>
<string name="updates_download_failed_http">Échec du téléchargement (HTTP %1$d)</string>
<string name="updates_empty_download_body">Corps de téléchargement vide</string>
<string name="updates_downloaded_file_missing">Le fichier de mise à jour téléchargé est introuvable.</string>
<string name="network_request_failed_http">Échec de la requête (HTTP %1$d)</string>
<string name="network_empty_response_body">Corps de réponse vide</string>
<string name="downloads_error_finalize_file_failed">Impossible de finaliser le fichier de téléchargement</string>
<string name="downloads_error_open_partial_file_failed">Impossible douvrir le fichier de téléchargement partiel</string>
<string name="downloads_error_partial_file_not_open">Le fichier de téléchargement partiel nest pas ouvert</string>
<string name="downloads_error_write_partial_file_failed">Impossible décrire dans le fichier de téléchargement partiel</string>
</resources>

View file

@ -1516,4 +1516,72 @@
<string name="network_please_check_connection">Please check your connection and try again.</string>
<string name="library_local_tab_title">Nuvio Library</string>
<string name="streams_plugin_repository_fallback">Plugin repository</string>
<string name="unit_bytes_tb">TB</string>
<string name="collections_editor_tmdb_genres_movie_placeholder">28,12</string>
<string name="collections_editor_tmdb_genres_series_placeholder">18,35</string>
<string name="collections_editor_tmdb_date_from_placeholder">2020-01-01</string>
<string name="collections_editor_tmdb_date_to_placeholder">2024-12-31</string>
<string name="collections_editor_tmdb_rating_min_placeholder">7.0</string>
<string name="collections_editor_tmdb_rating_max_placeholder">10</string>
<string name="collections_editor_tmdb_votes_min_placeholder">100</string>
<string name="collections_editor_tmdb_language_placeholder">en, ko, ja, hi</string>
<string name="collections_editor_tmdb_country_placeholder">US, KR, JP, IN</string>
<string name="collections_editor_tmdb_year_placeholder">2024</string>
<string name="collections_editor_trakt_list_id_format">ID %1$s</string>
<string name="player_addon_subtitle_display_format">%1$s (%2$s)</string>
<string name="trakt_connected_status">Connected to Trakt</string>
<string name="trakt_disconnected_status">Disconnected from Trakt</string>
<string name="collections_editor_trakt_enter_name_url_or_id">Enter a Trakt list name, URL, or ID</string>
<string name="collections_editor_trakt_enter_id_or_url">Enter a Trakt list ID or URL</string>
<string name="collections_editor_trakt_load_failed">Could not load Trakt list</string>
<string name="collections_editor_trakt_fallback_title">Trakt List %1$d</string>
<string name="collections_editor_trakt_resolved_subtitle">Resolved Trakt list</string>
<string name="collections_editor_trakt_no_lists_found">No Trakt lists found</string>
<string name="collections_editor_tmdb_invalid_id_or_url">Enter a valid TMDB ID or URL.</string>
<string name="collections_editor_tmdb_list_title_format">TMDB List %1$s</string>
<string name="collections_editor_tmdb_collection_title_format">TMDB Collection %1$s</string>
<string name="collections_editor_tmdb_production_title_format">TMDB Production %1$s</string>
<string name="collections_editor_tmdb_network_title_format">TMDB Network %1$s</string>
<string name="collections_editor_tmdb_person_title_format">TMDB Person %1$s</string>
<string name="collections_editor_tmdb_director_title_format">TMDB Director %1$s</string>
<string name="collections_editor_tmdb_discover_title">TMDB Discover</string>
<string name="collections_editor_tmdb_source_load_failed">Could not load TMDB source</string>
<string name="collections_folder_trakt_series_list">Trakt Series List</string>
<string name="collections_folder_trakt_movie_list">Trakt Movie List</string>
<string name="collections_tmdb_api_key_required">Add a TMDB API key in Settings to use TMDB sources.</string>
<string name="collections_tmdb_list_not_found">TMDB list not found</string>
<string name="collections_tmdb_collection_not_found">TMDB collection not found</string>
<string name="collections_tmdb_company_not_found">TMDB company not found</string>
<string name="collections_tmdb_network_not_found">TMDB network not found</string>
<string name="collections_tmdb_person_not_found">TMDB person not found</string>
<string name="collections_tmdb_missing_list_id">Missing TMDB list ID</string>
<string name="collections_tmdb_missing_collection_id">Missing TMDB collection ID</string>
<string name="collections_tmdb_missing_person_id">Missing TMDB person ID</string>
<string name="collections_tmdb_person_credits_not_found">TMDB person credits not found</string>
<string name="collections_tmdb_discover_no_data">TMDB discover returned no data</string>
<string name="collections_trakt_missing_list_id">Missing Trakt list ID</string>
<string name="collections_trakt_invalid_list_id_or_url">Enter a valid Trakt list ID or URL</string>
<string name="collections_trakt_missing_numeric_id">Trakt list did not include a numeric ID</string>
<string name="collections_trakt_request_failed">Trakt request failed</string>
<string name="collections_trakt_credentials_missing">Missing Trakt credentials.</string>
<string name="collections_trakt_list_items_count">%1$d items</string>
<string name="collections_trakt_list_likes_count">%1$d likes</string>
<string name="collections_trakt_public_list">Trakt public list</string>
<string name="collections_trakt_list_not_found_or_private">Trakt list not found or not public</string>
<string name="collections_trakt_rate_limit_reached">Trakt rate limit reached</string>
<string name="collections_trakt_error_with_code">%1$s (%2$d)</string>
<string name="details_comments_trakt_load_failed_with_code">Failed to load Trakt comments (%1$d)</string>
<string name="updates_no_channel_release">No update has been published yet.</string>
<string name="updates_github_api_error">GitHub releases API error: %1$d</string>
<string name="updates_release_missing_title">Release has no tag or name</string>
<string name="updates_apk_asset_missing">No APK asset found in the release</string>
<string name="updates_download_failed_http">Download failed with HTTP %1$d</string>
<string name="updates_empty_download_body">Empty download body</string>
<string name="updates_downloaded_file_missing">Downloaded update file is missing.</string>
<string name="network_request_failed_http">Request failed with HTTP %1$d</string>
<string name="network_empty_response_body">Empty response body</string>
<string name="downloads_error_finalize_file_failed">Failed to finalize download file</string>
<string name="downloads_error_open_partial_file_failed">Failed to open partial download file</string>
<string name="downloads_error_partial_file_not_open">Partial download file is not open</string>
<string name="downloads_error_write_partial_file_failed">Failed to write partial download file</string>
</resources>

View file

@ -11,6 +11,26 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.collections_editor_tmdb_collection_title_format
import nuvio.composeapp.generated.resources.collections_editor_tmdb_director_title_format
import nuvio.composeapp.generated.resources.collections_editor_tmdb_discover_title
import nuvio.composeapp.generated.resources.collections_editor_tmdb_invalid_id_or_url
import nuvio.composeapp.generated.resources.collections_editor_tmdb_list_title_format
import nuvio.composeapp.generated.resources.collections_editor_tmdb_network_title_format
import nuvio.composeapp.generated.resources.collections_editor_tmdb_person_title_format
import nuvio.composeapp.generated.resources.collections_editor_tmdb_production_title_format
import nuvio.composeapp.generated.resources.collections_editor_tmdb_source_load_failed
import nuvio.composeapp.generated.resources.collections_editor_trakt_enter_id_or_url
import nuvio.composeapp.generated.resources.collections_editor_trakt_enter_name_url_or_id
import nuvio.composeapp.generated.resources.collections_editor_trakt_fallback_title
import nuvio.composeapp.generated.resources.collections_editor_trakt_load_failed
import nuvio.composeapp.generated.resources.collections_editor_trakt_no_lists_found
import nuvio.composeapp.generated.resources.collections_editor_trakt_resolved_subtitle
import nuvio.composeapp.generated.resources.media_movies
import nuvio.composeapp.generated.resources.media_series
import org.jetbrains.compose.resources.getString
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@ -394,7 +414,9 @@ object CollectionEditorRepository {
val state = _uiState.value
val query = state.traktInput.trim()
if (query.isBlank()) {
_uiState.value = state.copy(traktSearchError = "Enter a Trakt list name, URL, or ID")
_uiState.value = state.copy(
traktSearchError = runBlocking { getString(Res.string.collections_editor_trakt_enter_name_url_or_id) },
)
return
}
@ -402,12 +424,13 @@ object CollectionEditorRepository {
val results = if (query.isTraktListIdentifierInput()) {
runCatching {
val metadata = TraktPublicListSourceResolver.listImportMetadata(query)
val id = metadata.traktListId ?: error("Could not load Trakt list")
val id = metadata.traktListId
?: error(getString(Res.string.collections_editor_trakt_load_failed))
listOf(
TraktPublicListSearchResult(
traktListId = id,
title = metadata.title ?: "Trakt List $id",
subtitle = "Resolved Trakt list",
title = metadata.title ?: getString(Res.string.collections_editor_trakt_fallback_title, id),
subtitle = getString(Res.string.collections_editor_trakt_resolved_subtitle),
coverImageUrl = metadata.coverImageUrl,
),
)
@ -419,7 +442,7 @@ object CollectionEditorRepository {
_uiState.value = _uiState.value.copy(
traktSearchResults = mapped,
traktSearchError = results.exceptionOrNull()?.message
?: if (mapped.isEmpty()) "No Trakt lists found" else null,
?: if (mapped.isEmpty()) getString(Res.string.collections_editor_trakt_no_lists_found) else null,
)
}
}
@ -624,19 +647,24 @@ object CollectionEditorRepository {
}
val id = TmdbCollectionSourceResolver.parseTmdbId(state.tmdbInput)
if (sourceType != TmdbCollectionSourceType.DISCOVER && id == null) {
_uiState.value = state.copy(tmdbSearchError = "Enter a valid TMDB ID or URL.")
_uiState.value = state.copy(
tmdbSearchError = runBlocking { getString(Res.string.collections_editor_tmdb_invalid_id_or_url) },
)
return
}
val mediaTypes = selectedMediaTypes(state, sourceType)
val baseTitle = state.tmdbTitleInput.ifBlank {
when (sourceType) {
TmdbCollectionSourceType.LIST -> "TMDB List ${id ?: ""}".trim()
TmdbCollectionSourceType.COLLECTION -> "TMDB Collection ${id ?: ""}".trim()
TmdbCollectionSourceType.COMPANY -> "TMDB Production ${id ?: ""}".trim()
TmdbCollectionSourceType.NETWORK -> "TMDB Network ${id ?: ""}".trim()
TmdbCollectionSourceType.PERSON -> "TMDB Person ${id ?: ""}".trim()
TmdbCollectionSourceType.DIRECTOR -> "TMDB Director ${id ?: ""}".trim()
TmdbCollectionSourceType.DISCOVER -> "TMDB Discover"
runBlocking {
val idArg = id?.toString().orEmpty()
when (sourceType) {
TmdbCollectionSourceType.LIST -> getString(Res.string.collections_editor_tmdb_list_title_format, idArg).trim()
TmdbCollectionSourceType.COLLECTION -> getString(Res.string.collections_editor_tmdb_collection_title_format, idArg).trim()
TmdbCollectionSourceType.COMPANY -> getString(Res.string.collections_editor_tmdb_production_title_format, idArg).trim()
TmdbCollectionSourceType.NETWORK -> getString(Res.string.collections_editor_tmdb_network_title_format, idArg).trim()
TmdbCollectionSourceType.PERSON -> getString(Res.string.collections_editor_tmdb_person_title_format, idArg).trim()
TmdbCollectionSourceType.DIRECTOR -> getString(Res.string.collections_editor_tmdb_director_title_format, idArg).trim()
TmdbCollectionSourceType.DISCOVER -> getString(Res.string.collections_editor_tmdb_discover_title)
}
}
}
val sources = mediaTypes.map { mediaType ->
@ -656,7 +684,8 @@ object CollectionEditorRepository {
val resolved = metadata.getOrNull()
if (metadata.isFailure) {
_uiState.value = _uiState.value.copy(
tmdbSearchError = metadata.exceptionOrNull()?.message ?: "Could not load TMDB source",
tmdbSearchError = metadata.exceptionOrNull()?.message
?: getString(Res.string.collections_editor_tmdb_source_load_failed),
)
return@launch
}
@ -701,7 +730,9 @@ object CollectionEditorRepository {
val state = _uiState.value
val input = state.traktInput.trim()
if (input.isBlank()) {
_uiState.value = state.copy(traktSearchError = "Enter a Trakt list ID or URL")
_uiState.value = state.copy(
traktSearchError = runBlocking { getString(Res.string.collections_editor_trakt_enter_id_or_url) },
)
return
}
@ -711,12 +742,15 @@ object CollectionEditorRepository {
val listId = resolved?.traktListId
if (metadata.isFailure || listId == null) {
_uiState.value = _uiState.value.copy(
traktSearchError = metadata.exceptionOrNull()?.message ?: "Could not load Trakt list",
traktSearchError = metadata.exceptionOrNull()?.message
?: getString(Res.string.collections_editor_trakt_load_failed),
)
return@launch
}
val title = state.traktTitleInput.ifBlank { resolved.title ?: "Trakt List $listId" }
val title = state.traktTitleInput.ifBlank {
resolved.title ?: getString(Res.string.collections_editor_trakt_fallback_title, listId)
}
addTraktSourcesToFolder(
sources = selectedTraktMediaTypes(state).map { mediaType ->
CollectionSource(
@ -881,9 +915,13 @@ private fun titleForMedia(
addSuffix: Boolean,
): String {
if (!addSuffix) return title
val suffix = when (mediaType) {
TmdbCollectionMediaType.MOVIE -> "Movies"
TmdbCollectionMediaType.TV -> "Series"
val suffix = runBlocking {
getString(
when (mediaType) {
TmdbCollectionMediaType.MOVIE -> Res.string.media_movies
TmdbCollectionMediaType.TV -> Res.string.media_series
},
)
}
return "$title $suffix"
}

View file

@ -1162,7 +1162,11 @@ private fun TmdbSourcePickerScreen(
label = stringResource(Res.string.collections_editor_tmdb_genres),
helper = stringResource(Res.string.collections_editor_tmdb_genres_helper),
value = state.tmdbFilters.withGenres.orEmpty(),
placeholder = if (state.tmdbMediaType == TmdbCollectionMediaType.MOVIE) "28,12" else "18,35",
placeholder = if (state.tmdbMediaType == TmdbCollectionMediaType.MOVIE) {
stringResource(Res.string.collections_editor_tmdb_genres_movie_placeholder)
} else {
stringResource(Res.string.collections_editor_tmdb_genres_series_placeholder)
},
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(withGenres = value.ifBlank { null })
@ -1173,7 +1177,7 @@ private fun TmdbSourcePickerScreen(
label = stringResource(Res.string.collections_editor_tmdb_date_from),
helper = stringResource(Res.string.collections_editor_tmdb_date_helper),
value = state.tmdbFilters.releaseDateGte.orEmpty(),
placeholder = "2020-01-01",
placeholder = stringResource(Res.string.collections_editor_tmdb_date_from_placeholder),
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(releaseDateGte = value.ifBlank { null })
@ -1184,7 +1188,7 @@ private fun TmdbSourcePickerScreen(
label = stringResource(Res.string.collections_editor_tmdb_date_to),
helper = stringResource(Res.string.collections_editor_tmdb_date_helper),
value = state.tmdbFilters.releaseDateLte.orEmpty(),
placeholder = "2024-12-31",
placeholder = stringResource(Res.string.collections_editor_tmdb_date_to_placeholder),
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(releaseDateLte = value.ifBlank { null })
@ -1195,7 +1199,7 @@ private fun TmdbSourcePickerScreen(
label = stringResource(Res.string.collections_editor_tmdb_rating_min),
helper = stringResource(Res.string.collections_editor_tmdb_rating_helper),
value = state.tmdbFilters.voteAverageGte?.toString().orEmpty(),
placeholder = "7.0",
placeholder = stringResource(Res.string.collections_editor_tmdb_rating_min_placeholder),
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(voteAverageGte = value.toDoubleOrNull())
@ -1206,7 +1210,7 @@ private fun TmdbSourcePickerScreen(
label = stringResource(Res.string.collections_editor_tmdb_rating_max),
helper = stringResource(Res.string.collections_editor_tmdb_rating_helper),
value = state.tmdbFilters.voteAverageLte?.toString().orEmpty(),
placeholder = "10",
placeholder = stringResource(Res.string.collections_editor_tmdb_rating_max_placeholder),
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(voteAverageLte = value.toDoubleOrNull())
@ -1217,7 +1221,7 @@ private fun TmdbSourcePickerScreen(
label = stringResource(Res.string.collections_editor_tmdb_votes_min),
helper = stringResource(Res.string.collections_editor_tmdb_votes_helper),
value = state.tmdbFilters.voteCountGte?.toString().orEmpty(),
placeholder = "100",
placeholder = stringResource(Res.string.collections_editor_tmdb_votes_min_placeholder),
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(voteCountGte = value.toIntOrNull())
@ -1241,7 +1245,7 @@ private fun TmdbSourcePickerScreen(
label = stringResource(Res.string.collections_editor_tmdb_language),
helper = stringResource(Res.string.collections_editor_tmdb_language_helper),
value = state.tmdbFilters.withOriginalLanguage.orEmpty(),
placeholder = "en, ko, ja, hi",
placeholder = stringResource(Res.string.collections_editor_tmdb_language_placeholder),
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(withOriginalLanguage = value.ifBlank { null })
@ -1265,7 +1269,7 @@ private fun TmdbSourcePickerScreen(
label = stringResource(Res.string.collections_editor_tmdb_country),
helper = stringResource(Res.string.collections_editor_tmdb_country_helper),
value = state.tmdbFilters.withOriginCountry.orEmpty(),
placeholder = "US, KR, JP, IN",
placeholder = stringResource(Res.string.collections_editor_tmdb_country_placeholder),
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(withOriginCountry = value.ifBlank { null })
@ -1347,7 +1351,7 @@ private fun TmdbSourcePickerScreen(
label = stringResource(Res.string.collections_editor_tmdb_year),
helper = stringResource(Res.string.collections_editor_tmdb_year_helper),
value = state.tmdbFilters.year?.toString().orEmpty(),
placeholder = "2024",
placeholder = stringResource(Res.string.collections_editor_tmdb_year_placeholder),
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(year = value.toIntOrNull())
@ -2352,7 +2356,7 @@ private fun traktSourceSubtitle(source: CollectionSource): String {
media,
traktSortLabel(source.sortBy),
traktDirectionLabel(source.sortHow),
"ID ${source.traktListId ?: ""}".trim(),
stringResource(Res.string.collections_editor_trakt_list_id_format, source.traktListId ?: ""),
).joinToString("")
}

View file

@ -26,6 +26,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.collections_folder_addon_not_found
import nuvio.composeapp.generated.resources.collections_folder_trakt_movie_list
import nuvio.composeapp.generated.resources.collections_folder_trakt_series_list
import nuvio.composeapp.generated.resources.collections_tab_all
import org.jetbrains.compose.resources.getString
@ -156,10 +158,14 @@ object FolderDetailRepository {
} else if (source.isTrakt) {
val mediaType = TmdbCollectionMediaType.fromString(source.mediaType)
val type = if (mediaType == TmdbCollectionMediaType.TV) "series" else "movie"
val typeLabel = if (mediaType == TmdbCollectionMediaType.TV) {
"Trakt Series List"
} else {
"Trakt Movie List"
val typeLabel = runBlocking {
getString(
if (mediaType == TmdbCollectionMediaType.TV) {
Res.string.collections_folder_trakt_series_list
} else {
Res.string.collections_folder_trakt_movie_list
},
)
}
add(
FolderTab(

View file

@ -13,6 +13,19 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.collections_tmdb_api_key_required
import nuvio.composeapp.generated.resources.collections_tmdb_collection_not_found
import nuvio.composeapp.generated.resources.collections_tmdb_company_not_found
import nuvio.composeapp.generated.resources.collections_tmdb_discover_no_data
import nuvio.composeapp.generated.resources.collections_tmdb_list_not_found
import nuvio.composeapp.generated.resources.collections_tmdb_missing_collection_id
import nuvio.composeapp.generated.resources.collections_tmdb_missing_list_id
import nuvio.composeapp.generated.resources.collections_tmdb_missing_person_id
import nuvio.composeapp.generated.resources.collections_tmdb_network_not_found
import nuvio.composeapp.generated.resources.collections_tmdb_person_credits_not_found
import nuvio.composeapp.generated.resources.collections_tmdb_person_not_found
import org.jetbrains.compose.resources.getString
import kotlin.math.roundToInt
object TmdbCollectionSourceResolver {
@ -22,7 +35,7 @@ object TmdbCollectionSourceResolver {
suspend fun resolve(source: CollectionSource, page: Int = 1): CatalogPage = withContext(Dispatchers.Default) {
val settings = TmdbSettingsRepository.snapshot()
val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() }
?: error("Add a TMDB API key in Settings to use TMDB sources.")
?: error(getString(Res.string.collections_tmdb_api_key_required))
val language = normalizeTmdbLanguage(settings.language)
val sourceType = source.tmdbType()
@ -41,7 +54,7 @@ object TmdbCollectionSourceResolver {
withContext(Dispatchers.Default) {
val settings = TmdbSettingsRepository.snapshot()
val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() }
?: error("Add a TMDB API key in Settings to use TMDB sources.")
?: error(getString(Res.string.collections_tmdb_api_key_required))
val language = normalizeTmdbLanguage(settings.language)
when (sourceType) {
TmdbCollectionSourceType.LIST -> {
@ -49,7 +62,7 @@ object TmdbCollectionSourceResolver {
endpoint = "list/$id",
apiKey = apiKey,
query = mapOf("language" to language, "page" to "1"),
) ?: error("TMDB list not found")
) ?: error(getString(Res.string.collections_tmdb_list_not_found))
TmdbSourceImportMetadata(title = body.name?.takeIf { it.isNotBlank() })
}
@ -58,7 +71,7 @@ object TmdbCollectionSourceResolver {
endpoint = "collection/$id",
apiKey = apiKey,
query = mapOf("language" to language),
) ?: error("TMDB collection not found")
) ?: error(getString(Res.string.collections_tmdb_collection_not_found))
TmdbSourceImportMetadata(
title = body.name?.takeIf { it.isNotBlank() },
coverImageUrl = imageUrl(body.posterPath, "w500") ?: imageUrl(body.backdropPath, "w1280"),
@ -69,7 +82,7 @@ object TmdbCollectionSourceResolver {
val body = fetch<TmdbCompanyResponse>(
endpoint = "company/$id",
apiKey = apiKey,
) ?: error("TMDB company not found")
) ?: error(getString(Res.string.collections_tmdb_company_not_found))
TmdbSourceImportMetadata(
title = body.name?.takeIf { it.isNotBlank() },
coverImageUrl = imageUrl(body.logoPath, "w500"),
@ -80,7 +93,7 @@ object TmdbCollectionSourceResolver {
val body = fetch<TmdbNetworkResponse>(
endpoint = "network/$id",
apiKey = apiKey,
) ?: error("TMDB network not found")
) ?: error(getString(Res.string.collections_tmdb_network_not_found))
TmdbSourceImportMetadata(
title = body.name?.takeIf { it.isNotBlank() },
coverImageUrl = imageUrl(body.logoPath, "w500"),
@ -93,7 +106,7 @@ object TmdbCollectionSourceResolver {
endpoint = "person/$id",
apiKey = apiKey,
query = mapOf("language" to language),
) ?: error("TMDB person not found")
) ?: error(getString(Res.string.collections_tmdb_person_not_found))
TmdbSourceImportMetadata(
title = body.name?.takeIf { it.isNotBlank() },
coverImageUrl = imageUrl(body.profilePath, "w500"),
@ -109,7 +122,7 @@ object TmdbCollectionSourceResolver {
if (trimmed.isBlank()) return@withContext emptyList()
val settings = TmdbSettingsRepository.snapshot()
val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() }
?: error("Add a TMDB API key in Settings to use TMDB sources.")
?: error(getString(Res.string.collections_tmdb_api_key_required))
fetch<TmdbCompanySearchResponse>(
endpoint = "search/company",
apiKey = apiKey,
@ -122,7 +135,7 @@ object TmdbCollectionSourceResolver {
if (trimmed.isBlank()) return@withContext emptyList()
val settings = TmdbSettingsRepository.snapshot()
val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() }
?: error("Add a TMDB API key in Settings to use TMDB sources.")
?: error(getString(Res.string.collections_tmdb_api_key_required))
val language = normalizeTmdbLanguage(settings.language)
fetch<TmdbCollectionSearchResponse>(
endpoint = "search/collection",
@ -136,7 +149,7 @@ object TmdbCollectionSourceResolver {
if (trimmed.isBlank()) return@withContext emptyMap()
val settings = TmdbSettingsRepository.snapshot()
val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() }
?: error("Add a TMDB API key in Settings to use TMDB sources.")
?: error(getString(Res.string.collections_tmdb_api_key_required))
fetch<TmdbKeywordSearchResponse>(
endpoint = "search/keyword",
apiKey = apiKey,
@ -152,7 +165,7 @@ object TmdbCollectionSourceResolver {
suspend fun genres(mediaType: TmdbCollectionMediaType): Map<Int, String> = withContext(Dispatchers.Default) {
val settings = TmdbSettingsRepository.snapshot()
val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() }
?: error("Add a TMDB API key in Settings to use TMDB sources.")
?: error(getString(Res.string.collections_tmdb_api_key_required))
val language = normalizeTmdbLanguage(settings.language)
val endpoint = when (mediaType) {
TmdbCollectionMediaType.MOVIE -> "genre/movie/list"
@ -200,12 +213,12 @@ object TmdbCollectionSourceResolver {
language: String,
page: Int,
): CatalogPage {
val id = source.tmdbId ?: error("Missing TMDB list ID")
val id = source.tmdbId ?: error(getString(Res.string.collections_tmdb_missing_list_id))
val body = fetch<TmdbListResponse>(
endpoint = "list/$id",
apiKey = apiKey,
query = mapOf("language" to language, "page" to page.toString()),
) ?: error("TMDB list not found")
) ?: error(getString(Res.string.collections_tmdb_list_not_found))
val items = body.items.orEmpty()
.mapNotNull { it.toPreview() }
.sortedFor(source.sortBy)
@ -222,12 +235,12 @@ object TmdbCollectionSourceResolver {
apiKey: String,
language: String,
): CatalogPage {
val id = source.tmdbId ?: error("Missing TMDB collection ID")
val id = source.tmdbId ?: error(getString(Res.string.collections_tmdb_missing_collection_id))
val body = fetch<TmdbCollectionResponse>(
endpoint = "collection/$id",
apiKey = apiKey,
query = mapOf("language" to language),
) ?: error("TMDB collection not found")
) ?: error(getString(Res.string.collections_tmdb_collection_not_found))
val items = body.parts.orEmpty()
.mapNotNull { it.toPreview(TmdbCollectionMediaType.MOVIE) }
.sortedFor(source.sortBy)
@ -240,13 +253,13 @@ object TmdbCollectionSourceResolver {
apiKey: String,
language: String,
): CatalogPage {
val id = source.tmdbId ?: error("Missing TMDB person ID")
val id = source.tmdbId ?: error(getString(Res.string.collections_tmdb_missing_person_id))
val mediaType = source.tmdbMediaType()
val body = fetch<TmdbPersonCreditsResponse>(
endpoint = "person/$id/combined_credits",
apiKey = apiKey,
query = mapOf("language" to language),
) ?: error("TMDB person credits not found")
) ?: error(getString(Res.string.collections_tmdb_person_credits_not_found))
val items = when (source.tmdbType()) {
TmdbCollectionSourceType.DIRECTOR -> body.crew.orEmpty()
.filter { it.job.equals("Director", ignoreCase = true) }
@ -287,7 +300,7 @@ object TmdbCollectionSourceResolver {
endpoint = endpoint,
apiKey = apiKey,
query = query,
) ?: error("TMDB discover returned no data")
) ?: error(getString(Res.string.collections_tmdb_discover_no_data))
val items = body.results.orEmpty()
.mapNotNull { it.toPreview(mediaType) }
.distinctBy { it.id }

View file

@ -85,7 +85,11 @@ object SubtitleRepository {
id = id,
url = url,
language = normalizedLang,
display = "${getLanguageLabelForCode(rawLang)} (${addon.displayTitle})",
display = getString(
Res.string.player_addon_subtitle_display_format,
getLanguageLabelForCode(rawLang),
addon.displayTitle,
),
)
)
}

View file

@ -283,7 +283,7 @@ object TraktAuthRepository {
refreshUserSettings()
publish(
isLoading = false,
statusMessage = "Connected to Trakt",
statusMessage = localizedString(Res.string.trakt_connected_status),
errorMessage = null,
)
}
@ -316,7 +316,7 @@ object TraktAuthRepository {
persist()
publish(
isLoading = false,
statusMessage = "Disconnected from Trakt",
statusMessage = localizedString(Res.string.trakt_disconnected_status),
errorMessage = null,
)
}

View file

@ -90,7 +90,9 @@ object TraktCommentsRepository {
return TraktCommentsPage(emptyList(), page, 0, 0)
}
if (response.status !in 200..299) {
throw IllegalStateException("Failed to load Trakt comments (${response.status})")
throw IllegalStateException(
getString(Res.string.details_comments_trakt_load_failed_with_code, response.status),
)
}
val dtos = commentsJson.decodeFromString<List<TraktCommentDto>>(response.body)

View file

@ -12,11 +12,27 @@ import com.nuvio.app.features.home.MetaPreview
import com.nuvio.app.features.home.PosterShape
import io.ktor.http.encodeURLParameter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.collections_editor_trakt_fallback_title
import nuvio.composeapp.generated.resources.collections_trakt_credentials_missing
import nuvio.composeapp.generated.resources.collections_trakt_error_with_code
import nuvio.composeapp.generated.resources.collections_trakt_invalid_list_id_or_url
import nuvio.composeapp.generated.resources.collections_trakt_list_items_count
import nuvio.composeapp.generated.resources.collections_trakt_list_likes_count
import nuvio.composeapp.generated.resources.collections_trakt_list_not_found_or_private
import nuvio.composeapp.generated.resources.collections_editor_trakt_load_failed
import nuvio.composeapp.generated.resources.collections_trakt_missing_list_id
import nuvio.composeapp.generated.resources.collections_trakt_missing_numeric_id
import nuvio.composeapp.generated.resources.collections_trakt_public_list
import nuvio.composeapp.generated.resources.collections_trakt_rate_limit_reached
import nuvio.composeapp.generated.resources.collections_trakt_request_failed
import org.jetbrains.compose.resources.getString
import kotlin.math.roundToInt
data class TraktPublicListImportMetadata(
@ -44,7 +60,7 @@ object TraktPublicListSourceResolver {
private val json = Json { ignoreUnknownKeys = true }
suspend fun resolve(source: CollectionSource, page: Int = 1): CatalogPage = withContext(Dispatchers.Default) {
val listId = source.traktListId?.takeIf { it > 0L } ?: error("Missing Trakt list ID")
val listId = source.traktListId?.takeIf { it > 0L } ?: error(getString(Res.string.collections_trakt_missing_list_id))
val mediaType = TmdbCollectionMediaType.fromString(source.mediaType)
val type = mediaType.toTraktType()
val sortBy = TraktListSort.normalize(source.sortBy)
@ -60,7 +76,7 @@ object TraktPublicListSourceResolver {
),
)
if (response.status !in 200..299) {
error(errorMessageFor(response.status, "Could not load Trakt list"))
error(errorMessageFor(response.status, getString(Res.string.collections_editor_trakt_load_failed)))
}
val rawItems = json.decodeFromString<List<PublicTraktListItemDto>>(response.body)
@ -76,12 +92,12 @@ object TraktPublicListSourceResolver {
}
suspend fun listImportMetadata(input: String): TraktPublicListImportMetadata = withContext(Dispatchers.Default) {
val idPath = parseTraktListPath(input) ?: error("Enter a valid Trakt list ID or URL")
val idPath = parseTraktListPath(input) ?: error(getString(Res.string.collections_trakt_invalid_list_id_or_url))
val list = requestJson<PublicTraktListSummaryDto>(
endpoint = "lists/$idPath",
query = mapOf("extended" to "full,images"),
)
val id = list.ids?.trakt ?: idPath.toLongOrNull() ?: error("Trakt list did not include a numeric ID")
val id = list.ids?.trakt ?: idPath.toLongOrNull() ?: error(getString(Res.string.collections_trakt_missing_numeric_id))
TraktPublicListImportMetadata(
title = list.name?.takeIf { it.isNotBlank() },
coverImageUrl = list.images?.posters.firstTraktImageUrl(),
@ -132,7 +148,7 @@ object TraktPublicListSourceResolver {
): T {
val response = requestRaw(endpoint = endpoint, query = query)
if (response.status !in 200..299) {
error(errorMessageFor(response.status, "Trakt request failed"))
error(errorMessageFor(response.status, getString(Res.string.collections_trakt_request_failed)))
}
return runCatching { json.decodeFromString<T>(response.body) }
.onFailure { error -> log.w(error) { "Failed to parse Trakt response for $endpoint" } }
@ -144,7 +160,7 @@ object TraktPublicListSourceResolver {
query: Map<String, String> = emptyMap(),
): RawHttpResponse {
if (TraktConfig.CLIENT_ID.isBlank()) {
error("Missing Trakt credentials in local.properties (TRAKT_CLIENT_ID).")
error(getString(Res.string.collections_trakt_credentials_missing))
}
val url = buildTraktUrl(endpoint, query)
return httpRequestRaw(
@ -237,21 +253,25 @@ object TraktPublicListSourceResolver {
private fun PublicTraktListSummaryDto.toPublicListResult(likeCount: Int? = null): TraktPublicListSearchResult? {
val id = ids?.trakt ?: return null
val listTitle = name?.takeIf { it.isNotBlank() } ?: "Trakt List $id"
val owner = user?.username?.takeIf { it.isNotBlank() }
val stats = buildList {
itemCount?.let { add("$it items") }
(likeCount ?: likes)?.let { add("$it likes") }
return runBlocking {
val listTitle = name?.takeIf { it.isNotBlank() }
?: getString(Res.string.collections_editor_trakt_fallback_title, id)
val owner = user?.username?.takeIf { it.isNotBlank() }
val stats = buildList {
itemCount?.let { add(getString(Res.string.collections_trakt_list_items_count, it)) }
(likeCount ?: likes)?.let { add(getString(Res.string.collections_trakt_list_likes_count, it)) }
}
val subtitle = (listOfNotNull(owner) + stats).joinToString("")
.ifBlank { getString(Res.string.collections_trakt_public_list) }
TraktPublicListSearchResult(
traktListId = id,
title = listTitle,
subtitle = subtitle,
coverImageUrl = images?.posters.firstTraktImageUrl(),
sortBy = sortBy,
sortHow = sortHow,
)
}
val subtitle = (listOfNotNull(owner) + stats).joinToString("").ifBlank { "Trakt public list" }
return TraktPublicListSearchResult(
traktListId = id,
title = listTitle,
subtitle = subtitle,
coverImageUrl = images?.posters.firstTraktImageUrl(),
sortBy = sortBy,
sortHow = sortHow,
)
}
private fun parseTraktListPath(input: String): String? {
@ -292,11 +312,11 @@ object TraktPublicListSourceResolver {
?.trim()
?.toIntOrNull()
private fun errorMessageFor(code: Int, fallback: String): String {
return when (code) {
401, 403, 404 -> "Trakt list not found or not public"
429 -> "Trakt rate limit reached"
else -> "$fallback ($code)"
private fun errorMessageFor(code: Int, fallback: String): String = runBlocking {
when (code) {
401, 403, 404 -> getString(Res.string.collections_trakt_list_not_found_or_private)
429 -> getString(Res.string.collections_trakt_rate_limit_reached)
else -> getString(Res.string.collections_trakt_error_with_code, fallback, code)
}
}
}

View file

@ -49,6 +49,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.coroutines.runBlocking
import nuvio.composeapp.generated.resources.*
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
@ -106,7 +107,7 @@ private val appUpdaterJson = Json {
}
private class NoChannelReleaseException : IllegalStateException(
"No cmp-rewrite release has been published yet.",
runBlocking { getString(Res.string.updates_no_channel_release) },
)
private object VersionUtils {
@ -158,7 +159,7 @@ private object AppUpdaterRepository {
body = "",
)
if (response.status !in 200..299) {
error("GitHub releases API error: ${response.status}")
error(getString(Res.string.updates_github_api_error, response.status))
}
val releases = appUpdaterJson.decodeFromString<List<GitHubReleaseDto>>(response.body)
@ -167,10 +168,10 @@ private object AppUpdaterRepository {
val tag = release.tagName?.takeIf { it.isNotBlank() }
?: release.name?.takeIf { it.isNotBlank() }
?: error("Release has no tag or name")
?: error(getString(Res.string.updates_release_missing_title))
val asset = chooseBestApkAsset(release.assets)
?: error("No APK asset found in the cmp-rewrite release")
?: error(getString(Res.string.updates_apk_asset_missing))
AppUpdate(
tag = tag,

View file

@ -15,6 +15,11 @@ import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.isSuccess
import kotlinx.coroutines.runBlocking
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.network_empty_response_body
import nuvio.composeapp.generated.resources.network_request_failed_http
import org.jetbrains.compose.resources.getString
import platform.Foundation.NSUserDefaults
actual object AddonStorage {
@ -83,10 +88,10 @@ actual suspend fun httpGetText(url: String): String =
.let { response ->
val payload = response.bodyAsText()
if (!response.status.isSuccess()) {
error("Request failed with HTTP ${response.status.value}")
error(runBlocking { getString(Res.string.network_request_failed_http, response.status.value) })
}
if (payload.isBlank()) {
throw IllegalStateException("Empty response body")
throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) })
}
payload
}
@ -101,10 +106,10 @@ actual suspend fun httpPostJson(url: String, body: String): String =
.let { response ->
val payload = response.bodyAsText()
if (!response.status.isSuccess()) {
error("Request failed with HTTP ${response.status.value}")
error(runBlocking { getString(Res.string.network_request_failed_http, response.status.value) })
}
if (payload.isBlank()) {
throw IllegalStateException("Empty response body")
throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) })
}
payload
}
@ -123,10 +128,10 @@ actual suspend fun httpGetTextWithHeaders(
.let { response ->
val payload = response.bodyAsText()
if (!response.status.isSuccess()) {
error("Request failed with HTTP ${response.status.value}")
error(runBlocking { getString(Res.string.network_request_failed_http, response.status.value) })
}
if (payload.isBlank()) {
throw IllegalStateException("Empty response body")
throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) })
}
payload
}
@ -148,10 +153,10 @@ actual suspend fun httpPostJsonWithHeaders(
.let { response ->
val payload = response.bodyAsText()
if (!response.status.isSuccess()) {
error("Request failed with HTTP ${response.status.value}")
error(runBlocking { getString(Res.string.network_request_failed_http, response.status.value) })
}
if (payload.isBlank()) {
throw IllegalStateException("Empty response body")
throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) })
}
payload
}

View file

@ -10,6 +10,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.download_failed
import nuvio.composeapp.generated.resources.downloads_error_finalize_file_failed
import nuvio.composeapp.generated.resources.downloads_error_open_partial_file_failed
import nuvio.composeapp.generated.resources.downloads_error_partial_file_not_open
import nuvio.composeapp.generated.resources.downloads_error_write_partial_file_failed
import nuvio.composeapp.generated.resources.network_request_failed_http
import org.jetbrains.compose.resources.getString
import platform.Foundation.NSError
import platform.Foundation.NSDate
import platform.Foundation.NSData
@ -99,7 +108,7 @@ internal actual object DownloadsPlatformDownloader {
}
if (result.statusCode !in 200..299) {
error("Request failed with HTTP ${result.statusCode}")
error(runBlocking { getString(Res.string.network_request_failed_http, result.statusCode) })
}
val isPartialResume = attemptedRangeRequest && result.statusCode == 206 && resumeFromBytes > 0L
@ -118,7 +127,7 @@ internal actual object DownloadsPlatformDownloader {
error = null,
)
if (!moved) {
error("Failed to finalize download file")
error(runBlocking { getString(Res.string.downloads_error_finalize_file_failed) })
}
val localFileUri = NSURL.fileURLWithPath(destinationPath).absoluteString ?: "file://$destinationPath"
@ -127,7 +136,7 @@ internal actual object DownloadsPlatformDownloader {
} catch (_: CancellationException) {
handle.cancelNativeTask()
} catch (error: Throwable) {
onFailure(error.message ?: "Download failed")
onFailure(error.message ?: runBlocking { getString(Res.string.download_failed) })
}
}
@ -248,7 +257,7 @@ private class IosDownloadDelegate(
)
outputFile = fopen(tempPath, if (isPartialResume) "ab" else "wb") ?: run {
fileError = IllegalStateException("Failed to open partial download file")
fileError = IllegalStateException(runBlocking { getString(Res.string.downloads_error_open_partial_file_failed) })
null
}
@ -266,7 +275,7 @@ private class IosDownloadDelegate(
if (fileError != null) return
val file = outputFile ?: run {
fileError = IllegalStateException("Partial download file is not open")
fileError = IllegalStateException(runBlocking { getString(Res.string.downloads_error_partial_file_not_open) })
return
}
@ -278,7 +287,7 @@ private class IosDownloadDelegate(
file,
).toLong()
if (wrote != bytesToWrite) {
fileError = IllegalStateException("Failed to write partial download file")
fileError = IllegalStateException(runBlocking { getString(Res.string.downloads_error_write_partial_file_failed) })
return
}
fflush(file)