mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-27 06:52:18 +00:00
feat: init desktop storafe
This commit is contained in:
parent
5dbf522b7a
commit
6222c10ee9
72 changed files with 2445 additions and 0 deletions
|
|
@ -217,6 +217,12 @@ kotlin {
|
|||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
jvm("desktop") {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
val iosTargets = listOf(
|
||||
iosArm64(),
|
||||
|
|
@ -281,6 +287,13 @@ kotlin {
|
|||
implementation(libs.androidx.media3.extractor)
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("lib-*.aar"))))
|
||||
}
|
||||
val desktopMain by getting {
|
||||
dependencies {
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
implementation(libs.ktor.client.cio)
|
||||
}
|
||||
}
|
||||
commonMain.dependencies {
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.network.ktor3)
|
||||
|
|
@ -309,6 +322,18 @@ kotlin {
|
|||
}
|
||||
}
|
||||
|
||||
compose.desktop {
|
||||
application {
|
||||
mainClass = "com.nuvio.app.MainKt"
|
||||
|
||||
nativeDistributions {
|
||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||
packageName = "Nuvio"
|
||||
packageVersion = releaseAppVersionName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
dependencies {
|
||||
add("fullImplementation", files("libs/quickjs-kt-android-1.0.5-nuvio.aar"))
|
||||
|
|
|
|||
16
composeApp/src/desktopMain/kotlin/com/nuvio/app/Main.kt
Normal file
16
composeApp/src/desktopMain/kotlin/com/nuvio/app/Main.kt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package com.nuvio.app
|
||||
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.WindowState
|
||||
import androidx.compose.ui.window.application
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
fun main() = application {
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "Nuvio",
|
||||
state = WindowState(width = 1280.dp, height = 820.dp),
|
||||
) {
|
||||
App()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.nuvio.app
|
||||
|
||||
class DesktopPlatform : Platform {
|
||||
override val name: String = "Desktop ${System.getProperty("os.name").orEmpty()}".trim()
|
||||
}
|
||||
|
||||
actual fun getPlatform(): Platform = DesktopPlatform()
|
||||
|
||||
internal actual val isIos: Boolean = false
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.nuvio.app.core.auth
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
|
||||
internal actual object AuthStorage {
|
||||
private val store = DesktopStorage.store("nuvio_auth")
|
||||
|
||||
actual fun loadAnonymousUserId(): String? =
|
||||
store.getString("anonymous_user_id")
|
||||
|
||||
actual fun saveAnonymousUserId(userId: String) {
|
||||
store.putString("anonymous_user_id", userId)
|
||||
}
|
||||
|
||||
actual fun clearAnonymousUserId() {
|
||||
store.remove("anonymous_user_id")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package com.nuvio.app.core.storage
|
||||
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.Comparator
|
||||
import java.util.Locale
|
||||
import java.util.Properties
|
||||
import kotlin.io.path.exists
|
||||
|
||||
internal object DesktopStorage {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
private val stores = mutableMapOf<String, Store>()
|
||||
|
||||
val rootDir: Path by lazy {
|
||||
resolveAppDataDir().also { Files.createDirectories(it) }
|
||||
}
|
||||
|
||||
fun store(name: String): Store = synchronized(stores) {
|
||||
stores.getOrPut(name) { Store(rootDir.resolve("$name.properties")) }
|
||||
}
|
||||
|
||||
fun wipe() {
|
||||
synchronized(stores) {
|
||||
stores.values.forEach(Store::clearInMemory)
|
||||
stores.clear()
|
||||
}
|
||||
if (!rootDir.exists()) return
|
||||
Files.walk(rootDir).use { stream ->
|
||||
stream
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.filter { it != rootDir }
|
||||
.forEach { path -> runCatching { Files.deleteIfExists(path) } }
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveAppDataDir(): Path {
|
||||
val osName = System.getProperty("os.name").orEmpty().lowercase(Locale.ROOT)
|
||||
val userHome = Paths.get(System.getProperty("user.home").orEmpty())
|
||||
return when {
|
||||
osName.contains("mac") -> userHome.resolve("Library/Application Support/Nuvio")
|
||||
osName.contains("win") -> {
|
||||
val appData = System.getenv("APPDATA")?.takeIf { it.isNotBlank() }
|
||||
(appData?.let(Paths::get) ?: userHome.resolve("AppData/Roaming")).resolve("Nuvio")
|
||||
}
|
||||
else -> {
|
||||
val xdgConfig = System.getenv("XDG_CONFIG_HOME")?.takeIf { it.isNotBlank() }
|
||||
(xdgConfig?.let(Paths::get) ?: userHome.resolve(".config")).resolve("nuvio")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class Store(
|
||||
private val file: Path,
|
||||
) {
|
||||
private val lock = Any()
|
||||
private val properties = Properties()
|
||||
private var loaded = false
|
||||
|
||||
fun contains(key: String): Boolean = synchronized(lock) {
|
||||
ensureLoaded()
|
||||
properties.containsKey(key)
|
||||
}
|
||||
|
||||
fun getString(key: String): String? = synchronized(lock) {
|
||||
ensureLoaded()
|
||||
properties.getProperty(key)
|
||||
}
|
||||
|
||||
fun putString(key: String, value: String?) = synchronized(lock) {
|
||||
ensureLoaded()
|
||||
if (value == null) {
|
||||
properties.remove(key)
|
||||
} else {
|
||||
properties.setProperty(key, value)
|
||||
}
|
||||
persist()
|
||||
}
|
||||
|
||||
fun getBoolean(key: String): Boolean? =
|
||||
getString(key)?.toBooleanStrictOrNull()
|
||||
|
||||
fun putBoolean(key: String, value: Boolean) {
|
||||
putString(key, value.toString())
|
||||
}
|
||||
|
||||
fun getInt(key: String): Int? =
|
||||
getString(key)?.toIntOrNull()
|
||||
|
||||
fun putInt(key: String, value: Int) {
|
||||
putString(key, value.toString())
|
||||
}
|
||||
|
||||
fun getFloat(key: String): Float? =
|
||||
getString(key)?.toFloatOrNull()
|
||||
|
||||
fun putFloat(key: String, value: Float) {
|
||||
putString(key, value.toString())
|
||||
}
|
||||
|
||||
fun getStringSet(key: String): Set<String>? =
|
||||
getString(key)?.let { payload ->
|
||||
runCatching { json.decodeFromString<List<String>>(payload).toSet() }.getOrNull()
|
||||
}
|
||||
|
||||
fun putStringSet(key: String, values: Set<String>) {
|
||||
putString(key, json.encodeToString(values.toList()))
|
||||
}
|
||||
|
||||
fun remove(key: String) = synchronized(lock) {
|
||||
ensureLoaded()
|
||||
properties.remove(key)
|
||||
persist()
|
||||
}
|
||||
|
||||
fun removeAll(keys: Iterable<String>) = synchronized(lock) {
|
||||
ensureLoaded()
|
||||
keys.forEach(properties::remove)
|
||||
persist()
|
||||
}
|
||||
|
||||
fun clearInMemory() = synchronized(lock) {
|
||||
properties.clear()
|
||||
loaded = false
|
||||
}
|
||||
|
||||
private fun ensureLoaded() {
|
||||
if (loaded) return
|
||||
loaded = true
|
||||
properties.clear()
|
||||
if (!file.exists()) return
|
||||
runCatching {
|
||||
Files.newInputStream(file).use { input ->
|
||||
properties.load(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun persist() {
|
||||
Files.createDirectories(file.parent)
|
||||
Files.newOutputStream(file).use { output ->
|
||||
properties.store(output, "Nuvio desktop preferences")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.nuvio.app.core.storage
|
||||
|
||||
internal actual object PlatformLocalAccountDataCleaner {
|
||||
actual fun wipe() {
|
||||
DesktopStorage.wipe()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.nuvio.app.core.sync
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
|
||||
internal actual object AppForegroundMonitor {
|
||||
actual fun events(): Flow<Unit> = emptyFlow()
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.ic_player_aspect_ratio
|
||||
import nuvio.composeapp.generated.resources.ic_player_audio_filled
|
||||
import nuvio.composeapp.generated.resources.ic_player_pause
|
||||
import nuvio.composeapp.generated.resources.ic_player_play
|
||||
import nuvio.composeapp.generated.resources.ic_player_subtitles
|
||||
import nuvio.composeapp.generated.resources.library_add_plus
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
@Composable
|
||||
actual fun appIconPainter(icon: AppIconResource): Painter =
|
||||
painterResource(
|
||||
when (icon) {
|
||||
AppIconResource.PlayerPlay -> Res.drawable.ic_player_play
|
||||
AppIconResource.PlayerPause -> Res.drawable.ic_player_pause
|
||||
AppIconResource.PlayerAspectRatio -> Res.drawable.ic_player_aspect_ratio
|
||||
AppIconResource.PlayerSubtitles -> Res.drawable.ic_player_subtitles
|
||||
AppIconResource.PlayerAudioFilled -> Res.drawable.ic_player_audio_filled
|
||||
AppIconResource.LibraryAddPlus -> Res.drawable.library_add_plus
|
||||
},
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
internal actual val nuvioPlatformExtraTopPadding: Dp = 0.dp
|
||||
internal actual val nuvioPlatformExtraBottomPadding: Dp = 0.dp
|
||||
internal actual val nuvioBottomNavigationExtraVerticalPadding: Dp = 0.dp
|
||||
|
||||
@Composable
|
||||
internal actual fun nuvioBottomNavigationBarInsets(): WindowInsets = WindowInsets(0.dp)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual fun PlatformBackHandler(
|
||||
enabled: Boolean,
|
||||
onBack: () -> Unit,
|
||||
) = Unit
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
actual fun platformExitApp() {
|
||||
exitProcess(0)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import coil3.ImageLoader
|
||||
|
||||
internal actual fun ImageLoader.Builder.configurePlatformImageLoader(): ImageLoader.Builder = this
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object PosterCardStyleStorage {
|
||||
private val store = DesktopStorage.store("nuvio_poster_card_style")
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
store.getString(ProfileScopedKey.of("poster_card_style"))
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString(ProfileScopedKey.of("poster_card_style"), payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.nuvio.app.features.addons
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.net.URI
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
import java.time.Duration
|
||||
|
||||
internal actual object AddonStorage {
|
||||
private val store = DesktopStorage.store("nuvio_addons")
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
actual fun loadInstalledAddonUrls(profileId: Int): List<String> =
|
||||
store.getString("installed_addon_urls_$profileId")
|
||||
?.let { payload -> runCatching { json.decodeFromString<List<String>>(payload) }.getOrNull() }
|
||||
?: emptyList()
|
||||
|
||||
actual fun saveInstalledAddonUrls(profileId: Int, urls: List<String>) {
|
||||
store.putString("installed_addon_urls_$profileId", json.encodeToString(urls))
|
||||
}
|
||||
|
||||
actual fun loadAddonEnabledStates(profileId: Int): Map<String, Boolean> =
|
||||
store.getString("addon_enabled_states_$profileId")
|
||||
?.let { payload -> runCatching { json.decodeFromString<Map<String, Boolean>>(payload) }.getOrNull() }
|
||||
?: emptyMap()
|
||||
|
||||
actual fun saveAddonEnabledStates(profileId: Int, states: Map<String, Boolean>) {
|
||||
store.putString("addon_enabled_states_$profileId", json.encodeToString(states))
|
||||
}
|
||||
}
|
||||
|
||||
private val desktopHttpClient: HttpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(30))
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.build()
|
||||
|
||||
actual suspend fun httpGetText(url: String): String =
|
||||
httpGetTextWithHeaders(url, emptyMap())
|
||||
|
||||
actual suspend fun httpPostJson(url: String, body: String): String =
|
||||
httpPostJsonWithHeaders(url, body, emptyMap())
|
||||
|
||||
actual suspend fun httpGetTextWithHeaders(
|
||||
url: String,
|
||||
headers: Map<String, String>,
|
||||
): String =
|
||||
httpRequestRaw("GET", url, headers, body = "").body
|
||||
|
||||
actual suspend fun httpPostJsonWithHeaders(
|
||||
url: String,
|
||||
body: String,
|
||||
headers: Map<String, String>,
|
||||
): String =
|
||||
httpRequestRaw(
|
||||
method = "POST",
|
||||
url = url,
|
||||
headers = mapOf("Content-Type" to "application/json") + headers,
|
||||
body = body,
|
||||
).body
|
||||
|
||||
actual suspend fun httpRequestRaw(
|
||||
method: String,
|
||||
url: String,
|
||||
headers: Map<String, String>,
|
||||
body: String,
|
||||
followRedirects: Boolean,
|
||||
): RawHttpResponse = withContext(Dispatchers.IO) {
|
||||
val client = if (followRedirects) {
|
||||
desktopHttpClient
|
||||
} else {
|
||||
HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(30))
|
||||
.followRedirects(HttpClient.Redirect.NEVER)
|
||||
.build()
|
||||
}
|
||||
val normalizedMethod = method.trim().uppercase().ifBlank { "GET" }
|
||||
val requestBuilder = HttpRequest.newBuilder()
|
||||
.uri(URI(url))
|
||||
.timeout(Duration.ofSeconds(60))
|
||||
.method(
|
||||
normalizedMethod,
|
||||
if (normalizedMethod == "GET" || normalizedMethod == "HEAD") {
|
||||
HttpRequest.BodyPublishers.noBody()
|
||||
} else {
|
||||
HttpRequest.BodyPublishers.ofString(body)
|
||||
},
|
||||
)
|
||||
|
||||
headers.forEach { (key, value) ->
|
||||
if (key.isNotBlank() && value.isNotBlank()) {
|
||||
requestBuilder.header(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
val response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
|
||||
RawHttpResponse(
|
||||
status = response.statusCode(),
|
||||
statusText = "HTTP ${response.statusCode()}",
|
||||
url = response.uri().toString(),
|
||||
body = response.body(),
|
||||
headers = response.headers().map().mapValues { (_, values) -> values.joinToString(",") },
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.collection
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object CollectionMobileSettingsStorage {
|
||||
private val store = DesktopStorage.store("nuvio_collection_mobile_settings")
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
store.getString(ProfileScopedKey.of("collection_mobile_settings"))
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString(ProfileScopedKey.of("collection_mobile_settings"), payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.collection
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object CollectionStorage {
|
||||
private val store = DesktopStorage.store("nuvio_collections")
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
store.getString(ProfileScopedKey.of("collections"))
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString(ProfileScopedKey.of("collections"), payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package com.nuvio.app.features.debrid
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
import com.nuvio.app.core.sync.decodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.decodeSyncInt
|
||||
import com.nuvio.app.core.sync.decodeSyncString
|
||||
import com.nuvio.app.core.sync.encodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.encodeSyncInt
|
||||
import com.nuvio.app.core.sync.encodeSyncString
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
internal actual object DebridSettingsStorage {
|
||||
private const val enabledKey = "debrid_enabled"
|
||||
private const val cloudLibraryEnabledKey = "debrid_cloud_library_enabled"
|
||||
private const val preferredResolverProviderIdKey = "debrid_preferred_resolver_provider_id"
|
||||
private const val torboxApiKeyKey = "debrid_torbox_api_key"
|
||||
private const val realDebridApiKeyKey = "debrid_real_debrid_api_key"
|
||||
private const val instantPlaybackPreparationLimitKey = "debrid_instant_playback_preparation_limit"
|
||||
private const val streamMaxResultsKey = "debrid_stream_max_results"
|
||||
private const val streamSortModeKey = "debrid_stream_sort_mode"
|
||||
private const val streamMinimumQualityKey = "debrid_stream_minimum_quality"
|
||||
private const val streamDolbyVisionFilterKey = "debrid_stream_dolby_vision_filter"
|
||||
private const val streamHdrFilterKey = "debrid_stream_hdr_filter"
|
||||
private const val streamCodecFilterKey = "debrid_stream_codec_filter"
|
||||
private const val streamPreferencesKey = "debrid_stream_preferences"
|
||||
private const val streamNameTemplateKey = "debrid_stream_name_template"
|
||||
private const val streamDescriptionTemplateKey = "debrid_stream_description_template"
|
||||
private val store = DesktopStorage.store("nuvio_debrid_settings")
|
||||
|
||||
actual fun loadEnabled(): Boolean? = loadBoolean(enabledKey)
|
||||
actual fun saveEnabled(enabled: Boolean) = saveBoolean(enabledKey, enabled)
|
||||
actual fun loadCloudLibraryEnabled(): Boolean? = loadBoolean(cloudLibraryEnabledKey)
|
||||
actual fun saveCloudLibraryEnabled(enabled: Boolean) = saveBoolean(cloudLibraryEnabledKey, enabled)
|
||||
actual fun loadPreferredResolverProviderId(): String? = loadString(preferredResolverProviderIdKey)
|
||||
actual fun savePreferredResolverProviderId(providerId: String) = saveString(preferredResolverProviderIdKey, providerId)
|
||||
actual fun loadProviderApiKey(providerId: String): String? = loadString(providerApiKeyKey(providerId))
|
||||
actual fun saveProviderApiKey(providerId: String, apiKey: String) = saveString(providerApiKeyKey(providerId), apiKey)
|
||||
actual fun loadTorboxApiKey(): String? = loadProviderApiKey(DebridProviders.TORBOX_ID)
|
||||
actual fun saveTorboxApiKey(apiKey: String) = saveProviderApiKey(DebridProviders.TORBOX_ID, apiKey)
|
||||
actual fun loadRealDebridApiKey(): String? = loadProviderApiKey(DebridProviders.REAL_DEBRID_ID)
|
||||
actual fun saveRealDebridApiKey(apiKey: String) = saveProviderApiKey(DebridProviders.REAL_DEBRID_ID, apiKey)
|
||||
actual fun loadInstantPlaybackPreparationLimit(): Int? = loadInt(instantPlaybackPreparationLimitKey)
|
||||
actual fun saveInstantPlaybackPreparationLimit(limit: Int) = saveInt(instantPlaybackPreparationLimitKey, limit)
|
||||
actual fun loadStreamMaxResults(): Int? = loadInt(streamMaxResultsKey)
|
||||
actual fun saveStreamMaxResults(maxResults: Int) = saveInt(streamMaxResultsKey, maxResults)
|
||||
actual fun loadStreamSortMode(): String? = loadString(streamSortModeKey)
|
||||
actual fun saveStreamSortMode(mode: String) = saveString(streamSortModeKey, mode)
|
||||
actual fun loadStreamMinimumQuality(): String? = loadString(streamMinimumQualityKey)
|
||||
actual fun saveStreamMinimumQuality(quality: String) = saveString(streamMinimumQualityKey, quality)
|
||||
actual fun loadStreamDolbyVisionFilter(): String? = loadString(streamDolbyVisionFilterKey)
|
||||
actual fun saveStreamDolbyVisionFilter(filter: String) = saveString(streamDolbyVisionFilterKey, filter)
|
||||
actual fun loadStreamHdrFilter(): String? = loadString(streamHdrFilterKey)
|
||||
actual fun saveStreamHdrFilter(filter: String) = saveString(streamHdrFilterKey, filter)
|
||||
actual fun loadStreamCodecFilter(): String? = loadString(streamCodecFilterKey)
|
||||
actual fun saveStreamCodecFilter(filter: String) = saveString(streamCodecFilterKey, filter)
|
||||
actual fun loadStreamPreferences(): String? = loadString(streamPreferencesKey)
|
||||
actual fun saveStreamPreferences(preferences: String) = saveString(streamPreferencesKey, preferences)
|
||||
actual fun loadStreamNameTemplate(): String? = loadString(streamNameTemplateKey)
|
||||
actual fun saveStreamNameTemplate(template: String) = saveString(streamNameTemplateKey, template)
|
||||
actual fun loadStreamDescriptionTemplate(): String? = loadString(streamDescriptionTemplateKey)
|
||||
actual fun saveStreamDescriptionTemplate(template: String) = saveString(streamDescriptionTemplateKey, template)
|
||||
|
||||
private fun loadBoolean(key: String): Boolean? = store.getBoolean(ProfileScopedKey.of(key))
|
||||
private fun saveBoolean(key: String, value: Boolean) = store.putBoolean(ProfileScopedKey.of(key), value)
|
||||
private fun loadInt(key: String): Int? = store.getInt(ProfileScopedKey.of(key))
|
||||
private fun saveInt(key: String, value: Int) = store.putInt(ProfileScopedKey.of(key), value)
|
||||
private fun loadString(key: String): String? = store.getString(ProfileScopedKey.of(key))
|
||||
private fun saveString(key: String, value: String) = store.putString(ProfileScopedKey.of(key), value)
|
||||
|
||||
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||
loadEnabled()?.let { put(enabledKey, encodeSyncBoolean(it)) }
|
||||
loadCloudLibraryEnabled()?.let { put(cloudLibraryEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadPreferredResolverProviderId()?.let { put(preferredResolverProviderIdKey, encodeSyncString(it)) }
|
||||
DebridProviders.all().forEach { provider ->
|
||||
loadProviderApiKey(provider.id)?.let { put(providerApiKeyKey(provider.id), encodeSyncString(it)) }
|
||||
}
|
||||
loadInstantPlaybackPreparationLimit()?.let { put(instantPlaybackPreparationLimitKey, encodeSyncInt(it)) }
|
||||
loadStreamMaxResults()?.let { put(streamMaxResultsKey, encodeSyncInt(it)) }
|
||||
loadStreamSortMode()?.let { put(streamSortModeKey, encodeSyncString(it)) }
|
||||
loadStreamMinimumQuality()?.let { put(streamMinimumQualityKey, encodeSyncString(it)) }
|
||||
loadStreamDolbyVisionFilter()?.let { put(streamDolbyVisionFilterKey, encodeSyncString(it)) }
|
||||
loadStreamHdrFilter()?.let { put(streamHdrFilterKey, encodeSyncString(it)) }
|
||||
loadStreamCodecFilter()?.let { put(streamCodecFilterKey, encodeSyncString(it)) }
|
||||
loadStreamPreferences()?.let { put(streamPreferencesKey, encodeSyncString(it)) }
|
||||
loadStreamNameTemplate()?.let { put(streamNameTemplateKey, encodeSyncString(it)) }
|
||||
loadStreamDescriptionTemplate()?.let { put(streamDescriptionTemplateKey, encodeSyncString(it)) }
|
||||
}
|
||||
|
||||
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||
store.removeAll(syncKeys().map(ProfileScopedKey::of))
|
||||
payload.decodeSyncBoolean(enabledKey)?.let(::saveEnabled)
|
||||
payload.decodeSyncBoolean(cloudLibraryEnabledKey)?.let(::saveCloudLibraryEnabled)
|
||||
payload.decodeSyncString(preferredResolverProviderIdKey)?.let(::savePreferredResolverProviderId)
|
||||
DebridProviders.all().forEach { provider ->
|
||||
payload.decodeSyncString(providerApiKeyKey(provider.id))?.let { saveProviderApiKey(provider.id, it) }
|
||||
}
|
||||
payload.decodeSyncInt(instantPlaybackPreparationLimitKey)?.let(::saveInstantPlaybackPreparationLimit)
|
||||
payload.decodeSyncInt(streamMaxResultsKey)?.let(::saveStreamMaxResults)
|
||||
payload.decodeSyncString(streamSortModeKey)?.let(::saveStreamSortMode)
|
||||
payload.decodeSyncString(streamMinimumQualityKey)?.let(::saveStreamMinimumQuality)
|
||||
payload.decodeSyncString(streamDolbyVisionFilterKey)?.let(::saveStreamDolbyVisionFilter)
|
||||
payload.decodeSyncString(streamHdrFilterKey)?.let(::saveStreamHdrFilter)
|
||||
payload.decodeSyncString(streamCodecFilterKey)?.let(::saveStreamCodecFilter)
|
||||
payload.decodeSyncString(streamPreferencesKey)?.let(::saveStreamPreferences)
|
||||
payload.decodeSyncString(streamNameTemplateKey)?.let(::saveStreamNameTemplate)
|
||||
payload.decodeSyncString(streamDescriptionTemplateKey)?.let(::saveStreamDescriptionTemplate)
|
||||
}
|
||||
|
||||
private fun syncKeys(): List<String> =
|
||||
listOf(
|
||||
enabledKey,
|
||||
cloudLibraryEnabledKey,
|
||||
preferredResolverProviderIdKey,
|
||||
instantPlaybackPreparationLimitKey,
|
||||
streamMaxResultsKey,
|
||||
streamSortModeKey,
|
||||
streamMinimumQualityKey,
|
||||
streamDolbyVisionFilterKey,
|
||||
streamHdrFilterKey,
|
||||
streamCodecFilterKey,
|
||||
streamPreferencesKey,
|
||||
streamNameTemplateKey,
|
||||
streamDescriptionTemplateKey,
|
||||
) + DebridProviders.all().map { providerApiKeyKey(it.id) }
|
||||
|
||||
private fun providerApiKeyKey(providerId: String): String {
|
||||
val normalized = DebridProviders.byId(providerId)?.id
|
||||
?: providerId.trim().lowercase().replace(Regex("[^a-z0-9_]+"), "_")
|
||||
return when (normalized) {
|
||||
DebridProviders.TORBOX_ID -> torboxApiKeyKey
|
||||
DebridProviders.REAL_DEBRID_ID -> realDebridApiKeyKey
|
||||
else -> "debrid_${normalized}_api_key"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.details
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object MetaScreenSettingsStorage {
|
||||
private val store = DesktopStorage.store("nuvio_meta_screen_settings")
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
store.getString(ProfileScopedKey.of("meta_screen_settings"))
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString(ProfileScopedKey.of("meta_screen_settings"), payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.details
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object SeasonViewModeStorage {
|
||||
private val store = DesktopStorage.store("nuvio_season_view_mode")
|
||||
|
||||
actual fun load(): SeasonViewMode? =
|
||||
SeasonViewMode.parse(store.getString(ProfileScopedKey.of("season_view_mode")))
|
||||
|
||||
actual fun save(mode: SeasonViewMode) {
|
||||
store.putString(ProfileScopedKey.of("season_view_mode"), SeasonViewMode.persist(mode))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.nuvio.app.features.details.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
actual fun HeroTrailerPlayerSurface(
|
||||
sourceUrl: String,
|
||||
sourceAudioUrl: String?,
|
||||
playWhenReady: Boolean,
|
||||
muted: Boolean,
|
||||
modifier: Modifier,
|
||||
onReady: () -> Unit,
|
||||
onEnded: () -> Unit,
|
||||
onError: () -> Unit,
|
||||
) {
|
||||
LaunchedEffect(sourceUrl) {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.nuvio.app.features.downloads
|
||||
|
||||
internal actual object DownloadsClock {
|
||||
actual fun nowEpochMs(): Long = System.currentTimeMillis()
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.nuvio.app.features.downloads
|
||||
|
||||
internal actual object DownloadsLiveStatusPlatform {
|
||||
actual fun onItemsChanged(items: List<DownloadItem>) = Unit
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
package com.nuvio.app.features.downloads
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.URI
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
import java.time.Duration
|
||||
import kotlin.io.path.createDirectories
|
||||
|
||||
private val desktopDownloadHttpClient: HttpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(60))
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.build()
|
||||
|
||||
internal actual object DownloadsPlatformDownloader {
|
||||
private val downloadsDir: File
|
||||
get() = File(DesktopStorage.rootDir.resolve("downloads").also { it.createDirectories() }.toUri())
|
||||
|
||||
actual fun start(
|
||||
request: DownloadPlatformRequest,
|
||||
onProgress: (downloadedBytes: Long, totalBytes: Long?) -> Unit,
|
||||
onSuccess: (localFileUri: String, totalBytes: Long?) -> Unit,
|
||||
onFailure: (message: String) -> Unit,
|
||||
): DownloadsTaskHandle {
|
||||
val job = SupervisorJob()
|
||||
val scope = CoroutineScope(job + Dispatchers.IO)
|
||||
|
||||
scope.launch {
|
||||
val destination = File(downloadsDir, request.destinationFileName)
|
||||
val tempFile = File(downloadsDir, "${request.destinationFileName}.part")
|
||||
|
||||
try {
|
||||
var resumeFromBytes = tempFile.takeIf { it.exists() }?.length()?.coerceAtLeast(0L) ?: 0L
|
||||
var attemptedRangeRequest = resumeFromBytes > 0L
|
||||
var response = sendDownloadRequest(request, if (attemptedRangeRequest) resumeFromBytes else null)
|
||||
|
||||
if (attemptedRangeRequest && response.statusCode() == 416) {
|
||||
tempFile.delete()
|
||||
resumeFromBytes = 0L
|
||||
attemptedRangeRequest = false
|
||||
response = sendDownloadRequest(request, null)
|
||||
}
|
||||
|
||||
if (response.statusCode() !in 200..299) {
|
||||
error("Download failed with HTTP ${response.statusCode()}")
|
||||
}
|
||||
|
||||
val isPartialResume = attemptedRangeRequest && response.statusCode() == 206 && resumeFromBytes > 0L
|
||||
val appendToTemp = isPartialResume
|
||||
val startingBytes = if (appendToTemp) resumeFromBytes else 0L
|
||||
if (!appendToTemp && tempFile.exists()) {
|
||||
tempFile.delete()
|
||||
}
|
||||
|
||||
val totalBytes = resolveTotalBytes(
|
||||
startingBytes = startingBytes,
|
||||
isPartialResume = isPartialResume,
|
||||
contentRangeHeader = response.headers().firstValue("Content-Range").orElse(null),
|
||||
contentLength = response.headers().firstValue("Content-Length").orElse(null)?.toLongOrNull(),
|
||||
)
|
||||
var downloadedBytes = startingBytes
|
||||
onProgress(downloadedBytes, totalBytes)
|
||||
|
||||
response.body().use { input ->
|
||||
FileOutputStream(tempFile, appendToTemp).use { output ->
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
while (true) {
|
||||
ensureActive()
|
||||
val read = input.read(buffer)
|
||||
if (read <= 0) break
|
||||
output.write(buffer, 0, read)
|
||||
downloadedBytes += read.toLong()
|
||||
onProgress(downloadedBytes, totalBytes)
|
||||
}
|
||||
output.flush()
|
||||
}
|
||||
}
|
||||
|
||||
if (destination.exists()) {
|
||||
destination.delete()
|
||||
}
|
||||
if (!tempFile.renameTo(destination)) {
|
||||
tempFile.copyTo(destination, overwrite = true)
|
||||
tempFile.delete()
|
||||
}
|
||||
|
||||
val finalSize = destination.length()
|
||||
onSuccess(destination.toURI().toString(), totalBytes ?: finalSize)
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
onFailure(error.message ?: "Download failed")
|
||||
}
|
||||
}
|
||||
|
||||
return DesktopDownloadsTaskHandle(job)
|
||||
}
|
||||
|
||||
actual fun removeFile(localFileUri: String?): Boolean {
|
||||
if (localFileUri.isNullOrBlank()) return false
|
||||
val file = localFileUri.toLocalFileOrNull() ?: return false
|
||||
return runCatching { file.delete() }.getOrDefault(false)
|
||||
}
|
||||
|
||||
actual fun removePartialFile(destinationFileName: String): Boolean {
|
||||
val tempFile = File(downloadsDir, "$destinationFileName.part")
|
||||
if (!tempFile.exists()) return true
|
||||
return runCatching { tempFile.delete() }.getOrDefault(false)
|
||||
}
|
||||
|
||||
actual fun resolveLocalFileUri(localFileUri: String?, destinationFileName: String): String? {
|
||||
localFileUri
|
||||
?.toLocalFileOrNull()
|
||||
?.takeIf { it.exists() }
|
||||
?.let { return it.toURI().toString() }
|
||||
|
||||
val fileName = destinationFileName.trim().takeIf { it.isNotBlank() }
|
||||
?: localFileUri?.toLocalFileOrNull()?.name?.takeIf { it.isNotBlank() }
|
||||
?: return null
|
||||
return File(downloadsDir, fileName).takeIf { it.exists() }?.toURI()?.toString()
|
||||
}
|
||||
|
||||
private fun sendDownloadRequest(
|
||||
request: DownloadPlatformRequest,
|
||||
rangeStart: Long?,
|
||||
): HttpResponse<java.io.InputStream> {
|
||||
val builder = HttpRequest.newBuilder()
|
||||
.uri(URI(request.sourceUrl))
|
||||
.timeout(Duration.ofSeconds(60))
|
||||
.GET()
|
||||
request.sourceHeaders.forEach { (key, value) ->
|
||||
if (key.isNotBlank() && value.isNotBlank()) {
|
||||
builder.header(key, value)
|
||||
}
|
||||
}
|
||||
if (rangeStart != null && rangeStart > 0L) {
|
||||
builder.header("Range", "bytes=$rangeStart-")
|
||||
}
|
||||
return desktopDownloadHttpClient.send(builder.build(), HttpResponse.BodyHandlers.ofInputStream())
|
||||
}
|
||||
}
|
||||
|
||||
private class DesktopDownloadsTaskHandle(
|
||||
private val job: Job,
|
||||
) : DownloadsTaskHandle {
|
||||
override fun cancel() {
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toLocalFileOrNull(): File? =
|
||||
runCatching {
|
||||
if (startsWith("file:")) {
|
||||
File(URI(this))
|
||||
} else {
|
||||
File(this)
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
private fun resolveTotalBytes(
|
||||
startingBytes: Long,
|
||||
isPartialResume: Boolean,
|
||||
contentRangeHeader: String?,
|
||||
contentLength: Long?,
|
||||
): Long? {
|
||||
parseContentRangeTotal(contentRangeHeader)?.let { return it }
|
||||
val normalizedLength = contentLength?.takeIf { it > 0L } ?: return null
|
||||
return if (isPartialResume && startingBytes > 0L) {
|
||||
startingBytes + normalizedLength
|
||||
} else {
|
||||
normalizedLength
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseContentRangeTotal(headerValue: String?): Long? {
|
||||
val value = headerValue?.trim().orEmpty()
|
||||
if (value.isBlank()) return null
|
||||
val slashIndex = value.lastIndexOf('/')
|
||||
if (slashIndex == -1 || slashIndex == value.lastIndex) return null
|
||||
val totalPart = value.substring(slashIndex + 1).trim()
|
||||
if (totalPart == "*") return null
|
||||
return totalPart.toLongOrNull()?.takeIf { it > 0L }
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.downloads
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object DownloadsStorage {
|
||||
private val store = DesktopStorage.store("nuvio_downloads")
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
store.getString(ProfileScopedKey.of("downloads"))
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString(ProfileScopedKey.of("downloads"), payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.home
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object HomeCatalogSettingsStorage {
|
||||
private val store = DesktopStorage.store("nuvio_home_catalog_settings")
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
store.getString(ProfileScopedKey.of("home_catalog_settings"))
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString(ProfileScopedKey.of("home_catalog_settings"), payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.nuvio.app.features.home.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.LocalPlatformContext
|
||||
import coil3.request.ImageRequest
|
||||
|
||||
@Composable
|
||||
internal actual fun CollectionCardRemoteImage(
|
||||
imageUrl: String,
|
||||
contentDescription: String,
|
||||
modifier: Modifier,
|
||||
contentScale: ContentScale,
|
||||
animateIfPossible: Boolean,
|
||||
) {
|
||||
val context = LocalPlatformContext.current
|
||||
val request = remember(context, imageUrl) {
|
||||
ImageRequest.Builder(context)
|
||||
.data(imageUrl)
|
||||
.memoryCacheKey("home-collection:$imageUrl")
|
||||
.diskCacheKey(imageUrl)
|
||||
.build()
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = request,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
contentScale = contentScale,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
internal actual object LibraryClock {
|
||||
actual fun nowEpochMs(): Long = System.currentTimeMillis()
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
|
||||
internal actual object LibraryStorage {
|
||||
private val store = DesktopStorage.store("nuvio_library")
|
||||
|
||||
actual fun loadPayload(profileId: Int): String? =
|
||||
store.getString("library_$profileId")
|
||||
|
||||
actual fun savePayload(profileId: Int, payload: String) {
|
||||
store.putString("library_$profileId", payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.nuvio.app.features.mdblist
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
import com.nuvio.app.core.sync.decodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.decodeSyncString
|
||||
import com.nuvio.app.core.sync.encodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.encodeSyncString
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
internal actual object MdbListSettingsStorage {
|
||||
private const val enabledKey = "mdblist_enabled"
|
||||
private const val apiKey = "mdblist_api_key"
|
||||
private const val useImdbKey = "mdblist_use_imdb"
|
||||
private const val useTmdbKey = "mdblist_use_tmdb"
|
||||
private const val useTomatoesKey = "mdblist_use_tomatoes"
|
||||
private const val useMetacriticKey = "mdblist_use_metacritic"
|
||||
private const val useTraktKey = "mdblist_use_trakt"
|
||||
private const val useLetterboxdKey = "mdblist_use_letterboxd"
|
||||
private const val useAudienceKey = "mdblist_use_audience"
|
||||
private val syncKeys = listOf(
|
||||
enabledKey,
|
||||
apiKey,
|
||||
useImdbKey,
|
||||
useTmdbKey,
|
||||
useTomatoesKey,
|
||||
useMetacriticKey,
|
||||
useTraktKey,
|
||||
useLetterboxdKey,
|
||||
useAudienceKey,
|
||||
)
|
||||
private val store = DesktopStorage.store("nuvio_mdblist_settings")
|
||||
|
||||
actual fun loadEnabled(): Boolean? = loadBoolean(enabledKey)
|
||||
actual fun saveEnabled(enabled: Boolean) = saveBoolean(enabledKey, enabled)
|
||||
actual fun loadApiKey(): String? = loadString(apiKey)
|
||||
actual fun saveApiKey(apiKey: String) = saveString(this.apiKey, apiKey)
|
||||
actual fun loadUseImdb(): Boolean? = loadBoolean(useImdbKey)
|
||||
actual fun saveUseImdb(enabled: Boolean) = saveBoolean(useImdbKey, enabled)
|
||||
actual fun loadUseTmdb(): Boolean? = loadBoolean(useTmdbKey)
|
||||
actual fun saveUseTmdb(enabled: Boolean) = saveBoolean(useTmdbKey, enabled)
|
||||
actual fun loadUseTomatoes(): Boolean? = loadBoolean(useTomatoesKey)
|
||||
actual fun saveUseTomatoes(enabled: Boolean) = saveBoolean(useTomatoesKey, enabled)
|
||||
actual fun loadUseMetacritic(): Boolean? = loadBoolean(useMetacriticKey)
|
||||
actual fun saveUseMetacritic(enabled: Boolean) = saveBoolean(useMetacriticKey, enabled)
|
||||
actual fun loadUseTrakt(): Boolean? = loadBoolean(useTraktKey)
|
||||
actual fun saveUseTrakt(enabled: Boolean) = saveBoolean(useTraktKey, enabled)
|
||||
actual fun loadUseLetterboxd(): Boolean? = loadBoolean(useLetterboxdKey)
|
||||
actual fun saveUseLetterboxd(enabled: Boolean) = saveBoolean(useLetterboxdKey, enabled)
|
||||
actual fun loadUseAudience(): Boolean? = loadBoolean(useAudienceKey)
|
||||
actual fun saveUseAudience(enabled: Boolean) = saveBoolean(useAudienceKey, enabled)
|
||||
|
||||
private fun loadString(key: String): String? = store.getString(ProfileScopedKey.of(key))
|
||||
private fun saveString(key: String, value: String) = store.putString(ProfileScopedKey.of(key), value)
|
||||
private fun loadBoolean(key: String): Boolean? = store.getBoolean(ProfileScopedKey.of(key))
|
||||
private fun saveBoolean(key: String, value: Boolean) = store.putBoolean(ProfileScopedKey.of(key), value)
|
||||
|
||||
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||
loadEnabled()?.let { put(enabledKey, encodeSyncBoolean(it)) }
|
||||
loadApiKey()?.let { put(apiKey, encodeSyncString(it)) }
|
||||
loadUseImdb()?.let { put(useImdbKey, encodeSyncBoolean(it)) }
|
||||
loadUseTmdb()?.let { put(useTmdbKey, encodeSyncBoolean(it)) }
|
||||
loadUseTomatoes()?.let { put(useTomatoesKey, encodeSyncBoolean(it)) }
|
||||
loadUseMetacritic()?.let { put(useMetacriticKey, encodeSyncBoolean(it)) }
|
||||
loadUseTrakt()?.let { put(useTraktKey, encodeSyncBoolean(it)) }
|
||||
loadUseLetterboxd()?.let { put(useLetterboxdKey, encodeSyncBoolean(it)) }
|
||||
loadUseAudience()?.let { put(useAudienceKey, encodeSyncBoolean(it)) }
|
||||
}
|
||||
|
||||
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||
store.removeAll(syncKeys.map(ProfileScopedKey::of))
|
||||
payload.decodeSyncBoolean(enabledKey)?.let(::saveEnabled)
|
||||
payload.decodeSyncString(apiKey)?.let(::saveApiKey)
|
||||
payload.decodeSyncBoolean(useImdbKey)?.let(::saveUseImdb)
|
||||
payload.decodeSyncBoolean(useTmdbKey)?.let(::saveUseTmdb)
|
||||
payload.decodeSyncBoolean(useTomatoesKey)?.let(::saveUseTomatoes)
|
||||
payload.decodeSyncBoolean(useMetacriticKey)?.let(::saveUseMetacritic)
|
||||
payload.decodeSyncBoolean(useTraktKey)?.let(::saveUseTrakt)
|
||||
payload.decodeSyncBoolean(useLetterboxdKey)?.let(::saveUseLetterboxd)
|
||||
payload.decodeSyncBoolean(useAudienceKey)?.let(::saveUseAudience)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.notifications
|
||||
|
||||
internal actual object EpisodeReleaseNotificationPlatform {
|
||||
actual suspend fun notificationsAuthorized(): Boolean = false
|
||||
|
||||
actual suspend fun requestAuthorization(): Boolean = false
|
||||
|
||||
actual suspend fun scheduleEpisodeReleaseNotifications(
|
||||
requests: List<EpisodeReleaseNotificationRequest>,
|
||||
) = Unit
|
||||
|
||||
actual suspend fun clearScheduledEpisodeReleaseNotifications() = Unit
|
||||
|
||||
actual suspend fun showTestNotification(request: EpisodeReleaseNotificationRequest) = Unit
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.notifications
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
internal actual object EpisodeReleaseNotificationsClock {
|
||||
private val formatter = DateTimeFormatter.ISO_LOCAL_DATE
|
||||
|
||||
actual fun isoDateFromEpochMs(epochMs: Long): String =
|
||||
Instant.ofEpochMilli(epochMs)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate()
|
||||
.format(formatter)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.notifications
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object EpisodeReleaseNotificationsStorage {
|
||||
private val store = DesktopStorage.store("nuvio_episode_release_notifications")
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
store.getString(ProfileScopedKey.of("episode_release_notifications"))
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString(ProfileScopedKey.of("episode_release_notifications"), payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.nuvio.app.features.p2p
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object P2pSettingsStorage {
|
||||
private const val p2pEnabledKey = "p2p_enabled"
|
||||
private const val enableUploadKey = "enable_upload"
|
||||
private const val hideTorrentStatsKey = "hide_torrent_stats"
|
||||
private val store = DesktopStorage.store("torrent_settings")
|
||||
|
||||
actual fun loadP2pEnabled(): Boolean? = loadBoolean(p2pEnabledKey)
|
||||
actual fun saveP2pEnabled(enabled: Boolean) = saveBoolean(p2pEnabledKey, enabled)
|
||||
actual fun loadEnableUpload(): Boolean? = loadBoolean(enableUploadKey)
|
||||
actual fun saveEnableUpload(enabled: Boolean) = saveBoolean(enableUploadKey, enabled)
|
||||
actual fun loadHideTorrentStats(): Boolean? = loadBoolean(hideTorrentStatsKey)
|
||||
actual fun saveHideTorrentStats(enabled: Boolean) = saveBoolean(hideTorrentStatsKey, enabled)
|
||||
|
||||
private fun loadBoolean(key: String): Boolean? = store.getBoolean(ProfileScopedKey.of(key))
|
||||
private fun saveBoolean(key: String, value: Boolean) = store.putBoolean(ProfileScopedKey.of(key), value)
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.nuvio.app.features.p2p
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
actual object P2pStreamingEngine {
|
||||
private val _state = MutableStateFlow<P2pStreamingState>(P2pStreamingState.Idle)
|
||||
|
||||
actual val state: StateFlow<P2pStreamingState> = _state.asStateFlow()
|
||||
|
||||
actual fun warmup() = Unit
|
||||
|
||||
actual fun cooldownWarmup() = Unit
|
||||
|
||||
actual suspend fun startStream(request: P2pStreamRequest): String {
|
||||
_state.value = P2pStreamingState.Error("P2P streaming is not available on desktop yet.")
|
||||
throw P2pStreamingException("P2P streaming is not available on desktop yet.")
|
||||
}
|
||||
|
||||
actual fun stopStream() {
|
||||
_state.value = P2pStreamingState.Idle
|
||||
}
|
||||
|
||||
actual fun shutdown() {
|
||||
_state.value = P2pStreamingState.Idle
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual fun rememberExternalPlayerLauncher(
|
||||
onResult: (ExternalPlaybackResult?) -> Unit,
|
||||
): (ExternalPlayerIntentResult.Success) -> Boolean =
|
||||
{ intentResult ->
|
||||
val launched = ExternalPlayerPlatform.launch(intentResult.intent)
|
||||
if (launched) {
|
||||
onResult(null)
|
||||
}
|
||||
launched
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import java.awt.Desktop
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
|
||||
private data class DesktopExternalPlayerIntent(
|
||||
val request: ExternalPlayerPlaybackRequest,
|
||||
val playerId: String?,
|
||||
)
|
||||
|
||||
internal actual object ExternalPlayerPlatform {
|
||||
private const val systemPlayerId = "system"
|
||||
|
||||
actual fun defaultPlayerId(): String? = systemPlayerId
|
||||
|
||||
actual fun availablePlayers(): List<ExternalPlayerApp> =
|
||||
listOf(ExternalPlayerApp(systemPlayerId, "System default"))
|
||||
|
||||
actual fun open(
|
||||
request: ExternalPlayerPlaybackRequest,
|
||||
playerId: String?,
|
||||
): ExternalPlayerOpenResult =
|
||||
if (openUri(request.sourceUrl)) {
|
||||
ExternalPlayerOpenResult.Opened
|
||||
} else {
|
||||
ExternalPlayerOpenResult.Failed
|
||||
}
|
||||
|
||||
actual fun buildIntent(
|
||||
request: ExternalPlayerPlaybackRequest,
|
||||
playerId: String?,
|
||||
): ExternalPlayerIntentResult =
|
||||
ExternalPlayerIntentResult.Success(DesktopExternalPlayerIntent(request, playerId))
|
||||
|
||||
internal fun launch(intent: Any): Boolean {
|
||||
val desktopIntent = intent as? DesktopExternalPlayerIntent ?: return false
|
||||
return open(desktopIntent.request, desktopIntent.playerId) == ExternalPlayerOpenResult.Opened
|
||||
}
|
||||
|
||||
private fun openUri(rawUri: String): Boolean {
|
||||
val uri = runCatching { URI(rawUri) }.getOrNull() ?: return false
|
||||
val desktop = runCatching { Desktop.getDesktop() }.getOrNull()
|
||||
|
||||
if (desktop != null && Desktop.isDesktopSupported()) {
|
||||
val opened = runCatching {
|
||||
if (uri.scheme.equals("file", ignoreCase = true)) {
|
||||
desktop.open(File(uri))
|
||||
} else {
|
||||
desktop.browse(uri)
|
||||
}
|
||||
}.isSuccess
|
||||
if (opened) return true
|
||||
}
|
||||
|
||||
return openWithPlatformCommand(rawUri)
|
||||
}
|
||||
|
||||
private fun openWithPlatformCommand(rawUri: String): Boolean {
|
||||
val osName = System.getProperty("os.name").orEmpty().lowercase()
|
||||
val command = when {
|
||||
osName.contains("mac") -> listOf("open", rawUri)
|
||||
osName.contains("win") -> listOf("rundll32", "url.dll,FileProtocolHandler", rawUri)
|
||||
else -> listOf("xdg-open", rawUri)
|
||||
}
|
||||
return runCatching { ProcessBuilder(command).start() }.isSuccess
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Composable
|
||||
actual fun PlatformPlayerSurface(
|
||||
sourceUrl: String,
|
||||
sourceAudioUrl: String?,
|
||||
sourceHeaders: Map<String, String>,
|
||||
sourceResponseHeaders: Map<String, String>,
|
||||
useYoutubeChunkedPlayback: Boolean,
|
||||
modifier: Modifier,
|
||||
playWhenReady: Boolean,
|
||||
resizeMode: PlayerResizeMode,
|
||||
useNativeController: Boolean,
|
||||
onControllerReady: (PlayerEngineController) -> Unit,
|
||||
onSnapshot: (PlayerPlaybackSnapshot) -> Unit,
|
||||
onError: (String?) -> Unit,
|
||||
) {
|
||||
val controller = remember { DesktopStubPlayerController() }
|
||||
|
||||
LaunchedEffect(controller) {
|
||||
onControllerReady(controller)
|
||||
onSnapshot(PlayerPlaybackSnapshot(isLoading = false))
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "Desktop in-app playback is not available yet.",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class DesktopStubPlayerController : PlayerEngineController {
|
||||
override fun play() = Unit
|
||||
override fun pause() = Unit
|
||||
override fun seekTo(positionMs: Long) = Unit
|
||||
override fun seekBy(offsetMs: Long) = Unit
|
||||
override fun retry() = Unit
|
||||
override fun setPlaybackSpeed(speed: Float) = Unit
|
||||
override fun getAudioTracks(): List<AudioTrack> = emptyList()
|
||||
override fun getSubtitleTracks(): List<SubtitleTrack> = emptyList()
|
||||
override fun selectAudioTrack(index: Int) = Unit
|
||||
override fun selectSubtitleTrack(index: Int) = Unit
|
||||
override fun setSubtitleUri(url: String) = Unit
|
||||
override fun clearExternalSubtitle() = Unit
|
||||
override fun clearExternalSubtitleAndSelect(trackIndex: Int) = Unit
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
internal actual object DeviceLanguagePreferences {
|
||||
actual fun preferredLanguageCodes(): List<String> =
|
||||
Locale.getDefault()
|
||||
.toLanguageTag()
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let(::listOf)
|
||||
?: emptyList()
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
|
||||
@Composable
|
||||
actual fun LockPlayerToLandscape() = Unit
|
||||
|
||||
@Composable
|
||||
actual fun EnterImmersivePlayerMode(keepScreenAwake: Boolean) = Unit
|
||||
|
||||
@Composable
|
||||
actual fun ManagePlayerPictureInPicture(
|
||||
isPlaying: Boolean,
|
||||
playerSize: IntSize,
|
||||
) = Unit
|
||||
|
||||
@Composable
|
||||
actual fun rememberPlayerGestureController(): PlayerGestureController? = null
|
||||
|
|
@ -0,0 +1,406 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
import com.nuvio.app.core.sync.decodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.decodeSyncFloat
|
||||
import com.nuvio.app.core.sync.decodeSyncInt
|
||||
import com.nuvio.app.core.sync.decodeSyncString
|
||||
import com.nuvio.app.core.sync.decodeSyncStringSet
|
||||
import com.nuvio.app.core.sync.encodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.encodeSyncFloat
|
||||
import com.nuvio.app.core.sync.encodeSyncInt
|
||||
import com.nuvio.app.core.sync.encodeSyncString
|
||||
import com.nuvio.app.core.sync.encodeSyncStringSet
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
internal actual object PlayerSettingsStorage {
|
||||
private const val showLoadingOverlayKey = "show_loading_overlay"
|
||||
private const val resizeModeKey = "resize_mode"
|
||||
private const val holdToSpeedEnabledKey = "hold_to_speed_enabled"
|
||||
private const val holdToSpeedValueKey = "hold_to_speed_value"
|
||||
private const val externalPlayerEnabledKey = "external_player_enabled"
|
||||
private const val externalPlayerForwardSubtitlesKey = "external_player_forward_subtitles"
|
||||
private const val externalPlayerIdKey = "external_player_id"
|
||||
private const val preferredAudioLanguageKey = "preferred_audio_language"
|
||||
private const val secondaryPreferredAudioLanguageKey = "secondary_preferred_audio_language"
|
||||
private const val preferredSubtitleLanguageKey = "preferred_subtitle_language"
|
||||
private const val secondaryPreferredSubtitleLanguageKey = "secondary_preferred_subtitle_language"
|
||||
private const val subtitleTextColorKey = "subtitle_text_color"
|
||||
private const val subtitleBackgroundColorKey = "subtitle_background_color"
|
||||
private const val subtitleOutlineColorKey = "subtitle_outline_color"
|
||||
private const val subtitleOutlineEnabledKey = "subtitle_outline_enabled"
|
||||
private const val subtitleOutlineWidthKey = "subtitle_outline_width"
|
||||
private const val subtitleBoldKey = "subtitle_bold"
|
||||
private const val subtitleFontSizeSpKey = "subtitle_font_size_sp"
|
||||
private const val subtitleBottomOffsetKey = "subtitle_bottom_offset"
|
||||
private const val subtitleUseForcedSubtitlesKey = "subtitle_use_forced_subtitles"
|
||||
private const val subtitleShowOnlyPreferredLanguagesKey = "subtitle_show_only_preferred_languages"
|
||||
private const val addonSubtitleStartupModeKey = "addon_subtitle_startup_mode"
|
||||
private const val streamReuseLastLinkEnabledKey = "stream_reuse_last_link_enabled"
|
||||
private const val streamReuseLastLinkCacheHoursKey = "stream_reuse_last_link_cache_hours"
|
||||
private const val decoderPriorityKey = "decoder_priority"
|
||||
private const val mapDV7ToHevcKey = "map_dv7_to_hevc"
|
||||
private const val tunnelingEnabledKey = "tunneling_enabled"
|
||||
private const val streamAutoPlayModeKey = "stream_auto_play_mode"
|
||||
private const val streamAutoPlaySourceKey = "stream_auto_play_source"
|
||||
private const val streamAutoPlaySelectedAddonsKey = "stream_auto_play_selected_addons"
|
||||
private const val streamAutoPlaySelectedPluginsKey = "stream_auto_play_selected_plugins"
|
||||
private const val streamAutoPlayRegexKey = "stream_auto_play_regex"
|
||||
private const val streamAutoPlayTimeoutSecondsKey = "stream_auto_play_timeout_seconds"
|
||||
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 streamAutoPlayReuseBingeGroupKey = "stream_auto_play_reuse_binge_group"
|
||||
private const val nextEpisodeThresholdModeKey = "next_episode_threshold_mode"
|
||||
private const val nextEpisodeThresholdPercentKey = "next_episode_threshold_percent_v2"
|
||||
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
|
||||
private const val useLibassKey = "use_libass"
|
||||
private const val libassRenderTypeKey = "libass_render_type"
|
||||
private const val iosVideoOutputPresetKey = "ios_video_output_preset"
|
||||
private const val iosToneMappingModeKey = "ios_tone_mapping_mode"
|
||||
private const val iosTargetPrimariesKey = "ios_target_primaries"
|
||||
private const val iosTargetTransferKey = "ios_target_transfer"
|
||||
private const val iosHardwareDecoderModeKey = "ios_hardware_decoder_mode"
|
||||
private const val iosAudioOutputModeKey = "ios_audio_output_mode"
|
||||
private const val iosExtendedDynamicRangeEnabledKey = "ios_extended_dynamic_range_enabled"
|
||||
private const val iosTargetColorspaceHintEnabledKey = "ios_target_colorspace_hint_enabled"
|
||||
private const val iosHdrComputePeakEnabledKey = "ios_hdr_compute_peak_enabled"
|
||||
private const val iosDebandEnabledKey = "ios_deband_enabled"
|
||||
private const val iosInterpolationEnabledKey = "ios_interpolation_enabled"
|
||||
private const val iosBrightnessKey = "ios_brightness"
|
||||
private const val iosContrastKey = "ios_contrast"
|
||||
private const val iosSaturationKey = "ios_saturation"
|
||||
private const val iosGammaKey = "ios_gamma"
|
||||
private val syncKeys = listOf(
|
||||
showLoadingOverlayKey,
|
||||
resizeModeKey,
|
||||
holdToSpeedEnabledKey,
|
||||
holdToSpeedValueKey,
|
||||
externalPlayerEnabledKey,
|
||||
externalPlayerForwardSubtitlesKey,
|
||||
externalPlayerIdKey,
|
||||
preferredAudioLanguageKey,
|
||||
secondaryPreferredAudioLanguageKey,
|
||||
preferredSubtitleLanguageKey,
|
||||
secondaryPreferredSubtitleLanguageKey,
|
||||
subtitleTextColorKey,
|
||||
subtitleBackgroundColorKey,
|
||||
subtitleOutlineColorKey,
|
||||
subtitleOutlineEnabledKey,
|
||||
subtitleOutlineWidthKey,
|
||||
subtitleBoldKey,
|
||||
subtitleFontSizeSpKey,
|
||||
subtitleBottomOffsetKey,
|
||||
subtitleUseForcedSubtitlesKey,
|
||||
subtitleShowOnlyPreferredLanguagesKey,
|
||||
addonSubtitleStartupModeKey,
|
||||
streamReuseLastLinkEnabledKey,
|
||||
streamReuseLastLinkCacheHoursKey,
|
||||
decoderPriorityKey,
|
||||
mapDV7ToHevcKey,
|
||||
tunnelingEnabledKey,
|
||||
streamAutoPlayModeKey,
|
||||
streamAutoPlaySourceKey,
|
||||
streamAutoPlaySelectedAddonsKey,
|
||||
streamAutoPlaySelectedPluginsKey,
|
||||
streamAutoPlayRegexKey,
|
||||
streamAutoPlayTimeoutSecondsKey,
|
||||
skipIntroEnabledKey,
|
||||
animeSkipEnabledKey,
|
||||
animeSkipClientIdKey,
|
||||
streamAutoPlayNextEpisodeEnabledKey,
|
||||
streamAutoPlayPreferBingeGroupKey,
|
||||
streamAutoPlayReuseBingeGroupKey,
|
||||
nextEpisodeThresholdModeKey,
|
||||
nextEpisodeThresholdPercentKey,
|
||||
nextEpisodeThresholdMinutesBeforeEndKey,
|
||||
useLibassKey,
|
||||
libassRenderTypeKey,
|
||||
iosVideoOutputPresetKey,
|
||||
iosToneMappingModeKey,
|
||||
iosTargetPrimariesKey,
|
||||
iosTargetTransferKey,
|
||||
iosHardwareDecoderModeKey,
|
||||
iosAudioOutputModeKey,
|
||||
iosExtendedDynamicRangeEnabledKey,
|
||||
iosTargetColorspaceHintEnabledKey,
|
||||
iosHdrComputePeakEnabledKey,
|
||||
iosDebandEnabledKey,
|
||||
iosInterpolationEnabledKey,
|
||||
iosBrightnessKey,
|
||||
iosContrastKey,
|
||||
iosSaturationKey,
|
||||
iosGammaKey,
|
||||
)
|
||||
private val store = DesktopStorage.store("nuvio_player_settings")
|
||||
|
||||
actual fun loadShowLoadingOverlay(): Boolean? = loadBoolean(showLoadingOverlayKey)
|
||||
actual fun saveShowLoadingOverlay(enabled: Boolean) = saveBoolean(showLoadingOverlayKey, enabled)
|
||||
actual fun loadResizeMode(): String? = loadString(resizeModeKey)
|
||||
actual fun saveResizeMode(mode: String) = saveString(resizeModeKey, mode)
|
||||
actual fun loadHoldToSpeedEnabled(): Boolean? = loadBoolean(holdToSpeedEnabledKey)
|
||||
actual fun saveHoldToSpeedEnabled(enabled: Boolean) = saveBoolean(holdToSpeedEnabledKey, enabled)
|
||||
actual fun loadHoldToSpeedValue(): Float? = loadFloat(holdToSpeedValueKey)
|
||||
actual fun saveHoldToSpeedValue(speed: Float) = saveFloat(holdToSpeedValueKey, speed)
|
||||
actual fun loadExternalPlayerEnabled(): Boolean? = loadBoolean(externalPlayerEnabledKey)
|
||||
actual fun saveExternalPlayerEnabled(enabled: Boolean) = saveBoolean(externalPlayerEnabledKey, enabled)
|
||||
actual fun loadExternalPlayerForwardSubtitles(): Boolean? = loadBoolean(externalPlayerForwardSubtitlesKey)
|
||||
actual fun saveExternalPlayerForwardSubtitles(enabled: Boolean) = saveBoolean(externalPlayerForwardSubtitlesKey, enabled)
|
||||
actual fun loadExternalPlayerId(): String? = loadString(externalPlayerIdKey)
|
||||
actual fun saveExternalPlayerId(playerId: String?) = saveOptionalString(externalPlayerIdKey, playerId)
|
||||
actual fun loadPreferredAudioLanguage(): String? = loadString(preferredAudioLanguageKey)
|
||||
actual fun savePreferredAudioLanguage(language: String) = saveString(preferredAudioLanguageKey, language)
|
||||
actual fun loadSecondaryPreferredAudioLanguage(): String? = loadString(secondaryPreferredAudioLanguageKey)
|
||||
actual fun saveSecondaryPreferredAudioLanguage(language: String?) = saveOptionalString(secondaryPreferredAudioLanguageKey, language)
|
||||
actual fun loadPreferredSubtitleLanguage(): String? = loadString(preferredSubtitleLanguageKey)
|
||||
actual fun savePreferredSubtitleLanguage(language: String) = saveString(preferredSubtitleLanguageKey, language)
|
||||
actual fun loadSecondaryPreferredSubtitleLanguage(): String? = loadString(secondaryPreferredSubtitleLanguageKey)
|
||||
actual fun saveSecondaryPreferredSubtitleLanguage(language: String?) = saveOptionalString(secondaryPreferredSubtitleLanguageKey, language)
|
||||
actual fun loadSubtitleTextColor(): String? = loadString(subtitleTextColorKey)
|
||||
actual fun saveSubtitleTextColor(colorHex: String) = saveString(subtitleTextColorKey, colorHex)
|
||||
actual fun loadSubtitleBackgroundColor(): String? = loadString(subtitleBackgroundColorKey)
|
||||
actual fun saveSubtitleBackgroundColor(colorHex: String) = saveString(subtitleBackgroundColorKey, colorHex)
|
||||
actual fun loadSubtitleOutlineColor(): String? = loadString(subtitleOutlineColorKey)
|
||||
actual fun saveSubtitleOutlineColor(colorHex: String) = saveString(subtitleOutlineColorKey, colorHex)
|
||||
actual fun loadSubtitleOutlineEnabled(): Boolean? = loadBoolean(subtitleOutlineEnabledKey)
|
||||
actual fun saveSubtitleOutlineEnabled(enabled: Boolean) = saveBoolean(subtitleOutlineEnabledKey, enabled)
|
||||
actual fun loadSubtitleOutlineWidth(): Int? = loadInt(subtitleOutlineWidthKey)
|
||||
actual fun saveSubtitleOutlineWidth(width: Int) = saveInt(subtitleOutlineWidthKey, width)
|
||||
actual fun loadSubtitleBold(): Boolean? = loadBoolean(subtitleBoldKey)
|
||||
actual fun saveSubtitleBold(enabled: Boolean) = saveBoolean(subtitleBoldKey, enabled)
|
||||
actual fun loadSubtitleFontSizeSp(): Int? = loadInt(subtitleFontSizeSpKey)
|
||||
actual fun saveSubtitleFontSizeSp(fontSizeSp: Int) = saveInt(subtitleFontSizeSpKey, fontSizeSp)
|
||||
actual fun loadSubtitleBottomOffset(): Int? = loadInt(subtitleBottomOffsetKey)
|
||||
actual fun saveSubtitleBottomOffset(bottomOffset: Int) = saveInt(subtitleBottomOffsetKey, bottomOffset)
|
||||
actual fun loadSubtitleUseForcedSubtitles(): Boolean? = loadBoolean(subtitleUseForcedSubtitlesKey)
|
||||
actual fun saveSubtitleUseForcedSubtitles(enabled: Boolean) = saveBoolean(subtitleUseForcedSubtitlesKey, enabled)
|
||||
actual fun loadSubtitleShowOnlyPreferredLanguages(): Boolean? = loadBoolean(subtitleShowOnlyPreferredLanguagesKey)
|
||||
actual fun saveSubtitleShowOnlyPreferredLanguages(enabled: Boolean) = saveBoolean(subtitleShowOnlyPreferredLanguagesKey, enabled)
|
||||
actual fun loadAddonSubtitleStartupMode(): String? = loadString(addonSubtitleStartupModeKey)
|
||||
actual fun saveAddonSubtitleStartupMode(mode: String) = saveString(addonSubtitleStartupModeKey, mode)
|
||||
actual fun loadStreamReuseLastLinkEnabled(): Boolean? = loadBoolean(streamReuseLastLinkEnabledKey)
|
||||
actual fun saveStreamReuseLastLinkEnabled(enabled: Boolean) = saveBoolean(streamReuseLastLinkEnabledKey, enabled)
|
||||
actual fun loadStreamReuseLastLinkCacheHours(): Int? = loadInt(streamReuseLastLinkCacheHoursKey)
|
||||
actual fun saveStreamReuseLastLinkCacheHours(hours: Int) = saveInt(streamReuseLastLinkCacheHoursKey, hours)
|
||||
actual fun loadDecoderPriority(): Int? = loadInt(decoderPriorityKey)
|
||||
actual fun saveDecoderPriority(priority: Int) = saveInt(decoderPriorityKey, priority)
|
||||
actual fun loadMapDV7ToHevc(): Boolean? = loadBoolean(mapDV7ToHevcKey)
|
||||
actual fun saveMapDV7ToHevc(enabled: Boolean) = saveBoolean(mapDV7ToHevcKey, enabled)
|
||||
actual fun loadTunnelingEnabled(): Boolean? = loadBoolean(tunnelingEnabledKey)
|
||||
actual fun saveTunnelingEnabled(enabled: Boolean) = saveBoolean(tunnelingEnabledKey, enabled)
|
||||
actual fun loadStreamAutoPlayMode(): String? = loadString(streamAutoPlayModeKey)
|
||||
actual fun saveStreamAutoPlayMode(mode: String) = saveString(streamAutoPlayModeKey, mode)
|
||||
actual fun loadStreamAutoPlaySource(): String? = loadString(streamAutoPlaySourceKey)
|
||||
actual fun saveStreamAutoPlaySource(source: String) = saveString(streamAutoPlaySourceKey, source)
|
||||
actual fun loadStreamAutoPlaySelectedAddons(): Set<String>? = loadStringSet(streamAutoPlaySelectedAddonsKey)
|
||||
actual fun saveStreamAutoPlaySelectedAddons(addons: Set<String>) = saveStringSet(streamAutoPlaySelectedAddonsKey, addons)
|
||||
actual fun loadStreamAutoPlaySelectedPlugins(): Set<String>? = loadStringSet(streamAutoPlaySelectedPluginsKey)
|
||||
actual fun saveStreamAutoPlaySelectedPlugins(plugins: Set<String>) = saveStringSet(streamAutoPlaySelectedPluginsKey, plugins)
|
||||
actual fun loadStreamAutoPlayRegex(): String? = loadString(streamAutoPlayRegexKey)
|
||||
actual fun saveStreamAutoPlayRegex(regex: String) = saveString(streamAutoPlayRegexKey, regex)
|
||||
actual fun loadStreamAutoPlayTimeoutSeconds(): Int? = loadInt(streamAutoPlayTimeoutSecondsKey)
|
||||
actual fun saveStreamAutoPlayTimeoutSeconds(seconds: Int) = saveInt(streamAutoPlayTimeoutSecondsKey, seconds)
|
||||
actual fun loadSkipIntroEnabled(): Boolean? = loadBoolean(skipIntroEnabledKey)
|
||||
actual fun saveSkipIntroEnabled(enabled: Boolean) = saveBoolean(skipIntroEnabledKey, enabled)
|
||||
actual fun loadAnimeSkipEnabled(): Boolean? = loadBoolean(animeSkipEnabledKey)
|
||||
actual fun saveAnimeSkipEnabled(enabled: Boolean) = saveBoolean(animeSkipEnabledKey, enabled)
|
||||
actual fun loadAnimeSkipClientId(): String? = loadString(animeSkipClientIdKey)
|
||||
actual fun saveAnimeSkipClientId(clientId: String) = 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) = saveBoolean(streamAutoPlayNextEpisodeEnabledKey, enabled)
|
||||
actual fun loadStreamAutoPlayPreferBingeGroup(): Boolean? = loadBoolean(streamAutoPlayPreferBingeGroupKey)
|
||||
actual fun saveStreamAutoPlayPreferBingeGroup(enabled: Boolean) = saveBoolean(streamAutoPlayPreferBingeGroupKey, enabled)
|
||||
actual fun loadStreamAutoPlayReuseBingeGroup(): Boolean? = loadBoolean(streamAutoPlayReuseBingeGroupKey)
|
||||
actual fun saveStreamAutoPlayReuseBingeGroup(enabled: Boolean) = saveBoolean(streamAutoPlayReuseBingeGroupKey, enabled)
|
||||
actual fun loadNextEpisodeThresholdMode(): String? = loadString(nextEpisodeThresholdModeKey)
|
||||
actual fun saveNextEpisodeThresholdMode(mode: String) = saveString(nextEpisodeThresholdModeKey, mode)
|
||||
actual fun loadNextEpisodeThresholdPercent(): Float? = loadFloat(nextEpisodeThresholdPercentKey)
|
||||
actual fun saveNextEpisodeThresholdPercent(percent: Float) = saveFloat(nextEpisodeThresholdPercentKey, percent)
|
||||
actual fun loadNextEpisodeThresholdMinutesBeforeEnd(): Float? = loadFloat(nextEpisodeThresholdMinutesBeforeEndKey)
|
||||
actual fun saveNextEpisodeThresholdMinutesBeforeEnd(minutes: Float) = saveFloat(nextEpisodeThresholdMinutesBeforeEndKey, minutes)
|
||||
actual fun loadUseLibass(): Boolean? = loadBoolean(useLibassKey)
|
||||
actual fun saveUseLibass(enabled: Boolean) = saveBoolean(useLibassKey, enabled)
|
||||
actual fun loadLibassRenderType(): String? = loadString(libassRenderTypeKey)
|
||||
actual fun saveLibassRenderType(renderType: String) = saveString(libassRenderTypeKey, renderType)
|
||||
actual fun loadIosVideoOutputPreset(): String? = loadString(iosVideoOutputPresetKey)
|
||||
actual fun saveIosVideoOutputPreset(preset: String) = saveString(iosVideoOutputPresetKey, preset)
|
||||
actual fun loadIosToneMappingMode(): String? = loadString(iosToneMappingModeKey)
|
||||
actual fun saveIosToneMappingMode(mode: String) = saveString(iosToneMappingModeKey, mode)
|
||||
actual fun loadIosTargetPrimaries(): String? = loadString(iosTargetPrimariesKey)
|
||||
actual fun saveIosTargetPrimaries(primaries: String) = saveString(iosTargetPrimariesKey, primaries)
|
||||
actual fun loadIosTargetTransfer(): String? = loadString(iosTargetTransferKey)
|
||||
actual fun saveIosTargetTransfer(transfer: String) = saveString(iosTargetTransferKey, transfer)
|
||||
actual fun loadIosHardwareDecoderMode(): String? = loadString(iosHardwareDecoderModeKey)
|
||||
actual fun saveIosHardwareDecoderMode(mode: String) = saveString(iosHardwareDecoderModeKey, mode)
|
||||
actual fun loadIosAudioOutputMode(): String? = loadString(iosAudioOutputModeKey)
|
||||
actual fun saveIosAudioOutputMode(mode: String) = saveString(iosAudioOutputModeKey, mode)
|
||||
actual fun loadIosExtendedDynamicRangeEnabled(): Boolean? = loadBoolean(iosExtendedDynamicRangeEnabledKey)
|
||||
actual fun saveIosExtendedDynamicRangeEnabled(enabled: Boolean) = saveBoolean(iosExtendedDynamicRangeEnabledKey, enabled)
|
||||
actual fun loadIosTargetColorspaceHintEnabled(): Boolean? = loadBoolean(iosTargetColorspaceHintEnabledKey)
|
||||
actual fun saveIosTargetColorspaceHintEnabled(enabled: Boolean) = saveBoolean(iosTargetColorspaceHintEnabledKey, enabled)
|
||||
actual fun loadIosHdrComputePeakEnabled(): Boolean? = loadBoolean(iosHdrComputePeakEnabledKey)
|
||||
actual fun saveIosHdrComputePeakEnabled(enabled: Boolean) = saveBoolean(iosHdrComputePeakEnabledKey, enabled)
|
||||
actual fun loadIosDebandEnabled(): Boolean? = loadBoolean(iosDebandEnabledKey)
|
||||
actual fun saveIosDebandEnabled(enabled: Boolean) = saveBoolean(iosDebandEnabledKey, enabled)
|
||||
actual fun loadIosInterpolationEnabled(): Boolean? = loadBoolean(iosInterpolationEnabledKey)
|
||||
actual fun saveIosInterpolationEnabled(enabled: Boolean) = saveBoolean(iosInterpolationEnabledKey, enabled)
|
||||
actual fun loadIosBrightness(): Int? = loadInt(iosBrightnessKey)
|
||||
actual fun saveIosBrightness(value: Int) = saveInt(iosBrightnessKey, value)
|
||||
actual fun loadIosContrast(): Int? = loadInt(iosContrastKey)
|
||||
actual fun saveIosContrast(value: Int) = saveInt(iosContrastKey, value)
|
||||
actual fun loadIosSaturation(): Int? = loadInt(iosSaturationKey)
|
||||
actual fun saveIosSaturation(value: Int) = saveInt(iosSaturationKey, value)
|
||||
actual fun loadIosGamma(): Int? = loadInt(iosGammaKey)
|
||||
actual fun saveIosGamma(value: Int) = saveInt(iosGammaKey, value)
|
||||
|
||||
private fun scoped(key: String): String = ProfileScopedKey.of(key)
|
||||
private fun loadString(key: String): String? = store.getString(scoped(key))
|
||||
private fun saveString(key: String, value: String) = store.putString(scoped(key), value)
|
||||
private fun saveOptionalString(key: String, value: String?) = store.putString(scoped(key), value?.takeIf { it.isNotBlank() })
|
||||
private fun loadBoolean(key: String): Boolean? = store.getBoolean(scoped(key))
|
||||
private fun saveBoolean(key: String, value: Boolean) = store.putBoolean(scoped(key), value)
|
||||
private fun loadInt(key: String): Int? = store.getInt(scoped(key))
|
||||
private fun saveInt(key: String, value: Int) = store.putInt(scoped(key), value)
|
||||
private fun loadFloat(key: String): Float? = store.getFloat(scoped(key))
|
||||
private fun saveFloat(key: String, value: Float) = store.putFloat(scoped(key), value)
|
||||
private fun loadStringSet(key: String): Set<String>? = store.getStringSet(scoped(key))
|
||||
private fun saveStringSet(key: String, values: Set<String>) = store.putStringSet(scoped(key), values)
|
||||
|
||||
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||
loadShowLoadingOverlay()?.let { put(showLoadingOverlayKey, encodeSyncBoolean(it)) }
|
||||
loadResizeMode()?.let { put(resizeModeKey, encodeSyncString(it)) }
|
||||
loadHoldToSpeedEnabled()?.let { put(holdToSpeedEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadHoldToSpeedValue()?.let { put(holdToSpeedValueKey, encodeSyncFloat(it)) }
|
||||
loadExternalPlayerEnabled()?.let { put(externalPlayerEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadExternalPlayerForwardSubtitles()?.let { put(externalPlayerForwardSubtitlesKey, encodeSyncBoolean(it)) }
|
||||
loadExternalPlayerId()?.let { put(externalPlayerIdKey, encodeSyncString(it)) }
|
||||
loadPreferredAudioLanguage()?.let { put(preferredAudioLanguageKey, encodeSyncString(it)) }
|
||||
loadSecondaryPreferredAudioLanguage()?.let { put(secondaryPreferredAudioLanguageKey, encodeSyncString(it)) }
|
||||
loadPreferredSubtitleLanguage()?.let { put(preferredSubtitleLanguageKey, encodeSyncString(it)) }
|
||||
loadSecondaryPreferredSubtitleLanguage()?.let { put(secondaryPreferredSubtitleLanguageKey, encodeSyncString(it)) }
|
||||
loadSubtitleTextColor()?.let { put(subtitleTextColorKey, encodeSyncString(it)) }
|
||||
loadSubtitleBackgroundColor()?.let { put(subtitleBackgroundColorKey, encodeSyncString(it)) }
|
||||
loadSubtitleOutlineColor()?.let { put(subtitleOutlineColorKey, encodeSyncString(it)) }
|
||||
loadSubtitleOutlineEnabled()?.let { put(subtitleOutlineEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadSubtitleOutlineWidth()?.let { put(subtitleOutlineWidthKey, encodeSyncInt(it)) }
|
||||
loadSubtitleBold()?.let { put(subtitleBoldKey, encodeSyncBoolean(it)) }
|
||||
loadSubtitleFontSizeSp()?.let { put(subtitleFontSizeSpKey, encodeSyncInt(it)) }
|
||||
loadSubtitleBottomOffset()?.let { put(subtitleBottomOffsetKey, encodeSyncInt(it)) }
|
||||
loadSubtitleUseForcedSubtitles()?.let { put(subtitleUseForcedSubtitlesKey, encodeSyncBoolean(it)) }
|
||||
loadSubtitleShowOnlyPreferredLanguages()?.let { put(subtitleShowOnlyPreferredLanguagesKey, encodeSyncBoolean(it)) }
|
||||
loadAddonSubtitleStartupMode()?.let { put(addonSubtitleStartupModeKey, encodeSyncString(it)) }
|
||||
loadStreamReuseLastLinkEnabled()?.let { put(streamReuseLastLinkEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadStreamReuseLastLinkCacheHours()?.let { put(streamReuseLastLinkCacheHoursKey, encodeSyncInt(it)) }
|
||||
loadDecoderPriority()?.let { put(decoderPriorityKey, encodeSyncInt(it)) }
|
||||
loadMapDV7ToHevc()?.let { put(mapDV7ToHevcKey, encodeSyncBoolean(it)) }
|
||||
loadTunnelingEnabled()?.let { put(tunnelingEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadStreamAutoPlayMode()?.let { put(streamAutoPlayModeKey, encodeSyncString(it)) }
|
||||
loadStreamAutoPlaySource()?.let { put(streamAutoPlaySourceKey, encodeSyncString(it)) }
|
||||
loadStreamAutoPlaySelectedAddons()?.let { put(streamAutoPlaySelectedAddonsKey, encodeSyncStringSet(it)) }
|
||||
loadStreamAutoPlaySelectedPlugins()?.let { put(streamAutoPlaySelectedPluginsKey, encodeSyncStringSet(it)) }
|
||||
loadStreamAutoPlayRegex()?.let { put(streamAutoPlayRegexKey, encodeSyncString(it)) }
|
||||
loadStreamAutoPlayTimeoutSeconds()?.let { put(streamAutoPlayTimeoutSecondsKey, encodeSyncInt(it)) }
|
||||
loadSkipIntroEnabled()?.let { put(skipIntroEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadAnimeSkipEnabled()?.let { put(animeSkipEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) }
|
||||
loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadStreamAutoPlayPreferBingeGroup()?.let { put(streamAutoPlayPreferBingeGroupKey, encodeSyncBoolean(it)) }
|
||||
loadStreamAutoPlayReuseBingeGroup()?.let { put(streamAutoPlayReuseBingeGroupKey, encodeSyncBoolean(it)) }
|
||||
loadNextEpisodeThresholdMode()?.let { put(nextEpisodeThresholdModeKey, encodeSyncString(it)) }
|
||||
loadNextEpisodeThresholdPercent()?.let { put(nextEpisodeThresholdPercentKey, encodeSyncFloat(it)) }
|
||||
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
|
||||
loadUseLibass()?.let { put(useLibassKey, encodeSyncBoolean(it)) }
|
||||
loadLibassRenderType()?.let { put(libassRenderTypeKey, encodeSyncString(it)) }
|
||||
loadIosVideoOutputPreset()?.let { put(iosVideoOutputPresetKey, encodeSyncString(it)) }
|
||||
loadIosToneMappingMode()?.let { put(iosToneMappingModeKey, encodeSyncString(it)) }
|
||||
loadIosTargetPrimaries()?.let { put(iosTargetPrimariesKey, encodeSyncString(it)) }
|
||||
loadIosTargetTransfer()?.let { put(iosTargetTransferKey, encodeSyncString(it)) }
|
||||
loadIosHardwareDecoderMode()?.let { put(iosHardwareDecoderModeKey, encodeSyncString(it)) }
|
||||
loadIosAudioOutputMode()?.let { put(iosAudioOutputModeKey, encodeSyncString(it)) }
|
||||
loadIosExtendedDynamicRangeEnabled()?.let { put(iosExtendedDynamicRangeEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosTargetColorspaceHintEnabled()?.let { put(iosTargetColorspaceHintEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosHdrComputePeakEnabled()?.let { put(iosHdrComputePeakEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosDebandEnabled()?.let { put(iosDebandEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosInterpolationEnabled()?.let { put(iosInterpolationEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosBrightness()?.let { put(iosBrightnessKey, encodeSyncInt(it)) }
|
||||
loadIosContrast()?.let { put(iosContrastKey, encodeSyncInt(it)) }
|
||||
loadIosSaturation()?.let { put(iosSaturationKey, encodeSyncInt(it)) }
|
||||
loadIosGamma()?.let { put(iosGammaKey, encodeSyncInt(it)) }
|
||||
}
|
||||
|
||||
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||
store.removeAll(syncKeys.map(::scoped))
|
||||
payload.decodeSyncBoolean(showLoadingOverlayKey)?.let(::saveShowLoadingOverlay)
|
||||
payload.decodeSyncString(resizeModeKey)?.let(::saveResizeMode)
|
||||
payload.decodeSyncBoolean(holdToSpeedEnabledKey)?.let(::saveHoldToSpeedEnabled)
|
||||
payload.decodeSyncFloat(holdToSpeedValueKey)?.let(::saveHoldToSpeedValue)
|
||||
payload.decodeSyncBoolean(externalPlayerEnabledKey)?.let(::saveExternalPlayerEnabled)
|
||||
payload.decodeSyncBoolean(externalPlayerForwardSubtitlesKey)?.let(::saveExternalPlayerForwardSubtitles)
|
||||
payload.decodeSyncString(externalPlayerIdKey)?.let(::saveExternalPlayerId)
|
||||
payload.decodeSyncString(preferredAudioLanguageKey)?.let(::savePreferredAudioLanguage)
|
||||
payload.decodeSyncString(secondaryPreferredAudioLanguageKey)?.let(::saveSecondaryPreferredAudioLanguage)
|
||||
payload.decodeSyncString(preferredSubtitleLanguageKey)?.let(::savePreferredSubtitleLanguage)
|
||||
payload.decodeSyncString(secondaryPreferredSubtitleLanguageKey)?.let(::saveSecondaryPreferredSubtitleLanguage)
|
||||
payload.decodeSyncString(subtitleTextColorKey)?.let(::saveSubtitleTextColor)
|
||||
payload.decodeSyncString(subtitleBackgroundColorKey)?.let(::saveSubtitleBackgroundColor)
|
||||
payload.decodeSyncString(subtitleOutlineColorKey)?.let(::saveSubtitleOutlineColor)
|
||||
payload.decodeSyncBoolean(subtitleOutlineEnabledKey)?.let(::saveSubtitleOutlineEnabled)
|
||||
payload.decodeSyncInt(subtitleOutlineWidthKey)?.let(::saveSubtitleOutlineWidth)
|
||||
payload.decodeSyncBoolean(subtitleBoldKey)?.let(::saveSubtitleBold)
|
||||
payload.decodeSyncInt(subtitleFontSizeSpKey)?.let(::saveSubtitleFontSizeSp)
|
||||
payload.decodeSyncInt(subtitleBottomOffsetKey)?.let(::saveSubtitleBottomOffset)
|
||||
payload.decodeSyncBoolean(subtitleUseForcedSubtitlesKey)?.let(::saveSubtitleUseForcedSubtitles)
|
||||
payload.decodeSyncBoolean(subtitleShowOnlyPreferredLanguagesKey)?.let(::saveSubtitleShowOnlyPreferredLanguages)
|
||||
payload.decodeSyncString(addonSubtitleStartupModeKey)?.let(::saveAddonSubtitleStartupMode)
|
||||
payload.decodeSyncBoolean(streamReuseLastLinkEnabledKey)?.let(::saveStreamReuseLastLinkEnabled)
|
||||
payload.decodeSyncInt(streamReuseLastLinkCacheHoursKey)?.let(::saveStreamReuseLastLinkCacheHours)
|
||||
payload.decodeSyncInt(decoderPriorityKey)?.let(::saveDecoderPriority)
|
||||
payload.decodeSyncBoolean(mapDV7ToHevcKey)?.let(::saveMapDV7ToHevc)
|
||||
payload.decodeSyncBoolean(tunnelingEnabledKey)?.let(::saveTunnelingEnabled)
|
||||
payload.decodeSyncString(streamAutoPlayModeKey)?.let(::saveStreamAutoPlayMode)
|
||||
payload.decodeSyncString(streamAutoPlaySourceKey)?.let(::saveStreamAutoPlaySource)
|
||||
payload.decodeSyncStringSet(streamAutoPlaySelectedAddonsKey)?.let(::saveStreamAutoPlaySelectedAddons)
|
||||
payload.decodeSyncStringSet(streamAutoPlaySelectedPluginsKey)?.let(::saveStreamAutoPlaySelectedPlugins)
|
||||
payload.decodeSyncString(streamAutoPlayRegexKey)?.let(::saveStreamAutoPlayRegex)
|
||||
payload.decodeSyncInt(streamAutoPlayTimeoutSecondsKey)?.let(::saveStreamAutoPlayTimeoutSeconds)
|
||||
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.decodeSyncBoolean(streamAutoPlayReuseBingeGroupKey)?.let(::saveStreamAutoPlayReuseBingeGroup)
|
||||
payload.decodeSyncString(nextEpisodeThresholdModeKey)?.let(::saveNextEpisodeThresholdMode)
|
||||
payload.decodeSyncFloat(nextEpisodeThresholdPercentKey)?.let(::saveNextEpisodeThresholdPercent)
|
||||
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
|
||||
payload.decodeSyncBoolean(useLibassKey)?.let(::saveUseLibass)
|
||||
payload.decodeSyncString(libassRenderTypeKey)?.let(::saveLibassRenderType)
|
||||
payload.decodeSyncString(iosVideoOutputPresetKey)?.let(::saveIosVideoOutputPreset)
|
||||
payload.decodeSyncString(iosToneMappingModeKey)?.let(::saveIosToneMappingMode)
|
||||
payload.decodeSyncString(iosTargetPrimariesKey)?.let(::saveIosTargetPrimaries)
|
||||
payload.decodeSyncString(iosTargetTransferKey)?.let(::saveIosTargetTransfer)
|
||||
payload.decodeSyncString(iosHardwareDecoderModeKey)?.let(::saveIosHardwareDecoderMode)
|
||||
payload.decodeSyncString(iosAudioOutputModeKey)?.let(::saveIosAudioOutputMode)
|
||||
payload.decodeSyncBoolean(iosExtendedDynamicRangeEnabledKey)?.let(::saveIosExtendedDynamicRangeEnabled)
|
||||
payload.decodeSyncBoolean(iosTargetColorspaceHintEnabledKey)?.let(::saveIosTargetColorspaceHintEnabled)
|
||||
payload.decodeSyncBoolean(iosHdrComputePeakEnabledKey)?.let(::saveIosHdrComputePeakEnabled)
|
||||
payload.decodeSyncBoolean(iosDebandEnabledKey)?.let(::saveIosDebandEnabled)
|
||||
payload.decodeSyncBoolean(iosInterpolationEnabledKey)?.let(::saveIosInterpolationEnabled)
|
||||
payload.decodeSyncInt(iosBrightnessKey)?.let(::saveIosBrightness)
|
||||
payload.decodeSyncInt(iosContrastKey)?.let(::saveIosContrast)
|
||||
payload.decodeSyncInt(iosSaturationKey)?.let(::saveIosSaturation)
|
||||
payload.decodeSyncInt(iosGammaKey)?.let(::saveIosGamma)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object PlayerTrackPreferenceStorage {
|
||||
private const val subtitleTypeKey = "subtitle_type"
|
||||
private const val subtitleLanguageKey = "subtitle_language"
|
||||
private const val subtitleNameKey = "subtitle_name"
|
||||
private const val subtitleTrackIdKey = "subtitle_track_id"
|
||||
private const val addonSubtitleIdKey = "addon_subtitle_id"
|
||||
private const val addonSubtitleUrlKey = "addon_subtitle_url"
|
||||
private const val addonSubtitleAddonNameKey = "addon_subtitle_addon_name"
|
||||
private const val audioLanguageKey = "audio_language"
|
||||
private const val audioNameKey = "audio_name"
|
||||
private const val audioTrackIdKey = "audio_track_id"
|
||||
private const val subtitleDelayMsKey = "subtitle_delay_ms"
|
||||
private val store = DesktopStorage.store("nuvio_player_track_preferences")
|
||||
|
||||
actual fun load(contentId: String): PersistedPlayerTrackPreference? {
|
||||
val id = contentId.normalizedStorageId() ?: return null
|
||||
val preference = PersistedPlayerTrackPreference(
|
||||
subtitleType = loadString(subtitleTypeKey, id),
|
||||
subtitleLanguage = loadString(subtitleLanguageKey, id),
|
||||
subtitleName = loadString(subtitleNameKey, id),
|
||||
subtitleTrackId = loadString(subtitleTrackIdKey, id),
|
||||
addonSubtitleId = loadString(addonSubtitleIdKey, id),
|
||||
addonSubtitleUrl = loadString(addonSubtitleUrlKey, id),
|
||||
addonSubtitleAddonName = loadString(addonSubtitleAddonNameKey, id),
|
||||
audioLanguage = loadString(audioLanguageKey, id),
|
||||
audioName = loadString(audioNameKey, id),
|
||||
audioTrackId = loadString(audioTrackIdKey, id),
|
||||
)
|
||||
return preference.takeIf {
|
||||
listOf(
|
||||
it.subtitleType,
|
||||
it.subtitleLanguage,
|
||||
it.subtitleName,
|
||||
it.subtitleTrackId,
|
||||
it.addonSubtitleId,
|
||||
it.addonSubtitleUrl,
|
||||
it.addonSubtitleAddonName,
|
||||
it.audioLanguage,
|
||||
it.audioName,
|
||||
it.audioTrackId,
|
||||
).any { value -> !value.isNullOrBlank() }
|
||||
}
|
||||
}
|
||||
|
||||
actual fun save(contentId: String, preference: PersistedPlayerTrackPreference) {
|
||||
val id = contentId.normalizedStorageId() ?: return
|
||||
putOptionalString(subtitleTypeKey, id, preference.subtitleType)
|
||||
putOptionalString(subtitleLanguageKey, id, preference.subtitleLanguage)
|
||||
putOptionalString(subtitleNameKey, id, preference.subtitleName)
|
||||
putOptionalString(subtitleTrackIdKey, id, preference.subtitleTrackId)
|
||||
putOptionalString(addonSubtitleIdKey, id, preference.addonSubtitleId)
|
||||
putOptionalString(addonSubtitleUrlKey, id, preference.addonSubtitleUrl)
|
||||
putOptionalString(addonSubtitleAddonNameKey, id, preference.addonSubtitleAddonName)
|
||||
putOptionalString(audioLanguageKey, id, preference.audioLanguage)
|
||||
putOptionalString(audioNameKey, id, preference.audioName)
|
||||
putOptionalString(audioTrackIdKey, id, preference.audioTrackId)
|
||||
}
|
||||
|
||||
actual fun loadSubtitleDelayMs(videoId: String): Int? {
|
||||
val id = videoId.normalizedStorageId() ?: return null
|
||||
return store.getInt(scopedKey(subtitleDelayMsKey, id))
|
||||
}
|
||||
|
||||
actual fun saveSubtitleDelayMs(videoId: String, delayMs: Int) {
|
||||
val id = videoId.normalizedStorageId() ?: return
|
||||
store.putInt(
|
||||
scopedKey(subtitleDelayMsKey, id),
|
||||
delayMs.coerceIn(SUBTITLE_DELAY_MIN_MS, SUBTITLE_DELAY_MAX_MS),
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadString(field: String, contentId: String): String? =
|
||||
store.getString(scopedKey(field, contentId))?.takeIf { it.isNotBlank() }
|
||||
|
||||
private fun putOptionalString(field: String, contentId: String, value: String?) {
|
||||
val key = scopedKey(field, contentId)
|
||||
if (value.isNullOrBlank()) {
|
||||
store.remove(key)
|
||||
} else {
|
||||
store.putString(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scopedKey(field: String, contentId: String): String =
|
||||
ProfileScopedKey.of("$field|$contentId")
|
||||
|
||||
private fun String.normalizedStorageId(): String? =
|
||||
trim().takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.nuvio.app.features.player.skip
|
||||
|
||||
import java.time.LocalDate
|
||||
|
||||
internal actual fun currentDateComponents(): DateComponents {
|
||||
val today = LocalDate.now()
|
||||
return DateComponents(
|
||||
year = today.year,
|
||||
month = today.monthValue,
|
||||
day = today.dayOfMonth,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.nuvio.app.features.plugins
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.plugins_error_unavailable_build
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
actual object PluginRepository {
|
||||
private val disabledState = MutableStateFlow(PluginsUiState(pluginsEnabled = false))
|
||||
|
||||
actual val uiState: StateFlow<PluginsUiState> = disabledState.asStateFlow()
|
||||
|
||||
actual fun initialize() = Unit
|
||||
actual fun onProfileChanged(profileId: Int) = Unit
|
||||
actual fun clearLocalState() = Unit
|
||||
actual suspend fun pullFromServer(profileId: Int) = Unit
|
||||
|
||||
actual suspend fun addRepository(rawUrl: String): AddPluginRepositoryResult =
|
||||
AddPluginRepositoryResult.Error(getString(Res.string.plugins_error_unavailable_build))
|
||||
|
||||
actual fun removeRepository(manifestUrl: String) = Unit
|
||||
actual fun refreshAll() = Unit
|
||||
actual fun refreshRepository(manifestUrl: String, pushAfterRefresh: Boolean) = Unit
|
||||
actual fun toggleScraper(scraperId: String, enabled: Boolean) = Unit
|
||||
actual fun setPluginsEnabled(enabled: Boolean) = Unit
|
||||
actual fun setGroupStreamsByRepository(enabled: Boolean) = Unit
|
||||
actual fun getEnabledScrapersForType(type: String): List<PluginScraper> = emptyList()
|
||||
|
||||
actual suspend fun testScraper(scraperId: String): Result<List<PluginRuntimeResult>> =
|
||||
Result.failure(UnsupportedOperationException(getString(Res.string.plugins_error_unavailable_build)))
|
||||
|
||||
actual suspend fun executeScraper(
|
||||
scraper: PluginScraper,
|
||||
tmdbId: String,
|
||||
mediaType: String,
|
||||
season: Int?,
|
||||
episode: Int?,
|
||||
): Result<List<PluginRuntimeResult>> =
|
||||
Result.failure(UnsupportedOperationException(getString(Res.string.plugins_error_unavailable_build)))
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.nuvio.app.features.profiles
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
|
||||
internal actual object AvatarStorage {
|
||||
private val store = DesktopStorage.store("nuvio_avatars")
|
||||
|
||||
actual fun loadPayload(): String? = store.getString("avatars")
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString("avatars", payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.nuvio.app.features.profiles
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
|
||||
internal actual object ProfilePinCacheStorage {
|
||||
private val store = DesktopStorage.store("nuvio_profile_pin_cache")
|
||||
|
||||
actual fun loadPayload(profileIndex: Int): String? =
|
||||
store.getString(key(profileIndex))
|
||||
|
||||
actual fun savePayload(profileIndex: Int, payload: String) {
|
||||
store.putString(key(profileIndex), payload)
|
||||
}
|
||||
|
||||
actual fun removePayload(profileIndex: Int) {
|
||||
store.remove(key(profileIndex))
|
||||
}
|
||||
|
||||
private fun key(profileIndex: Int): String = "profile_pin_$profileIndex"
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.nuvio.app.features.profiles
|
||||
|
||||
import java.security.MessageDigest
|
||||
|
||||
internal actual object ProfilePinCrypto {
|
||||
actual fun sha256Hex(value: String): String =
|
||||
MessageDigest
|
||||
.getInstance("SHA-256")
|
||||
.digest(value.toByteArray(Charsets.UTF_8))
|
||||
.joinToString("") { byte -> "%02x".format(byte) }
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.nuvio.app.features.profiles
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
|
||||
internal actual object ProfileStorage {
|
||||
private val store = DesktopStorage.store("nuvio_profiles")
|
||||
|
||||
actual fun loadPayload(): String? = store.getString("profiles")
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString("profiles", payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.search
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object SearchHistoryStorage {
|
||||
private val store = DesktopStorage.store("nuvio_search_history")
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
store.getString(ProfileScopedKey.of("search_history"))
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString(ProfileScopedKey.of("search_history"), payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.nuvio.app.features.settings
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
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
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
@Composable
|
||||
internal actual fun integrationLogoPainter(logo: IntegrationLogo): Painter =
|
||||
painterResource(
|
||||
when (logo) {
|
||||
IntegrationLogo.Tmdb -> Res.drawable.rating_tmdb
|
||||
IntegrationLogo.Trakt -> Res.drawable.trakt_tv_favicon
|
||||
IntegrationLogo.MdbList -> Res.drawable.mdblist_logo
|
||||
IntegrationLogo.IntroDb -> Res.drawable.introdb_favicon
|
||||
},
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.nuvio.app.features.settings
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
|
||||
internal actual fun LazyListScope.pluginsSettingsContent() = Unit
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.nuvio.app.features.settings
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
import com.nuvio.app.core.sync.decodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.decodeSyncString
|
||||
import com.nuvio.app.core.sync.encodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.encodeSyncString
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import java.util.Locale
|
||||
|
||||
internal actual object ThemeSettingsStorage {
|
||||
private const val selectedThemeKey = "selected_theme"
|
||||
private const val amoledEnabledKey = "amoled_enabled"
|
||||
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 store = DesktopStorage.store("nuvio_theme_settings")
|
||||
|
||||
actual fun loadSelectedTheme(): String? =
|
||||
store.getString(ProfileScopedKey.of(selectedThemeKey))
|
||||
|
||||
actual fun saveSelectedTheme(themeName: String) {
|
||||
store.putString(ProfileScopedKey.of(selectedThemeKey), themeName)
|
||||
}
|
||||
|
||||
actual fun loadAmoledEnabled(): Boolean? =
|
||||
store.getBoolean(ProfileScopedKey.of(amoledEnabledKey))
|
||||
|
||||
actual fun saveAmoledEnabled(enabled: Boolean) {
|
||||
store.putBoolean(ProfileScopedKey.of(amoledEnabledKey), enabled)
|
||||
}
|
||||
|
||||
actual fun loadLiquidGlassNativeTabBarEnabled(): Boolean? =
|
||||
store.getBoolean(ProfileScopedKey.of(liquidGlassNativeTabBarEnabledKey))
|
||||
|
||||
actual fun saveLiquidGlassNativeTabBarEnabled(enabled: Boolean) {
|
||||
store.putBoolean(ProfileScopedKey.of(liquidGlassNativeTabBarEnabledKey), enabled)
|
||||
}
|
||||
|
||||
actual fun loadSelectedAppLanguage(): String? =
|
||||
store.getString(selectedAppLanguageKey)
|
||||
?: Locale.getDefault().toLanguageTag().takeIf { it.isNotBlank() }
|
||||
|
||||
actual fun saveSelectedAppLanguage(languageCode: String) {
|
||||
store.putString(selectedAppLanguageKey, languageCode)
|
||||
}
|
||||
|
||||
actual fun applySelectedAppLanguage(languageCode: String) {
|
||||
Locale.setDefault(Locale.forLanguageTag(languageCode))
|
||||
}
|
||||
|
||||
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) {
|
||||
store.removeAll(profileScopedSyncKeys.map(ProfileScopedKey::of) + selectedAppLanguageKey)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.nuvio.app.features.streams
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object BingeGroupCacheStorage {
|
||||
private val store = DesktopStorage.store("nuvio_binge_group_cache")
|
||||
|
||||
actual fun load(hashedKey: String): String? =
|
||||
store.getString(ProfileScopedKey.of(hashedKey))
|
||||
|
||||
actual fun save(hashedKey: String, value: String) {
|
||||
store.putString(ProfileScopedKey.of(hashedKey), value)
|
||||
}
|
||||
|
||||
actual fun remove(hashedKey: String) {
|
||||
store.remove(ProfileScopedKey.of(hashedKey))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.nuvio.app.features.streams
|
||||
|
||||
internal actual fun epochMs(): Long = System.currentTimeMillis()
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.nuvio.app.features.streams
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
import com.nuvio.app.core.sync.decodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.decodeSyncString
|
||||
import com.nuvio.app.core.sync.encodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.encodeSyncString
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
internal actual object StreamBadgeSettingsStorage {
|
||||
private const val streamBadgeRulesKey = "stream_badge_rules"
|
||||
private const val showFileSizeBadgesKey = "show_file_size_badges"
|
||||
private const val streamBadgePlacementKey = "stream_badge_placement"
|
||||
private const val legacyDebridStreamBadgeRulesKey = "debrid_stream_badge_rules"
|
||||
private val syncKeys = listOf(streamBadgeRulesKey, showFileSizeBadgesKey, streamBadgePlacementKey)
|
||||
private val store = DesktopStorage.store("nuvio_stream_badge_settings")
|
||||
private val legacyDebridStore = DesktopStorage.store("nuvio_debrid_settings")
|
||||
|
||||
actual fun loadStreamBadgeRules(): String? = loadString(streamBadgeRulesKey)
|
||||
actual fun saveStreamBadgeRules(rules: String) = saveString(streamBadgeRulesKey, rules)
|
||||
actual fun loadShowFileSizeBadges(): Boolean? = loadBoolean(showFileSizeBadgesKey)
|
||||
actual fun saveShowFileSizeBadges(enabled: Boolean) = saveBoolean(showFileSizeBadgesKey, enabled)
|
||||
actual fun loadStreamBadgePlacement(): String? = loadString(streamBadgePlacementKey)
|
||||
actual fun saveStreamBadgePlacement(placement: String) = saveString(streamBadgePlacementKey, placement)
|
||||
|
||||
actual fun loadLegacyDebridStreamBadgeRules(): String? =
|
||||
legacyDebridStore.getString(ProfileScopedKey.of(legacyDebridStreamBadgeRulesKey))
|
||||
|
||||
actual fun clearLegacyDebridStreamBadgeRules() {
|
||||
legacyDebridStore.remove(ProfileScopedKey.of(legacyDebridStreamBadgeRulesKey))
|
||||
}
|
||||
|
||||
private fun loadString(key: String): String? = store.getString(ProfileScopedKey.of(key))
|
||||
private fun saveString(key: String, value: String) = store.putString(ProfileScopedKey.of(key), value)
|
||||
private fun loadBoolean(key: String): Boolean? = store.getBoolean(ProfileScopedKey.of(key))
|
||||
private fun saveBoolean(key: String, value: Boolean) = store.putBoolean(ProfileScopedKey.of(key), value)
|
||||
|
||||
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||
loadStreamBadgeRules()?.let { put(streamBadgeRulesKey, encodeSyncString(it)) }
|
||||
loadShowFileSizeBadges()?.let { put(showFileSizeBadgesKey, encodeSyncBoolean(it)) }
|
||||
loadStreamBadgePlacement()?.let { put(streamBadgePlacementKey, encodeSyncString(it)) }
|
||||
}
|
||||
|
||||
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||
store.removeAll(syncKeys.map(ProfileScopedKey::of))
|
||||
payload.decodeSyncString(streamBadgeRulesKey)?.let(::saveStreamBadgeRules)
|
||||
payload.decodeSyncBoolean(showFileSizeBadgesKey)?.let(::saveShowFileSizeBadges)
|
||||
payload.decodeSyncString(streamBadgePlacementKey)?.let(::saveStreamBadgePlacement)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.nuvio.app.features.streams
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object StreamLinkCacheStorage {
|
||||
private val store = DesktopStorage.store("nuvio_stream_link_cache")
|
||||
|
||||
actual fun loadEntry(hashedKey: String): String? =
|
||||
store.getString(ProfileScopedKey.of(hashedKey))
|
||||
|
||||
actual fun saveEntry(hashedKey: String, payload: String) {
|
||||
store.putString(ProfileScopedKey.of(hashedKey), payload)
|
||||
}
|
||||
|
||||
actual fun removeEntry(hashedKey: String) {
|
||||
store.remove(ProfileScopedKey.of(hashedKey))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package com.nuvio.app.features.tmdb
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
import com.nuvio.app.core.sync.decodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.decodeSyncString
|
||||
import com.nuvio.app.core.sync.encodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.encodeSyncString
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
internal actual object TmdbSettingsStorage {
|
||||
private const val enabledKey = "tmdb_enabled"
|
||||
private const val apiKeyKey = "tmdb_api_key"
|
||||
private const val languageKey = "tmdb_language"
|
||||
private const val useTrailersKey = "tmdb_use_trailers"
|
||||
private const val useArtworkKey = "tmdb_use_artwork"
|
||||
private const val useBasicInfoKey = "tmdb_use_basic_info"
|
||||
private const val useDetailsKey = "tmdb_use_details"
|
||||
private const val useCreditsKey = "tmdb_use_credits"
|
||||
private const val useProductionsKey = "tmdb_use_productions"
|
||||
private const val useNetworksKey = "tmdb_use_networks"
|
||||
private const val useEpisodesKey = "tmdb_use_episodes"
|
||||
private const val useSeasonPostersKey = "tmdb_use_season_posters"
|
||||
private const val useMoreLikeThisKey = "tmdb_use_more_like_this"
|
||||
private const val useCollectionsKey = "tmdb_use_collections"
|
||||
private val syncKeys = listOf(
|
||||
enabledKey,
|
||||
apiKeyKey,
|
||||
languageKey,
|
||||
useTrailersKey,
|
||||
useArtworkKey,
|
||||
useBasicInfoKey,
|
||||
useDetailsKey,
|
||||
useCreditsKey,
|
||||
useProductionsKey,
|
||||
useNetworksKey,
|
||||
useEpisodesKey,
|
||||
useSeasonPostersKey,
|
||||
useMoreLikeThisKey,
|
||||
useCollectionsKey,
|
||||
)
|
||||
private val store = DesktopStorage.store("nuvio_tmdb_settings")
|
||||
|
||||
actual fun loadEnabled(): Boolean? = loadBoolean(enabledKey)
|
||||
actual fun saveEnabled(enabled: Boolean) = saveBoolean(enabledKey, enabled)
|
||||
actual fun loadApiKey(): String? = loadString(apiKeyKey)
|
||||
actual fun saveApiKey(apiKey: String) = saveString(apiKeyKey, apiKey)
|
||||
actual fun loadLanguage(): String? = loadString(languageKey)
|
||||
actual fun saveLanguage(language: String) = saveString(languageKey, language)
|
||||
actual fun loadUseTrailers(): Boolean? = loadBoolean(useTrailersKey)
|
||||
actual fun saveUseTrailers(enabled: Boolean) = saveBoolean(useTrailersKey, enabled)
|
||||
actual fun loadUseArtwork(): Boolean? = loadBoolean(useArtworkKey)
|
||||
actual fun saveUseArtwork(enabled: Boolean) = saveBoolean(useArtworkKey, enabled)
|
||||
actual fun loadUseBasicInfo(): Boolean? = loadBoolean(useBasicInfoKey)
|
||||
actual fun saveUseBasicInfo(enabled: Boolean) = saveBoolean(useBasicInfoKey, enabled)
|
||||
actual fun loadUseDetails(): Boolean? = loadBoolean(useDetailsKey)
|
||||
actual fun saveUseDetails(enabled: Boolean) = saveBoolean(useDetailsKey, enabled)
|
||||
actual fun loadUseCredits(): Boolean? = loadBoolean(useCreditsKey)
|
||||
actual fun saveUseCredits(enabled: Boolean) = saveBoolean(useCreditsKey, enabled)
|
||||
actual fun loadUseProductions(): Boolean? = loadBoolean(useProductionsKey)
|
||||
actual fun saveUseProductions(enabled: Boolean) = saveBoolean(useProductionsKey, enabled)
|
||||
actual fun loadUseNetworks(): Boolean? = loadBoolean(useNetworksKey)
|
||||
actual fun saveUseNetworks(enabled: Boolean) = saveBoolean(useNetworksKey, enabled)
|
||||
actual fun loadUseEpisodes(): Boolean? = loadBoolean(useEpisodesKey)
|
||||
actual fun saveUseEpisodes(enabled: Boolean) = saveBoolean(useEpisodesKey, enabled)
|
||||
actual fun loadUseSeasonPosters(): Boolean? = loadBoolean(useSeasonPostersKey)
|
||||
actual fun saveUseSeasonPosters(enabled: Boolean) = saveBoolean(useSeasonPostersKey, enabled)
|
||||
actual fun loadUseMoreLikeThis(): Boolean? = loadBoolean(useMoreLikeThisKey)
|
||||
actual fun saveUseMoreLikeThis(enabled: Boolean) = saveBoolean(useMoreLikeThisKey, enabled)
|
||||
actual fun loadUseCollections(): Boolean? = loadBoolean(useCollectionsKey)
|
||||
actual fun saveUseCollections(enabled: Boolean) = saveBoolean(useCollectionsKey, enabled)
|
||||
|
||||
private fun loadString(key: String): String? = store.getString(ProfileScopedKey.of(key))
|
||||
private fun saveString(key: String, value: String) = store.putString(ProfileScopedKey.of(key), value)
|
||||
private fun loadBoolean(key: String): Boolean? = store.getBoolean(ProfileScopedKey.of(key))
|
||||
private fun saveBoolean(key: String, value: Boolean) = store.putBoolean(ProfileScopedKey.of(key), value)
|
||||
|
||||
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||
loadEnabled()?.let { put(enabledKey, encodeSyncBoolean(it)) }
|
||||
loadApiKey()?.let { put(apiKeyKey, encodeSyncString(it)) }
|
||||
loadLanguage()?.let { put(languageKey, encodeSyncString(it)) }
|
||||
loadUseTrailers()?.let { put(useTrailersKey, encodeSyncBoolean(it)) }
|
||||
loadUseArtwork()?.let { put(useArtworkKey, encodeSyncBoolean(it)) }
|
||||
loadUseBasicInfo()?.let { put(useBasicInfoKey, encodeSyncBoolean(it)) }
|
||||
loadUseDetails()?.let { put(useDetailsKey, encodeSyncBoolean(it)) }
|
||||
loadUseCredits()?.let { put(useCreditsKey, encodeSyncBoolean(it)) }
|
||||
loadUseProductions()?.let { put(useProductionsKey, encodeSyncBoolean(it)) }
|
||||
loadUseNetworks()?.let { put(useNetworksKey, encodeSyncBoolean(it)) }
|
||||
loadUseEpisodes()?.let { put(useEpisodesKey, encodeSyncBoolean(it)) }
|
||||
loadUseSeasonPosters()?.let { put(useSeasonPostersKey, encodeSyncBoolean(it)) }
|
||||
loadUseMoreLikeThis()?.let { put(useMoreLikeThisKey, encodeSyncBoolean(it)) }
|
||||
loadUseCollections()?.let { put(useCollectionsKey, encodeSyncBoolean(it)) }
|
||||
}
|
||||
|
||||
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||
store.removeAll(syncKeys.map(ProfileScopedKey::of))
|
||||
payload.decodeSyncBoolean(enabledKey)?.let(::saveEnabled)
|
||||
payload.decodeSyncString(apiKeyKey)?.let(::saveApiKey)
|
||||
payload.decodeSyncString(languageKey)?.let(::saveLanguage)
|
||||
payload.decodeSyncBoolean(useTrailersKey)?.let(::saveUseTrailers)
|
||||
payload.decodeSyncBoolean(useArtworkKey)?.let(::saveUseArtwork)
|
||||
payload.decodeSyncBoolean(useBasicInfoKey)?.let(::saveUseBasicInfo)
|
||||
payload.decodeSyncBoolean(useDetailsKey)?.let(::saveUseDetails)
|
||||
payload.decodeSyncBoolean(useCreditsKey)?.let(::saveUseCredits)
|
||||
payload.decodeSyncBoolean(useProductionsKey)?.let(::saveUseProductions)
|
||||
payload.decodeSyncBoolean(useNetworksKey)?.let(::saveUseNetworks)
|
||||
payload.decodeSyncBoolean(useEpisodesKey)?.let(::saveUseEpisodes)
|
||||
payload.decodeSyncBoolean(useSeasonPostersKey)?.let(::saveUseSeasonPosters)
|
||||
payload.decodeSyncBoolean(useMoreLikeThisKey)?.let(::saveUseMoreLikeThis)
|
||||
payload.decodeSyncBoolean(useCollectionsKey)?.let(::saveUseCollections)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.nuvio.app.features.trailer
|
||||
|
||||
actual object TrailerPlaybackResolver {
|
||||
actual suspend fun resolveFromYouTubeUrl(youtubeUrl: String): TrailerPlaybackSource? = null
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.nuvio.app.features.trakt
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.trakt_logo_wordmark
|
||||
import nuvio.composeapp.generated.resources.trakt_tv_favicon
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
@Composable
|
||||
actual fun traktBrandPainter(asset: TraktBrandAsset): Painter =
|
||||
painterResource(
|
||||
when (asset) {
|
||||
TraktBrandAsset.Glyph -> Res.drawable.trakt_tv_favicon
|
||||
TraktBrandAsset.Wordmark -> Res.drawable.trakt_logo_wordmark
|
||||
},
|
||||
)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.nuvio.app.features.trakt
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
import com.nuvio.app.core.sync.decodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.encodeSyncBoolean
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
internal actual object TraktCommentsStorage {
|
||||
private const val enabledKey = "trakt_comments_enabled"
|
||||
private val store = DesktopStorage.store("nuvio_trakt_comments")
|
||||
|
||||
actual fun loadEnabled(): Boolean? =
|
||||
store.getBoolean(ProfileScopedKey.of(enabledKey))
|
||||
|
||||
actual fun saveEnabled(enabled: Boolean) {
|
||||
store.putBoolean(ProfileScopedKey.of(enabledKey), enabled)
|
||||
}
|
||||
|
||||
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||
loadEnabled()?.let { put(enabledKey, encodeSyncBoolean(it)) }
|
||||
}
|
||||
|
||||
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||
store.remove(ProfileScopedKey.of(enabledKey))
|
||||
payload.decodeSyncBoolean(enabledKey)?.let(::saveEnabled)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.nuvio.app.features.trakt
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeParseException
|
||||
|
||||
internal actual object TraktPlatformClock {
|
||||
actual fun nowEpochMs(): Long = System.currentTimeMillis()
|
||||
|
||||
actual fun parseIsoDateTimeToEpochMs(value: String): Long? =
|
||||
try {
|
||||
Instant.parse(value).toEpochMilli()
|
||||
} catch (_: DateTimeParseException) {
|
||||
null
|
||||
}
|
||||
|
||||
actual fun availableProcessors(): Int =
|
||||
Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.nuvio.app.features.trakt
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object TraktAuthStorage {
|
||||
private val store = DesktopStorage.store("nuvio_trakt_auth")
|
||||
|
||||
actual fun loadPayload(): String? = store.getString("trakt_auth")
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString("trakt_auth", payload)
|
||||
}
|
||||
}
|
||||
|
||||
internal actual object TraktLibraryStorage {
|
||||
private val store = DesktopStorage.store("nuvio_trakt_library")
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
store.getString(ProfileScopedKey.of("trakt_library"))
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString(ProfileScopedKey.of("trakt_library"), payload)
|
||||
}
|
||||
}
|
||||
|
||||
internal actual object TraktSettingsStorage {
|
||||
private val store = DesktopStorage.store("nuvio_trakt_settings")
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
store.getString(ProfileScopedKey.of("trakt_settings"))
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString(ProfileScopedKey.of("trakt_settings"), payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.nuvio.app.features.watched
|
||||
|
||||
actual object WatchedClock {
|
||||
actual fun nowEpochMs(): Long = System.currentTimeMillis()
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.nuvio.app.features.watched
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
|
||||
actual object WatchedStorage {
|
||||
private val store = DesktopStorage.store("nuvio_watched")
|
||||
|
||||
actual fun loadPayload(profileId: Int): String? =
|
||||
store.getString("watched_$profileId")
|
||||
|
||||
actual fun savePayload(profileId: Int, payload: String) {
|
||||
store.putString("watched_$profileId", payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object ContinueWatchingEnrichmentStorage {
|
||||
private val store = DesktopStorage.store("nuvio_continue_watching_enrichment")
|
||||
|
||||
actual fun loadPayload(key: String): String? =
|
||||
store.getString(ProfileScopedKey.of(key.scopedKey()))
|
||||
|
||||
actual fun savePayload(key: String, payload: String) {
|
||||
store.putString(ProfileScopedKey.of(key.scopedKey()), payload)
|
||||
}
|
||||
|
||||
actual fun removePayload(key: String) {
|
||||
store.remove(ProfileScopedKey.of(key.scopedKey()))
|
||||
}
|
||||
|
||||
private fun String.scopedKey(): String = "continue_watching_enrichment_$this"
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
internal actual object ContinueWatchingPreferencesStorage {
|
||||
private val store = DesktopStorage.store("nuvio_continue_watching_preferences")
|
||||
|
||||
actual fun loadPayload(): String? =
|
||||
store.getString(ProfileScopedKey.of("continue_watching_preferences"))
|
||||
|
||||
actual fun savePayload(payload: String) {
|
||||
store.putString(ProfileScopedKey.of("continue_watching_preferences"), payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import java.time.LocalDate
|
||||
|
||||
actual object CurrentDateProvider {
|
||||
actual fun todayIsoDate(): String = LocalDate.now().toString()
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
|
||||
internal actual object ResumePromptStorage {
|
||||
private val store = DesktopStorage.store("nuvio_resume_prompt")
|
||||
|
||||
actual fun loadWasInPlayer(): Boolean =
|
||||
store.getBoolean("was_in_player") ?: false
|
||||
|
||||
actual fun saveWasInPlayer(value: Boolean) {
|
||||
store.putBoolean("was_in_player", value)
|
||||
}
|
||||
|
||||
actual fun loadLastPlayerVideoId(): String? =
|
||||
store.getString("last_player_video_id")
|
||||
|
||||
actual fun saveLastPlayerVideoId(videoId: String?) {
|
||||
store.putString("last_player_video_id", videoId?.takeIf { it.isNotBlank() })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
internal actual object WatchProgressClock {
|
||||
actual fun nowEpochMs(): Long = System.currentTimeMillis()
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
|
||||
internal actual object WatchProgressStorage {
|
||||
private val store = DesktopStorage.store("nuvio_watch_progress")
|
||||
|
||||
actual fun loadPayload(profileId: Int): String? =
|
||||
store.getString("watch_progress_$profileId")
|
||||
|
||||
actual fun savePayload(profileId: Int, payload: String) {
|
||||
store.putString("watch_progress_$profileId", payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ kermit = "2.0.5"
|
|||
junit = "4.13.2"
|
||||
kotlin = "2.3.0"
|
||||
kotlinx-serialization = "1.8.1"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
ktor = "3.4.1"
|
||||
material3 = "1.11.0-alpha07"
|
||||
androidx-media3 = "1.8.0"
|
||||
|
|
@ -53,7 +54,9 @@ coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }
|
|||
coil-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
|
||||
coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
|
||||
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
|
||||
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
|
||||
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
|
||||
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" }
|
||||
|
|
|
|||
Loading…
Reference in a new issue