mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 15:01:59 +00:00
Merge 71bea9a00f into 70d3eee9d2
This commit is contained in:
commit
0970d24205
22 changed files with 220 additions and 224 deletions
|
|
@ -1,10 +1,5 @@
|
|||
package com.nuvio.app.features.addons
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.generic_addon
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
data class AddonManifest(
|
||||
val id: String,
|
||||
val name: String,
|
||||
|
|
@ -60,7 +55,7 @@ data class ManagedAddon(
|
|||
get() = userSetName?.takeIf { it.isNotBlank() && it != manifest?.name }
|
||||
?: manifest?.name
|
||||
?: manifestUrl.substringBefore("?").substringAfterLast("/").ifBlank {
|
||||
runBlocking { getString(Res.string.generic_addon) }
|
||||
"Addon"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -57,6 +58,7 @@ import com.nuvio.app.core.ui.NuvioStatusModal
|
|||
import com.nuvio.app.core.ui.NuvioSurfaceCard
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import kotlinx.coroutines.launch
|
||||
import sh.calvin.reorderable.ReorderableCollectionItemScope
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
|
@ -69,6 +71,7 @@ fun CollectionManagementScreen(
|
|||
) {
|
||||
val collections by CollectionRepository.collections.collectAsState()
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var showImportDialog by remember { mutableStateOf(false) }
|
||||
var importText by remember { mutableStateOf("") }
|
||||
var importError by remember { mutableStateOf<String?>(null) }
|
||||
|
|
@ -171,14 +174,16 @@ fun CollectionManagementScreen(
|
|||
importError = null
|
||||
},
|
||||
onConfirm = {
|
||||
val result = CollectionRepository.validateJson(importText)
|
||||
if (result.valid) {
|
||||
CollectionRepository.importFromJson(importText)
|
||||
showImportDialog = false
|
||||
importText = ""
|
||||
importError = null
|
||||
} else {
|
||||
importError = result.error
|
||||
coroutineScope.launch {
|
||||
val result = CollectionRepository.validateJson(importText)
|
||||
if (result.valid) {
|
||||
CollectionRepository.importFromJson(importText)
|
||||
showImportDialog = false
|
||||
importText = ""
|
||||
importError = null
|
||||
} else {
|
||||
importError = result.error
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
|
@ -135,11 +134,11 @@ object CollectionRepository {
|
|||
}
|
||||
}
|
||||
|
||||
fun validateJson(jsonString: String): ValidationResult {
|
||||
suspend fun validateJson(jsonString: String): ValidationResult {
|
||||
if (jsonString.isBlank()) {
|
||||
return ValidationResult(
|
||||
valid = false,
|
||||
error = runBlocking { getString(Res.string.collections_import_error_empty_json) },
|
||||
error = getString(Res.string.collections_import_error_empty_json),
|
||||
)
|
||||
}
|
||||
return try {
|
||||
|
|
@ -149,55 +148,45 @@ object CollectionRepository {
|
|||
if (c.id.isBlank()) {
|
||||
return ValidationResult(
|
||||
valid = false,
|
||||
error = runBlocking {
|
||||
getString(Res.string.collections_import_error_collection_blank_id, ci + 1)
|
||||
},
|
||||
error = getString(Res.string.collections_import_error_collection_blank_id, ci + 1),
|
||||
)
|
||||
}
|
||||
if (c.title.isBlank()) {
|
||||
return ValidationResult(
|
||||
valid = false,
|
||||
error = runBlocking {
|
||||
getString(Res.string.collections_import_error_collection_blank_title, c.id)
|
||||
},
|
||||
error = getString(Res.string.collections_import_error_collection_blank_title, c.id),
|
||||
)
|
||||
}
|
||||
c.folders.forEachIndexed { fi, f ->
|
||||
if (f.id.isBlank()) {
|
||||
return ValidationResult(
|
||||
valid = false,
|
||||
error = runBlocking {
|
||||
getString(
|
||||
Res.string.collections_import_error_folder_blank_id,
|
||||
fi + 1,
|
||||
c.title,
|
||||
)
|
||||
},
|
||||
error = getString(
|
||||
Res.string.collections_import_error_folder_blank_id,
|
||||
fi + 1,
|
||||
c.title,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (f.title.isBlank()) {
|
||||
return ValidationResult(
|
||||
valid = false,
|
||||
error = runBlocking {
|
||||
getString(
|
||||
Res.string.collections_import_error_folder_blank_title,
|
||||
f.id,
|
||||
c.title,
|
||||
)
|
||||
},
|
||||
error = getString(
|
||||
Res.string.collections_import_error_folder_blank_title,
|
||||
f.id,
|
||||
c.title,
|
||||
),
|
||||
)
|
||||
}
|
||||
f.resolvedSources.forEachIndexed { si, s ->
|
||||
if (s.hasInvalidTraktListId()) {
|
||||
return ValidationResult(
|
||||
valid = false,
|
||||
error = runBlocking {
|
||||
getString(
|
||||
Res.string.collections_import_error_trakt_list_id,
|
||||
si + 1,
|
||||
f.title,
|
||||
)
|
||||
},
|
||||
error = getString(
|
||||
Res.string.collections_import_error_trakt_list_id,
|
||||
si + 1,
|
||||
f.title,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -208,13 +197,11 @@ object CollectionRepository {
|
|||
if (invalidAddon || invalidTmdb) {
|
||||
return ValidationResult(
|
||||
valid = false,
|
||||
error = runBlocking {
|
||||
getString(
|
||||
Res.string.collections_import_error_source_blank_fields,
|
||||
si + 1,
|
||||
f.title,
|
||||
)
|
||||
},
|
||||
error = getString(
|
||||
Res.string.collections_import_error_source_blank_fields,
|
||||
si + 1,
|
||||
f.title,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -229,9 +216,7 @@ object CollectionRepository {
|
|||
} catch (e: Exception) {
|
||||
ValidationResult(
|
||||
valid = false,
|
||||
error = runBlocking {
|
||||
getString(Res.string.collections_import_error_invalid_json, e.message.orEmpty())
|
||||
},
|
||||
error = getString(Res.string.collections_import_error_invalid_json, e.message.orEmpty()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ 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_folder_addon_not_found
|
||||
import nuvio.composeapp.generated.resources.collections_tab_all
|
||||
|
|
@ -93,7 +92,7 @@ object FolderDetailRepository {
|
|||
private var activeCollectionId: String? = null
|
||||
private var activeFolderId: String? = null
|
||||
|
||||
fun initialize(collectionId: String, folderId: String) {
|
||||
suspend fun initialize(collectionId: String, folderId: String) {
|
||||
val current = _uiState.value
|
||||
if (
|
||||
activeCollectionId == collectionId &&
|
||||
|
|
@ -128,7 +127,7 @@ object FolderDetailRepository {
|
|||
if (showAll) {
|
||||
add(
|
||||
FolderTab(
|
||||
label = runBlocking { getString(Res.string.collections_tab_all) },
|
||||
label = getString(Res.string.collections_tab_all),
|
||||
isAllTab = true,
|
||||
isLoading = true,
|
||||
),
|
||||
|
|
@ -213,12 +212,14 @@ object FolderDetailRepository {
|
|||
val catalogSource = source.addonCatalogSource()
|
||||
val resolvedCatalog = catalogSource?.let { addons.findCollectionCatalog(it) }
|
||||
if (!source.isTmdb && !source.isTrakt && resolvedCatalog == null) {
|
||||
val errorMessage = getString(
|
||||
Res.string.collections_folder_addon_not_found,
|
||||
catalogSource?.addonId.orEmpty(),
|
||||
)
|
||||
updateTab(tabIndex) {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = runBlocking {
|
||||
getString(Res.string.collections_folder_addon_not_found, catalogSource?.addonId.orEmpty())
|
||||
},
|
||||
error = errorMessage,
|
||||
)
|
||||
}
|
||||
return@forEachIndexed
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.nuvio.app.features.details
|
|||
import com.nuvio.app.features.streams.StreamBehaviorHints
|
||||
import com.nuvio.app.features.streams.StreamItem
|
||||
import com.nuvio.app.features.streams.StreamProxyHeaders
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
|
@ -20,7 +19,7 @@ import org.jetbrains.compose.resources.getString
|
|||
internal object MetaDetailsParser {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun parse(payload: String): MetaDetails {
|
||||
suspend fun parse(payload: String): MetaDetails {
|
||||
val root = json.parseToJsonElement(payload).asJsonObjectOrNull()
|
||||
?: error("Expected top-level JSON object in response")
|
||||
val meta = root.extractMetaObject()
|
||||
|
|
@ -217,59 +216,71 @@ internal object MetaDetailsParser {
|
|||
return merged.values.toList()
|
||||
}
|
||||
|
||||
private fun JsonObject.videos(): List<MetaVideo> =
|
||||
array("videos").mapNotNull { element ->
|
||||
val video = element as? JsonObject ?: return@mapNotNull null
|
||||
val id = video.string("id") ?: return@mapNotNull null
|
||||
val title = video.string("title") ?: video.string("name") ?: return@mapNotNull null
|
||||
MetaVideo(
|
||||
id = id,
|
||||
title = title,
|
||||
released = video.string("released"),
|
||||
thumbnail = video.string("thumbnail"),
|
||||
seasonPoster = video.string("seasonPoster") ?: video.string("season_poster_path"),
|
||||
season = video.int("season"),
|
||||
episode = video.int("episode"),
|
||||
overview = video.string("overview") ?: video.string("description"),
|
||||
runtime = video.int("runtime"),
|
||||
streams = video.embeddedStreams(),
|
||||
private suspend fun JsonObject.videos(): List<MetaVideo> {
|
||||
val result = mutableListOf<MetaVideo>()
|
||||
for (element in array("videos")) {
|
||||
val video = element as? JsonObject ?: continue
|
||||
val id = video.string("id") ?: continue
|
||||
val title = video.string("title") ?: video.string("name") ?: continue
|
||||
result.add(
|
||||
MetaVideo(
|
||||
id = id,
|
||||
title = title,
|
||||
released = video.string("released"),
|
||||
thumbnail = video.string("thumbnail"),
|
||||
seasonPoster = video.string("seasonPoster") ?: video.string("season_poster_path"),
|
||||
season = video.int("season"),
|
||||
episode = video.int("episode"),
|
||||
overview = video.string("overview") ?: video.string("description"),
|
||||
runtime = video.int("runtime"),
|
||||
streams = video.embeddedStreams(),
|
||||
),
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun JsonObject.trailers(): List<MetaTrailer> =
|
||||
array("trailers").mapNotNull { element ->
|
||||
val trailer = element as? JsonObject ?: return@mapNotNull null
|
||||
private suspend fun JsonObject.trailers(): List<MetaTrailer> {
|
||||
val result = mutableListOf<MetaTrailer>()
|
||||
for (element in array("trailers")) {
|
||||
val trailer = element as? JsonObject ?: continue
|
||||
val key = trailer.string("key")
|
||||
?: trailer.string("source")
|
||||
?: trailer.string("ytId")
|
||||
?: trailer.string("ytid")
|
||||
?: return@mapNotNull null
|
||||
?: continue
|
||||
|
||||
val normalizedKey = key.trim()
|
||||
if (normalizedKey.isEmpty()) return@mapNotNull null
|
||||
if (normalizedKey.isEmpty()) continue
|
||||
|
||||
MetaTrailer(
|
||||
id = trailer.string("id")?.takeIf(String::isNotBlank) ?: normalizedKey,
|
||||
key = normalizedKey,
|
||||
name = trailer.string("name")?.takeIf(String::isNotBlank) ?: runBlocking { getString(Res.string.generic_trailer) },
|
||||
site = trailer.string("site")?.takeIf(String::isNotBlank) ?: "YouTube",
|
||||
size = trailer.int("size"),
|
||||
type = trailer.string("type")?.takeIf(String::isNotBlank) ?: runBlocking { getString(Res.string.generic_trailer) },
|
||||
official = trailer.boolean("official") == true,
|
||||
publishedAt = trailer.string("published_at") ?: trailer.string("publishedAt"),
|
||||
seasonNumber = trailer.int("seasonNumber") ?: trailer.int("season_number"),
|
||||
displayName = trailer.string("displayName")?.takeIf(String::isNotBlank),
|
||||
result.add(
|
||||
MetaTrailer(
|
||||
id = trailer.string("id")?.takeIf(String::isNotBlank) ?: normalizedKey,
|
||||
key = normalizedKey,
|
||||
name = trailer.string("name")?.takeIf(String::isNotBlank)
|
||||
?: getString(Res.string.generic_trailer),
|
||||
site = trailer.string("site")?.takeIf(String::isNotBlank) ?: "YouTube",
|
||||
size = trailer.int("size"),
|
||||
type = trailer.string("type")?.takeIf(String::isNotBlank) ?: "",
|
||||
official = trailer.boolean("official") == true,
|
||||
publishedAt = trailer.string("published_at") ?: trailer.string("publishedAt"),
|
||||
seasonNumber = trailer.int("seasonNumber") ?: trailer.int("season_number"),
|
||||
displayName = trailer.string("displayName")?.takeIf(String::isNotBlank),
|
||||
),
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun JsonObject.embeddedStreams(): List<StreamItem> {
|
||||
private suspend fun JsonObject.embeddedStreams(): List<StreamItem> {
|
||||
val arr = this["streams"] as? JsonArray ?: return emptyList()
|
||||
return arr.mapNotNull { element ->
|
||||
val obj = element as? JsonObject ?: return@mapNotNull null
|
||||
val result = mutableListOf<StreamItem>()
|
||||
for (element in arr) {
|
||||
val obj = element as? JsonObject ?: continue
|
||||
val url = obj.string("url")
|
||||
val infoHash = obj.string("infoHash")
|
||||
val externalUrl = obj.string("externalUrl")
|
||||
if (url == null && infoHash == null && externalUrl == null) return@mapNotNull null
|
||||
if (url == null && infoHash == null && externalUrl == null) continue
|
||||
|
||||
val hintsObj = obj["behaviorHints"] as? JsonObject
|
||||
val proxyHeaders = hintsObj
|
||||
|
|
@ -278,25 +289,28 @@ internal object MetaDetailsParser {
|
|||
val streamData = obj["streamData"] as? JsonObject
|
||||
val addonName = streamData?.string("addon")
|
||||
?: obj.string("name")
|
||||
?: runBlocking { getString(Res.string.source_embedded) }
|
||||
StreamItem(
|
||||
name = obj.string("name"),
|
||||
description = obj.string("description") ?: obj.string("title"),
|
||||
url = url,
|
||||
infoHash = infoHash,
|
||||
fileIdx = obj.int("fileIdx"),
|
||||
externalUrl = externalUrl,
|
||||
addonName = addonName,
|
||||
addonId = "embedded",
|
||||
behaviorHints = StreamBehaviorHints(
|
||||
bingeGroup = hintsObj?.string("bingeGroup"),
|
||||
notWebReady = (hintsObj?.boolean("notWebReady") ?: false) || proxyHeaders != null,
|
||||
videoSize = hintsObj?.long("videoSize"),
|
||||
filename = hintsObj?.string("filename"),
|
||||
proxyHeaders = proxyHeaders,
|
||||
?: getString(Res.string.source_embedded)
|
||||
result.add(
|
||||
StreamItem(
|
||||
name = obj.string("name"),
|
||||
description = obj.string("description") ?: obj.string("title"),
|
||||
url = url,
|
||||
infoHash = infoHash,
|
||||
fileIdx = obj.int("fileIdx"),
|
||||
externalUrl = externalUrl,
|
||||
addonName = addonName,
|
||||
addonId = "embedded",
|
||||
behaviorHints = StreamBehaviorHints(
|
||||
bingeGroup = hintsObj?.string("bingeGroup"),
|
||||
notWebReady = (hintsObj?.boolean("notWebReady") ?: false) || proxyHeaders != null,
|
||||
videoSize = hintsObj?.long("videoSize"),
|
||||
filename = hintsObj?.string("filename"),
|
||||
proxyHeaders = proxyHeaders,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun JsonObject.objectValue(name: String): JsonObject? =
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ import com.nuvio.app.features.watchprogress.WatchProgressEntry
|
|||
import com.nuvio.app.features.watchprogress.WatchProgressRepository
|
||||
import com.nuvio.app.features.watchprogress.buildPlaybackVideoId
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
|
||||
import com.nuvio.app.features.settings.titleRes
|
||||
import com.nuvio.app.features.watching.application.WatchingActions
|
||||
import com.nuvio.app.features.watching.application.WatchingState
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -1176,7 +1177,7 @@ private fun ConfiguredMetaSections(
|
|||
RenderSection(groupMembers.first().key)
|
||||
} else {
|
||||
TabbedSectionGroup(
|
||||
tabs = groupMembers.map { it.key to it.title },
|
||||
tabs = groupMembers.map { it.key to stringResource(it.key.titleRes) },
|
||||
) { activeKey ->
|
||||
RenderSection(activeKey, showHeader = false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.nuvio.app.features.details
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
|
|
@ -11,7 +10,6 @@ import kotlinx.serialization.encodeToString
|
|||
import kotlinx.serialization.json.Json
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
enum class MetaScreenSectionKey {
|
||||
ACTIONS,
|
||||
|
|
@ -33,8 +31,6 @@ enum class MetaScreenSectionKey {
|
|||
|
||||
data class MetaScreenSectionItem(
|
||||
val key: MetaScreenSectionKey,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val enabled: Boolean,
|
||||
val order: Int,
|
||||
val tabGroup: Int? = null,
|
||||
|
|
@ -160,7 +156,6 @@ object MetaScreenSettingsRepository {
|
|||
private var tabLayout: Boolean = false
|
||||
private var episodeCardStyle: MetaEpisodeCardStyle = MetaEpisodeCardStyle.Horizontal
|
||||
private var blurUnwatchedEpisodes: Boolean = false
|
||||
private fun localizedString(resource: StringResource): String = runBlocking { getString(resource) }
|
||||
|
||||
fun ensureLoaded() {
|
||||
if (hasLoaded) return
|
||||
|
|
@ -345,8 +340,6 @@ object MetaScreenSettingsRepository {
|
|||
val preference = preferences[definition.key]
|
||||
MetaScreenSectionItem(
|
||||
key = definition.key,
|
||||
title = localizedString(definition.titleRes),
|
||||
description = localizedString(definition.descriptionRes),
|
||||
enabled = preference?.enabled ?: true,
|
||||
order = preference?.order ?: 0,
|
||||
tabGroup = preference?.tabGroup,
|
||||
|
|
|
|||
|
|
@ -58,10 +58,8 @@ import nuvio.composeapp.generated.resources.rating_rotten_tomatoes
|
|||
import nuvio.composeapp.generated.resources.rating_tmdb
|
||||
import nuvio.composeapp.generated.resources.rating_trakt
|
||||
import org.jetbrains.compose.resources.DrawableResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
|
@ -215,6 +213,8 @@ private fun DetailRatingsRow(
|
|||
|
||||
if (orderedRatings.isEmpty()) return
|
||||
|
||||
val audienceScoreLabel = stringResource(Res.string.rating_audience_score)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -228,7 +228,7 @@ private fun DetailRatingsRow(
|
|||
) {
|
||||
Image(
|
||||
painter = painterResource(visuals.logo),
|
||||
contentDescription = visuals.displayName,
|
||||
contentDescription = if (visuals.source == PROVIDER_AUDIENCE) audienceScoreLabel else visuals.displayName,
|
||||
modifier = Modifier.size(width = visuals.logoWidth, height = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
|
@ -350,7 +350,7 @@ private val ratingVisuals = listOf(
|
|||
),
|
||||
RatingVisuals(
|
||||
source = PROVIDER_AUDIENCE,
|
||||
displayName = runBlocking { getString(Res.string.rating_audience_score) },
|
||||
displayName = "",
|
||||
logo = Res.drawable.rating_audience_score,
|
||||
logoWidth = 16.dp,
|
||||
valueColor = Color(0xFFFA320A),
|
||||
|
|
|
|||
|
|
@ -76,9 +76,7 @@ import com.nuvio.app.features.details.seasonSortKey
|
|||
import com.nuvio.app.features.watchprogress.WatchProgressEntry
|
||||
import com.nuvio.app.features.watchprogress.buildPlaybackVideoId
|
||||
import com.nuvio.app.features.watching.application.WatchingState
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import kotlin.math.absoluteValue
|
||||
|
|
@ -1306,18 +1304,20 @@ private fun seriesContentSizing(maxWidthDp: Float): SeriesContentSizing =
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Int.label(): String =
|
||||
if (this <= 0) {
|
||||
runBlocking { getString(Res.string.episodes_specials) }
|
||||
stringResource(Res.string.episodes_specials)
|
||||
} else {
|
||||
runBlocking { getString(Res.string.episodes_season, this@label) }
|
||||
stringResource(Res.string.episodes_season, this@label)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MetaVideo.episodeBadge(): String =
|
||||
when {
|
||||
episode != null || season != null ->
|
||||
localizedSeasonEpisodeCode(seasonNumber = season, episodeNumber = episode).orEmpty()
|
||||
else -> runBlocking { getString(Res.string.details_episode_badge_file) }
|
||||
else -> stringResource(Res.string.details_episode_badge_file)
|
||||
}
|
||||
|
||||
private fun MetaVideo.seasonEpisodeKey(): Pair<Int, Int>? {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.nuvio.app.features.home
|
|||
import com.nuvio.app.features.addons.ManagedAddon
|
||||
import com.nuvio.app.features.collection.Collection
|
||||
import com.nuvio.app.features.collection.CollectionRepository
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -124,7 +123,7 @@ object HomeCatalogSettingsRepository {
|
|||
_uiState.value = HomeCatalogSettingsUiState()
|
||||
}
|
||||
|
||||
fun syncCatalogs(addons: List<ManagedAddon>) {
|
||||
suspend fun syncCatalogs(addons: List<ManagedAddon>) {
|
||||
ensureLoaded()
|
||||
definitions = buildHomeCatalogDefinitions(addons)
|
||||
collectionDefinitions = buildCollectionDefinitions(CollectionRepository.collections.value)
|
||||
|
|
@ -138,7 +137,7 @@ object HomeCatalogSettingsRepository {
|
|||
persist()
|
||||
}
|
||||
|
||||
fun syncCollections(collections: List<Collection>) {
|
||||
suspend fun syncCollections(collections: List<Collection>) {
|
||||
ensureLoaded()
|
||||
collectionDefinitions = buildCollectionDefinitions(collections)
|
||||
normalizePreferences()
|
||||
|
|
@ -530,13 +529,19 @@ internal data class CollectionCatalogDefinition(
|
|||
val isPinnedToTop: Boolean,
|
||||
)
|
||||
|
||||
internal fun buildCollectionDefinitions(collections: List<Collection>): List<CollectionCatalogDefinition> =
|
||||
collections.filter { it.folders.isNotEmpty() }.map { collection ->
|
||||
CollectionCatalogDefinition(
|
||||
key = "collection_${collection.id}",
|
||||
collectionId = collection.id,
|
||||
title = collection.title,
|
||||
subtitle = runBlocking { getString(Res.string.collections_folder_count, collection.folders.size) },
|
||||
isPinnedToTop = collection.pinToTop,
|
||||
internal suspend fun buildCollectionDefinitions(collections: List<Collection>): List<CollectionCatalogDefinition> {
|
||||
val result = mutableListOf<CollectionCatalogDefinition>()
|
||||
for (collection in collections) {
|
||||
if (collection.folders.isEmpty()) continue
|
||||
result.add(
|
||||
CollectionCatalogDefinition(
|
||||
key = "collection_${collection.id}",
|
||||
collectionId = collection.id,
|
||||
title = collection.title,
|
||||
subtitle = getString(Res.string.collections_folder_count, collection.folders.size),
|
||||
isPinnedToTop = collection.pinToTop,
|
||||
),
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ private fun PlayerHeader(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = streamTitle,
|
||||
text = streamTitle.ifBlank { stringResource(Res.string.stream_default_name) },
|
||||
style = typeScale.labelSm.copy(
|
||||
fontSize = metrics.metadataSize,
|
||||
lineHeight = metrics.metadataSize * 1.25f,
|
||||
|
|
|
|||
|
|
@ -629,7 +629,7 @@ private fun EpisodeSourceStreamRow(
|
|||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stream.streamLabel,
|
||||
text = stream.streamLabel.ifBlank { stringResource(Res.string.stream_default_name) },
|
||||
color = colorScheme.onSurface,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ private fun SourceStreamRow(
|
|||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stream.streamLabel,
|
||||
text = stream.streamLabel.ifBlank { stringResource(Res.string.stream_default_name) },
|
||||
color = colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontSize = 14.sp,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
|
@ -44,7 +43,6 @@ import kotlinx.serialization.json.buildJsonObject
|
|||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import kotlinx.serialization.json.put
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
@Serializable
|
||||
|
|
@ -58,7 +56,6 @@ object ProfileRepository {
|
|||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val log = Logger.withTag("ProfileRepository")
|
||||
private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true }
|
||||
private fun localizedString(resource: StringResource): String = runBlocking { getString(resource) }
|
||||
|
||||
private val _state = MutableStateFlow(ProfileState())
|
||||
val state: StateFlow<ProfileState> = _state.asStateFlow()
|
||||
|
|
@ -414,7 +411,7 @@ object ProfileRepository {
|
|||
ProfilePinCacheStorage.savePayload(profileIndex, json.encodeToString(payload))
|
||||
}
|
||||
|
||||
private fun verifyPinLocally(profileIndex: Int, pin: String): PinVerifyResult {
|
||||
private suspend fun verifyPinLocally(profileIndex: Int, pin: String): PinVerifyResult {
|
||||
val profile = _state.value.profiles.find { it.profileIndex == profileIndex }
|
||||
if (profile?.pinEnabled != true) {
|
||||
return PinVerifyResult(unlocked = true)
|
||||
|
|
@ -424,7 +421,7 @@ object ProfileRepository {
|
|||
if (payload.isEmpty()) {
|
||||
return PinVerifyResult(
|
||||
unlocked = false,
|
||||
message = localizedString(Res.string.profile_pin_offline_verification_requires_online),
|
||||
message = getString(Res.string.profile_pin_offline_verification_requires_online),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -432,7 +429,7 @@ object ProfileRepository {
|
|||
json.decodeFromString<CachedProfilePinPayload>(payload)
|
||||
}.getOrNull() ?: return PinVerifyResult(
|
||||
unlocked = false,
|
||||
message = localizedString(Res.string.profile_pin_offline_verification_requires_online),
|
||||
message = getString(Res.string.profile_pin_offline_verification_requires_online),
|
||||
)
|
||||
|
||||
if (
|
||||
|
|
@ -443,7 +440,7 @@ object ProfileRepository {
|
|||
ProfilePinCacheStorage.removePayload(profileIndex)
|
||||
return PinVerifyResult(
|
||||
unlocked = false,
|
||||
message = localizedString(Res.string.profile_pin_changed_requires_refresh),
|
||||
message = getString(Res.string.profile_pin_changed_requires_refresh),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -451,7 +448,7 @@ object ProfileRepository {
|
|||
return if (digest == cached.digest) {
|
||||
PinVerifyResult(unlocked = true)
|
||||
} else {
|
||||
PinVerifyResult(unlocked = false, message = localizedString(Res.string.pin_incorrect))
|
||||
PinVerifyResult(unlocked = false, message = getString(Res.string.pin_incorrect))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -487,7 +487,7 @@ private val MetaEpisodeCardStyle.descriptionRes: StringResource
|
|||
MetaEpisodeCardStyle.List -> Res.string.settings_meta_episode_style_list_description
|
||||
}
|
||||
|
||||
private val MetaScreenSectionKey.titleRes: StringResource
|
||||
internal val MetaScreenSectionKey.titleRes: StringResource
|
||||
get() = when (this) {
|
||||
MetaScreenSectionKey.ACTIONS -> Res.string.settings_meta_actions
|
||||
MetaScreenSectionKey.OVERVIEW -> Res.string.settings_meta_overview
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
|
|
@ -49,6 +50,7 @@ import com.nuvio.app.features.trakt.WatchProgressSource
|
|||
import com.nuvio.app.features.trakt.TRAKT_CONTINUE_WATCHING_DAYS_CAP_ALL
|
||||
import com.nuvio.app.features.trakt.normalizeTraktContinueWatchingDaysCap
|
||||
import com.nuvio.app.features.trakt.traktBrandPainter
|
||||
import kotlinx.coroutines.launch
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.action_cancel
|
||||
import nuvio.composeapp.generated.resources.settings_playback_dialog_close
|
||||
|
|
@ -581,6 +583,7 @@ private fun TraktConnectionCard(
|
|||
uiState: TraktAuthUiState,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val horizontalPadding = if (isTablet) 20.dp else 16.dp
|
||||
val verticalPadding = if (isTablet) 18.dp else 16.dp
|
||||
val failedOpenBrowserMessage = stringResource(Res.string.settings_trakt_failed_open_browser)
|
||||
|
|
@ -641,15 +644,17 @@ private fun TraktConnectionCard(
|
|||
)
|
||||
Button(
|
||||
onClick = {
|
||||
val authUrl = TraktAuthRepository.pendingAuthorizationUrl()
|
||||
?: TraktAuthRepository.onConnectRequested()
|
||||
if (authUrl == null) return@Button
|
||||
runCatching { uriHandler.openUri(authUrl) }
|
||||
.onFailure {
|
||||
TraktAuthRepository.onAuthLaunchFailed(
|
||||
it.message ?: failedOpenBrowserMessage,
|
||||
)
|
||||
}
|
||||
coroutineScope.launch {
|
||||
val authUrl = TraktAuthRepository.pendingAuthorizationUrl()
|
||||
?: TraktAuthRepository.onConnectRequested()
|
||||
if (authUrl == null) return@launch
|
||||
runCatching { uriHandler.openUri(authUrl) }
|
||||
.onFailure {
|
||||
TraktAuthRepository.onAuthLaunchFailed(
|
||||
it.message ?: failedOpenBrowserMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !uiState.isLoading,
|
||||
) {
|
||||
|
|
@ -675,13 +680,15 @@ private fun TraktConnectionCard(
|
|||
)
|
||||
Button(
|
||||
onClick = {
|
||||
val authUrl = TraktAuthRepository.onConnectRequested() ?: return@Button
|
||||
runCatching { uriHandler.openUri(authUrl) }
|
||||
.onFailure {
|
||||
TraktAuthRepository.onAuthLaunchFailed(
|
||||
it.message ?: failedOpenBrowserMessage,
|
||||
)
|
||||
}
|
||||
coroutineScope.launch {
|
||||
val authUrl = TraktAuthRepository.onConnectRequested() ?: return@launch
|
||||
runCatching { uriHandler.openUri(authUrl) }
|
||||
.onFailure {
|
||||
TraktAuthRepository.onAuthLaunchFailed(
|
||||
it.message ?: failedOpenBrowserMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = uiState.credentialsConfigured && !uiState.isLoading,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
package com.nuvio.app.features.streams
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
data class StreamItem(
|
||||
val name: String? = null,
|
||||
val title: String? = null,
|
||||
|
|
@ -20,7 +16,7 @@ data class StreamItem(
|
|||
val clientResolve: StreamClientResolve? = null,
|
||||
) {
|
||||
val streamLabel: String
|
||||
get() = name ?: runBlocking { getString(Res.string.stream_default_name) }
|
||||
get() = name.orEmpty()
|
||||
|
||||
val streamSubtitle: String?
|
||||
get() = description
|
||||
|
|
|
|||
|
|
@ -994,7 +994,7 @@ private fun StreamCard(
|
|||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stream.streamLabel,
|
||||
text = stream.streamLabel.ifBlank { stringResource(Res.string.stream_default_name) },
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
|
|
@ -1060,7 +1060,7 @@ private fun StreamActionsSheet(
|
|||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stream.streamLabel,
|
||||
text = stream.streamLabel.ifBlank { stringResource(Res.string.stream_default_name) },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
|
@ -97,7 +96,7 @@ object TmdbMetadataService {
|
|||
|
||||
val detail = PersonDetail(
|
||||
tmdbId = person.id ?: personId,
|
||||
name = person.name ?: runBlocking { getString(Res.string.generic_unknown) },
|
||||
name = person.name ?: getString(Res.string.generic_unknown),
|
||||
biography = biography,
|
||||
birthday = person.birthday?.takeIf { it.isNotBlank() },
|
||||
deathday = person.deathday?.takeIf { it.isNotBlank() },
|
||||
|
|
@ -327,7 +326,7 @@ object TmdbMetadataService {
|
|||
header = header ?: TmdbEntityHeader(
|
||||
id = entityId,
|
||||
kind = entityKind,
|
||||
name = fallbackName?.takeIf { it.isNotBlank() } ?: runBlocking { getString(Res.string.generic_unknown) },
|
||||
name = fallbackName?.takeIf { it.isNotBlank() } ?: getString(Res.string.generic_unknown),
|
||||
logo = null,
|
||||
originCountry = null,
|
||||
secondaryLabel = null,
|
||||
|
|
@ -442,7 +441,7 @@ object TmdbMetadataService {
|
|||
kind = entityKind,
|
||||
name = it.name?.takeIf { n -> n.isNotBlank() }
|
||||
?: fallbackName?.takeIf { n -> n.isNotBlank() }
|
||||
?: runBlocking { getString(Res.string.generic_unknown) },
|
||||
?: getString(Res.string.generic_unknown),
|
||||
logo = buildImageUrl(it.logoPath, "w500"),
|
||||
originCountry = it.originCountry?.takeIf { c -> c.isNotBlank() },
|
||||
secondaryLabel = it.headquarters?.takeIf { h -> h.isNotBlank() },
|
||||
|
|
@ -458,7 +457,7 @@ object TmdbMetadataService {
|
|||
kind = entityKind,
|
||||
name = it.name?.takeIf { n -> n.isNotBlank() }
|
||||
?: fallbackName?.takeIf { n -> n.isNotBlank() }
|
||||
?: runBlocking { getString(Res.string.generic_unknown) },
|
||||
?: getString(Res.string.generic_unknown),
|
||||
logo = buildImageUrl(it.logoPath, "w500"),
|
||||
originCountry = it.originCountry?.takeIf { c -> c.isNotBlank() },
|
||||
secondaryLabel = it.headquarters?.takeIf { h -> h.isNotBlank() },
|
||||
|
|
@ -1112,8 +1111,8 @@ object TmdbMetadataService {
|
|||
endpoint = "$mediaType/$tmdbId/videos",
|
||||
language = language,
|
||||
)
|
||||
allVideos += primaryVideos.map { video ->
|
||||
video.toMetaTrailer(
|
||||
for (video in primaryVideos) {
|
||||
allVideos += video.toMetaTrailer(
|
||||
seasonNumber = null,
|
||||
displayName = video.name,
|
||||
)
|
||||
|
|
@ -1137,23 +1136,22 @@ object TmdbMetadataService {
|
|||
}.awaitAll()
|
||||
}
|
||||
|
||||
seasonVideos.forEach { (seasonNumber, videos) ->
|
||||
allVideos += videos.map { video ->
|
||||
video.toMetaTrailer(
|
||||
for ((seasonNumber, videos) in seasonVideos) {
|
||||
for (video in videos) {
|
||||
allVideos += video.toMetaTrailer(
|
||||
seasonNumber = seasonNumber,
|
||||
displayName = runBlocking {
|
||||
getString(
|
||||
Res.string.trailer_season_label,
|
||||
seasonNumber,
|
||||
video.name.orEmpty(),
|
||||
)
|
||||
},
|
||||
displayName = getString(
|
||||
Res.string.trailer_season_label,
|
||||
seasonNumber,
|
||||
video.name.orEmpty(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val genericTrailerLabel = getString(Res.string.generic_trailer)
|
||||
val byCategory = linkedMapOf<String, MutableList<MetaTrailer>>()
|
||||
allVideos
|
||||
.asSequence()
|
||||
|
|
@ -1162,7 +1160,7 @@ object TmdbMetadataService {
|
|||
}
|
||||
.forEach { trailer ->
|
||||
byCategory.getOrPut(
|
||||
trailer.type.ifBlank { runBlocking { getString(Res.string.generic_trailer) } },
|
||||
trailer.type.ifBlank { genericTrailerLabel },
|
||||
) { mutableListOf() }
|
||||
.add(trailer)
|
||||
}
|
||||
|
|
@ -1184,10 +1182,7 @@ object TmdbMetadataService {
|
|||
val sortedCategories = byCategory.keys.sortedWith(
|
||||
compareBy<String> { category ->
|
||||
when {
|
||||
category.equals(
|
||||
runBlocking { getString(Res.string.generic_trailer) },
|
||||
ignoreCase = true,
|
||||
) -> 0
|
||||
category.equals(genericTrailerLabel, ignoreCase = true) -> 0
|
||||
byCategory[category].orEmpty().any { it.official } -> 1
|
||||
else -> 2
|
||||
}
|
||||
|
|
@ -1300,17 +1295,21 @@ internal fun normalizeTmdbLanguage(language: String?): String {
|
|||
}
|
||||
}
|
||||
|
||||
private fun buildPeople(
|
||||
private suspend fun buildPeople(
|
||||
details: TmdbDetailsResponse,
|
||||
credits: TmdbCreditsResponse?,
|
||||
mediaType: String,
|
||||
): List<MetaPerson> {
|
||||
val creatorRole = getString(Res.string.person_role_creator)
|
||||
val directorRole = getString(Res.string.person_role_director)
|
||||
val writerRole = getString(Res.string.person_role_writer)
|
||||
|
||||
val creators = if (mediaType == "tv") {
|
||||
details.createdBy.mapNotNull { creator ->
|
||||
val name = creator.name?.trim()?.takeIf(String::isNotBlank) ?: return@mapNotNull null
|
||||
MetaPerson(
|
||||
name = name,
|
||||
role = runBlocking { getString(Res.string.person_role_creator) },
|
||||
role = creatorRole,
|
||||
photo = buildImageUrl(creator.profilePath, "w500"),
|
||||
tmdbId = creator.id,
|
||||
)
|
||||
|
|
@ -1325,7 +1324,7 @@ private fun buildPeople(
|
|||
val name = crew.name?.trim()?.takeIf(String::isNotBlank) ?: return@mapNotNull null
|
||||
MetaPerson(
|
||||
name = name,
|
||||
role = runBlocking { getString(Res.string.person_role_director) },
|
||||
role = directorRole,
|
||||
photo = buildImageUrl(crew.profilePath, "w500"),
|
||||
tmdbId = crew.id,
|
||||
)
|
||||
|
|
@ -1340,7 +1339,7 @@ private fun buildPeople(
|
|||
val name = crew.name?.trim()?.takeIf(String::isNotBlank) ?: return@mapNotNull null
|
||||
MetaPerson(
|
||||
name = name,
|
||||
role = runBlocking { getString(Res.string.person_role_writer) },
|
||||
role = writerRole,
|
||||
photo = buildImageUrl(crew.profilePath, "w500"),
|
||||
tmdbId = crew.id,
|
||||
)
|
||||
|
|
@ -1543,12 +1542,12 @@ private data class TmdbVideoResult(
|
|||
@SerialName("published_at") val publishedAt: String? = null,
|
||||
)
|
||||
|
||||
private fun TmdbVideoResult.toMetaTrailer(
|
||||
private suspend fun TmdbVideoResult.toMetaTrailer(
|
||||
seasonNumber: Int?,
|
||||
displayName: String?,
|
||||
): MetaTrailer {
|
||||
val videoKey = key?.trim().orEmpty()
|
||||
val videoName = name?.trim().takeUnless { it.isNullOrBlank() } ?: runBlocking { getString(Res.string.generic_trailer) }
|
||||
val videoName = name?.trim().takeUnless { it.isNullOrBlank() } ?: getString(Res.string.generic_trailer)
|
||||
val trailerId = id?.trim().takeUnless { it.isNullOrBlank() } ?: videoKey
|
||||
return MetaTrailer(
|
||||
id = trailerId,
|
||||
|
|
@ -1556,7 +1555,7 @@ private fun TmdbVideoResult.toMetaTrailer(
|
|||
name = videoName,
|
||||
site = site?.trim().takeUnless { it.isNullOrBlank() } ?: "YouTube",
|
||||
size = size,
|
||||
type = type?.trim().takeUnless { it.isNullOrBlank() } ?: runBlocking { getString(Res.string.generic_trailer) },
|
||||
type = type?.trim().takeUnless { it.isNullOrBlank() } ?: "",
|
||||
official = official == true,
|
||||
publishedAt = publishedAt,
|
||||
seasonNumber = seasonNumber,
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ import kotlinx.serialization.json.Json
|
|||
import kotlin.random.Random
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object TraktAuthRepository {
|
||||
private const val BASE_URL = "https://api.trakt.tv"
|
||||
|
|
@ -68,10 +66,10 @@ object TraktAuthRepository {
|
|||
fun hasRequiredCredentials(): Boolean =
|
||||
TraktConfig.CLIENT_ID.isNotBlank() && TraktConfig.CLIENT_SECRET.isNotBlank()
|
||||
|
||||
fun onConnectRequested(): String? {
|
||||
suspend fun onConnectRequested(): String? {
|
||||
ensureLoaded()
|
||||
if (!hasRequiredCredentials()) {
|
||||
publish(errorMessage = localizedString(Res.string.trakt_missing_credentials))
|
||||
publish(errorMessage = getString(Res.string.trakt_missing_credentials))
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +80,7 @@ object TraktAuthRepository {
|
|||
)
|
||||
persist()
|
||||
publish(
|
||||
statusMessage = localizedString(Res.string.trakt_complete_sign_in_browser),
|
||||
statusMessage = getString(Res.string.trakt_complete_sign_in_browser),
|
||||
errorMessage = null,
|
||||
)
|
||||
|
||||
|
|
@ -187,7 +185,7 @@ object TraktAuthRepository {
|
|||
persist()
|
||||
publish(
|
||||
isLoading = false,
|
||||
errorMessage = localizedString(Res.string.trakt_invalid_callback),
|
||||
errorMessage = getString(Res.string.trakt_invalid_callback),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
|
@ -195,7 +193,7 @@ object TraktAuthRepository {
|
|||
val errorCode = parsedUrl.parameters["error"]
|
||||
if (!errorCode.isNullOrBlank()) {
|
||||
val errorDescription = parsedUrl.parameters["error_description"]
|
||||
?: localizedString(Res.string.trakt_authorization_denied)
|
||||
?: getString(Res.string.trakt_authorization_denied)
|
||||
clearPendingAuthorization()
|
||||
persist()
|
||||
publish(
|
||||
|
|
@ -211,7 +209,7 @@ object TraktAuthRepository {
|
|||
persist()
|
||||
publish(
|
||||
isLoading = false,
|
||||
errorMessage = localizedString(Res.string.trakt_missing_auth_code),
|
||||
errorMessage = getString(Res.string.trakt_missing_auth_code),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
|
@ -223,7 +221,7 @@ object TraktAuthRepository {
|
|||
persist()
|
||||
publish(
|
||||
isLoading = false,
|
||||
errorMessage = localizedString(Res.string.trakt_invalid_callback_state),
|
||||
errorMessage = getString(Res.string.trakt_invalid_callback_state),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
|
@ -255,7 +253,7 @@ object TraktAuthRepository {
|
|||
if (response == null) {
|
||||
clearPendingAuthorization()
|
||||
persist()
|
||||
publish(isLoading = false, errorMessage = localizedString(Res.string.trakt_sign_in_complete_failed))
|
||||
publish(isLoading = false, errorMessage = getString(Res.string.trakt_sign_in_complete_failed))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -266,7 +264,7 @@ object TraktAuthRepository {
|
|||
if (parsed == null) {
|
||||
clearPendingAuthorization()
|
||||
persist()
|
||||
publish(isLoading = false, errorMessage = localizedString(Res.string.trakt_invalid_token_response))
|
||||
publish(isLoading = false, errorMessage = getString(Res.string.trakt_invalid_token_response))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -494,4 +492,3 @@ private data class TraktUserDto(
|
|||
private data class TraktUserIdsDto(
|
||||
val slug: String? = null,
|
||||
)
|
||||
private fun localizedString(resource: StringResource): String = runBlocking { getString(resource) }
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import co.touchlab.kermit.Logger
|
|||
import com.nuvio.app.features.addons.httpGetTextWithHeaders
|
||||
import com.nuvio.app.features.addons.httpRequestRaw
|
||||
import com.nuvio.app.features.details.MetaDetails
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.Json
|
||||
|
|
@ -100,7 +99,8 @@ object TraktCommentsRepository {
|
|||
val itemCount = response.headers["X-Pagination-Item-Count"]?.toIntOrNull()
|
||||
?: response.headers["x-pagination-item-count"]?.toIntOrNull()
|
||||
?: dtos.size
|
||||
val selected = filterDisplayableComments(dtos).map(::toReviewModel)
|
||||
val userFallback = getString(Res.string.trakt_user_fallback)
|
||||
val selected = filterDisplayableComments(dtos).map { dto -> toReviewModel(dto, userFallback) }
|
||||
|
||||
cacheMutex.withLock {
|
||||
val cached = cache[cacheKey]
|
||||
|
|
@ -222,11 +222,11 @@ private fun stripInlineSpoilerMarkup(comment: String?): String {
|
|||
.trim()
|
||||
}
|
||||
|
||||
private fun toReviewModel(dto: TraktCommentDto): TraktCommentReview {
|
||||
private fun toReviewModel(dto: TraktCommentDto, userFallback: String): TraktCommentReview {
|
||||
val authorDisplayName = dto.user?.name
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: dto.user?.username?.takeIf { it.isNotBlank() }
|
||||
?: runBlocking { getString(Res.string.trakt_user_fallback) }
|
||||
?: userFallback
|
||||
|
||||
return TraktCommentReview(
|
||||
id = dto.id,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.nuvio.app.features.details
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
|
@ -9,12 +10,12 @@ class MetaDetailsParserTest {
|
|||
@Test
|
||||
fun `parse rejects null meta object without json object cast crash`() {
|
||||
assertFailsWith<IllegalStateException> {
|
||||
MetaDetailsParser.parse("""{"meta":null}""")
|
||||
runBlocking { MetaDetailsParser.parse("""{"meta":null}""") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse accepts bare meta object response`() {
|
||||
fun `parse accepts bare meta object response`() = runBlocking {
|
||||
val result = MetaDetailsParser.parse(
|
||||
"""
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue