This commit is contained in:
foXaCe 2026-05-16 06:08:00 +02:00 committed by GitHub
commit 60977fe1d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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
@ -153,10 +158,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

@ -1192,4 +1192,72 @@
<string name="unit_bytes_kb">Ko</string>
<string name="unit_bytes_mb">Mo</string>
<string name="unit_bytes_gb">Go</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

@ -1322,4 +1322,72 @@
<string name="unit_bytes_kb">KB</string>
<string name="unit_bytes_mb">MB</string>
<string name="unit_bytes_gb">GB</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

@ -84,7 +84,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 {
@ -54,10 +59,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
}
@ -72,10 +77,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
}
@ -94,10 +99,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
}
@ -119,10 +124,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)