diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts
new file mode 100644
index 000000000..1e658cae4
--- /dev/null
+++ b/androidApp/build.gradle.kts
@@ -0,0 +1,118 @@
+import java.util.Properties
+
+fun readXcconfigValue(file: File, key: String): String? {
+ if (!file.exists()) return null
+ return file.readLines()
+ .asSequence()
+ .map(String::trim)
+ .filter { it.isNotEmpty() && !it.startsWith("#") && it.contains('=') }
+ .map { line ->
+ val separatorIndex = line.indexOf('=')
+ line.substring(0, separatorIndex).trim() to line.substring(separatorIndex + 1).trim()
+ }
+ .firstOrNull { (entryKey, _) -> entryKey == key }
+ ?.second
+}
+
+plugins {
+ alias(libs.plugins.androidApplication)
+}
+
+val localProps = Properties().apply {
+ val propsFile = rootProject.file("local.properties")
+ if (propsFile.exists()) propsFile.inputStream().use { load(it) }
+}
+val releaseStoreFile = localProps.getProperty("NUVIO_RELEASE_STORE_FILE")?.takeIf { it.isNotBlank() }
+val releaseStorePassword = localProps.getProperty("NUVIO_RELEASE_STORE_PASSWORD")?.takeIf { it.isNotBlank() }
+val releaseKeyAlias = localProps.getProperty("NUVIO_RELEASE_KEY_ALIAS")?.takeIf { it.isNotBlank() }
+val releaseKeyPassword = localProps.getProperty("NUVIO_RELEASE_KEY_PASSWORD")?.takeIf { it.isNotBlank() }
+val releaseKeystore = releaseStoreFile?.let(rootProject::file)
+val appVersionConfigFile = rootProject.file("iosApp/Configuration/Version.xcconfig")
+val releaseAppVersionName = readXcconfigValue(appVersionConfigFile, "MARKETING_VERSION")
+ ?: error("MARKETING_VERSION is missing from ${appVersionConfigFile.path}")
+val releaseAppVersionCode = readXcconfigValue(appVersionConfigFile, "CURRENT_PROJECT_VERSION")
+ ?.toIntOrNull()
+ ?: error("CURRENT_PROJECT_VERSION is missing or invalid in ${appVersionConfigFile.path}")
+
+android {
+ namespace = "com.nuvio.android"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+ compileSdkMinor = libs.versions.android.compileSdkMinor.get().toInt()
+
+ signingConfigs {
+ create("release") {
+ if (releaseKeystore != null && releaseStorePassword != null && releaseKeyAlias != null && releaseKeyPassword != null) {
+ storeFile = releaseKeystore
+ storePassword = releaseStorePassword
+ keyAlias = releaseKeyAlias
+ keyPassword = releaseKeyPassword
+ }
+ }
+ }
+
+ defaultConfig {
+ applicationId = "com.nuvio.app"
+ minSdk = libs.versions.android.minSdk.get().toInt()
+ targetSdk = libs.versions.android.targetSdk.get().toInt()
+ versionCode = releaseAppVersionCode
+ versionName = releaseAppVersionName
+ }
+
+ flavorDimensions += "distribution"
+ productFlavors {
+ create("full") {
+ dimension = "distribution"
+ }
+ create("playstore") {
+ dimension = "distribution"
+ }
+ }
+
+ sourceSets.getByName("full") {
+ manifest.srcFile("src/full/AndroidManifest.xml")
+ jniLibs.directories.add("../composeApp/src/full/jniLibs")
+ }
+
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ jniLibs {
+ useLegacyPackaging = true
+ pickFirsts += listOf(
+ "lib/*/libc++_shared.so",
+ "lib/*/libavcodec.so",
+ "lib/*/libavutil.so",
+ "lib/*/libswscale.so",
+ "lib/*/libswresample.so"
+ )
+ }
+ }
+
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "../composeApp/proguard-rules.pro",
+ )
+ signingConfig = signingConfigs.getByName("release")
+ ndk {
+ debugSymbolLevel = "FULL"
+ }
+ }
+ }
+
+ compileOptions {
+ isCoreLibraryDesugaringEnabled = true
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+}
+
+dependencies {
+ implementation(project(":composeApp"))
+ coreLibraryDesugaring(libs.desugar.jdk.libs)
+ debugImplementation(libs.compose.uiTooling)
+}
diff --git a/androidApp/src/full/AndroidManifest.xml b/androidApp/src/full/AndroidManifest.xml
new file mode 100644
index 000000000..47497cd7b
--- /dev/null
+++ b/androidApp/src/full/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..0d702e768
--- /dev/null
+++ b/androidApp/src/main/AndroidManifest.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build.gradle.kts b/build.gradle.kts
index cf780d5a6..11682b292 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,7 +3,8 @@ plugins {
// in each subproject's classloader
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
+ alias(libs.plugins.androidKotlinMultiplatformLibrary) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
-}
\ No newline at end of file
+}
diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index 964b76961..d0ef2456f 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -1,4 +1,3 @@
-import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
@@ -176,7 +175,7 @@ fun readXcconfigValue(file: File, key: String): String? {
plugins {
alias(libs.plugins.kotlinMultiplatform)
- alias(libs.plugins.androidApplication)
+ alias(libs.plugins.androidKotlinMultiplatformLibrary)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.kotlinxSerialization)
@@ -186,11 +185,6 @@ val supabaseProps = Properties().apply {
val propsFile = rootProject.file("local.properties")
if (propsFile.exists()) propsFile.inputStream().use { load(it) }
}
-val releaseStoreFile = supabaseProps.getProperty("NUVIO_RELEASE_STORE_FILE")?.takeIf { it.isNotBlank() }
-val releaseStorePassword = supabaseProps.getProperty("NUVIO_RELEASE_STORE_PASSWORD")?.takeIf { it.isNotBlank() }
-val releaseKeyAlias = supabaseProps.getProperty("NUVIO_RELEASE_KEY_ALIAS")?.takeIf { it.isNotBlank() }
-val releaseKeyPassword = supabaseProps.getProperty("NUVIO_RELEASE_KEY_PASSWORD")?.takeIf { it.isNotBlank() }
-val releaseKeystore = releaseStoreFile?.let(rootProject::file)
val appVersionConfigFile = rootProject.file("iosApp/Configuration/Version.xcconfig")
val releaseAppVersionName = readXcconfigValue(appVersionConfigFile, "MARKETING_VERSION")
?: error("MARKETING_VERSION is missing from ${appVersionConfigFile.path}")
@@ -217,13 +211,38 @@ val generatedRuntimeConfigDir = layout.buildDirectory.dir("generated/runtime-con
val requestedGradleTasks = gradle.startParameter.taskNames.map { taskName ->
taskName.substringAfterLast(':').lowercase()
}
-val isAndroidAppBundleBuild = requestedGradleTasks.any { taskName ->
- taskName == "bundle" ||
- taskName == "bundlerelease" ||
- taskName == "bundledebug" ||
- taskName.startsWith("bundleplaystore") ||
- taskName.startsWith("bundlefull") ||
- taskName.endsWith("bundle")
+val requestedAndroidDistributions = requestedGradleTasks.mapNotNull { taskName ->
+ when {
+ "playstore" in taskName -> "playstore"
+ "full" in taskName -> "full"
+ else -> null
+ }
+}.toSet()
+require(requestedAndroidDistributions.size <= 1) {
+ "Build Android full and playstore distributions separately, or set -Pnuvio.android.distribution=full|playstore."
+}
+val configuredAndroidDistribution = providers.gradleProperty("nuvio.android.distribution").orNull
+ ?: supabaseProps.getProperty("NUVIO_ANDROID_DISTRIBUTION")
+val isAmbiguousAndroidPackageTask = requestedGradleTasks.any { taskName ->
+ taskName == "build" ||
+ taskName.startsWith("assemble") ||
+ taskName.startsWith("bundle")
+} && requestedAndroidDistributions.isEmpty()
+require(configuredAndroidDistribution != null || !isAmbiguousAndroidPackageTask) {
+ "Set -Pnuvio.android.distribution=full|playstore for aggregate Android assemble/bundle tasks."
+}
+val androidDistribution = (
+ configuredAndroidDistribution
+ ?: requestedAndroidDistributions.singleOrNull()
+ ?: "playstore"
+ ).trim().lowercase()
+require(androidDistribution == "playstore" || androidDistribution == "full") {
+ "nuvio.android.distribution must be 'playstore' or 'full'."
+}
+val androidDistributionSourceDir = if (androidDistribution == "full") {
+ "src/androidFull/kotlin"
+} else {
+ "src/androidPlaystore/kotlin"
}
val runtimeLocalProperties = Properties().apply {
val file = rootProject.file("local.properties")
@@ -254,7 +273,16 @@ tasks.withType>().configureEach {
}
kotlin {
- androidTarget {
+ android {
+ namespace = "com.nuvio.app"
+ compileSdk {
+ version = release(libs.versions.android.compileSdk.get().toInt()) {
+ minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt()
+ }
+ }
+ minSdk = libs.versions.android.minSdk.get().toInt()
+ androidResources.enable = true
+
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
@@ -298,35 +326,52 @@ kotlin {
val commonMain by getting {
kotlin.srcDir(generatedRuntimeConfigDir)
}
- androidMain.dependencies {
- implementation(libs.compose.uiToolingPreview)
- implementation(libs.androidx.appcompat)
- implementation(libs.androidx.activity.compose)
- implementation(libs.androidx.core.splashscreen)
- implementation(libs.androidx.work.runtime)
- implementation(libs.coil.gif)
- implementation("androidx.recyclerview:recyclerview:1.4.0")
- implementation("com.squareup.okhttp3:okhttp:4.12.0")
- implementation("com.google.code.gson:gson:2.11.0")
- implementation("io.github.peerless2012:ass-media:0.4.0-beta01")
- implementation(libs.ktor.client.android)
- implementation(libs.androidx.media3.exoplayer.hls)
- implementation(libs.androidx.media3.exoplayer.dash)
- implementation(libs.androidx.media3.exoplayer.smoothstreaming)
- implementation(libs.androidx.media3.exoplayer.rtsp)
- implementation(libs.androidx.media3.datasource)
- implementation(libs.androidx.media3.datasource.okhttp)
- implementation(libs.androidx.media3.decoder)
- implementation(libs.androidx.media3.session)
- implementation(libs.androidx.media3.common)
- implementation(libs.androidx.media3.container)
- implementation(libs.androidx.media3.extractor)
- implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("lib-*.aar"))))
+ androidMain {
+ kotlin.srcDir(project.file(androidDistributionSourceDir))
+ if (androidDistribution == "full") {
+ kotlin.srcDir(fullCommonSourceDir)
+ }
+
+ dependencies {
+ implementation(libs.compose.uiToolingPreview)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.core.splashscreen)
+ implementation(libs.androidx.work.runtime)
+ implementation(libs.coil.gif)
+ implementation("androidx.recyclerview:recyclerview:1.4.0")
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("com.google.code.gson:gson:2.11.0")
+ implementation("io.github.peerless2012:ass-media:0.4.0-beta01")
+ implementation(libs.ktor.client.android)
+ implementation(libs.androidx.media3.exoplayer.hls)
+ implementation(libs.androidx.media3.exoplayer.dash)
+ implementation(libs.androidx.media3.exoplayer.smoothstreaming)
+ implementation(libs.androidx.media3.exoplayer.rtsp)
+ implementation(libs.androidx.media3.datasource)
+ implementation(libs.androidx.media3.datasource.okhttp)
+ implementation(libs.androidx.media3.decoder)
+ implementation(libs.androidx.media3.session)
+ implementation(libs.androidx.media3.common)
+ implementation(libs.androidx.media3.container)
+ implementation(libs.androidx.media3.extractor)
+ implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("lib-*.aar"))))
+ if (androidDistribution == "full") {
+ implementation(files("libs/quickjs-kt-android-1.0.5-nuvio.aar"))
+ implementation(libs.ksoup)
+ }
+ }
}
commonMain.dependencies {
- implementation(libs.coil.compose)
- implementation(libs.coil.network.ktor3)
- implementation(libs.coil.svg)
+ implementation("io.coil-kt.coil3:coil-compose:${libs.versions.coil.get()}") {
+ exclude(group = "org.jetbrains.skiko", module = "skiko")
+ }
+ implementation("io.coil-kt.coil3:coil-network-ktor3:${libs.versions.coil.get()}") {
+ exclude(group = "org.jetbrains.skiko", module = "skiko")
+ }
+ implementation("io.coil-kt.coil3:coil-svg:${libs.versions.coil.get()}") {
+ exclude(group = "org.jetbrains.skiko", module = "skiko")
+ }
implementation("dev.chrisbanes.haze:haze:1.7.2")
implementation(libs.compose.runtime)
implementation(libs.compose.foundation)
@@ -351,94 +396,11 @@ kotlin {
}
}
-afterEvaluate {
- dependencies {
- add("fullImplementation", files("libs/quickjs-kt-android-1.0.5-nuvio.aar"))
- add("fullImplementation", libs.ksoup)
- }
-}
-
configurations.matching { it.name == "iosMainImplementation" }.configureEach {
project.dependencies.add(name, libs.ktor.client.darwin)
}
-dependencies {
- coreLibraryDesugaring(libs.desugar.jdk.libs)
- debugImplementation(libs.compose.uiTooling)
-}
-
configurations.all {
exclude(group = "androidx.media3", module = "media3-exoplayer")
exclude(group = "androidx.media3", module = "media3-ui")
}
-
-android {
- namespace = "com.nuvio.app"
- compileSdk = libs.versions.android.compileSdk.get().toInt()
-
- signingConfigs {
- create("release") {
- if (releaseKeystore != null && releaseStorePassword != null && releaseKeyAlias != null && releaseKeyPassword != null) {
- storeFile = releaseKeystore
- storePassword = releaseStorePassword
- keyAlias = releaseKeyAlias
- keyPassword = releaseKeyPassword
- }
- }
- }
-
- defaultConfig {
- applicationId = "com.nuvio.app"
- minSdk = libs.versions.android.minSdk.get().toInt()
- targetSdk = libs.versions.android.targetSdk.get().toInt()
- versionCode = releaseAppVersionCode
- versionName = releaseAppVersionName
- }
- flavorDimensions += "distribution"
- productFlavors {
- create("full") {
- dimension = "distribution"
- }
- create("playstore") {
- dimension = "distribution"
- }
- }
- sourceSets.getByName("full") {
- manifest.srcFile("src/androidFull/AndroidManifest.xml")
- java.srcDir(fullCommonSourceDir)
- }
- packaging {
- resources {
- excludes += "/META-INF/{AL2.0,LGPL2.1}"
- }
- jniLibs {
- useLegacyPackaging = true
- pickFirsts += listOf(
- "lib/*/libc++_shared.so",
- "lib/*/libavcodec.so",
- "lib/*/libavutil.so",
- "lib/*/libswscale.so",
- "lib/*/libswresample.so"
- )
- }
- }
- buildTypes {
- getByName("release") {
- isMinifyEnabled = true
- isShrinkResources = true
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro",
- )
- signingConfig = signingConfigs.getByName("release")
- ndk {
- debugSymbolLevel = "FULL"
- }
- }
- }
- compileOptions {
- isCoreLibraryDesugaringEnabled = true
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
-}
diff --git a/composeApp/proguard-rules.pro b/composeApp/proguard-rules.pro
index 34c5cfd6b..7bd3949ea 100644
--- a/composeApp/proguard-rules.pro
+++ b/composeApp/proguard-rules.pro
@@ -42,6 +42,8 @@
# TorrServer based P2P streaming.
-keep class com.nuvio.app.features.p2p.** { *; }
+-keep class androidx.work.impl.WorkDatabase_Impl { *; }
+
# Media3 / ExoPlayer classes from local AAR decoders and stock modules.
-dontwarn androidx.media3.**
-keep class androidx.media3.** { *; }
diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml
index 855965355..8072ee00d 100644
--- a/composeApp/src/androidMain/AndroidManifest.xml
+++ b/composeApp/src/androidMain/AndroidManifest.xml
@@ -1,57 +1,2 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt
index fcd2af127..f3364343d 100644
--- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt
+++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt
@@ -215,7 +215,7 @@ actual fun PlatformPlayerSurface(
val loadControl = DefaultLoadControl.Builder()
.setTargetBufferBytes(100 * 1024 * 1024)
.setBufferDurationsMs(
- DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
+ 15_000,
70_000,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
5_000
diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.android.kt
index 6a2e1584d..07954478b 100644
--- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.android.kt
+++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.android.kt
@@ -29,7 +29,7 @@ actual object ThemeSettingsStorage {
fun initialize(context: Context) {
preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
- applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.ENGLISH.code)
+ applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.DEVICE.code)
}
actual fun loadSelectedTheme(): String? =
@@ -84,9 +84,13 @@ actual object ThemeSettingsStorage {
}
actual fun applySelectedAppLanguage(languageCode: String) {
- AppCompatDelegate.setApplicationLocales(
- LocaleListCompat.forLanguageTags(languageCode),
- )
+ if (languageCode.equals("device", ignoreCase = true)) {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+ } else {
+ AppCompatDelegate.setApplicationLocales(
+ LocaleListCompat.forLanguageTags(languageCode),
+ )
+ }
}
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
@@ -103,6 +107,6 @@ actual object ThemeSettingsStorage {
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
payload.decodeSyncBoolean(liquidGlassNativeTabBarEnabledKey)?.let(::saveLiquidGlassNativeTabBarEnabled)
- applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.ENGLISH.code)
+ applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.DEVICE.code)
}
}
diff --git a/composeApp/src/commonMain/composeResources/values-pl/strings.xml b/composeApp/src/commonMain/composeResources/values-pl/strings.xml
index f179fcf56..e8091e03d 100644
--- a/composeApp/src/commonMain/composeResources/values-pl/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-pl/strings.xml
@@ -1459,4 +1459,415 @@
Logo dodatku
Pokazuj logo i nazwę dodatku obok źródeł.
Wyświetlanie
+ Język oryginalny
+ Wymaga włączonego wzbogacania TMDB
+ Język urządzenia
+ W manifeście brakuje \"%1$s\"
+ Biblioteka w chmurze jest wyłączona.
+ Biblioteka w chmurze nie jest dostępna dla %1$s.
+ Filmy
+ Seriale
+ Rozpoznana lista Trakt
+ Kolekcja TMDB %1$s
+ Australia
+ Kanada
+ Niemcy
+ US, KR, JP, IN
+ 2020-01-01
+ 2024-12-31
+ Reżyser TMDB %1$s
+ TMDB Odkrywaj
+ TMDB Odkrywaj
+ 28,12
+ 18,35
+ Wprowadź prawidłowy ID lub URL TMDB.
+ Wprowadź prawidłowy ID lub URL TMDB.
+ en, ko, ja, hi
+ Lista TMDB %1$s
+ Nie udało się załadować źródła TMDB
+ Sieć TMDB %1$s
+ Osoba TMDB %1$s
+ Produkcja TMDB %1$s
+ 10
+ 7.0
+ Nie udało się załadować źródła TMDB
+ 100
+ 2024
+ Wprowadź ID lub URL listy Trakt
+ Wprowadź nazwę, URL lub ID listy Trakt
+ Lista Trakt %1$d
+ Wprowadź ID lub URL listy Trakt
+ Wprowadź nazwę, URL lub ID listy Trakt
+ ID %1$s
+ Lista Trakt %1$s
+ Nie udało się załadować listy Trakt
+ Nie udało się załadować listy Trakt
+ Nie znaleziono list Trakt
+ Rozpoznana lista Trakt
+ US, KR, JP, IN
+ Lista filmów Trakt
+ Lista seriali Trakt
+ Dodaj klucz API TMDB w Ustawieniach, aby korzystać ze źródeł TMDB.
+ Nie znaleziono kolekcji TMDB
+ Nie znaleziono firmy TMDB
+ TMDB Odkrywaj nie zwrócił danych
+ Nie znaleziono listy TMDB
+ Brak ID kolekcji TMDB
+ Brak ID listy TMDB
+ Brak ID osoby TMDB
+ Nie znaleziono sieci TMDB
+ Nie znaleziono filmografii osoby TMDB
+ Nie znaleziono osoby TMDB
+ Brak danych uwierzytelniających Trakt.
+ %1$s (%2$d)
+ Wprowadź prawidłowy ID lub URL listy Trakt
+ %1$d elementów
+ %1$d polubień
+ Lista Trakt nie znaleziona lub niepubliczna
+ Brak ID listy Trakt
+ Lista Trakt nie zawiera numerycznego ID
+ Publiczna lista Trakt
+ Osiągnięto limit zapytań Trakt
+ Zapytanie Trakt nie powiodło się
+ Nie znaleziono linii napisów
+ Nie udało się załadować linii napisów
+ Zaawansowane
+ Źródła
+ Uruchamianie i zachowanie profilu.
+ ZAAWANSOWANE
+ Wyświetlanie wyników źródeł i reguły badge URL.
+ Napędzane przez TMDB
+ Napędzane przez Trakt
+ Więcej akcji
+ Nie udało się załadować komentarzy Trakt (%1$d)
+ %1$dh %2$dm
+ %1$dh
+ %1$dm
+ Nie udało się sfinalizować pobieranego pliku
+ Nie udało się sfinalizować pobieranego pliku
+ Nie udało się otworzyć częściowego pliku
+ Nie udało się otworzyć częściowego pliku
+ Częściowy plik nie jest otwarty
+ Częściowy plik nie jest otwarty
+ Nie udało się zapisać częściowego pliku
+ Nie udało się zapisać częściowego pliku
+ Systemowy odtwarzacz Android
+ Pozostało %1$dh %2$dm
+ Pozostało %1$dm
+ Biblioteka Nuvio
+ Problem z połączeniem
+ Pusta treść odpowiedzi
+ Pusta treść odpowiedzi
+ Zapytanie nie powiodło się z kodem HTTP %1$d
+ Sprawdź połączenie i spróbuj ponownie.
+ Zapytanie nie powiodło się z kodem HTTP %1$d
+ Ten strumień wykorzystuje technologię peer-to-peer (P2P). Włączając P2P, przyjmujesz do wiadomości i zgadzasz się, że:\n\n• Twój adres IP będzie widoczny dla innych użytkowników w sieci\n• Ponosisz wyłączną odpowiedzialność za treści, do których uzyskujesz dostęp\n• Potwierdzasz, że masz prawo do strumieniowania tych treści w swojej jurysdykcji\n• Nuvio nie hostuje, nie dystrybuuje ani nie kontroluje żadnych treści P2P\n• Nuvio nie ponosi odpowiedzialności za jakiekolwiek konsekwencje prawne wynikające z korzystania ze strumieniowania P2P\n\nKorzystasz z tej funkcji wyłącznie na własne ryzyko. P2P można wyłączyć w dowolnym momencie w Ustawieniach.
+ Anuluj
+ Włącz P2P
+ Strumieniowanie P2P
+ Nieznany błąd torrenta
+ Zgłoś intro
+ Ustawienia wideo
+ %1$s (%2$s)
+ Silnik odtwarzacza MPV niedostępny. Przebuduj aplikację.
+ Nie udało się uruchomić torrenta: %1$s
+ Silnik odtwarzacza MPV niedostępny. Przebuduj aplikację.
+ Błąd torrenta: %1$s
+ Nie można odtworzyć tego strumienia.
+ Wyłączony
+ Najbliżej starszego zachowania MPV na iOS.
+ Kompatybilność
+ Użyj własnych zaawansowanych wartości poniżej.
+ Własne
+ Najlepsze dla iPhone\'ów i iPadów z obsługą HDR.
+ Natywne EDR
+ Bardziej przewidywalne biele i czernie na wyjściu SDR.
+ Mapowanie tonalne SDR
+ %1$s buforowane · %2$s · %3$s
+ Łączenie z peerami…
+ %1$d seedów · %2$d peerów
+ Uruchamianie silnika P2P…
+ %1$d peerów · %2$d seedów · %3$d%%
+ %1$s · %2$s
+ Jasność
+ Kontrast
+ Deband
+ Zmniejsz pasmowanie kolorów kosztem wydajności.
+ Gamma
+ Wykrywanie szczytów HDR
+ Szacuj szczytową jasność HDR gdy metadane są błędne lub brakujące.
+ Interpolacja klatek
+ Płynny ruch gdy mpv może czysto zsynchronizować z wyświetlaczem.
+ Preset wyjściowy
+ Resetuj strojenie
+ Nasycenie
+ Wideo
+ Mapowanie tonalne
+ Wtyczki wyłączone
+ Wtyczki włączone
+ %1$d dostawców
+ Odświeżanie
+ %1$d repozytoriów
+ Brak klucza API TMDB
+ Klucz API TMDB ustawiony
+ Instalowanie…
+ Zainstaluj repozytorium wtyczek
+ Testowanie…
+ Testuj dostawcę
+ Usuń repozytorium wtyczek
+ Odśwież repozytorium wtyczek
+ Brak dostępnych dostawców.
+ Dodaj URL repozytorium, aby zainstalować wtyczki do odkrywania źródeł.
+ Nie zainstalowano jeszcze repozytoriów wtyczek.
+ Używaj dostawców wtyczek podczas wyszukiwania źródeł.
+ Włącz dostawców wtyczek globalnie
+ To repozytorium wtyczek jest już zainstalowane.
+ Wprowadź URL repozytorium wtyczek.
+ Wprowadź prawidłowy URL wtyczki.
+ Nie udało się zainstalować repozytorium wtyczek
+ Nie znaleziono dostawcy
+ Nie udało się odświeżyć repozytorium
+ Wtyczki nie są dostępne w tej wersji.
+ W Źródłach pokazuj jednego dostawcę na repozytorium zamiast na źródło.
+ Grupuj dostawców wtyczek wg repozytorium
+ URL manifestu wtyczki
+ Brak nazwy w manifeście.
+ Manifest nie ma dostawców.
+ Brak wersji w manifeście.
+ Brak nazwy w manifeście.
+ Manifest nie ma dostawców.
+ Brak wersji w manifeście.
+ Zainstalowano %1$s.
+ Wyłączony przez repozytorium
+ Brak opisu
+ v%1$s
+ Repozytorium wtyczek
+ To repozytorium wtyczek jest już zainstalowane.
+ Nie udało się zainstalować repozytorium wtyczek
+ Nie udało się odświeżyć repozytorium
+ Wersja %1$s
+ DODAJ REPOZYTORIUM
+ ZAINSTALOWANE REPOZYTORIA
+ PRZEGLĄD
+ DOSTAWCY
+ Błąd
+ Test dostawcy nie powiódł się
+ Wyniki testu (%1$d)
+ Dostawcy wtyczek wymagają klucza API TMDB. Ustaw go na ekranie TMDB, w przeciwnym razie dostawcy nie będą działać poprawnie.
+ Brak wyników wyszukiwania dla %1$s.
+ Backend synchronizacji
+ Wyczyść pamięć Kontynuuj oglądanie
+ Pamięć wyczyszczona
+ Usuń dane Kontynuuj oglądanie i odśwież postęp
+ Zapamiętaj ostatni profil
+ Zapamiętaj ostatnio wybrany profil przy uruchomieniu
+ PAMIĘĆ PODRĘCZNA
+ URUCHAMIANIE
+ Karta
+ Karta pozioma w stylu TV
+ Dowiedz się więcej
+ Maks. wyników
+ Ogranicz liczbę wyświetlanych wyników.
+ Limit na jakość
+ Ogranicz powtarzające się wyniki BluRay, WEB-DL, REMUX po sortowaniu.
+ Limit na rozdzielczość
+ Ogranicz powtarzające się wyniki 2160p, 1080p, 720p po sortowaniu.
+ Wprowadź jedną grupę na linię.
+ Wszystkie wyniki
+ %1$d wyników
+ Wykluczone tagi audio
+ Ukryj wybrane tagi audio.
+ Wykluczone kanały
+ Ukryj wybrane układy kanałów.
+ Wykluczone kodowania
+ Ukryj wybrane kodowania.
+ Wykluczone języki
+ Ukryj wyniki gdzie każdy język jest wykluczony.
+ Wykluczone jakości
+ Ukryj wybrane jakości.
+ Wykluczone grupy release
+ Ukryj wybrane grupy release.
+ Wykluczone rozdzielczości
+ Ukryj wybrane rozdzielczości.
+ Wykluczone tagi wizualne
+ Ukryj DV, HDR, 10bit, 3D i podobne tagi.
+ Preferowane tagi audio
+ Sortuj Atmos, TrueHD, DTS, AAC i podobne tagi.
+ Preferowane kanały
+ Sortuj preferowane układy kanałów jako pierwsze.
+ Preferowane kodowania
+ Sortuj AV1, HEVC, AVC i podobne kodowania.
+ Preferowane języki
+ Sortuj preferowane języki audio jako pierwsze.
+ Preferowane jakości
+ Sortuj wybrane jakości jako pierwsze, w domyślnej kolejności.
+ Preferowane rozdzielczości
+ Sortuj wybrane rozdzielczości jako pierwsze, w domyślnej kolejności.
+ Preferowane tagi wizualne
+ Sortuj DV, HDR, 10bit, IMAX i podobne tagi.
+ Wymagane tagi audio
+ Wymagaj Atmos, TrueHD, DTS, AAC i podobnych tagów.
+ Wymagane kanały
+ Pokazuj tylko wybrane układy kanałów.
+ Wymagane kodowania
+ Wymagaj AV1, HEVC, AVC i podobnych kodowań.
+ Wymagane języki
+ Pokazuj tylko wyniki z wybranymi językami.
+ Wymagane jakości
+ Pokazuj tylko wybrane jakości.
+ Wymagane grupy release
+ Pokazuj tylko wybrane grupy release.
+ Wymagane rozdzielczości
+ Pokazuj tylko wybrane rozdzielczości.
+ Wymagane tagi wizualne
+ Wymagaj DV, HDR, 10bit, IMAX, SDR i podobnych tagów.
+ Zarządzanie wynikami
+ Dowolne
+ %1$d wybranych
+ %1$dGB+
+ Zakres rozmiaru
+ Filtruj wyniki wg rozmiaru pliku.
+ %1$d-%2$dGB
+ Do %1$dGB
+ Najlepszy dźwięk najpierw
+ Najlepsza jakość najpierw
+ Język najpierw
+ Największe najpierw
+ Oryginalna kolejność
+ Sortuj wyniki
+ Wybierz kolejność wyników.
+ Najmniejsze najpierw
+ Format domyślny
+ Format oryginalny
+ Grupa %1$d
+ Inne badge Fusion
+ Podgląd
+ %1$d badge Fusion z tego URL
+ Brak badge Fusion w tym URL.
+ Podgląd badge Fusion
+ Brak zaimportowanych URL badge Fusion.
+ %1$d/%2$d URL, %3$d aktywnych badge Fusion
+ Aktywny
+ Nieaktywny
+ URL JSON badge Fusion
+ %1$d/%2$d URL Fusion zaimportowanych
+ %1$s, %2$d włączonych badge, %3$d grup
+ Odtwarzanie zwiastuna w tle
+ Odtwarzaj podgląd zwiastuna w tle szczegółów gdy dostępny.
+ Ukryj bufor, seedy, peery i prędkość pobierania podczas ładowania i odtwarzania
+ Ukryj statystyki torrenta
+ Zezwalaj na strumienie peer-to-peer (torrent)
+ Strumieniowanie P2P
+ Nieprawidłowy klucz API lub błąd połączenia
+ Nieprawidłowy klucz API lub błąd połączenia
+ Wyjście audio
+ Użyj starszego wyjścia AudioUnit.
+ Najpierw AVFoundation, potem AudioUnit jako zapasowe.
+ Eksperymentalna obsługa Spatial Audio i wielokanałowego wyjścia.
+ Wyjście audio
+ Wyjście audio iOS
+ Wskazówka koloru wyświetlacza
+ Pozwól mpv celować w aktywną przestrzeń kolorów wyświetlacza.
+ Rozszerzony zakres dynamiczny
+ Domyślny tryb wyjścia Metal dla nowych sesji odtwarzania.
+ Dekoder sprzętowy
+ Dekoder sprzętowy
+ Docelowe kolory podstawowe
+ Docelowe kolory podstawowe
+ Docelowy transfer
+ Docelowy transfer
+ Wyjście wideo iOS
+ Gesty dotykowe
+ Pozwól na przesunięcia i podwójne dotknięcia na powierzchni odtwarzacza do przewijania, regulacji jasności lub głośności.
+ Wprowadź URL JSON badge.
+ Import badge nie powiódł się.
+ Możesz zaimportować do %1$d URL badge.
+ Na dole
+ Wybierz czy badge Fusion i rozmiaru pojawiają się nad czy pod kartami źródeł.
+ Wybierz gdzie badge źródeł pojawiają się na kartach.
+ Pozycja badge
+ Pozycja badge
+ Na górze
+ Styl Fusion
+ URL badge musi zaczynać się od http:// lub https://.
+ Importuj do %1$d URL JSON badge Fusion. Każdy URL można aktualizować lub usuwać osobno.
+ Zarządzaj zaimportowanymi URL JSON badge Fusion.
+ URL badge Fusion
+ Pokazuj badge rozmiaru pliku w wynikach źródeł i panelu źródeł odtwarzacza.
+ Badge rozmiaru
+ Repozytorium wtyczek
+ Zgłoś intro
+ Wyślij
+ Przechwyć
+ Przechwyć
+ CZAS KOŃCA (MM:SS)
+ CZAS KOŃCA (MM:SS)
+ Intro
+ Outro
+ Podsumowanie
+ TYP SEGMENTU
+ TYP SEGMENTU
+ CZAS STARTU (MM:SS)
+ CZAS STARTU (MM:SS)
+ Wyślij
+ Zgłoś znaczniki czasu
+ Dodaj klucz API TMDB w Ustawieniach, aby korzystać ze źródeł TMDB.
+ Kolekcja TMDB %1$d
+ Nie znaleziono kolekcji TMDB
+ Produkcja TMDB %1$d
+ Nie znaleziono firmy TMDB
+ Reżyser TMDB %1$d
+ TMDB Odkrywaj nie zwrócił danych
+ TMDB Odkrywaj
+ Wprowadź prawidłowy ID lub URL TMDB.
+ Lista TMDB %1$d
+ Nie znaleziono listy TMDB
+ Nie udało się załadować źródła TMDB
+ Brak ID kolekcji TMDB
+ Brak ID listy TMDB
+ Brak ID osoby TMDB
+ Sieć TMDB %1$d
+ Nie znaleziono sieci TMDB
+ Nie znaleziono filmografii osoby TMDB
+ Osoba TMDB %1$d
+ Nie znaleziono osoby TMDB
+ Połączono z Trakt
+ Połączono z Trakt
+ Rozłączono z Trakt
+ Rozłączono z Trakt
+ Nie udało się dodać do listy Trakt
+ Nie udało się dodać do listy obserwowanych Trakt
+ Autoryzacja Trakt wygasła
+ Pusta treść odpowiedzi
+ Osiągnięto limit list Trakt
+ Nie znaleziono listy Trakt
+ Brak kompatybilnych ID Trakt
+ Osiągnięto limit zapytań Trakt
+ Zapytanie Trakt nie powiodło się
+ Wybierz źródło rekomendacji na stronach szczegółów.
+ Źródło Podobnych
+ Wybierz skąd pochodzą rekomendacje na stronach szczegółów
+ Źródło Podobnych
+ TMDB
+ Trakt
+ Publiczna lista Trakt
+ Wprowadź prawidłowy ID lub URL listy Trakt
+ %1$d elementów
+ %1$d polubień
+ Brak danych uwierzytelniających Trakt w local.properties (TRAKT_CLIENT_ID).
+ Brak ID listy Trakt
+ Lista Trakt nie zawiera numerycznego ID
+ Lista Trakt nie znaleziona lub niepubliczna
+ Osiągnięto limit zapytań Trakt
+ Zapytanie Trakt nie powiodło się
+ TB
+ Brak pliku APK w wydaniu
+ Pobrany plik aktualizacji nie istnieje.
+ Pusta treść pobierania
+ Pobieranie nie powiodło się z kodem HTTP %1$d
+ Pobrany plik aktualizacji nie istnieje.
+ Pusta treść pobierania
+ Błąd API GitHub releases: %1$d
+ Nie opublikowano jeszcze żadnej aktualizacji.
+ Wydanie nie ma tagu ani nazwy
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index eddd4964a..58cf290ae 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -536,6 +536,7 @@
AMOLED Black
Use pure black backgrounds for OLED screens.
App Language
+ Device language
Choose Language
Settings for the Continue Watching section.
Liquid Glass
@@ -864,6 +865,8 @@
Not set
Default (media file)
Device language
+ Original language
+ Requires TMDB enrichment to be enabled
Forced
None
All subtitles
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt
index a431641b9..42fb5aafa 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt
@@ -2004,6 +2004,7 @@ private fun MainAppContent(
parentMetaType = launch.parentMetaType ?: launch.type,
initialPositionMs = launch.resumePositionMs ?: 0L,
initialProgressFraction = launch.resumeProgressFraction,
+ contentLanguage = cached.contentLanguage,
)
if (playerSettings.externalPlayerEnabled) {
openExternalPlayback(playerLaunch)
@@ -2430,6 +2431,7 @@ private fun MainAppContent(
torrentTrackers = launch.torrentTrackers,
initialPositionMs = launch.initialPositionMs,
initialProgressFraction = launch.initialProgressFraction,
+ contentLanguage = launch.contentLanguage,
onBack = {
ResumePromptRepository.markPlayerExitedNormally()
PlayerLaunchStore.remove(route.launchId)
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailHero.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailHero.kt
index a97e8ee6d..8e502f6f0 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailHero.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailHero.kt
@@ -184,14 +184,18 @@ fun DetailHero(
Box(
modifier = Modifier
.fillMaxWidth()
- .height(260.dp)
+ .height(if (isTablet) 360.dp else 320.dp)
.align(Alignment.BottomCenter)
.background(
Brush.verticalGradient(
- colors = listOf(
- Color.Transparent,
- MaterialTheme.colorScheme.background.copy(alpha = 0.7f),
- MaterialTheme.colorScheme.background,
+ colorStops = arrayOf(
+ 0.00f to Color.Transparent,
+ 0.16f to MaterialTheme.colorScheme.background.copy(alpha = 0.04f),
+ 0.32f to MaterialTheme.colorScheme.background.copy(alpha = 0.14f),
+ 0.50f to MaterialTheme.colorScheme.background.copy(alpha = 0.34f),
+ 0.68f to MaterialTheme.colorScheme.background.copy(alpha = 0.62f),
+ 0.84f to MaterialTheme.colorScheme.background.copy(alpha = 0.84f),
+ 1.00f to MaterialTheme.colorScheme.background,
),
),
),
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerLanguagePreferences.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerLanguagePreferences.kt
index 6f5c2348d..8f9e28bfc 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerLanguagePreferences.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerLanguagePreferences.kt
@@ -84,6 +84,7 @@ import nuvio.composeapp.generated.resources.settings_playback_option_default
import nuvio.composeapp.generated.resources.settings_playback_option_device_language
import nuvio.composeapp.generated.resources.settings_playback_option_forced
import nuvio.composeapp.generated.resources.settings_playback_option_none
+import nuvio.composeapp.generated.resources.settings_playback_option_original
import nuvio.composeapp.generated.resources.subtitle_language_unknown
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
@@ -97,6 +98,7 @@ data class LanguagePreferenceOption(
object AudioLanguageOption {
const val DEFAULT = "default"
const val DEVICE = "device"
+ const val ORIGINAL = "original"
}
object SubtitleLanguageOption {
@@ -463,6 +465,8 @@ fun languageLabelForCode(code: String?): String = when {
code.equals(AudioLanguageOption.DEVICE, ignoreCase = true) ||
code.equals(SubtitleLanguageOption.DEVICE, ignoreCase = true) ->
stringResource(Res.string.settings_playback_option_device_language)
+ code.equals(AudioLanguageOption.ORIGINAL, ignoreCase = true) ->
+ stringResource(Res.string.settings_playback_option_original)
else -> languageLabelResForCode(code)?.let { stringResource(it) }
?: stringResource(Res.string.subtitle_language_unknown)
}
@@ -477,6 +481,8 @@ suspend fun getLanguageLabelForCode(code: String?): String = when {
code.equals(AudioLanguageOption.DEVICE, ignoreCase = true) ||
code.equals(SubtitleLanguageOption.DEVICE, ignoreCase = true) ->
getString(Res.string.settings_playback_option_device_language)
+ code.equals(AudioLanguageOption.ORIGINAL, ignoreCase = true) ->
+ getString(Res.string.settings_playback_option_original)
else -> languageLabelResForCode(code)?.let { getString(it) }
?: getString(Res.string.subtitle_language_unknown)
}
@@ -485,6 +491,7 @@ fun resolvePreferredAudioLanguageTargets(
preferredAudioLanguage: String,
secondaryPreferredAudioLanguage: String?,
deviceLanguages: List,
+ contentOriginalLanguage: String? = null,
): List {
fun normalize(language: String?): String? {
val normalized = normalizeLanguageCode(language)
@@ -495,6 +502,7 @@ fun resolvePreferredAudioLanguageTargets(
SubtitleLanguageOption.NONE,
SubtitleLanguageOption.FORCED,
-> null
+ AudioLanguageOption.ORIGINAL -> contentOriginalLanguage?.trim()?.lowercase()?.takeIf { it.isNotBlank() }
else -> normalized
}
}
@@ -511,6 +519,21 @@ fun resolvePreferredAudioLanguageTargets(
+ listOfNotNull(normalize(secondaryPreferredAudioLanguage))
).distinct()
+ AudioLanguageOption.ORIGINAL -> {
+ val originalLang = contentOriginalLanguage?.trim()?.lowercase()?.takeIf { it.isNotBlank() }
+ if (originalLang != null) {
+ listOfNotNull(
+ originalLang,
+ normalize(secondaryPreferredAudioLanguage),
+ ).distinct()
+ } else {
+ // Fallback to device languages when original language is unknown
+ (deviceLanguages.mapNotNull(::normalize)
+ + listOfNotNull(normalize(secondaryPreferredAudioLanguage))
+ ).distinct()
+ }
+ }
+
else -> listOfNotNull(
normalize(preferredAudioLanguage),
normalize(secondaryPreferredAudioLanguage),
@@ -575,3 +598,51 @@ fun inferForcedSubtitleTrack(
if ("forced" in text) return true
return text.contains("songs") && text.contains("sign")
}
+
+/**
+ * Best-effort mapping from country name/code to ISO 639-1 primary language.
+ * Used as a fallback when [resolveContentLanguage] has no explicit language field.
+ */
+fun countryToLanguageCode(country: String?): String? {
+ val normalized = country?.trim()?.lowercase()?.takeIf { it.isNotBlank() } ?: return null
+ return COUNTRY_TO_LANGUAGE_MAP[normalized]
+}
+
+/**
+ * Resolves the original content language as an ISO 639-1 code.
+ * Falls back to country-based inference when the explicit language field is absent.
+ */
+fun resolveContentLanguage(language: String?, country: String?): String? {
+ normalizeLanguageCode(language)?.let { return it }
+ countryToLanguageCode(country)?.let { return it }
+ return null
+}
+
+private val COUNTRY_TO_LANGUAGE_MAP = mapOf(
+ // ISO 3166-1 alpha-2
+ "jp" to "ja", "kr" to "ko", "cn" to "zh", "tw" to "zh",
+ "fr" to "fr", "de" to "de", "it" to "it", "es" to "es",
+ "pt" to "pt", "br" to "pt", "ru" to "ru", "in" to "hi",
+ "tr" to "tr", "pl" to "pl", "nl" to "nl", "se" to "sv",
+ "no" to "no", "dk" to "da", "fi" to "fi", "th" to "th",
+ "il" to "he", "cz" to "cs", "ro" to "ro", "hu" to "hu",
+ "ua" to "uk", "gr" to "el",
+ // ISO 3166-1 alpha-3
+ "jpn" to "ja", "kor" to "ko", "chn" to "zh", "twn" to "zh",
+ "fra" to "fr", "deu" to "de", "ita" to "it", "esp" to "es",
+ "prt" to "pt", "bra" to "pt", "rus" to "ru", "ind" to "hi",
+ "tur" to "tr", "pol" to "pl", "nld" to "nl", "swe" to "sv",
+ "nor" to "no", "dnk" to "da", "fin" to "fi", "tha" to "th",
+ "isr" to "he", "cze" to "cs", "rou" to "ro", "hun" to "hu",
+ "ukr" to "uk", "grc" to "el",
+ // Common full names
+ "japan" to "ja", "south korea" to "ko", "korea" to "ko",
+ "china" to "zh", "taiwan" to "zh", "france" to "fr",
+ "germany" to "de", "italy" to "it", "spain" to "es",
+ "portugal" to "pt", "brazil" to "pt", "russia" to "ru",
+ "india" to "hi", "turkey" to "tr", "poland" to "pl",
+ "netherlands" to "nl", "sweden" to "sv", "norway" to "no",
+ "denmark" to "da", "finland" to "fi", "thailand" to "th",
+ "israel" to "he", "romania" to "ro", "hungary" to "hu",
+ "ukraine" to "uk", "greece" to "el",
+)
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt
index e5c4d81c1..2d52d6c61 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt
@@ -50,6 +50,7 @@ data class PlayerLaunch(
val torrentTrackers: List = emptyList(),
val initialPositionMs: Long = 0L,
val initialProgressFraction: Float? = null,
+ val contentLanguage: String? = null,
)
object PlayerLaunchStore {
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt
index a4cce8c28..d3882286c 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt
@@ -38,6 +38,7 @@ fun PlayerScreen(
torrentTrackers: List = emptyList(),
initialPositionMs: Long = 0L,
initialProgressFraction: Float? = null,
+ contentLanguage: String? = null,
) {
PlayerScreenContent(
PlayerScreenArgs(
@@ -74,6 +75,7 @@ fun PlayerScreen(
torrentTrackers = torrentTrackers,
initialPositionMs = initialPositionMs,
initialProgressFraction = initialProgressFraction,
+ contentLanguage = contentLanguage,
)
)
}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenArgs.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenArgs.kt
index 6f5c229da..d5e549015 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenArgs.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenArgs.kt
@@ -36,4 +36,5 @@ internal data class PlayerScreenArgs(
val torrentTrackers: List,
val initialPositionMs: Long,
val initialProgressFraction: Float?,
+ val contentLanguage: String? = null,
)
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeSourceActions.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeSourceActions.kt
index ade131f57..43095cdc8 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeSourceActions.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeSourceActions.kt
@@ -482,5 +482,9 @@ private fun PlayerScreenRuntime.saveDirectStreamForReuse(
videoSize = stream.behaviorHints.videoSize,
bingeGroup = stream.behaviorHints.bingeGroup,
streamType = stream.streamType,
+ contentLanguage = resolveContentLanguage(
+ language = metaUiState.meta?.language,
+ country = metaUiState.meta?.country,
+ ),
)
}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt
index 118baba9c..098c258f5 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt
@@ -152,6 +152,10 @@ internal fun PlayerScreenRuntime.refreshTracks() {
preferredAudioLanguage = playerSettingsUiState.preferredAudioLanguage,
secondaryPreferredAudioLanguage = playerSettingsUiState.secondaryPreferredAudioLanguage,
deviceLanguages = DeviceLanguagePreferences.preferredLanguageCodes(),
+ contentOriginalLanguage = resolveContentLanguage(
+ language = metaUiState.meta?.language,
+ country = metaUiState.meta?.country,
+ ) ?: args.contentLanguage,
)
if (preferredAudioTargets.isEmpty()) {
preferredAudioSelectionApplied = true
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt
index 4cdb8924a..85bcf497a 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt
@@ -13,12 +13,14 @@ import nuvio.composeapp.generated.resources.lang_portuguese_portugal
import nuvio.composeapp.generated.resources.lang_spanish
import nuvio.composeapp.generated.resources.lang_turkish
import nuvio.composeapp.generated.resources.lang_norwegian
+import nuvio.composeapp.generated.resources.settings_appearance_app_language_device
import org.jetbrains.compose.resources.StringResource
enum class AppLanguage(
val code: String,
val labelRes: StringResource,
) {
+ DEVICE("device", Res.string.settings_appearance_app_language_device),
CZECH("cs", Res.string.lang_czech),
ENGLISH("en", Res.string.lang_english),
FRENCH("fr", Res.string.lang_french),
@@ -35,6 +37,6 @@ enum class AppLanguage(
companion object {
fun fromCode(code: String?): AppLanguage =
- entries.firstOrNull { it.code.equals(code, ignoreCase = true) } ?: ENGLISH
+ entries.firstOrNull { it.code.equals(code, ignoreCase = true) } ?: DEVICE
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PlaybackSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PlaybackSettingsPage.kt
index 54718c44f..6dc6f3093 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PlaybackSettingsPage.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PlaybackSettingsPage.kt
@@ -404,6 +404,7 @@ private fun PlaybackSettingsSection(
description = when (preferredAudioLanguage) {
AudioLanguageOption.DEFAULT -> stringResource(Res.string.settings_playback_option_default)
AudioLanguageOption.DEVICE -> stringResource(Res.string.settings_playback_option_device_language)
+ AudioLanguageOption.ORIGINAL -> stringResource(Res.string.settings_playback_option_original)
else -> languageLabelForCode(preferredAudioLanguage)
},
enabled = audioLanguageEnabled,
@@ -1090,11 +1091,13 @@ private fun PlaybackSettingsSection(
}
if (showPreferredAudioDialog) {
+ val originalHint = stringResource(Res.string.settings_playback_option_original_hint)
LanguageSelectionDialog(
title = stringResource(Res.string.settings_playback_preferred_audio_language),
options = listOf(
LanguageSelectionOption(AudioLanguageOption.DEFAULT, stringResource(Res.string.settings_playback_option_default)),
LanguageSelectionOption(AudioLanguageOption.DEVICE, stringResource(Res.string.settings_playback_option_device_language)),
+ LanguageSelectionOption(AudioLanguageOption.ORIGINAL, stringResource(Res.string.settings_playback_option_original), description = originalHint),
) + AvailableLanguageOptions.map { option ->
LanguageSelectionOption(option.code, stringResource(option.labelRes))
},
@@ -1108,10 +1111,12 @@ private fun PlaybackSettingsSection(
}
if (showSecondaryAudioDialog) {
+ val originalHint = stringResource(Res.string.settings_playback_option_original_hint)
LanguageSelectionDialog(
title = stringResource(Res.string.settings_playback_secondary_audio_language),
options = listOf(
LanguageSelectionOption(null, stringResource(Res.string.settings_playback_option_none)),
+ LanguageSelectionOption(AudioLanguageOption.ORIGINAL, stringResource(Res.string.settings_playback_option_original), description = originalHint),
) + AvailableLanguageOptions.map { option ->
LanguageSelectionOption(option.code, stringResource(option.labelRes))
},
@@ -1441,6 +1446,7 @@ private fun formatReuseCacheDuration(hours: Int): String = when {
private data class LanguageSelectionOption(
val value: String?,
val label: String,
+ val description: String? = null,
)
@Composable
@@ -1720,12 +1726,20 @@ private fun LanguageSelectionDialog(
.padding(horizontal = 14.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
- Text(
- text = option.label,
- style = MaterialTheme.typography.bodyLarge,
- color = MaterialTheme.colorScheme.onSurface,
- modifier = Modifier.weight(1f),
- )
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = option.label,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ if (!option.description.isNullOrBlank()) {
+ Text(
+ text = option.description,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
Box(
modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center,
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsRepository.kt
index 431b9d20e..217faa940 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsRepository.kt
@@ -17,7 +17,7 @@ object ThemeSettingsRepository {
private val _liquidGlassNativeTabBarEnabled = MutableStateFlow(false)
val liquidGlassNativeTabBarEnabled: StateFlow = _liquidGlassNativeTabBarEnabled.asStateFlow()
- private val _selectedAppLanguage = MutableStateFlow(AppLanguage.ENGLISH)
+ private val _selectedAppLanguage = MutableStateFlow(AppLanguage.DEVICE)
val selectedAppLanguage: StateFlow = _selectedAppLanguage.asStateFlow()
private var hasLoaded = false
@@ -38,7 +38,7 @@ object ThemeSettingsRepository {
_liquidGlassNativeTabBarEnabled.value = false
NativeTabBridge.publishAccentColor(AppTheme.WHITE.nativeTabAccentHex())
NativeTabBridge.publishLiquidGlassEnabled(false)
- _selectedAppLanguage.value = AppLanguage.ENGLISH
+ _selectedAppLanguage.value = AppLanguage.DEVICE
}
private fun loadFromDisk() {
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamLinkCacheRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamLinkCacheRepository.kt
index e3a7e8efc..d1b830227 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamLinkCacheRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamLinkCacheRepository.kt
@@ -19,6 +19,7 @@ data class CachedStreamLink(
val sources: List = emptyList(),
val bingeGroup: String? = null,
val streamType: String? = null,
+ val contentLanguage: String? = null,
)
internal expect fun epochMs(): Long
@@ -56,6 +57,7 @@ object StreamLinkCacheRepository {
sources: List = emptyList(),
bingeGroup: String? = null,
streamType: String? = null,
+ contentLanguage: String? = null,
) {
if (url.isNotBlank() && url.hasLikelyExpiringPlaybackCredentials()) {
remove(contentKey)
@@ -77,6 +79,7 @@ object StreamLinkCacheRepository {
sources = sources,
bingeGroup = bingeGroup,
streamType = streamType,
+ contentLanguage = contentLanguage,
)
val payload = json.encodeToString(CachedStreamLink.serializer(), entry)
StreamLinkCacheStorage.saveEntry(hashedKey(contentKey), payload)
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt
index 8fdf7e116..bdf8f0ece 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt
@@ -71,6 +71,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
+import com.nuvio.app.core.build.AppFeaturePolicy
import com.nuvio.app.core.ui.NuvioBackButton
import com.nuvio.app.core.ui.NuvioBottomSheetActionRow
import com.nuvio.app.core.ui.NuvioBottomSheetDivider
@@ -84,6 +85,9 @@ import androidx.compose.material3.rememberModalBottomSheetState
import coil3.compose.AsyncImage
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
import com.nuvio.app.features.debrid.DebridSettingsRepository
+import com.nuvio.app.features.debrid.DirectDebridPlayableResult
+import com.nuvio.app.features.debrid.DirectDebridPlaybackResolver
+import com.nuvio.app.features.debrid.toastMessage
import com.nuvio.app.features.player.PlayerSettingsRepository
import com.nuvio.app.features.watchprogress.WatchProgressRepository
import kotlinx.coroutines.launch
@@ -144,6 +148,7 @@ fun StreamsScreen(
val streamLinkCopiedText = stringResource(Res.string.streams_link_copied)
val noDirectStreamLinkText = stringResource(Res.string.streams_no_direct_link)
var streamActionsTarget by remember(videoId) { mutableStateOf(null) }
+ val downloadScope = rememberCoroutineScope()
var preferredFilterApplied by remember(videoId) { mutableStateOf(false) }
var autoPlayOverlayLogoLoadError by remember(logo) { mutableStateOf(false) }
val autoPlayOverlayLogoUrl = logo?.takeIf { it.isNotBlank() }
@@ -355,27 +360,88 @@ fun StreamsScreen(
if (!directUrl.isNullOrBlank()) {
clipboardManager.setText(AnnotatedString(directUrl))
NuvioToastController.show(streamLinkCopiedText)
+ } else if (DirectDebridPlaybackResolver.shouldResolveToPlayableStream(stream)) {
+ downloadScope.launch {
+ val resolved = DirectDebridPlaybackResolver.resolveToPlayableStream(
+ stream = stream,
+ season = seasonNumber,
+ episode = episodeNumber,
+ )
+ when (resolved) {
+ is DirectDebridPlayableResult.Success -> {
+ val resolvedUrl = resolved.stream.playableDirectUrl
+ if (!resolvedUrl.isNullOrBlank()) {
+ clipboardManager.setText(AnnotatedString(resolvedUrl))
+ NuvioToastController.show(streamLinkCopiedText)
+ } else {
+ NuvioToastController.show(noDirectStreamLinkText)
+ }
+ }
+ else -> {
+ val message = resolved.toastMessage()
+ if (message != null) {
+ NuvioToastController.show(message)
+ }
+ }
+ }
+ }
} else {
NuvioToastController.show(noDirectStreamLinkText)
}
},
onDownload = { stream ->
- val result = DownloadsRepository.enqueueFromStream(
- contentType = type,
- videoId = videoId,
- parentMetaId = parentMetaId,
- parentMetaType = parentMetaType,
- title = title,
- logo = logo,
- poster = poster,
- background = background,
- seasonNumber = seasonNumber,
- episodeNumber = episodeNumber,
- episodeTitle = episodeTitle,
- episodeThumbnail = episodeThumbnail,
- stream = stream,
- )
- NuvioToastController.show(result.toastMessage())
+ if (DirectDebridPlaybackResolver.shouldResolveToPlayableStream(stream)) {
+ downloadScope.launch {
+ val resolved = DirectDebridPlaybackResolver.resolveToPlayableStream(
+ stream = stream,
+ season = seasonNumber,
+ episode = episodeNumber,
+ )
+ when (resolved) {
+ is DirectDebridPlayableResult.Success -> {
+ val result = DownloadsRepository.enqueueFromStream(
+ contentType = type,
+ videoId = videoId,
+ parentMetaId = parentMetaId,
+ parentMetaType = parentMetaType,
+ title = title,
+ logo = logo,
+ poster = poster,
+ background = background,
+ seasonNumber = seasonNumber,
+ episodeNumber = episodeNumber,
+ episodeTitle = episodeTitle,
+ episodeThumbnail = episodeThumbnail,
+ stream = resolved.stream,
+ )
+ NuvioToastController.show(result.toastMessage())
+ }
+ else -> {
+ val message = resolved.toastMessage()
+ if (message != null) {
+ NuvioToastController.show(message)
+ }
+ }
+ }
+ }
+ } else {
+ val result = DownloadsRepository.enqueueFromStream(
+ contentType = type,
+ videoId = videoId,
+ parentMetaId = parentMetaId,
+ parentMetaType = parentMetaType,
+ title = title,
+ logo = logo,
+ poster = poster,
+ background = background,
+ seasonNumber = seasonNumber,
+ episodeNumber = episodeNumber,
+ episodeTitle = episodeTitle,
+ episodeThumbnail = episodeThumbnail,
+ stream = stream,
+ )
+ NuvioToastController.show(result.toastMessage())
+ }
},
onOpen = { stream, openExternally ->
onStreamActionOpen(
@@ -792,6 +858,7 @@ internal fun StreamList(
val hasGroups = filteredGroups.isNotEmpty()
val hasAnyStreams = filteredGroups.any { it.streams.isNotEmpty() }
val anyLoading = filteredGroups.any { it.isLoading }
+ val torrentNotSupportedText = stringResource(Res.string.streams_torrent_not_supported)
val streamBadgeSettings by remember {
StreamBadgeSettingsRepository.ensureLoaded()
StreamBadgeSettingsRepository.uiState
@@ -829,6 +896,7 @@ internal fun StreamList(
showFileSizeBadges = streamBadgeSettings.showFileSizeBadges,
showAddonLogo = streamBadgeSettings.showAddonLogo,
badgePlacement = streamBadgeSettings.badgePlacement,
+ torrentNotSupportedText = torrentNotSupportedText,
onStreamSelected = onStreamSelected,
onStreamLongPress = onStreamLongPress,
resumePositionMs = resumePositionMs,
@@ -857,6 +925,7 @@ private fun LazyListScope.streamSection(
showFileSizeBadges: Boolean,
showAddonLogo: Boolean,
badgePlacement: StreamBadgePlacement,
+ torrentNotSupportedText: String,
onStreamSelected: (stream: StreamItem, resumePositionMs: Long?, resumeProgressFraction: Float?) -> Unit,
onStreamLongPress: (StreamItem) -> Unit,
resumePositionMs: Long?,
@@ -898,20 +967,27 @@ private fun LazyListScope.streamSection(
)
},
) { _, stream ->
+ val isSelectable = stream.isSelectableForPlayback(debridEnabled)
+ val isUnsupportedTorrentStream =
+ stream.needsLocalDebridResolve &&
+ !AppFeaturePolicy.p2pEnabled &&
+ !(debridEnabled && stream.isAddonDebridCandidate)
StreamCard(
stream = stream,
- enabled = stream.isSelectableForPlayback(debridEnabled),
+ enabled = isSelectable || isUnsupportedTorrentStream,
appendInstantServiceToDefaultName = appendInstantServiceToDefaultName,
showFileSizeBadges = showFileSizeBadges,
showAddonLogo = showAddonLogo,
badgePlacement = badgePlacement,
onClick = {
- if (stream.isSelectableForPlayback(debridEnabled)) {
+ if (isSelectable) {
onStreamSelected(stream, resumePositionMs, resumeProgressFraction)
+ } else if (isUnsupportedTorrentStream) {
+ NuvioToastController.show(torrentNotSupportedText)
}
},
onLongClick = {
- if (stream.playableDirectUrl != null) {
+ if (stream.playableDirectUrl != null || stream.isAddonDebridCandidate) {
onStreamLongPress(stream)
}
},
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressRepository.kt
index 4d510c137..3e178918d 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressRepository.kt
@@ -741,13 +741,19 @@ object WatchProgressRepository {
ensureLoaded()
if (videoIds.isEmpty()) return
- if (shouldUseTraktProgress()) {
+ val useTraktProgress = shouldUseTraktProgress()
+ if (useTraktProgress) {
val entriesToRemove = currentEntries().filter { entry -> entry.videoId in videoIds }
+ val locallyRemovedEntries = removeStoredLocalEntries(entriesToRemove)
videoIds.forEach(TraktProgressRepository::applyOptimisticRemoval)
+ if (locallyRemovedEntries.isNotEmpty()) {
+ persist()
+ }
publish()
- if (entriesToRemove.isNotEmpty()) {
+ val traktEntriesToRemove = entriesToRemove.filter { entry -> entry.shouldAttemptTraktPlaybackDelete() }
+ if (traktEntriesToRemove.isNotEmpty()) {
syncScope.launch {
- entriesToRemove.forEach { entry ->
+ traktEntriesToRemove.forEach { entry ->
runCatching {
TraktProgressRepository.removeProgress(
contentId = entry.parentMetaId,
@@ -783,6 +789,7 @@ object WatchProgressRepository {
val normalizedContentId = contentId.trim()
if (normalizedContentId.isBlank()) return
+ val useTraktProgress = shouldUseTraktProgress()
val entriesToRemove = currentEntries().filter { entry ->
if (entry.parentMetaId != normalizedContentId) {
false
@@ -794,13 +801,21 @@ object WatchProgressRepository {
}
if (entriesToRemove.isEmpty()) return
- if (shouldUseTraktProgress()) {
+ if (useTraktProgress) {
+ val locallyRemovedEntries = removeStoredLocalEntries(entriesToRemove)
TraktProgressRepository.applyOptimisticRemoval(
contentId = normalizedContentId,
seasonNumber = seasonNumber,
episodeNumber = episodeNumber,
)
+ if (locallyRemovedEntries.isNotEmpty()) {
+ persist()
+ }
publish()
+ val shouldAttemptTraktDelete = entriesToRemove.any { entry -> entry.shouldAttemptTraktPlaybackDelete() }
+ if (!shouldAttemptTraktDelete) {
+ return
+ }
syncScope.launch {
runCatching {
TraktProgressRepository.removeProgress(
@@ -809,6 +824,7 @@ object WatchProgressRepository {
episodeNumber = episodeNumber,
)
}.onFailure { error ->
+ if (error is CancellationException) throw error
log.e(error) { "Failed to remove Trakt watch progress" }
}
}
@@ -1031,6 +1047,14 @@ object WatchProgressRepository {
source = TraktSettingsRepository.uiState.value.watchProgressSource,
)
+ private fun WatchProgressEntry.shouldAttemptTraktPlaybackDelete(): Boolean =
+ isTraktCompatibleId(parentMetaId)
+
+ private fun removeStoredLocalEntries(entries: Collection): List =
+ entries.mapNotNull { entry ->
+ entriesByVideoId.remove(entry.videoId)
+ }
+
private fun currentEntries(): List {
return if (shouldUseTraktProgress()) {
// Merge Trakt remote progress with local-only entries that use
diff --git a/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginRuntime.kt b/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginRuntime.kt
index 3a003d8ef..c749c63b4 100644
--- a/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginRuntime.kt
+++ b/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginRuntime.kt
@@ -22,13 +22,13 @@ import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonPrimitive
-import kotlinx.coroutines.runBlocking
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.generic_unknown
import org.jetbrains.compose.resources.getString
import kotlin.random.Random
private const val PLUGIN_TIMEOUT_MS = 60_000L
+private const val SLOW_PLUGIN_FETCH_MS = 2_000L
private const val MAX_FETCH_BODY_CHARS = 256 * 1024
private const val MAX_FETCH_HEADER_VALUE_CHARS = 8 * 1024
private const val FETCH_TRUNCATION_SUFFIX = "\n...[truncated]"
@@ -49,7 +49,7 @@ internal object PluginRuntime {
episode: Int?,
scraperId: String,
scraperSettings: Map = emptyMap(),
- ): List = withContext(Dispatchers.Default) {
+ ): List = withContext(Dispatchers.IO) {
withTimeout(PLUGIN_TIMEOUT_MS) {
executePluginInternal(
code = code,
@@ -78,7 +78,7 @@ internal object PluginRuntime {
var resultJson = "[]"
try {
- quickJs(Dispatchers.Default) {
+ quickJs(Dispatchers.IO) {
define("console") {
function("log") { args ->
log.d { "Plugin:$scraperId ${args.joinToString(" ") { it?.toString() ?: "null" }}" }
@@ -328,7 +328,8 @@ internal object PluginRuntime {
headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
- val response = runBlocking {
+ val startedAt = kotlin.time.TimeSource.Monotonic.markNow()
+ val response = runBlocking(Dispatchers.IO) {
httpRequestRaw(
method = method,
url = url,
@@ -337,6 +338,10 @@ internal object PluginRuntime {
followRedirects = followRedirects,
)
}
+ val elapsed = startedAt.elapsedNow()
+ if (elapsed.inWholeMilliseconds >= SLOW_PLUGIN_FETCH_MS) {
+ log.w { "Slow plugin fetch $method $url status=${response.status} elapsed=$elapsed" }
+ }
val responseHeaders = response.headers.mapValues { (_, value) ->
truncateString(value, MAX_FETCH_HEADER_VALUE_CHARS)
diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt
index bf14a4528..f36f0a921 100644
--- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt
+++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt
@@ -72,6 +72,11 @@ actual object ThemeSettingsStorage {
}
actual fun applySelectedAppLanguage(languageCode: String) {
+ if (languageCode.equals("device", ignoreCase = true)) {
+ NSUserDefaults.standardUserDefaults.removeObjectForKey("AppleLanguages")
+ NSUserDefaults.standardUserDefaults.synchronize()
+ return
+ }
val normalizedCode = languageCode
.trim()
.takeIf { it.isNotBlank() }
@@ -97,6 +102,6 @@ actual object ThemeSettingsStorage {
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
payload.decodeSyncBoolean(liquidGlassNativeTabBarEnabledKey)?.let(::saveLiquidGlassNativeTabBarEnabled)
- applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.ENGLISH.code)
+ applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.DEVICE.code)
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 39ae4c826..a9b1b5475 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,25 +1,26 @@
[versions]
-agp = "8.13.2"
-android-compileSdk = "36"
+agp = "9.2.0"
+android-compileSdk = "37"
+android-compileSdkMinor = "0"
android-minSdk = "24"
android-targetSdk = "36"
androidx-activity = "1.12.2"
-androidx-navigation = "2.9.2"
+androidx-navigation = "2.10.0-alpha02"
androidx-appcompat = "1.7.1"
androidx-core = "1.17.0"
androidx-core-splashscreen = "1.0.1"
androidx-espresso = "3.7.0"
-androidx-lifecycle = "2.11.0-beta01"
+androidx-lifecycle = "2.11.0-beta02"
androidx-work = "2.10.3"
androidx-testExt = "1.3.0"
-composeMultiplatform = "1.11.1"
+composeMultiplatform = "1.12.0-alpha02"
coil = "3.5.0-beta01"
kermit = "2.0.5"
junit = "4.13.2"
kotlin = "2.3.0"
kotlinx-serialization = "1.8.1"
ktor = "3.4.1"
-material3 = "1.11.0-alpha07"
+material3 = "1.12.0-alpha02"
androidx-media3 = "1.8.0"
supabase = "3.4.1"
quickjsKt = "1.0.5"
@@ -80,6 +81,7 @@ desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
+androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index d4081da47..c61a118f7 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/iosApp/Configuration/Version.xcconfig b/iosApp/Configuration/Version.xcconfig
index 232b20376..6f9ed2c37 100644
--- a/iosApp/Configuration/Version.xcconfig
+++ b/iosApp/Configuration/Version.xcconfig
@@ -1,3 +1,3 @@
-CURRENT_PROJECT_VERSION=80
-MARKETING_VERSION=0.2.8
+CURRENT_PROJECT_VERSION=83
+MARKETING_VERSION=0.2.11
diff --git a/settings.gradle.kts b/settings.gradle.kts
index e08f97b53..884dc8e94 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -28,4 +28,5 @@ dependencyResolutionManagement {
}
}
-include(":composeApp")
\ No newline at end of file
+include(":composeApp")
+include(":androidApp")