mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 23:42:04 +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 android.provider.Settings
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
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.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import org.jetbrains.compose.resources.getString
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
@ -63,10 +69,10 @@ object AndroidAppUpdaterPlatform {
|
||||||
|
|
||||||
httpClient.newCall(request).execute().use { response ->
|
httpClient.newCall(request).execute().use { response ->
|
||||||
if (!response.isSuccessful) {
|
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 }
|
val totalBytes = body.contentLength().takeIf { it > 0L }
|
||||||
body.byteStream().use { input ->
|
body.byteStream().use { input ->
|
||||||
FileOutputStream(destination).use { output ->
|
FileOutputStream(destination).use { output ->
|
||||||
|
|
@ -115,7 +121,7 @@ object AndroidAppUpdaterPlatform {
|
||||||
fun installDownloadedApk(path: String): Result<Unit> = runCatching {
|
fun installDownloadedApk(path: String): Result<Unit> = runCatching {
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
val apkFile = File(path)
|
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(
|
val apkUri = FileProvider.getUriForFile(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,12 @@ import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import com.nuvio.app.core.network.IPv4FirstDns
|
import com.nuvio.app.core.network.IPv4FirstDns
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
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.ResponseBody
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
@ -153,10 +158,10 @@ private suspend fun executeTextRequest(
|
||||||
addonHttpClient.newCall(request).execute().use { response ->
|
addonHttpClient.newCall(request).execute().use { response ->
|
||||||
val payload = readResponseBody(response.body)
|
val payload = readResponseBody(response.body)
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
error("Request failed with HTTP ${response.code}")
|
error(runBlocking { getString(Res.string.network_request_failed_http, response.code) })
|
||||||
}
|
}
|
||||||
if (payload.isBlank()) {
|
if (payload.isBlank()) {
|
||||||
throw IllegalStateException("Empty response body")
|
throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) })
|
||||||
}
|
}
|
||||||
payload
|
payload
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ internal actual object DownloadsLiveStatusPlatform {
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.addAction(
|
.addAction(
|
||||||
0,
|
0,
|
||||||
"Pause",
|
runBlocking { getString(Res.string.compose_action_pause) },
|
||||||
buildActionPendingIntent(
|
buildActionPendingIntent(
|
||||||
context = context,
|
context = context,
|
||||||
action = DownloadsNotificationActionReceiver.actionPause,
|
action = DownloadsNotificationActionReceiver.actionPause,
|
||||||
|
|
@ -178,7 +178,15 @@ internal actual object DownloadsLiveStatusPlatform {
|
||||||
|
|
||||||
private fun formatBytes(bytes: Long): String {
|
private fun formatBytes(bytes: Long): String {
|
||||||
val safe = bytes.coerceAtLeast(0L).toDouble()
|
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 value = safe
|
||||||
var unitIndex = 0
|
var unitIndex = 0
|
||||||
while (value >= 1024.0 && unitIndex < units.lastIndex) {
|
while (value >= 1024.0 && unitIndex < units.lastIndex) {
|
||||||
|
|
|
||||||
|
|
@ -1192,4 +1192,72 @@
|
||||||
<string name="unit_bytes_kb">Ko</string>
|
<string name="unit_bytes_kb">Ko</string>
|
||||||
<string name="unit_bytes_mb">Mo</string>
|
<string name="unit_bytes_mb">Mo</string>
|
||||||
<string name="unit_bytes_gb">Go</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>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1322,4 +1322,72 @@
|
||||||
<string name="unit_bytes_kb">KB</string>
|
<string name="unit_bytes_kb">KB</string>
|
||||||
<string name="unit_bytes_mb">MB</string>
|
<string name="unit_bytes_mb">MB</string>
|
||||||
<string name="unit_bytes_gb">GB</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>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,26 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
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.ExperimentalUuidApi
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
|
@ -394,7 +414,9 @@ object CollectionEditorRepository {
|
||||||
val state = _uiState.value
|
val state = _uiState.value
|
||||||
val query = state.traktInput.trim()
|
val query = state.traktInput.trim()
|
||||||
if (query.isBlank()) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -402,12 +424,13 @@ object CollectionEditorRepository {
|
||||||
val results = if (query.isTraktListIdentifierInput()) {
|
val results = if (query.isTraktListIdentifierInput()) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val metadata = TraktPublicListSourceResolver.listImportMetadata(query)
|
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(
|
listOf(
|
||||||
TraktPublicListSearchResult(
|
TraktPublicListSearchResult(
|
||||||
traktListId = id,
|
traktListId = id,
|
||||||
title = metadata.title ?: "Trakt List $id",
|
title = metadata.title ?: getString(Res.string.collections_editor_trakt_fallback_title, id),
|
||||||
subtitle = "Resolved Trakt list",
|
subtitle = getString(Res.string.collections_editor_trakt_resolved_subtitle),
|
||||||
coverImageUrl = metadata.coverImageUrl,
|
coverImageUrl = metadata.coverImageUrl,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -419,7 +442,7 @@ object CollectionEditorRepository {
|
||||||
_uiState.value = _uiState.value.copy(
|
_uiState.value = _uiState.value.copy(
|
||||||
traktSearchResults = mapped,
|
traktSearchResults = mapped,
|
||||||
traktSearchError = results.exceptionOrNull()?.message
|
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)
|
val id = TmdbCollectionSourceResolver.parseTmdbId(state.tmdbInput)
|
||||||
if (sourceType != TmdbCollectionSourceType.DISCOVER && id == null) {
|
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
|
return
|
||||||
}
|
}
|
||||||
val mediaTypes = selectedMediaTypes(state, sourceType)
|
val mediaTypes = selectedMediaTypes(state, sourceType)
|
||||||
val baseTitle = state.tmdbTitleInput.ifBlank {
|
val baseTitle = state.tmdbTitleInput.ifBlank {
|
||||||
when (sourceType) {
|
runBlocking {
|
||||||
TmdbCollectionSourceType.LIST -> "TMDB List ${id ?: ""}".trim()
|
val idArg = id?.toString().orEmpty()
|
||||||
TmdbCollectionSourceType.COLLECTION -> "TMDB Collection ${id ?: ""}".trim()
|
when (sourceType) {
|
||||||
TmdbCollectionSourceType.COMPANY -> "TMDB Production ${id ?: ""}".trim()
|
TmdbCollectionSourceType.LIST -> getString(Res.string.collections_editor_tmdb_list_title_format, idArg).trim()
|
||||||
TmdbCollectionSourceType.NETWORK -> "TMDB Network ${id ?: ""}".trim()
|
TmdbCollectionSourceType.COLLECTION -> getString(Res.string.collections_editor_tmdb_collection_title_format, idArg).trim()
|
||||||
TmdbCollectionSourceType.PERSON -> "TMDB Person ${id ?: ""}".trim()
|
TmdbCollectionSourceType.COMPANY -> getString(Res.string.collections_editor_tmdb_production_title_format, idArg).trim()
|
||||||
TmdbCollectionSourceType.DIRECTOR -> "TMDB Director ${id ?: ""}".trim()
|
TmdbCollectionSourceType.NETWORK -> getString(Res.string.collections_editor_tmdb_network_title_format, idArg).trim()
|
||||||
TmdbCollectionSourceType.DISCOVER -> "TMDB Discover"
|
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 ->
|
val sources = mediaTypes.map { mediaType ->
|
||||||
|
|
@ -656,7 +684,8 @@ object CollectionEditorRepository {
|
||||||
val resolved = metadata.getOrNull()
|
val resolved = metadata.getOrNull()
|
||||||
if (metadata.isFailure) {
|
if (metadata.isFailure) {
|
||||||
_uiState.value = _uiState.value.copy(
|
_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
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
@ -701,7 +730,9 @@ object CollectionEditorRepository {
|
||||||
val state = _uiState.value
|
val state = _uiState.value
|
||||||
val input = state.traktInput.trim()
|
val input = state.traktInput.trim()
|
||||||
if (input.isBlank()) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -711,12 +742,15 @@ object CollectionEditorRepository {
|
||||||
val listId = resolved?.traktListId
|
val listId = resolved?.traktListId
|
||||||
if (metadata.isFailure || listId == null) {
|
if (metadata.isFailure || listId == null) {
|
||||||
_uiState.value = _uiState.value.copy(
|
_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
|
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(
|
addTraktSourcesToFolder(
|
||||||
sources = selectedTraktMediaTypes(state).map { mediaType ->
|
sources = selectedTraktMediaTypes(state).map { mediaType ->
|
||||||
CollectionSource(
|
CollectionSource(
|
||||||
|
|
@ -881,9 +915,13 @@ private fun titleForMedia(
|
||||||
addSuffix: Boolean,
|
addSuffix: Boolean,
|
||||||
): String {
|
): String {
|
||||||
if (!addSuffix) return title
|
if (!addSuffix) return title
|
||||||
val suffix = when (mediaType) {
|
val suffix = runBlocking {
|
||||||
TmdbCollectionMediaType.MOVIE -> "Movies"
|
getString(
|
||||||
TmdbCollectionMediaType.TV -> "Series"
|
when (mediaType) {
|
||||||
|
TmdbCollectionMediaType.MOVIE -> Res.string.media_movies
|
||||||
|
TmdbCollectionMediaType.TV -> Res.string.media_series
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return "$title $suffix"
|
return "$title $suffix"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1162,7 +1162,11 @@ private fun TmdbSourcePickerScreen(
|
||||||
label = stringResource(Res.string.collections_editor_tmdb_genres),
|
label = stringResource(Res.string.collections_editor_tmdb_genres),
|
||||||
helper = stringResource(Res.string.collections_editor_tmdb_genres_helper),
|
helper = stringResource(Res.string.collections_editor_tmdb_genres_helper),
|
||||||
value = state.tmdbFilters.withGenres.orEmpty(),
|
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 ->
|
onValueChange = { value ->
|
||||||
CollectionEditorRepository.updateTmdbFilters {
|
CollectionEditorRepository.updateTmdbFilters {
|
||||||
it.copy(withGenres = value.ifBlank { null })
|
it.copy(withGenres = value.ifBlank { null })
|
||||||
|
|
@ -1173,7 +1177,7 @@ private fun TmdbSourcePickerScreen(
|
||||||
label = stringResource(Res.string.collections_editor_tmdb_date_from),
|
label = stringResource(Res.string.collections_editor_tmdb_date_from),
|
||||||
helper = stringResource(Res.string.collections_editor_tmdb_date_helper),
|
helper = stringResource(Res.string.collections_editor_tmdb_date_helper),
|
||||||
value = state.tmdbFilters.releaseDateGte.orEmpty(),
|
value = state.tmdbFilters.releaseDateGte.orEmpty(),
|
||||||
placeholder = "2020-01-01",
|
placeholder = stringResource(Res.string.collections_editor_tmdb_date_from_placeholder),
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
CollectionEditorRepository.updateTmdbFilters {
|
CollectionEditorRepository.updateTmdbFilters {
|
||||||
it.copy(releaseDateGte = value.ifBlank { null })
|
it.copy(releaseDateGte = value.ifBlank { null })
|
||||||
|
|
@ -1184,7 +1188,7 @@ private fun TmdbSourcePickerScreen(
|
||||||
label = stringResource(Res.string.collections_editor_tmdb_date_to),
|
label = stringResource(Res.string.collections_editor_tmdb_date_to),
|
||||||
helper = stringResource(Res.string.collections_editor_tmdb_date_helper),
|
helper = stringResource(Res.string.collections_editor_tmdb_date_helper),
|
||||||
value = state.tmdbFilters.releaseDateLte.orEmpty(),
|
value = state.tmdbFilters.releaseDateLte.orEmpty(),
|
||||||
placeholder = "2024-12-31",
|
placeholder = stringResource(Res.string.collections_editor_tmdb_date_to_placeholder),
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
CollectionEditorRepository.updateTmdbFilters {
|
CollectionEditorRepository.updateTmdbFilters {
|
||||||
it.copy(releaseDateLte = value.ifBlank { null })
|
it.copy(releaseDateLte = value.ifBlank { null })
|
||||||
|
|
@ -1195,7 +1199,7 @@ private fun TmdbSourcePickerScreen(
|
||||||
label = stringResource(Res.string.collections_editor_tmdb_rating_min),
|
label = stringResource(Res.string.collections_editor_tmdb_rating_min),
|
||||||
helper = stringResource(Res.string.collections_editor_tmdb_rating_helper),
|
helper = stringResource(Res.string.collections_editor_tmdb_rating_helper),
|
||||||
value = state.tmdbFilters.voteAverageGte?.toString().orEmpty(),
|
value = state.tmdbFilters.voteAverageGte?.toString().orEmpty(),
|
||||||
placeholder = "7.0",
|
placeholder = stringResource(Res.string.collections_editor_tmdb_rating_min_placeholder),
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
CollectionEditorRepository.updateTmdbFilters {
|
CollectionEditorRepository.updateTmdbFilters {
|
||||||
it.copy(voteAverageGte = value.toDoubleOrNull())
|
it.copy(voteAverageGte = value.toDoubleOrNull())
|
||||||
|
|
@ -1206,7 +1210,7 @@ private fun TmdbSourcePickerScreen(
|
||||||
label = stringResource(Res.string.collections_editor_tmdb_rating_max),
|
label = stringResource(Res.string.collections_editor_tmdb_rating_max),
|
||||||
helper = stringResource(Res.string.collections_editor_tmdb_rating_helper),
|
helper = stringResource(Res.string.collections_editor_tmdb_rating_helper),
|
||||||
value = state.tmdbFilters.voteAverageLte?.toString().orEmpty(),
|
value = state.tmdbFilters.voteAverageLte?.toString().orEmpty(),
|
||||||
placeholder = "10",
|
placeholder = stringResource(Res.string.collections_editor_tmdb_rating_max_placeholder),
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
CollectionEditorRepository.updateTmdbFilters {
|
CollectionEditorRepository.updateTmdbFilters {
|
||||||
it.copy(voteAverageLte = value.toDoubleOrNull())
|
it.copy(voteAverageLte = value.toDoubleOrNull())
|
||||||
|
|
@ -1217,7 +1221,7 @@ private fun TmdbSourcePickerScreen(
|
||||||
label = stringResource(Res.string.collections_editor_tmdb_votes_min),
|
label = stringResource(Res.string.collections_editor_tmdb_votes_min),
|
||||||
helper = stringResource(Res.string.collections_editor_tmdb_votes_helper),
|
helper = stringResource(Res.string.collections_editor_tmdb_votes_helper),
|
||||||
value = state.tmdbFilters.voteCountGte?.toString().orEmpty(),
|
value = state.tmdbFilters.voteCountGte?.toString().orEmpty(),
|
||||||
placeholder = "100",
|
placeholder = stringResource(Res.string.collections_editor_tmdb_votes_min_placeholder),
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
CollectionEditorRepository.updateTmdbFilters {
|
CollectionEditorRepository.updateTmdbFilters {
|
||||||
it.copy(voteCountGte = value.toIntOrNull())
|
it.copy(voteCountGte = value.toIntOrNull())
|
||||||
|
|
@ -1241,7 +1245,7 @@ private fun TmdbSourcePickerScreen(
|
||||||
label = stringResource(Res.string.collections_editor_tmdb_language),
|
label = stringResource(Res.string.collections_editor_tmdb_language),
|
||||||
helper = stringResource(Res.string.collections_editor_tmdb_language_helper),
|
helper = stringResource(Res.string.collections_editor_tmdb_language_helper),
|
||||||
value = state.tmdbFilters.withOriginalLanguage.orEmpty(),
|
value = state.tmdbFilters.withOriginalLanguage.orEmpty(),
|
||||||
placeholder = "en, ko, ja, hi",
|
placeholder = stringResource(Res.string.collections_editor_tmdb_language_placeholder),
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
CollectionEditorRepository.updateTmdbFilters {
|
CollectionEditorRepository.updateTmdbFilters {
|
||||||
it.copy(withOriginalLanguage = value.ifBlank { null })
|
it.copy(withOriginalLanguage = value.ifBlank { null })
|
||||||
|
|
@ -1265,7 +1269,7 @@ private fun TmdbSourcePickerScreen(
|
||||||
label = stringResource(Res.string.collections_editor_tmdb_country),
|
label = stringResource(Res.string.collections_editor_tmdb_country),
|
||||||
helper = stringResource(Res.string.collections_editor_tmdb_country_helper),
|
helper = stringResource(Res.string.collections_editor_tmdb_country_helper),
|
||||||
value = state.tmdbFilters.withOriginCountry.orEmpty(),
|
value = state.tmdbFilters.withOriginCountry.orEmpty(),
|
||||||
placeholder = "US, KR, JP, IN",
|
placeholder = stringResource(Res.string.collections_editor_tmdb_country_placeholder),
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
CollectionEditorRepository.updateTmdbFilters {
|
CollectionEditorRepository.updateTmdbFilters {
|
||||||
it.copy(withOriginCountry = value.ifBlank { null })
|
it.copy(withOriginCountry = value.ifBlank { null })
|
||||||
|
|
@ -1347,7 +1351,7 @@ private fun TmdbSourcePickerScreen(
|
||||||
label = stringResource(Res.string.collections_editor_tmdb_year),
|
label = stringResource(Res.string.collections_editor_tmdb_year),
|
||||||
helper = stringResource(Res.string.collections_editor_tmdb_year_helper),
|
helper = stringResource(Res.string.collections_editor_tmdb_year_helper),
|
||||||
value = state.tmdbFilters.year?.toString().orEmpty(),
|
value = state.tmdbFilters.year?.toString().orEmpty(),
|
||||||
placeholder = "2024",
|
placeholder = stringResource(Res.string.collections_editor_tmdb_year_placeholder),
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
CollectionEditorRepository.updateTmdbFilters {
|
CollectionEditorRepository.updateTmdbFilters {
|
||||||
it.copy(year = value.toIntOrNull())
|
it.copy(year = value.toIntOrNull())
|
||||||
|
|
@ -2352,7 +2356,7 @@ private fun traktSourceSubtitle(source: CollectionSource): String {
|
||||||
media,
|
media,
|
||||||
traktSortLabel(source.sortBy),
|
traktSortLabel(source.sortBy),
|
||||||
traktDirectionLabel(source.sortHow),
|
traktDirectionLabel(source.sortHow),
|
||||||
"ID ${source.traktListId ?: ""}".trim(),
|
stringResource(Res.string.collections_editor_trakt_list_id_format, source.traktListId ?: ""),
|
||||||
).joinToString(" • ")
|
).joinToString(" • ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import nuvio.composeapp.generated.resources.Res
|
import nuvio.composeapp.generated.resources.Res
|
||||||
import nuvio.composeapp.generated.resources.collections_folder_addon_not_found
|
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 nuvio.composeapp.generated.resources.collections_tab_all
|
||||||
import org.jetbrains.compose.resources.getString
|
import org.jetbrains.compose.resources.getString
|
||||||
|
|
||||||
|
|
@ -156,10 +158,14 @@ object FolderDetailRepository {
|
||||||
} else if (source.isTrakt) {
|
} else if (source.isTrakt) {
|
||||||
val mediaType = TmdbCollectionMediaType.fromString(source.mediaType)
|
val mediaType = TmdbCollectionMediaType.fromString(source.mediaType)
|
||||||
val type = if (mediaType == TmdbCollectionMediaType.TV) "series" else "movie"
|
val type = if (mediaType == TmdbCollectionMediaType.TV) "series" else "movie"
|
||||||
val typeLabel = if (mediaType == TmdbCollectionMediaType.TV) {
|
val typeLabel = runBlocking {
|
||||||
"Trakt Series List"
|
getString(
|
||||||
} else {
|
if (mediaType == TmdbCollectionMediaType.TV) {
|
||||||
"Trakt Movie List"
|
Res.string.collections_folder_trakt_series_list
|
||||||
|
} else {
|
||||||
|
Res.string.collections_folder_trakt_movie_list
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
add(
|
add(
|
||||||
FolderTab(
|
FolderTab(
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,19 @@ import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
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
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
object TmdbCollectionSourceResolver {
|
object TmdbCollectionSourceResolver {
|
||||||
|
|
@ -22,7 +35,7 @@ object TmdbCollectionSourceResolver {
|
||||||
suspend fun resolve(source: CollectionSource, page: Int = 1): CatalogPage = withContext(Dispatchers.Default) {
|
suspend fun resolve(source: CollectionSource, page: Int = 1): CatalogPage = withContext(Dispatchers.Default) {
|
||||||
val settings = TmdbSettingsRepository.snapshot()
|
val settings = TmdbSettingsRepository.snapshot()
|
||||||
val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() }
|
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 language = normalizeTmdbLanguage(settings.language)
|
||||||
val sourceType = source.tmdbType()
|
val sourceType = source.tmdbType()
|
||||||
|
|
||||||
|
|
@ -41,7 +54,7 @@ object TmdbCollectionSourceResolver {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val settings = TmdbSettingsRepository.snapshot()
|
val settings = TmdbSettingsRepository.snapshot()
|
||||||
val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() }
|
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 language = normalizeTmdbLanguage(settings.language)
|
||||||
when (sourceType) {
|
when (sourceType) {
|
||||||
TmdbCollectionSourceType.LIST -> {
|
TmdbCollectionSourceType.LIST -> {
|
||||||
|
|
@ -49,7 +62,7 @@ object TmdbCollectionSourceResolver {
|
||||||
endpoint = "list/$id",
|
endpoint = "list/$id",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
query = mapOf("language" to language, "page" to "1"),
|
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() })
|
TmdbSourceImportMetadata(title = body.name?.takeIf { it.isNotBlank() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +71,7 @@ object TmdbCollectionSourceResolver {
|
||||||
endpoint = "collection/$id",
|
endpoint = "collection/$id",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
query = mapOf("language" to language),
|
query = mapOf("language" to language),
|
||||||
) ?: error("TMDB collection not found")
|
) ?: error(getString(Res.string.collections_tmdb_collection_not_found))
|
||||||
TmdbSourceImportMetadata(
|
TmdbSourceImportMetadata(
|
||||||
title = body.name?.takeIf { it.isNotBlank() },
|
title = body.name?.takeIf { it.isNotBlank() },
|
||||||
coverImageUrl = imageUrl(body.posterPath, "w500") ?: imageUrl(body.backdropPath, "w1280"),
|
coverImageUrl = imageUrl(body.posterPath, "w500") ?: imageUrl(body.backdropPath, "w1280"),
|
||||||
|
|
@ -69,7 +82,7 @@ object TmdbCollectionSourceResolver {
|
||||||
val body = fetch<TmdbCompanyResponse>(
|
val body = fetch<TmdbCompanyResponse>(
|
||||||
endpoint = "company/$id",
|
endpoint = "company/$id",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
) ?: error("TMDB company not found")
|
) ?: error(getString(Res.string.collections_tmdb_company_not_found))
|
||||||
TmdbSourceImportMetadata(
|
TmdbSourceImportMetadata(
|
||||||
title = body.name?.takeIf { it.isNotBlank() },
|
title = body.name?.takeIf { it.isNotBlank() },
|
||||||
coverImageUrl = imageUrl(body.logoPath, "w500"),
|
coverImageUrl = imageUrl(body.logoPath, "w500"),
|
||||||
|
|
@ -80,7 +93,7 @@ object TmdbCollectionSourceResolver {
|
||||||
val body = fetch<TmdbNetworkResponse>(
|
val body = fetch<TmdbNetworkResponse>(
|
||||||
endpoint = "network/$id",
|
endpoint = "network/$id",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
) ?: error("TMDB network not found")
|
) ?: error(getString(Res.string.collections_tmdb_network_not_found))
|
||||||
TmdbSourceImportMetadata(
|
TmdbSourceImportMetadata(
|
||||||
title = body.name?.takeIf { it.isNotBlank() },
|
title = body.name?.takeIf { it.isNotBlank() },
|
||||||
coverImageUrl = imageUrl(body.logoPath, "w500"),
|
coverImageUrl = imageUrl(body.logoPath, "w500"),
|
||||||
|
|
@ -93,7 +106,7 @@ object TmdbCollectionSourceResolver {
|
||||||
endpoint = "person/$id",
|
endpoint = "person/$id",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
query = mapOf("language" to language),
|
query = mapOf("language" to language),
|
||||||
) ?: error("TMDB person not found")
|
) ?: error(getString(Res.string.collections_tmdb_person_not_found))
|
||||||
TmdbSourceImportMetadata(
|
TmdbSourceImportMetadata(
|
||||||
title = body.name?.takeIf { it.isNotBlank() },
|
title = body.name?.takeIf { it.isNotBlank() },
|
||||||
coverImageUrl = imageUrl(body.profilePath, "w500"),
|
coverImageUrl = imageUrl(body.profilePath, "w500"),
|
||||||
|
|
@ -109,7 +122,7 @@ object TmdbCollectionSourceResolver {
|
||||||
if (trimmed.isBlank()) return@withContext emptyList()
|
if (trimmed.isBlank()) return@withContext emptyList()
|
||||||
val settings = TmdbSettingsRepository.snapshot()
|
val settings = TmdbSettingsRepository.snapshot()
|
||||||
val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() }
|
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>(
|
fetch<TmdbCompanySearchResponse>(
|
||||||
endpoint = "search/company",
|
endpoint = "search/company",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
|
|
@ -122,7 +135,7 @@ object TmdbCollectionSourceResolver {
|
||||||
if (trimmed.isBlank()) return@withContext emptyList()
|
if (trimmed.isBlank()) return@withContext emptyList()
|
||||||
val settings = TmdbSettingsRepository.snapshot()
|
val settings = TmdbSettingsRepository.snapshot()
|
||||||
val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() }
|
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 language = normalizeTmdbLanguage(settings.language)
|
||||||
fetch<TmdbCollectionSearchResponse>(
|
fetch<TmdbCollectionSearchResponse>(
|
||||||
endpoint = "search/collection",
|
endpoint = "search/collection",
|
||||||
|
|
@ -136,7 +149,7 @@ object TmdbCollectionSourceResolver {
|
||||||
if (trimmed.isBlank()) return@withContext emptyMap()
|
if (trimmed.isBlank()) return@withContext emptyMap()
|
||||||
val settings = TmdbSettingsRepository.snapshot()
|
val settings = TmdbSettingsRepository.snapshot()
|
||||||
val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() }
|
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>(
|
fetch<TmdbKeywordSearchResponse>(
|
||||||
endpoint = "search/keyword",
|
endpoint = "search/keyword",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
|
|
@ -152,7 +165,7 @@ object TmdbCollectionSourceResolver {
|
||||||
suspend fun genres(mediaType: TmdbCollectionMediaType): Map<Int, String> = withContext(Dispatchers.Default) {
|
suspend fun genres(mediaType: TmdbCollectionMediaType): Map<Int, String> = withContext(Dispatchers.Default) {
|
||||||
val settings = TmdbSettingsRepository.snapshot()
|
val settings = TmdbSettingsRepository.snapshot()
|
||||||
val apiKey = settings.apiKey.trim().takeIf { it.isNotBlank() }
|
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 language = normalizeTmdbLanguage(settings.language)
|
||||||
val endpoint = when (mediaType) {
|
val endpoint = when (mediaType) {
|
||||||
TmdbCollectionMediaType.MOVIE -> "genre/movie/list"
|
TmdbCollectionMediaType.MOVIE -> "genre/movie/list"
|
||||||
|
|
@ -200,12 +213,12 @@ object TmdbCollectionSourceResolver {
|
||||||
language: String,
|
language: String,
|
||||||
page: Int,
|
page: Int,
|
||||||
): CatalogPage {
|
): 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>(
|
val body = fetch<TmdbListResponse>(
|
||||||
endpoint = "list/$id",
|
endpoint = "list/$id",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
query = mapOf("language" to language, "page" to page.toString()),
|
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()
|
val items = body.items.orEmpty()
|
||||||
.mapNotNull { it.toPreview() }
|
.mapNotNull { it.toPreview() }
|
||||||
.sortedFor(source.sortBy)
|
.sortedFor(source.sortBy)
|
||||||
|
|
@ -222,12 +235,12 @@ object TmdbCollectionSourceResolver {
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
language: String,
|
language: String,
|
||||||
): CatalogPage {
|
): 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>(
|
val body = fetch<TmdbCollectionResponse>(
|
||||||
endpoint = "collection/$id",
|
endpoint = "collection/$id",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
query = mapOf("language" to language),
|
query = mapOf("language" to language),
|
||||||
) ?: error("TMDB collection not found")
|
) ?: error(getString(Res.string.collections_tmdb_collection_not_found))
|
||||||
val items = body.parts.orEmpty()
|
val items = body.parts.orEmpty()
|
||||||
.mapNotNull { it.toPreview(TmdbCollectionMediaType.MOVIE) }
|
.mapNotNull { it.toPreview(TmdbCollectionMediaType.MOVIE) }
|
||||||
.sortedFor(source.sortBy)
|
.sortedFor(source.sortBy)
|
||||||
|
|
@ -240,13 +253,13 @@ object TmdbCollectionSourceResolver {
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
language: String,
|
language: String,
|
||||||
): CatalogPage {
|
): 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 mediaType = source.tmdbMediaType()
|
||||||
val body = fetch<TmdbPersonCreditsResponse>(
|
val body = fetch<TmdbPersonCreditsResponse>(
|
||||||
endpoint = "person/$id/combined_credits",
|
endpoint = "person/$id/combined_credits",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
query = mapOf("language" to language),
|
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()) {
|
val items = when (source.tmdbType()) {
|
||||||
TmdbCollectionSourceType.DIRECTOR -> body.crew.orEmpty()
|
TmdbCollectionSourceType.DIRECTOR -> body.crew.orEmpty()
|
||||||
.filter { it.job.equals("Director", ignoreCase = true) }
|
.filter { it.job.equals("Director", ignoreCase = true) }
|
||||||
|
|
@ -287,7 +300,7 @@ object TmdbCollectionSourceResolver {
|
||||||
endpoint = endpoint,
|
endpoint = endpoint,
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
query = query,
|
query = query,
|
||||||
) ?: error("TMDB discover returned no data")
|
) ?: error(getString(Res.string.collections_tmdb_discover_no_data))
|
||||||
val items = body.results.orEmpty()
|
val items = body.results.orEmpty()
|
||||||
.mapNotNull { it.toPreview(mediaType) }
|
.mapNotNull { it.toPreview(mediaType) }
|
||||||
.distinctBy { it.id }
|
.distinctBy { it.id }
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,11 @@ object SubtitleRepository {
|
||||||
id = id,
|
id = id,
|
||||||
url = url,
|
url = url,
|
||||||
language = normalizedLang,
|
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()
|
refreshUserSettings()
|
||||||
publish(
|
publish(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
statusMessage = "Connected to Trakt",
|
statusMessage = localizedString(Res.string.trakt_connected_status),
|
||||||
errorMessage = null,
|
errorMessage = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -316,7 +316,7 @@ object TraktAuthRepository {
|
||||||
persist()
|
persist()
|
||||||
publish(
|
publish(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
statusMessage = "Disconnected from Trakt",
|
statusMessage = localizedString(Res.string.trakt_disconnected_status),
|
||||||
errorMessage = null,
|
errorMessage = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,9 @@ object TraktCommentsRepository {
|
||||||
return TraktCommentsPage(emptyList(), page, 0, 0)
|
return TraktCommentsPage(emptyList(), page, 0, 0)
|
||||||
}
|
}
|
||||||
if (response.status !in 200..299) {
|
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)
|
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 com.nuvio.app.features.home.PosterShape
|
||||||
import io.ktor.http.encodeURLParameter
|
import io.ktor.http.encodeURLParameter
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
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
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
data class TraktPublicListImportMetadata(
|
data class TraktPublicListImportMetadata(
|
||||||
|
|
@ -44,7 +60,7 @@ object TraktPublicListSourceResolver {
|
||||||
private val json = Json { ignoreUnknownKeys = true }
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
suspend fun resolve(source: CollectionSource, page: Int = 1): CatalogPage = withContext(Dispatchers.Default) {
|
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 mediaType = TmdbCollectionMediaType.fromString(source.mediaType)
|
||||||
val type = mediaType.toTraktType()
|
val type = mediaType.toTraktType()
|
||||||
val sortBy = TraktListSort.normalize(source.sortBy)
|
val sortBy = TraktListSort.normalize(source.sortBy)
|
||||||
|
|
@ -60,7 +76,7 @@ object TraktPublicListSourceResolver {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (response.status !in 200..299) {
|
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)
|
val rawItems = json.decodeFromString<List<PublicTraktListItemDto>>(response.body)
|
||||||
|
|
@ -76,12 +92,12 @@ object TraktPublicListSourceResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun listImportMetadata(input: String): TraktPublicListImportMetadata = withContext(Dispatchers.Default) {
|
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>(
|
val list = requestJson<PublicTraktListSummaryDto>(
|
||||||
endpoint = "lists/$idPath",
|
endpoint = "lists/$idPath",
|
||||||
query = mapOf("extended" to "full,images"),
|
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(
|
TraktPublicListImportMetadata(
|
||||||
title = list.name?.takeIf { it.isNotBlank() },
|
title = list.name?.takeIf { it.isNotBlank() },
|
||||||
coverImageUrl = list.images?.posters.firstTraktImageUrl(),
|
coverImageUrl = list.images?.posters.firstTraktImageUrl(),
|
||||||
|
|
@ -132,7 +148,7 @@ object TraktPublicListSourceResolver {
|
||||||
): T {
|
): T {
|
||||||
val response = requestRaw(endpoint = endpoint, query = query)
|
val response = requestRaw(endpoint = endpoint, query = query)
|
||||||
if (response.status !in 200..299) {
|
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) }
|
return runCatching { json.decodeFromString<T>(response.body) }
|
||||||
.onFailure { error -> log.w(error) { "Failed to parse Trakt response for $endpoint" } }
|
.onFailure { error -> log.w(error) { "Failed to parse Trakt response for $endpoint" } }
|
||||||
|
|
@ -144,7 +160,7 @@ object TraktPublicListSourceResolver {
|
||||||
query: Map<String, String> = emptyMap(),
|
query: Map<String, String> = emptyMap(),
|
||||||
): RawHttpResponse {
|
): RawHttpResponse {
|
||||||
if (TraktConfig.CLIENT_ID.isBlank()) {
|
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)
|
val url = buildTraktUrl(endpoint, query)
|
||||||
return httpRequestRaw(
|
return httpRequestRaw(
|
||||||
|
|
@ -237,21 +253,25 @@ object TraktPublicListSourceResolver {
|
||||||
|
|
||||||
private fun PublicTraktListSummaryDto.toPublicListResult(likeCount: Int? = null): TraktPublicListSearchResult? {
|
private fun PublicTraktListSummaryDto.toPublicListResult(likeCount: Int? = null): TraktPublicListSearchResult? {
|
||||||
val id = ids?.trakt ?: return null
|
val id = ids?.trakt ?: return null
|
||||||
val listTitle = name?.takeIf { it.isNotBlank() } ?: "Trakt List $id"
|
return runBlocking {
|
||||||
val owner = user?.username?.takeIf { it.isNotBlank() }
|
val listTitle = name?.takeIf { it.isNotBlank() }
|
||||||
val stats = buildList {
|
?: getString(Res.string.collections_editor_trakt_fallback_title, id)
|
||||||
itemCount?.let { add("$it items") }
|
val owner = user?.username?.takeIf { it.isNotBlank() }
|
||||||
(likeCount ?: likes)?.let { add("$it likes") }
|
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? {
|
private fun parseTraktListPath(input: String): String? {
|
||||||
|
|
@ -292,11 +312,11 @@ object TraktPublicListSourceResolver {
|
||||||
?.trim()
|
?.trim()
|
||||||
?.toIntOrNull()
|
?.toIntOrNull()
|
||||||
|
|
||||||
private fun errorMessageFor(code: Int, fallback: String): String {
|
private fun errorMessageFor(code: Int, fallback: String): String = runBlocking {
|
||||||
return when (code) {
|
when (code) {
|
||||||
401, 403, 404 -> "Trakt list not found or not public"
|
401, 403, 404 -> getString(Res.string.collections_trakt_list_not_found_or_private)
|
||||||
429 -> "Trakt rate limit reached"
|
429 -> getString(Res.string.collections_trakt_rate_limit_reached)
|
||||||
else -> "$fallback ($code)"
|
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.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import nuvio.composeapp.generated.resources.*
|
import nuvio.composeapp.generated.resources.*
|
||||||
import org.jetbrains.compose.resources.getString
|
import org.jetbrains.compose.resources.getString
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
|
@ -106,7 +107,7 @@ private val appUpdaterJson = Json {
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NoChannelReleaseException : IllegalStateException(
|
private class NoChannelReleaseException : IllegalStateException(
|
||||||
"No cmp-rewrite release has been published yet.",
|
runBlocking { getString(Res.string.updates_no_channel_release) },
|
||||||
)
|
)
|
||||||
|
|
||||||
private object VersionUtils {
|
private object VersionUtils {
|
||||||
|
|
@ -158,7 +159,7 @@ private object AppUpdaterRepository {
|
||||||
body = "",
|
body = "",
|
||||||
)
|
)
|
||||||
if (response.status !in 200..299) {
|
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)
|
val releases = appUpdaterJson.decodeFromString<List<GitHubReleaseDto>>(response.body)
|
||||||
|
|
@ -167,10 +168,10 @@ private object AppUpdaterRepository {
|
||||||
|
|
||||||
val tag = release.tagName?.takeIf { it.isNotBlank() }
|
val tag = release.tagName?.takeIf { it.isNotBlank() }
|
||||||
?: release.name?.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)
|
val asset = chooseBestApkAsset(release.assets)
|
||||||
?: error("No APK asset found in the cmp-rewrite release")
|
?: error(getString(Res.string.updates_apk_asset_missing))
|
||||||
|
|
||||||
AppUpdate(
|
AppUpdate(
|
||||||
tag = tag,
|
tag = tag,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ import io.ktor.http.ContentType
|
||||||
import io.ktor.http.HttpHeaders
|
import io.ktor.http.HttpHeaders
|
||||||
import io.ktor.http.HttpMethod
|
import io.ktor.http.HttpMethod
|
||||||
import io.ktor.http.isSuccess
|
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
|
import platform.Foundation.NSUserDefaults
|
||||||
|
|
||||||
actual object AddonStorage {
|
actual object AddonStorage {
|
||||||
|
|
@ -54,10 +59,10 @@ actual suspend fun httpGetText(url: String): String =
|
||||||
.let { response ->
|
.let { response ->
|
||||||
val payload = response.bodyAsText()
|
val payload = response.bodyAsText()
|
||||||
if (!response.status.isSuccess()) {
|
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()) {
|
if (payload.isBlank()) {
|
||||||
throw IllegalStateException("Empty response body")
|
throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) })
|
||||||
}
|
}
|
||||||
payload
|
payload
|
||||||
}
|
}
|
||||||
|
|
@ -72,10 +77,10 @@ actual suspend fun httpPostJson(url: String, body: String): String =
|
||||||
.let { response ->
|
.let { response ->
|
||||||
val payload = response.bodyAsText()
|
val payload = response.bodyAsText()
|
||||||
if (!response.status.isSuccess()) {
|
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()) {
|
if (payload.isBlank()) {
|
||||||
throw IllegalStateException("Empty response body")
|
throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) })
|
||||||
}
|
}
|
||||||
payload
|
payload
|
||||||
}
|
}
|
||||||
|
|
@ -94,10 +99,10 @@ actual suspend fun httpGetTextWithHeaders(
|
||||||
.let { response ->
|
.let { response ->
|
||||||
val payload = response.bodyAsText()
|
val payload = response.bodyAsText()
|
||||||
if (!response.status.isSuccess()) {
|
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()) {
|
if (payload.isBlank()) {
|
||||||
throw IllegalStateException("Empty response body")
|
throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) })
|
||||||
}
|
}
|
||||||
payload
|
payload
|
||||||
}
|
}
|
||||||
|
|
@ -119,10 +124,10 @@ actual suspend fun httpPostJsonWithHeaders(
|
||||||
.let { response ->
|
.let { response ->
|
||||||
val payload = response.bodyAsText()
|
val payload = response.bodyAsText()
|
||||||
if (!response.status.isSuccess()) {
|
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()) {
|
if (payload.isBlank()) {
|
||||||
throw IllegalStateException("Empty response body")
|
throw IllegalStateException(runBlocking { getString(Res.string.network_empty_response_body) })
|
||||||
}
|
}
|
||||||
payload
|
payload
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,15 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
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.NSError
|
||||||
import platform.Foundation.NSDate
|
import platform.Foundation.NSDate
|
||||||
import platform.Foundation.NSData
|
import platform.Foundation.NSData
|
||||||
|
|
@ -99,7 +108,7 @@ internal actual object DownloadsPlatformDownloader {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.statusCode !in 200..299) {
|
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
|
val isPartialResume = attemptedRangeRequest && result.statusCode == 206 && resumeFromBytes > 0L
|
||||||
|
|
@ -118,7 +127,7 @@ internal actual object DownloadsPlatformDownloader {
|
||||||
error = null,
|
error = null,
|
||||||
)
|
)
|
||||||
if (!moved) {
|
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"
|
val localFileUri = NSURL.fileURLWithPath(destinationPath).absoluteString ?: "file://$destinationPath"
|
||||||
|
|
@ -127,7 +136,7 @@ internal actual object DownloadsPlatformDownloader {
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
handle.cancelNativeTask()
|
handle.cancelNativeTask()
|
||||||
} catch (error: Throwable) {
|
} 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 {
|
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
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,7 +275,7 @@ private class IosDownloadDelegate(
|
||||||
if (fileError != null) return
|
if (fileError != null) return
|
||||||
|
|
||||||
val file = outputFile ?: run {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -278,7 +287,7 @@ private class IosDownloadDelegate(
|
||||||
file,
|
file,
|
||||||
).toLong()
|
).toLong()
|
||||||
if (wrote != bytesToWrite) {
|
if (wrote != bytesToWrite) {
|
||||||
fileError = IllegalStateException("Failed to write partial download file")
|
fileError = IllegalStateException(runBlocking { getString(Res.string.downloads_error_write_partial_file_failed) })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fflush(file)
|
fflush(file)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue