mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 07:21:58 +00:00
Merge 3183eababc into 70d3eee9d2
This commit is contained in:
commit
60977fe1d5
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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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