mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-05 19:11:11 +00:00
merge: PR #1025 feat(i18n) extract remaining hardcoded strings
Resolved conflicts in values/strings.xml and values-fr/strings.xml by concatenating both blocks (no duplicate keys).
This commit is contained in:
commit
6ef8c42065
16 changed files with 364 additions and 107 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1459,4 +1459,72 @@
|
|||
<string name="settings_debrid_formatter_reset_title">Réinitialiser la mise en forme</string>
|
||||
<string name="settings_debrid_not_set">Non défini</string>
|
||||
<string name="settings_debrid_prepare_stream_count_warning">Privilégie une valeur basse quand 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.</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 n’a 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 d’ID 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 j’aime</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 n’a 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 n’a 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 d’ouvrir le fichier de téléchargement partiel</string>
|
||||
<string name="downloads_error_partial_file_not_open">Le fichier de téléchargement partiel n’est pas ouvert</string>
|
||||
<string name="downloads_error_write_partial_file_failed">Impossible d’écrire dans le fichier de téléchargement partiel</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1516,4 +1516,72 @@
|
|||
<string name="network_please_check_connection">Please check your connection and try again.</string>
|
||||
<string name="library_local_tab_title">Nuvio Library</string>
|
||||
<string name="streams_plugin_repository_fallback">Plugin repository</string>
|
||||
<string name="unit_bytes_tb">TB</string>
|
||||
<string name="collections_editor_tmdb_genres_movie_placeholder">28,12</string>
|
||||
<string name="collections_editor_tmdb_genres_series_placeholder">18,35</string>
|
||||
<string name="collections_editor_tmdb_date_from_placeholder">2020-01-01</string>
|
||||
<string name="collections_editor_tmdb_date_to_placeholder">2024-12-31</string>
|
||||
<string name="collections_editor_tmdb_rating_min_placeholder">7.0</string>
|
||||
<string name="collections_editor_tmdb_rating_max_placeholder">10</string>
|
||||
<string name="collections_editor_tmdb_votes_min_placeholder">100</string>
|
||||
<string name="collections_editor_tmdb_language_placeholder">en, ko, ja, hi</string>
|
||||
<string name="collections_editor_tmdb_country_placeholder">US, KR, JP, IN</string>
|
||||
<string name="collections_editor_tmdb_year_placeholder">2024</string>
|
||||
<string name="collections_editor_trakt_list_id_format">ID %1$s</string>
|
||||
<string name="player_addon_subtitle_display_format">%1$s (%2$s)</string>
|
||||
<string name="trakt_connected_status">Connected to Trakt</string>
|
||||
<string name="trakt_disconnected_status">Disconnected from Trakt</string>
|
||||
<string name="collections_editor_trakt_enter_name_url_or_id">Enter a Trakt list name, URL, or ID</string>
|
||||
<string name="collections_editor_trakt_enter_id_or_url">Enter a Trakt list ID or URL</string>
|
||||
<string name="collections_editor_trakt_load_failed">Could not load Trakt list</string>
|
||||
<string name="collections_editor_trakt_fallback_title">Trakt List %1$d</string>
|
||||
<string name="collections_editor_trakt_resolved_subtitle">Resolved Trakt list</string>
|
||||
<string name="collections_editor_trakt_no_lists_found">No Trakt lists found</string>
|
||||
<string name="collections_editor_tmdb_invalid_id_or_url">Enter a valid TMDB ID or URL.</string>
|
||||
<string name="collections_editor_tmdb_list_title_format">TMDB List %1$s</string>
|
||||
<string name="collections_editor_tmdb_collection_title_format">TMDB Collection %1$s</string>
|
||||
<string name="collections_editor_tmdb_production_title_format">TMDB Production %1$s</string>
|
||||
<string name="collections_editor_tmdb_network_title_format">TMDB Network %1$s</string>
|
||||
<string name="collections_editor_tmdb_person_title_format">TMDB Person %1$s</string>
|
||||
<string name="collections_editor_tmdb_director_title_format">TMDB Director %1$s</string>
|
||||
<string name="collections_editor_tmdb_discover_title">TMDB Discover</string>
|
||||
<string name="collections_editor_tmdb_source_load_failed">Could not load TMDB source</string>
|
||||
<string name="collections_folder_trakt_series_list">Trakt Series List</string>
|
||||
<string name="collections_folder_trakt_movie_list">Trakt Movie List</string>
|
||||
<string name="collections_tmdb_api_key_required">Add a TMDB API key in Settings to use TMDB sources.</string>
|
||||
<string name="collections_tmdb_list_not_found">TMDB list not found</string>
|
||||
<string name="collections_tmdb_collection_not_found">TMDB collection not found</string>
|
||||
<string name="collections_tmdb_company_not_found">TMDB company not found</string>
|
||||
<string name="collections_tmdb_network_not_found">TMDB network not found</string>
|
||||
<string name="collections_tmdb_person_not_found">TMDB person not found</string>
|
||||
<string name="collections_tmdb_missing_list_id">Missing TMDB list ID</string>
|
||||
<string name="collections_tmdb_missing_collection_id">Missing TMDB collection ID</string>
|
||||
<string name="collections_tmdb_missing_person_id">Missing TMDB person ID</string>
|
||||
<string name="collections_tmdb_person_credits_not_found">TMDB person credits not found</string>
|
||||
<string name="collections_tmdb_discover_no_data">TMDB discover returned no data</string>
|
||||
<string name="collections_trakt_missing_list_id">Missing Trakt list ID</string>
|
||||
<string name="collections_trakt_invalid_list_id_or_url">Enter a valid Trakt list ID or URL</string>
|
||||
<string name="collections_trakt_missing_numeric_id">Trakt list did not include a numeric ID</string>
|
||||
<string name="collections_trakt_request_failed">Trakt request failed</string>
|
||||
<string name="collections_trakt_credentials_missing">Missing Trakt credentials.</string>
|
||||
<string name="collections_trakt_list_items_count">%1$d items</string>
|
||||
<string name="collections_trakt_list_likes_count">%1$d likes</string>
|
||||
<string name="collections_trakt_public_list">Trakt public list</string>
|
||||
<string name="collections_trakt_list_not_found_or_private">Trakt list not found or not public</string>
|
||||
<string name="collections_trakt_rate_limit_reached">Trakt rate limit reached</string>
|
||||
<string name="collections_trakt_error_with_code">%1$s (%2$d)</string>
|
||||
<string name="details_comments_trakt_load_failed_with_code">Failed to load Trakt comments (%1$d)</string>
|
||||
<string name="updates_no_channel_release">No update has been published yet.</string>
|
||||
<string name="updates_github_api_error">GitHub releases API error: %1$d</string>
|
||||
<string name="updates_release_missing_title">Release has no tag or name</string>
|
||||
<string name="updates_apk_asset_missing">No APK asset found in the release</string>
|
||||
<string name="updates_download_failed_http">Download failed with HTTP %1$d</string>
|
||||
<string name="updates_empty_download_body">Empty download body</string>
|
||||
<string name="updates_downloaded_file_missing">Downloaded update file is missing.</string>
|
||||
<string name="network_request_failed_http">Request failed with HTTP %1$d</string>
|
||||
<string name="network_empty_response_body">Empty response body</string>
|
||||
<string name="downloads_error_finalize_file_failed">Failed to finalize download file</string>
|
||||
<string name="downloads_error_open_partial_file_failed">Failed to open partial download file</string>
|
||||
<string name="downloads_error_partial_file_not_open">Partial download file is not open</string>
|
||||
<string name="downloads_error_write_partial_file_failed">Failed to write partial download file</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(" • ")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue