diff --git a/composeApp/src/androidFull/kotlin/com/nuvio/app/features/updater/AndroidAppUpdaterPlatform.kt b/composeApp/src/androidFull/kotlin/com/nuvio/app/features/updater/AndroidAppUpdaterPlatform.kt index 72c2e50e8..390ce23ff 100644 --- a/composeApp/src/androidFull/kotlin/com/nuvio/app/features/updater/AndroidAppUpdaterPlatform.kt +++ b/composeApp/src/androidFull/kotlin/com/nuvio/app/features/updater/AndroidAppUpdaterPlatform.kt @@ -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 = 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, diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.android.kt index 4549add8c..01afe4ef3 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.android.kt @@ -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 } diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/downloads/DownloadsLiveStatusPlatform.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/downloads/DownloadsLiveStatusPlatform.android.kt index f8f5d9d3f..ba21cd680 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/downloads/DownloadsLiveStatusPlatform.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/downloads/DownloadsLiveStatusPlatform.android.kt @@ -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) { diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings.xml b/composeApp/src/commonMain/composeResources/values-fr/strings.xml index d2061b8ea..58565a6f5 100644 --- a/composeApp/src/commonMain/composeResources/values-fr/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-fr/strings.xml @@ -1459,4 +1459,72 @@ Réinitialiser la mise en forme Non défini Privilégie une valeur basse quand c’est 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 à l’avance. + To + 28,12 + 18,35 + 2020-01-01 + 2024-12-31 + 7.0 + 10 + 100 + en, ko, ja, hi + US, KR, JP, IN + 2024 + ID %1$s + %1$s (%2$s) + Connecté à Trakt + Déconnecté de Trakt + Saisis un nom, une URL ou un ID de liste Trakt + Saisis un ID ou une URL de liste Trakt + Impossible de charger la liste Trakt + Liste Trakt %1$d + Liste Trakt résolue + Aucune liste Trakt trouvée + Saisis un ID ou une URL TMDB valide. + Liste TMDB %1$s + Collection TMDB %1$s + Production TMDB %1$s + Chaîne TMDB %1$s + Personne TMDB %1$s + Réalisateur TMDB %1$s + Découverte TMDB + Impossible de charger la source TMDB + Liste Trakt de séries + Liste Trakt de films + Ajoute une clé API TMDB dans les Paramètres pour utiliser les sources TMDB. + Liste TMDB introuvable + Collection TMDB introuvable + Société TMDB introuvable + Chaîne TMDB introuvable + Personne TMDB introuvable + ID de liste TMDB manquant + ID de collection TMDB manquant + ID de personne TMDB manquant + Crédits TMDB de la personne introuvables + Découverte TMDB n’a retourné aucune donnée + ID de liste Trakt manquant + Saisis un ID ou une URL de liste Trakt valide + La liste Trakt ne contient pas d’ID numérique + Échec de la requête Trakt + Identifiants Trakt manquants. + %1$d éléments + %1$d j’aime + Liste Trakt publique + Liste Trakt introuvable ou non publique + Limite de requêtes Trakt atteinte + %1$s (%2$d) + Échec du chargement des commentaires Trakt (%1$d) + Aucune mise à jour n’a encore été publiée. + Erreur API releases GitHub : %1$d + La release n’a pas de tag ni de nom + Aucun fichier APK trouvé dans la release + Échec du téléchargement (HTTP %1$d) + Corps de téléchargement vide + Le fichier de mise à jour téléchargé est introuvable. + Échec de la requête (HTTP %1$d) + Corps de réponse vide + Impossible de finaliser le fichier de téléchargement + Impossible d’ouvrir le fichier de téléchargement partiel + Le fichier de téléchargement partiel n’est pas ouvert + Impossible d’écrire dans le fichier de téléchargement partiel diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index d682b80ec..acc3d145e 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -1516,4 +1516,72 @@ Please check your connection and try again. Nuvio Library Plugin repository + TB + 28,12 + 18,35 + 2020-01-01 + 2024-12-31 + 7.0 + 10 + 100 + en, ko, ja, hi + US, KR, JP, IN + 2024 + ID %1$s + %1$s (%2$s) + Connected to Trakt + Disconnected from Trakt + Enter a Trakt list name, URL, or ID + Enter a Trakt list ID or URL + Could not load Trakt list + Trakt List %1$d + Resolved Trakt list + No Trakt lists found + Enter a valid TMDB ID or URL. + TMDB List %1$s + TMDB Collection %1$s + TMDB Production %1$s + TMDB Network %1$s + TMDB Person %1$s + TMDB Director %1$s + TMDB Discover + Could not load TMDB source + Trakt Series List + Trakt Movie List + Add a TMDB API key in Settings to use TMDB sources. + TMDB list not found + TMDB collection not found + TMDB company not found + TMDB network not found + TMDB person not found + Missing TMDB list ID + Missing TMDB collection ID + Missing TMDB person ID + TMDB person credits not found + TMDB discover returned no data + Missing Trakt list ID + Enter a valid Trakt list ID or URL + Trakt list did not include a numeric ID + Trakt request failed + Missing Trakt credentials. + %1$d items + %1$d likes + Trakt public list + Trakt list not found or not public + Trakt rate limit reached + %1$s (%2$d) + Failed to load Trakt comments (%1$d) + No update has been published yet. + GitHub releases API error: %1$d + Release has no tag or name + No APK asset found in the release + Download failed with HTTP %1$d + Empty download body + Downloaded update file is missing. + Request failed with HTTP %1$d + Empty response body + Failed to finalize download file + Failed to open partial download file + Partial download file is not open + Failed to write partial download file diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorRepository.kt index 70b5204ff..5f4b41e73 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorRepository.kt @@ -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" } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt index f5e0b6b8c..cd77ca225 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt @@ -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(" • ") } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailRepository.kt index d5c7a172b..d4e6d9ef5 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailRepository.kt @@ -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( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt index 8f683a731..d8d9176cc 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt @@ -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( 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( 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( 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( 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( endpoint = "search/keyword", apiKey = apiKey, @@ -152,7 +165,7 @@ object TmdbCollectionSourceResolver { suspend fun genres(mediaType: TmdbCollectionMediaType): Map = 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( 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( 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( 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 } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/SubtitleRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/SubtitleRepository.kt index 8df539c6f..578c809b0 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/SubtitleRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/SubtitleRepository.kt @@ -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, + ), ) ) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthRepository.kt index 3fed8022c..cba36b92a 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthRepository.kt @@ -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, ) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktCommentsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktCommentsRepository.kt index a2bd8a038..973e54956 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktCommentsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktCommentsRepository.kt @@ -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>(response.body) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktPublicListSourceResolver.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktPublicListSourceResolver.kt index e14682453..4b66e8f93 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktPublicListSourceResolver.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktPublicListSourceResolver.kt @@ -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>(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( 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(response.body) } .onFailure { error -> log.w(error) { "Failed to parse Trakt response for $endpoint" } } @@ -144,7 +160,7 @@ object TraktPublicListSourceResolver { query: Map = 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) } } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/updater/AppUpdater.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/updater/AppUpdater.kt index 44ce4441f..75427d6a4 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/updater/AppUpdater.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/updater/AppUpdater.kt @@ -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>(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, diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.ios.kt index 7dc979e2a..8b6cfc112 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.ios.kt @@ -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 } diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/downloads/DownloadsPlatformDownloader.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/downloads/DownloadsPlatformDownloader.ios.kt index 733bec21c..a0bfc724e 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/downloads/DownloadsPlatformDownloader.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/downloads/DownloadsPlatformDownloader.ios.kt @@ -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)