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")