From 632d30c6c8890e792e3965da285f29c80eb97894 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Tue, 12 May 2026 13:56:36 +0530 Subject: [PATCH] update required stubs --- composeApp/build.gradle.kts | 13 ++++++ .../app/core/ui/NativeTabBridge.desktop.kt | 18 ++++++++ .../features/addons/AddonPlatform.desktop.kt | 14 +++++-- ...CollectionMobileSettingsStorage.desktop.kt | 16 +++++++ .../downloads/DownloadsDesktop.desktop.kt | 16 ++++++- .../features/player/PlayerDesktop.desktop.kt | 24 ++++++++++- .../profiles/AvatarStorage.desktop.kt | 15 +++++++ .../ProfileHoverHapticFeedback.desktop.kt | 7 ++++ .../ProfilePinCacheStorage.desktop.kt | 20 +++++++++ .../profiles/ProfilePinCrypto.desktop.kt | 12 ++++++ .../settings/SettingsDesktop.desktop.kt | 42 +++++++++++++++++-- .../trakt/TraktSettingsStorage.desktop.kt | 16 +++++++ 12 files changed, 204 insertions(+), 9 deletions(-) create mode 100644 composeApp/src/desktopMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.desktop.kt create mode 100644 composeApp/src/desktopMain/kotlin/com/nuvio/app/features/collection/CollectionMobileSettingsStorage.desktop.kt create mode 100644 composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/AvatarStorage.desktop.kt create mode 100644 composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/ProfileHoverHapticFeedback.desktop.kt create mode 100644 composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/ProfilePinCacheStorage.desktop.kt create mode 100644 composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/ProfilePinCrypto.desktop.kt create mode 100644 composeApp/src/desktopMain/kotlin/com/nuvio/app/features/trakt/TraktSettingsStorage.desktop.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 53083c98..e788134a 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -367,6 +367,19 @@ tasks.matching { it.name == "packageReleaseDistributionForCurrentOS" || it.name finalizedBy(renameReleaseDmgArtifact) } +val buildDesktopMpvBridge = tasks.register("buildDesktopMpvBridge") { + onlyIf { System.getProperty("os.name").contains("Mac", ignoreCase = true) } + workingDir = rootProject.file("MPVKit") + commandLine("swift", "build", "-c", "release", "--product", "DesktopMPVBridge") + inputs.file(rootProject.file("MPVKit/Package.swift")) + inputs.dir(rootProject.file("MPVKit/Sources/DesktopMPVBridge")) + outputs.dir(rootProject.file("MPVKit/.build")) +} + +tasks.matching { it.name == "run" || it.name == "desktopRun" }.configureEach { + dependsOn(buildDesktopMpvBridge) +} + configurations.all { exclude(group = "androidx.media3", module = "media3-exoplayer") exclude(group = "androidx.media3", module = "media3-ui") diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.desktop.kt new file mode 100644 index 00000000..c7c556c5 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.desktop.kt @@ -0,0 +1,18 @@ +package com.nuvio.app.core.ui + +internal actual fun isLiquidGlassNativeTabBarSupported(): Boolean = false + +internal actual fun publishLiquidGlassNativeTabBarEnabled(enabled: Boolean) = Unit + +internal actual fun publishNativeTabBarVisible(visible: Boolean) = Unit + +internal actual fun publishNativeSelectedTab(tabName: String) = Unit + +internal actual fun publishNativeTabAccentColor(hexColor: String) = Unit + +internal actual fun publishNativeProfileTabIcon( + name: String?, + avatarColorHex: String?, + avatarImageUrl: String?, + avatarBackgroundColorHex: String?, +) = Unit diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.desktop.kt index 7dc3401b..c3c7ec44 100644 --- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.desktop.kt +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.desktop.kt @@ -35,6 +35,11 @@ private val addonHttpClient: HttpClient = HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.NORMAL) .build() +private val addonHttpClientNoRedirects: HttpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(60)) + .followRedirects(HttpClient.Redirect.NEVER) + .build() + private const val maxRawResponseBodyChars = 1024 * 1024 private const val truncationSuffix = "\n...[truncated]" @@ -54,6 +59,7 @@ private suspend fun executeRequest( url: String, headers: Map, body: String, + followRedirects: Boolean = true, ) = withContext(Dispatchers.IO) { val builder = HttpRequest.newBuilder() .uri(URI.create(url)) @@ -69,7 +75,8 @@ private suspend fun executeRequest( builder.method(method.uppercase(), HttpRequest.BodyPublishers.noBody()) }.build() - addonHttpClient.send(request, HttpResponse.BodyHandlers.ofString()) + val client = if (followRedirects) addonHttpClient else addonHttpClientNoRedirects + client.send(request, HttpResponse.BodyHandlers.ofString()) } private suspend fun executeTextRequest( @@ -137,8 +144,9 @@ actual suspend fun httpRequestRaw( url: String, headers: Map, body: String, + followRedirects: Boolean, ): RawHttpResponse { - val response = executeRequest(method, url, headers, body) + val response = executeRequest(method, url, headers, body, followRedirects) val payload = response.body() val limitedPayload = if (payload.length > maxRawResponseBodyChars) { payload.take(maxRawResponseBodyChars) + truncationSuffix @@ -154,4 +162,4 @@ actual suspend fun httpRequestRaw( key.lowercase() to values.joinToString(",") }, ) -} \ No newline at end of file +} diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/collection/CollectionMobileSettingsStorage.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/collection/CollectionMobileSettingsStorage.desktop.kt new file mode 100644 index 00000000..51836427 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/collection/CollectionMobileSettingsStorage.desktop.kt @@ -0,0 +1,16 @@ +package com.nuvio.app.features.collection + +import com.nuvio.app.core.storage.ProfileScopedKey +import com.nuvio.app.desktop.DesktopPreferences + +internal actual object CollectionMobileSettingsStorage { + private const val preferencesName = "nuvio_collection_mobile_settings" + private const val payloadKey = "collection_mobile_settings_payload" + + actual fun loadPayload(): String? = + DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(payloadKey)) + + actual fun savePayload(payload: String) { + DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(payloadKey), payload) + } +} diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/downloads/DownloadsDesktop.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/downloads/DownloadsDesktop.desktop.kt index b74671b8..c5da1c4b 100644 --- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/downloads/DownloadsDesktop.desktop.kt +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/downloads/DownloadsDesktop.desktop.kt @@ -2,6 +2,8 @@ package com.nuvio.app.features.downloads import com.nuvio.app.core.storage.ProfileScopedKey import com.nuvio.app.desktop.DesktopPreferences +import java.io.File +import java.net.URI internal actual object DownloadsStorage { private const val preferencesName = "nuvio_downloads" @@ -33,12 +35,24 @@ internal actual object DownloadsPlatformDownloader { actual fun removeFile(localFileUri: String?): Boolean = false actual fun removePartialFile(destinationFileName: String): Boolean = false + + actual fun resolveLocalFileUri(localFileUri: String?, destinationFileName: String): String? = + localFileUri + ?.toLocalFileOrNull() + ?.takeIf { it.exists() } + ?.toURI() + ?.toString() } +private fun String.toLocalFileOrNull(): File? = + runCatching { + if (startsWith("file://")) File(URI(this)) else File(this) + }.getOrNull() + internal actual object DownloadsLiveStatusPlatform { actual fun onItemsChanged(items: List) = Unit } internal actual object DownloadsClock { actual fun nowEpochMs(): Long = System.currentTimeMillis() -} \ No newline at end of file +} diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/PlayerDesktop.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/PlayerDesktop.desktop.kt index c743cd3e..d9c50699 100644 --- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/PlayerDesktop.desktop.kt +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/PlayerDesktop.desktop.kt @@ -466,6 +466,8 @@ internal actual object PlayerSettingsStorage { private const val skipIntroEnabledKey = "skip_intro_enabled" private const val animeSkipEnabledKey = "animeskip_enabled" private const val animeSkipClientIdKey = "animeskip_client_id" + private const val introDbApiKeyKey = "introdb_api_key" + private const val introSubmitEnabledKey = "intro_submit_enabled" private const val streamAutoPlayNextEpisodeEnabledKey = "stream_auto_play_next_episode_enabled" private const val streamAutoPlayPreferBingeGroupKey = "stream_auto_play_prefer_binge_group" private const val nextEpisodeThresholdModeKey = "next_episode_threshold_mode" @@ -496,6 +498,8 @@ internal actual object PlayerSettingsStorage { skipIntroEnabledKey, animeSkipEnabledKey, animeSkipClientIdKey, + introDbApiKeyKey, + introSubmitEnabledKey, streamAutoPlayNextEpisodeEnabledKey, streamAutoPlayPreferBingeGroupKey, nextEpisodeThresholdModeKey, @@ -661,6 +665,18 @@ internal actual object PlayerSettingsStorage { saveString(animeSkipClientIdKey, clientId) } + actual fun loadIntroDbApiKey(): String? = loadString(introDbApiKeyKey) + + actual fun saveIntroDbApiKey(apiKey: String) { + saveString(introDbApiKeyKey, apiKey) + } + + actual fun loadIntroSubmitEnabled(): Boolean? = loadBoolean(introSubmitEnabledKey) + + actual fun saveIntroSubmitEnabled(enabled: Boolean) { + saveBoolean(introSubmitEnabledKey, enabled) + } + actual fun loadStreamAutoPlayNextEpisodeEnabled(): Boolean? = loadBoolean(streamAutoPlayNextEpisodeEnabledKey) actual fun saveStreamAutoPlayNextEpisodeEnabled(enabled: Boolean) { @@ -726,6 +742,8 @@ internal actual object PlayerSettingsStorage { loadSkipIntroEnabled()?.let { put(skipIntroEnabledKey, encodeSyncBoolean(it)) } loadAnimeSkipEnabled()?.let { put(animeSkipEnabledKey, encodeSyncBoolean(it)) } loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) } + loadIntroDbApiKey()?.let { put(introDbApiKeyKey, encodeSyncString(it)) } + loadIntroSubmitEnabled()?.let { put(introSubmitEnabledKey, encodeSyncBoolean(it)) } loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) } loadStreamAutoPlayPreferBingeGroup()?.let { put(streamAutoPlayPreferBingeGroupKey, encodeSyncBoolean(it)) } loadNextEpisodeThresholdMode()?.let { put(nextEpisodeThresholdModeKey, encodeSyncString(it)) } @@ -760,6 +778,8 @@ internal actual object PlayerSettingsStorage { payload.decodeSyncBoolean(skipIntroEnabledKey)?.let(::saveSkipIntroEnabled) payload.decodeSyncBoolean(animeSkipEnabledKey)?.let(::saveAnimeSkipEnabled) payload.decodeSyncString(animeSkipClientIdKey)?.let(::saveAnimeSkipClientId) + payload.decodeSyncString(introDbApiKeyKey)?.let(::saveIntroDbApiKey) + payload.decodeSyncBoolean(introSubmitEnabledKey)?.let(::saveIntroSubmitEnabled) payload.decodeSyncBoolean(streamAutoPlayNextEpisodeEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeEnabled) payload.decodeSyncBoolean(streamAutoPlayPreferBingeGroupKey)?.let(::saveStreamAutoPlayPreferBingeGroup) payload.decodeSyncString(nextEpisodeThresholdModeKey)?.let(::saveNextEpisodeThresholdMode) @@ -819,7 +839,7 @@ internal actual object PlayerSettingsStorage { actual fun LockPlayerToLandscape() = Unit @Composable -actual fun EnterImmersivePlayerMode() = Unit +actual fun EnterImmersivePlayerMode(keepScreenAwake: Boolean) = Unit @Composable actual fun ManagePlayerPictureInPicture( @@ -828,4 +848,4 @@ actual fun ManagePlayerPictureInPicture( ) = Unit @Composable -actual fun rememberPlayerGestureController(): PlayerGestureController? = null \ No newline at end of file +actual fun rememberPlayerGestureController(): PlayerGestureController? = null diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/AvatarStorage.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/AvatarStorage.desktop.kt new file mode 100644 index 00000000..adad22cc --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/AvatarStorage.desktop.kt @@ -0,0 +1,15 @@ +package com.nuvio.app.features.profiles + +import com.nuvio.app.desktop.DesktopPreferences + +internal actual object AvatarStorage { + private const val preferencesName = "nuvio_avatar_cache" + private const val payloadKey = "avatar_catalog_payload" + + actual fun loadPayload(): String? = + DesktopPreferences.getString(preferencesName, payloadKey) + + actual fun savePayload(payload: String) { + DesktopPreferences.putString(preferencesName, payloadKey, payload) + } +} diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/ProfileHoverHapticFeedback.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/ProfileHoverHapticFeedback.desktop.kt new file mode 100644 index 00000000..486f1477 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/ProfileHoverHapticFeedback.desktop.kt @@ -0,0 +1,7 @@ +package com.nuvio.app.features.profiles + +internal actual object ProfileHoverHapticFeedback { + actual fun prepare() = Unit + actual fun perform() = Unit + actual fun release() = Unit +} diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/ProfilePinCacheStorage.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/ProfilePinCacheStorage.desktop.kt new file mode 100644 index 00000000..4693f35c --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/ProfilePinCacheStorage.desktop.kt @@ -0,0 +1,20 @@ +package com.nuvio.app.features.profiles + +import com.nuvio.app.desktop.DesktopPreferences + +internal actual object ProfilePinCacheStorage { + private const val preferencesName = "nuvio_profile_pin_cache" + + actual fun loadPayload(profileIndex: Int): String? = + DesktopPreferences.getString(preferencesName, payloadKey(profileIndex)) + + actual fun savePayload(profileIndex: Int, payload: String) { + DesktopPreferences.putString(preferencesName, payloadKey(profileIndex), payload) + } + + actual fun removePayload(profileIndex: Int) { + DesktopPreferences.remove(preferencesName, payloadKey(profileIndex)) + } + + private fun payloadKey(profileIndex: Int): String = "profile_pin_cache_$profileIndex" +} diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/ProfilePinCrypto.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/ProfilePinCrypto.desktop.kt new file mode 100644 index 00000000..3f9ab3b1 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/profiles/ProfilePinCrypto.desktop.kt @@ -0,0 +1,12 @@ +package com.nuvio.app.features.profiles + +import java.security.MessageDigest + +internal actual object ProfilePinCrypto { + actual fun sha256Hex(value: String): String { + val digest = MessageDigest.getInstance("SHA-256").digest(value.encodeToByteArray()) + return digest.joinToString(separator = "") { byte -> + byte.toUByte().toString(16).padStart(2, '0') + } + } +} diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/settings/SettingsDesktop.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/settings/SettingsDesktop.desktop.kt index 3101c166..9dc180c8 100644 --- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/settings/SettingsDesktop.desktop.kt +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/settings/SettingsDesktop.desktop.kt @@ -13,6 +13,7 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.introdb_favicon import nuvio.composeapp.generated.resources.mdblist_logo import nuvio.composeapp.generated.resources.rating_tmdb import nuvio.composeapp.generated.resources.trakt_tv_favicon @@ -22,7 +23,14 @@ internal actual object ThemeSettingsStorage { private const val preferencesName = "nuvio_theme_settings" private const val selectedThemeKey = "selected_theme" private const val amoledEnabledKey = "amoled_enabled" - private val syncKeys = listOf(selectedThemeKey, amoledEnabledKey) + private const val liquidGlassNativeTabBarEnabledKey = "liquid_glass_native_tab_bar_enabled" + private const val selectedAppLanguageKey = "selected_app_language" + private val profileScopedSyncKeys = listOf( + selectedThemeKey, + amoledEnabledKey, + liquidGlassNativeTabBarEnabledKey, + ) + private val globalSyncKeys = listOf(selectedAppLanguageKey) actual fun loadSelectedTheme(): String? = DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(selectedThemeKey)) @@ -38,16 +46,43 @@ internal actual object ThemeSettingsStorage { DesktopPreferences.putBoolean(preferencesName, ProfileScopedKey.of(amoledEnabledKey), enabled) } + actual fun loadLiquidGlassNativeTabBarEnabled(): Boolean? = + DesktopPreferences.getBoolean(preferencesName, ProfileScopedKey.of(liquidGlassNativeTabBarEnabledKey)) + + actual fun saveLiquidGlassNativeTabBarEnabled(enabled: Boolean) { + DesktopPreferences.putBoolean(preferencesName, ProfileScopedKey.of(liquidGlassNativeTabBarEnabledKey), enabled) + } + + actual fun loadSelectedAppLanguage(): String? { + val value = DesktopPreferences.getString(preferencesName, selectedAppLanguageKey) + if (value != null) return value + val legacy = DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(selectedAppLanguageKey)) + if (legacy != null) saveSelectedAppLanguage(legacy) + return legacy + } + + actual fun saveSelectedAppLanguage(languageCode: String) { + DesktopPreferences.putString(preferencesName, selectedAppLanguageKey, languageCode) + } + + actual fun applySelectedAppLanguage(languageCode: String) = Unit + actual fun exportToSyncPayload(): JsonObject = buildJsonObject { loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) } loadAmoledEnabled()?.let { put(amoledEnabledKey, encodeSyncBoolean(it)) } + loadLiquidGlassNativeTabBarEnabled()?.let { put(liquidGlassNativeTabBarEnabledKey, encodeSyncBoolean(it)) } + loadSelectedAppLanguage()?.let { put(selectedAppLanguageKey, encodeSyncString(it)) } } actual fun replaceFromSyncPayload(payload: JsonObject) { - syncKeys.forEach { DesktopPreferences.remove(preferencesName, ProfileScopedKey.of(it)) } + profileScopedSyncKeys.forEach { DesktopPreferences.remove(preferencesName, ProfileScopedKey.of(it)) } + globalSyncKeys.forEach { DesktopPreferences.remove(preferencesName, it) } payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme) payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled) + payload.decodeSyncBoolean(liquidGlassNativeTabBarEnabledKey)?.let(::saveLiquidGlassNativeTabBarEnabled) + payload.decodeSyncString(selectedAppLanguageKey)?.let(::saveSelectedAppLanguage) + applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.ENGLISH.code) } } @@ -59,4 +94,5 @@ internal actual fun integrationLogoPainter(logo: IntegrationLogo): Painter = IntegrationLogo.Tmdb -> painterResource(Res.drawable.rating_tmdb) IntegrationLogo.Trakt -> painterResource(Res.drawable.trakt_tv_favicon) IntegrationLogo.MdbList -> painterResource(Res.drawable.mdblist_logo) - } \ No newline at end of file + IntegrationLogo.IntroDb -> painterResource(Res.drawable.introdb_favicon) + } diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/trakt/TraktSettingsStorage.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/trakt/TraktSettingsStorage.desktop.kt new file mode 100644 index 00000000..f9a852da --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/trakt/TraktSettingsStorage.desktop.kt @@ -0,0 +1,16 @@ +package com.nuvio.app.features.trakt + +import com.nuvio.app.core.storage.ProfileScopedKey +import com.nuvio.app.desktop.DesktopPreferences + +internal actual object TraktSettingsStorage { + private const val preferencesName = "nuvio_trakt_settings" + private const val payloadKey = "trakt_settings_payload" + + actual fun loadPayload(): String? = + DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(payloadKey)) + + actual fun savePayload(payload: String) { + DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(payloadKey), payload) + } +}