diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index b6917cd2..92ee5269 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -425,6 +425,10 @@
Nuvio uses IMDb Non-Commercial Datasets, including title.ratings.tsv.gz, for IMDb ratings and vote counts. Information courtesy of IMDb (https://www.imdb.com). Used with permission. IMDb data is for personal and non-commercial use under IMDb's terms.
Trakt
Nuvio connects to Trakt for account authentication, watched history, progress sync, library data, ratings, lists, and comments. Nuvio is not affiliated with or endorsed by Trakt.
+ Premiumize
+ Nuvio connects to Premiumize for account authentication, cloud library access, cache checks, and cloud playback features. Nuvio is not affiliated with or endorsed by Premiumize.
+ TorBox
+ Nuvio connects to TorBox for account authentication, cloud library access, cache checks, and cloud playback features. Nuvio is not affiliated with or endorsed by TorBox.
MDBList
Nuvio uses MDBList for ratings and external score provider data. Nuvio is not affiliated with or endorsed by MDBList.
IntroDB
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt
index 804a5e22..f70408b1 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt
@@ -104,6 +104,27 @@ private fun ContinueWatchingItem.continueWatchingArtworkUrl(
)
}
+private fun ContinueWatchingItem.continueWatchingPosterArtworkUrl(
+ useEpisodeThumbnails: Boolean,
+): String? {
+ if (seasonNumber == null || episodeNumber == null) {
+ return continueWatchingArtworkUrl(useEpisodeThumbnails)
+ }
+
+ val normalizedEpisodeThumbnail = episodeThumbnail?.trim()?.takeIf { it.isNotBlank() }
+ val nonEpisodeImageUrl = imageUrl
+ ?.trim()
+ ?.takeIf { it.isNotBlank() && it != normalizedEpisodeThumbnail }
+
+ return firstNonBlank(
+ poster,
+ background,
+ nonEpisodeImageUrl,
+ if (useEpisodeThumbnails) episodeThumbnail else null,
+ imageUrl,
+ )
+}
+
private fun firstNonBlank(vararg values: String?): String? =
values.firstOrNull { value -> !value.isNullOrBlank() }?.trim()
@@ -508,8 +529,11 @@ private fun ContinueWatchingPosterCard(
)
.posterCardClickable(onClick = onClick, onLongClick = onLongClick),
) {
- val shouldBlurArtwork = blurNextUp && useEpisodeThumbnails && item.isNextUp
- val imageUrl = item.continueWatchingArtworkUrl(useEpisodeThumbnails)
+ val imageUrl = item.continueWatchingPosterArtworkUrl(useEpisodeThumbnails)
+ val shouldBlurArtwork = blurNextUp &&
+ useEpisodeThumbnails &&
+ item.isNextUp &&
+ imageUrl == firstNonBlank(item.episodeThumbnail)
if (imageUrl != null) {
AsyncImage(
model = cloudLibraryDisplayArtworkUrl(imageUrl),
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/DebridSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/DebridSettingsPage.kt
index 3ea86ce7..80eb22b1 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/DebridSettingsPage.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/DebridSettingsPage.kt
@@ -131,7 +131,6 @@ internal fun LazyListScope.debridSettingsContent(
) {
item {
var showResolverProviderDialog by rememberSaveable { mutableStateOf(false) }
- val uriHandler = LocalUriHandler.current
val resolverProviders = settings.resolverServices.map { it.provider }
val activeResolverProvider = settings.activeResolverCredential?.provider
SettingsSection(
@@ -144,15 +143,6 @@ internal fun LazyListScope.debridSettingsContent(
text = stringResource(Res.string.settings_debrid_experimental_notice),
)
SettingsGroupDivider(isTablet = isTablet)
- DebridPreferenceRow(
- isTablet = isTablet,
- title = "Learn more",
- description = "Cloud Library, connected accounts, and playable-link preparation.",
- value = "Open",
- enabled = true,
- onClick = { runCatching { uriHandler.openUri(CLOUD_SERVICES_FAQ_URL) } },
- )
- SettingsGroupDivider(isTablet = isTablet)
SettingsSwitchRow(
title = stringResource(Res.string.settings_debrid_cloud_library),
description = stringResource(Res.string.settings_debrid_cloud_library_description),
@@ -272,7 +262,10 @@ internal fun LazyListScope.debridSettingsContent(
}
}
- if (!settings.canResolvePlayableLinks) return
+ if (!settings.canResolvePlayableLinks) {
+ debridLearnMoreFooterItem(isTablet)
+ return
+ }
item {
var showPrepareCountDialog by rememberSaveable { mutableStateOf(false) }
@@ -461,6 +454,35 @@ internal fun LazyListScope.debridSettingsContent(
null -> Unit
}
}
+
+ debridLearnMoreFooterItem(isTablet)
+}
+
+private fun LazyListScope.debridLearnMoreFooterItem(isTablet: Boolean) {
+ item {
+ val uriHandler = LocalUriHandler.current
+ DebridLearnMoreFooter(
+ isTablet = isTablet,
+ onClick = { runCatching { uriHandler.openUri(CLOUD_SERVICES_FAQ_URL) } },
+ )
+ }
+}
+
+@Composable
+private fun DebridLearnMoreFooter(
+ isTablet: Boolean,
+ onClick: () -> Unit,
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = if (isTablet) 4.dp else 0.dp, bottom = if (isTablet) 10.dp else 6.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ TextButton(onClick = onClick) {
+ Text("Learn more")
+ }
+ }
}
private enum class DebridTemplateField {
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/LicensesAttributionsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/LicensesAttributionsPage.kt
index 7efecdf5..d86eb0a6 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/LicensesAttributionsPage.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/LicensesAttributionsPage.kt
@@ -28,8 +28,12 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import coil3.compose.AsyncImage
import com.nuvio.app.core.ui.NuvioScreen
import com.nuvio.app.core.ui.NuvioScreenHeader
+import com.nuvio.app.features.cloud.PremiumizeCloudLibraryPosterUrl
+import com.nuvio.app.features.cloud.TorboxCloudLibraryPosterUrl
+import com.nuvio.app.features.cloud.cloudLibraryDisplayArtworkUrl
import com.nuvio.app.isIos
import nuvio.composeapp.generated.resources.*
import org.jetbrains.compose.resources.StringResource
@@ -38,6 +42,8 @@ import org.jetbrains.compose.resources.stringResource
private const val TmdbUrl = "https://www.themoviedb.org"
private const val ImdbDatasetsUrl = "https://developer.imdb.com/non-commercial-datasets/"
private const val TraktUrl = "https://trakt.tv"
+private const val PremiumizeUrl = "https://www.premiumize.me"
+private const val TorboxUrl = "https://torbox.app"
private const val MdbListUrl = "https://mdblist.com"
private const val IntroDbUrl = "https://introdb.app/"
private const val NuvioRepositoryUrl = "https://github.com/NuvioMedia/NuvioMobile"
@@ -48,6 +54,7 @@ private data class AttributionItem(
val titleRes: StringResource,
val bodyRes: StringResource,
val logo: IntegrationLogo?,
+ val logoUrl: String? = null,
val link: String,
)
@@ -165,14 +172,26 @@ private fun AttributionRow(
body = stringResource(item.bodyRes),
link = item.link,
isTablet = isTablet,
- leading = item.logo?.let { logo ->
- {
- IntegrationLogoImage(
- painter = integrationLogoPainter(logo),
- contentDescription = title,
- isTablet = isTablet,
- )
+ leading = when {
+ item.logo != null -> item.logo.let { logo ->
+ {
+ IntegrationLogoImage(
+ painter = integrationLogoPainter(logo),
+ contentDescription = title,
+ isTablet = isTablet,
+ )
+ }
}
+ item.logoUrl != null -> item.logoUrl.let { logoUrl ->
+ {
+ ProviderLogoImage(
+ url = logoUrl,
+ contentDescription = title,
+ isTablet = isTablet,
+ )
+ }
+ }
+ else -> null
},
onOpen = { uriHandler.openUri(item.link) },
)
@@ -274,6 +293,22 @@ private fun IntegrationLogoImage(
)
}
+@Composable
+private fun ProviderLogoImage(
+ url: String,
+ contentDescription: String,
+ isTablet: Boolean,
+) {
+ AsyncImage(
+ model = url,
+ contentDescription = contentDescription,
+ modifier = Modifier
+ .padding(top = 2.dp)
+ .size(if (isTablet) 46.dp else 40.dp),
+ contentScale = ContentScale.Fit,
+ )
+}
+
@Composable
private fun PlainStackDivider() {
HorizontalDivider(
@@ -295,6 +330,20 @@ private fun attributionItems(): List = listOf(
logo = IntegrationLogo.Trakt,
link = TraktUrl,
),
+ AttributionItem(
+ titleRes = Res.string.settings_licenses_attributions_premiumize_title,
+ bodyRes = Res.string.settings_licenses_attributions_premiumize_body,
+ logo = null,
+ logoUrl = PremiumizeCloudLibraryPosterUrl,
+ link = PremiumizeUrl,
+ ),
+ AttributionItem(
+ titleRes = Res.string.settings_licenses_attributions_torbox_title,
+ bodyRes = Res.string.settings_licenses_attributions_torbox_body,
+ logo = null,
+ logoUrl = cloudLibraryDisplayArtworkUrl(TorboxCloudLibraryPosterUrl),
+ link = TorboxUrl,
+ ),
AttributionItem(
titleRes = Res.string.settings_licenses_attributions_mdblist_title,
bodyRes = Res.string.settings_licenses_attributions_mdblist_body,
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
index 978ea2e2..03187cba 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
@@ -262,6 +262,8 @@ internal fun settingsSearchEntries(
PlaybackSearchRow("nuvio-license", stringResource(Res.string.settings_licenses_attributions_nuvio_title), stringResource(Res.string.settings_licenses_attributions_nuvio_license)),
PlaybackSearchRow("tmdb-attribution", stringResource(Res.string.settings_licenses_attributions_tmdb_title), stringResource(Res.string.settings_licenses_attributions_tmdb_body)),
PlaybackSearchRow("trakt-attribution", stringResource(Res.string.settings_licenses_attributions_trakt_title), stringResource(Res.string.settings_licenses_attributions_trakt_body)),
+ PlaybackSearchRow("premiumize-attribution", stringResource(Res.string.settings_licenses_attributions_premiumize_title), stringResource(Res.string.settings_licenses_attributions_premiumize_body)),
+ PlaybackSearchRow("torbox-attribution", stringResource(Res.string.settings_licenses_attributions_torbox_title), stringResource(Res.string.settings_licenses_attributions_torbox_body)),
PlaybackSearchRow("mdblist-attribution", stringResource(Res.string.settings_licenses_attributions_mdblist_title), stringResource(Res.string.settings_licenses_attributions_mdblist_body)),
PlaybackSearchRow("introdb-attribution", stringResource(Res.string.settings_licenses_attributions_introdb_title), stringResource(Res.string.settings_licenses_attributions_introdb_body)),
PlaybackSearchRow("imdb-datasets", stringResource(Res.string.settings_licenses_attributions_imdb_title), stringResource(Res.string.settings_licenses_attributions_imdb_body)),