mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
feat: inti storage
This commit is contained in:
parent
21793b8ac7
commit
9f7f465a25
31 changed files with 1804 additions and 2 deletions
|
|
@ -1,4 +1,3 @@
|
||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
|
||||||
import org.gradle.api.DefaultTask
|
import org.gradle.api.DefaultTask
|
||||||
import org.gradle.api.file.DirectoryProperty
|
import org.gradle.api.file.DirectoryProperty
|
||||||
import org.gradle.api.file.RegularFileProperty
|
import org.gradle.api.file.RegularFileProperty
|
||||||
|
|
@ -165,6 +164,12 @@ kotlin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jvm("desktop") {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(JvmTarget.JVM_11)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val iosTargets = listOf(
|
val iosTargets = listOf(
|
||||||
iosArm64(),
|
iosArm64(),
|
||||||
iosSimulatorArm64()
|
iosSimulatorArm64()
|
||||||
|
|
@ -203,6 +208,13 @@ kotlin {
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
kotlin.srcDir(generatedRuntimeConfigDir)
|
kotlin.srcDir(generatedRuntimeConfigDir)
|
||||||
}
|
}
|
||||||
|
val desktopMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
implementation(libs.ktor.client.java)
|
||||||
|
implementation(libs.kotlinx.coroutines.swing)
|
||||||
|
}
|
||||||
|
}
|
||||||
androidMain.dependencies {
|
androidMain.dependencies {
|
||||||
implementation(libs.compose.uiToolingPreview)
|
implementation(libs.compose.uiToolingPreview)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|
@ -265,6 +277,12 @@ dependencies {
|
||||||
debugImplementation(libs.compose.uiTooling)
|
debugImplementation(libs.compose.uiTooling)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compose.desktop {
|
||||||
|
application {
|
||||||
|
mainClass = "com.nuvio.app.DesktopAppKt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
configurations.all {
|
configurations.all {
|
||||||
exclude(group = "androidx.media3", module = "media3-exoplayer")
|
exclude(group = "androidx.media3", module = "media3-exoplayer")
|
||||||
exclude(group = "androidx.media3", module = "media3-ui")
|
exclude(group = "androidx.media3", module = "media3-ui")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.nuvio.app
|
||||||
|
|
||||||
|
import androidx.compose.ui.window.Window
|
||||||
|
import androidx.compose.ui.window.application
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
Window(
|
||||||
|
onCloseRequest = ::exitApplication,
|
||||||
|
title = "Nuvio",
|
||||||
|
) {
|
||||||
|
App()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.nuvio.app
|
||||||
|
|
||||||
|
private class DesktopPlatform : Platform {
|
||||||
|
override val name: String = "Desktop"
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun getPlatform(): Platform = DesktopPlatform()
|
||||||
|
|
||||||
|
internal actual val isIos: Boolean = false
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.nuvio.app.core.auth
|
||||||
|
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
|
||||||
|
internal actual object AuthStorage {
|
||||||
|
private const val preferencesName = "nuvio_auth"
|
||||||
|
private const val anonymousUserIdKey = "anonymous_user_id"
|
||||||
|
|
||||||
|
actual fun loadAnonymousUserId(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, anonymousUserIdKey)
|
||||||
|
|
||||||
|
actual fun saveAnonymousUserId(userId: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, anonymousUserIdKey, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun clearAnonymousUserId() {
|
||||||
|
DesktopPreferences.remove(preferencesName, anonymousUserIdKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.nuvio.app.core.storage
|
||||||
|
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
|
||||||
|
internal actual object PlatformLocalAccountDataCleaner {
|
||||||
|
private val preferenceNames = listOf(
|
||||||
|
"nuvio_addons",
|
||||||
|
"nuvio_library",
|
||||||
|
"nuvio_home_catalog_settings",
|
||||||
|
"nuvio_meta_screen_settings",
|
||||||
|
"nuvio_player_settings",
|
||||||
|
"nuvio_profile_cache",
|
||||||
|
"nuvio_search_history",
|
||||||
|
"nuvio_theme_settings",
|
||||||
|
"nuvio_poster_card_style",
|
||||||
|
"nuvio_mdblist_settings",
|
||||||
|
"nuvio_trakt_auth",
|
||||||
|
"nuvio_trakt_comments",
|
||||||
|
"nuvio_trakt_library",
|
||||||
|
"nuvio_watched",
|
||||||
|
"nuvio_stream_link_cache",
|
||||||
|
"nuvio_continue_watching_preferences",
|
||||||
|
"nuvio_cw_enrichment",
|
||||||
|
"nuvio_resume_prompt",
|
||||||
|
"nuvio_episode_release_notifications",
|
||||||
|
"nuvio_watch_progress",
|
||||||
|
"nuvio_plugins",
|
||||||
|
"nuvio_collections",
|
||||||
|
"nuvio_downloads",
|
||||||
|
"nuvio_tmdb_settings",
|
||||||
|
"nuvio_season_view_mode",
|
||||||
|
)
|
||||||
|
|
||||||
|
actual fun wipe() {
|
||||||
|
preferenceNames.forEach(DesktopPreferences::clearNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.nuvio.app.core.sync
|
||||||
|
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
|
||||||
|
internal actual object AppForegroundMonitor {
|
||||||
|
actual fun events(): Flow<Unit> = callbackFlow {
|
||||||
|
trySend(Unit)
|
||||||
|
awaitClose {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package com.nuvio.app.core.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
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
|
||||||
|
|
||||||
|
internal actual val nuvioPlatformExtraTopPadding: Dp = 0.dp
|
||||||
|
internal actual val nuvioPlatformExtraBottomPadding: Dp = 0.dp
|
||||||
|
internal actual val nuvioBottomNavigationExtraVerticalPadding: Dp = 6.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal actual fun nuvioBottomNavigationBarInsets(): WindowInsets = WindowInsets(0, 0, 0, 0)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun PlatformBackHandler(
|
||||||
|
enabled: Boolean,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
) = Unit
|
||||||
|
|
||||||
|
@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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
internal actual fun ImageLoader.Builder.configurePlatformImageLoader(): ImageLoader.Builder = this
|
||||||
|
|
||||||
|
actual fun platformExitApp() {
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object PosterCardStyleStorage {
|
||||||
|
private const val preferencesName = "nuvio_poster_card_style"
|
||||||
|
private const val payloadKey = "poster_card_style_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(payloadKey))
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(payloadKey), payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
package com.nuvio.app.desktop
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.util.Base64
|
||||||
|
import kotlin.io.path.createDirectories
|
||||||
|
import kotlin.io.path.deleteExisting
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.io.path.inputStream
|
||||||
|
import kotlin.io.path.isDirectory
|
||||||
|
import kotlin.io.path.listDirectoryEntries
|
||||||
|
import kotlin.io.path.outputStream
|
||||||
|
import kotlin.io.path.readText
|
||||||
|
import kotlin.io.path.writeText
|
||||||
|
|
||||||
|
internal object DesktopPreferences {
|
||||||
|
private const val setSeparator = "\u001F"
|
||||||
|
private val keyEncoder = Base64.getUrlEncoder().withoutPadding()
|
||||||
|
|
||||||
|
private val rootDir: Path by lazy {
|
||||||
|
Paths.get(
|
||||||
|
System.getProperty("user.home"),
|
||||||
|
"Library",
|
||||||
|
"Application Support",
|
||||||
|
"Nuvio",
|
||||||
|
"preferences",
|
||||||
|
).apply {
|
||||||
|
createDirectories()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun namespaceDir(namespace: String): Path =
|
||||||
|
rootDir.resolve(encodePathPart(namespace)).apply {
|
||||||
|
createDirectories()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun keyFile(namespace: String, key: String): Path =
|
||||||
|
namespaceDir(namespace).resolve(encodePathPart(key))
|
||||||
|
|
||||||
|
private fun encodePathPart(value: String): String =
|
||||||
|
keyEncoder.encodeToString(value.toByteArray(StandardCharsets.UTF_8))
|
||||||
|
|
||||||
|
fun contains(namespace: String, key: String): Boolean =
|
||||||
|
keyFile(namespace, key).exists()
|
||||||
|
|
||||||
|
fun getString(namespace: String, key: String): String? =
|
||||||
|
keyFile(namespace, key)
|
||||||
|
.takeIf(Path::exists)
|
||||||
|
?.readText(StandardCharsets.UTF_8)
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun putString(namespace: String, key: String, value: String) {
|
||||||
|
keyFile(namespace, key).writeText(value, StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putNullableString(namespace: String, key: String, value: String?) {
|
||||||
|
if (value == null) {
|
||||||
|
remove(namespace, key)
|
||||||
|
} else {
|
||||||
|
putString(namespace, key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBoolean(namespace: String, key: String): Boolean? =
|
||||||
|
getString(namespace, key)?.toBooleanStrictOrNull()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun putBoolean(namespace: String, key: String, value: Boolean) {
|
||||||
|
putString(namespace, key, value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInt(namespace: String, key: String): Int? =
|
||||||
|
getString(namespace, key)?.toIntOrNull()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun putInt(namespace: String, key: String, value: Int) {
|
||||||
|
putString(namespace, key, value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFloat(namespace: String, key: String): Float? =
|
||||||
|
getString(namespace, key)?.toFloatOrNull()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun putFloat(namespace: String, key: String, value: Float) {
|
||||||
|
putString(namespace, key, value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStringSet(namespace: String, key: String): Set<String>? {
|
||||||
|
val raw = getString(namespace, key) ?: return null
|
||||||
|
if (raw.isEmpty()) return emptySet()
|
||||||
|
return raw.split(setSeparator)
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(String::isNotEmpty)
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putStringSet(namespace: String, key: String, values: Set<String>) {
|
||||||
|
putString(namespace, key, values.toSortedSet().joinToString(setSeparator))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun remove(namespace: String, key: String) {
|
||||||
|
runCatching {
|
||||||
|
keyFile(namespace, key).deleteExisting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun clearNode(namespace: String) {
|
||||||
|
runCatching {
|
||||||
|
deleteRecursively(namespaceDir(namespace))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteRecursively(path: Path) {
|
||||||
|
if (!path.exists()) return
|
||||||
|
if (path.isDirectory()) {
|
||||||
|
path.listDirectoryEntries().forEach(::deleteRecursively)
|
||||||
|
}
|
||||||
|
path.deleteExisting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
package com.nuvio.app.features.addons
|
||||||
|
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.http.HttpClient
|
||||||
|
import java.net.http.HttpRequest
|
||||||
|
import java.net.http.HttpResponse
|
||||||
|
import java.time.Duration
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
internal actual object AddonStorage {
|
||||||
|
private const val preferencesName = "nuvio_addons"
|
||||||
|
private const val addonUrlsKey = "installed_manifest_urls"
|
||||||
|
|
||||||
|
actual fun loadInstalledAddonUrls(profileId: Int): List<String> =
|
||||||
|
DesktopPreferences.getString(preferencesName, "${addonUrlsKey}_$profileId")
|
||||||
|
.orEmpty()
|
||||||
|
.lineSequence()
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(String::isNotEmpty)
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
actual fun saveInstalledAddonUrls(profileId: Int, urls: List<String>) {
|
||||||
|
DesktopPreferences.putString(
|
||||||
|
preferencesName,
|
||||||
|
"${addonUrlsKey}_$profileId",
|
||||||
|
urls.joinToString(separator = "\n"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val addonHttpClient: HttpClient = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(60))
|
||||||
|
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private const val maxRawResponseBodyChars = 1024 * 1024
|
||||||
|
private const val truncationSuffix = "\n...[truncated]"
|
||||||
|
|
||||||
|
private fun requestAllowsBody(method: String): Boolean =
|
||||||
|
when (method.uppercase()) {
|
||||||
|
"POST", "PUT", "PATCH", "DELETE" -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<String, String>.withoutAcceptEncoding(): Map<String, String> =
|
||||||
|
entries
|
||||||
|
.filterNot { (key, _) -> key.equals("Accept-Encoding", ignoreCase = true) }
|
||||||
|
.associate { (key, value) -> key to value }
|
||||||
|
|
||||||
|
private suspend fun executeRequest(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: Map<String, String>,
|
||||||
|
body: String,
|
||||||
|
) = withContext(Dispatchers.IO) {
|
||||||
|
val builder = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(url))
|
||||||
|
.timeout(Duration.ofSeconds(60))
|
||||||
|
|
||||||
|
headers.withoutAcceptEncoding().forEach { (key, value) ->
|
||||||
|
builder.header(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = if (requestAllowsBody(method)) {
|
||||||
|
builder.method(method.uppercase(), HttpRequest.BodyPublishers.ofString(body))
|
||||||
|
} else {
|
||||||
|
builder.method(method.uppercase(), HttpRequest.BodyPublishers.noBody())
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
addonHttpClient.send(request, HttpResponse.BodyHandlers.ofString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun executeTextRequest(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: Map<String, String> = emptyMap(),
|
||||||
|
body: String = "",
|
||||||
|
): String {
|
||||||
|
val response = executeRequest(method, url, headers, body)
|
||||||
|
val payload = response.body()
|
||||||
|
if (response.statusCode() !in 200..299) {
|
||||||
|
error("Request failed with HTTP ${response.statusCode()}")
|
||||||
|
}
|
||||||
|
if (payload.isBlank()) {
|
||||||
|
throw IllegalStateException("Empty response body")
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
actual suspend fun httpGetText(url: String): String =
|
||||||
|
executeTextRequest(
|
||||||
|
method = "GET",
|
||||||
|
url = url,
|
||||||
|
headers = mapOf("Accept" to "application/json"),
|
||||||
|
)
|
||||||
|
|
||||||
|
actual suspend fun httpPostJson(url: String, body: String): String =
|
||||||
|
executeTextRequest(
|
||||||
|
method = "POST",
|
||||||
|
url = url,
|
||||||
|
headers = mapOf(
|
||||||
|
"Accept" to "application/json",
|
||||||
|
"Content-Type" to "application/json",
|
||||||
|
),
|
||||||
|
body = body,
|
||||||
|
)
|
||||||
|
|
||||||
|
actual suspend fun httpGetTextWithHeaders(
|
||||||
|
url: String,
|
||||||
|
headers: Map<String, String>,
|
||||||
|
): String =
|
||||||
|
executeTextRequest(
|
||||||
|
method = "GET",
|
||||||
|
url = url,
|
||||||
|
headers = mapOf("Accept" to "application/json") + headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
actual suspend fun httpPostJsonWithHeaders(
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: Map<String, String>,
|
||||||
|
): String =
|
||||||
|
executeTextRequest(
|
||||||
|
method = "POST",
|
||||||
|
url = url,
|
||||||
|
headers = mapOf(
|
||||||
|
"Accept" to "application/json",
|
||||||
|
"Content-Type" to "application/json",
|
||||||
|
) + headers,
|
||||||
|
body = body,
|
||||||
|
)
|
||||||
|
|
||||||
|
actual suspend fun httpRequestRaw(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: Map<String, String>,
|
||||||
|
body: String,
|
||||||
|
): RawHttpResponse {
|
||||||
|
val response = executeRequest(method, url, headers, body)
|
||||||
|
val payload = response.body()
|
||||||
|
val limitedPayload = if (payload.length > maxRawResponseBodyChars) {
|
||||||
|
payload.take(maxRawResponseBodyChars) + truncationSuffix
|
||||||
|
} else {
|
||||||
|
payload
|
||||||
|
}
|
||||||
|
return RawHttpResponse(
|
||||||
|
status = response.statusCode(),
|
||||||
|
statusText = response.version().toString(),
|
||||||
|
url = response.uri().toString(),
|
||||||
|
body = limitedPayload,
|
||||||
|
headers = response.headers().map().entries.associate { (key, values) ->
|
||||||
|
key.lowercase() to values.joinToString(",")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.nuvio.app.features.collection
|
||||||
|
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
|
||||||
|
internal actual object CollectionStorage {
|
||||||
|
private const val preferencesName = "nuvio_collections"
|
||||||
|
private const val payloadKey = "collections_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(payloadKey))
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(payloadKey), payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.nuvio.app.features.details
|
||||||
|
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
|
||||||
|
internal actual object MetaScreenSettingsStorage {
|
||||||
|
private const val preferencesName = "nuvio_meta_screen_settings"
|
||||||
|
private const val payloadKey = "meta_screen_settings_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(payloadKey))
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(payloadKey), payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object SeasonViewModeStorage {
|
||||||
|
private const val preferencesName = "nuvio_season_view_mode"
|
||||||
|
private const val valueKey = "season_view_mode"
|
||||||
|
|
||||||
|
actual fun load(): SeasonViewMode? =
|
||||||
|
SeasonViewMode.parse(
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(valueKey)),
|
||||||
|
)
|
||||||
|
|
||||||
|
actual fun save(mode: SeasonViewMode) {
|
||||||
|
DesktopPreferences.putString(
|
||||||
|
preferencesName,
|
||||||
|
ProfileScopedKey.of(valueKey),
|
||||||
|
SeasonViewMode.persist(mode),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package com.nuvio.app.features.downloads
|
||||||
|
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
|
||||||
|
internal actual object DownloadsStorage {
|
||||||
|
private const val preferencesName = "nuvio_downloads"
|
||||||
|
private const val payloadKey = "downloads_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(payloadKey))
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(payloadKey), payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object NoOpDownloadsTaskHandle : DownloadsTaskHandle {
|
||||||
|
override fun cancel() = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object DownloadsPlatformDownloader {
|
||||||
|
actual fun start(
|
||||||
|
request: DownloadPlatformRequest,
|
||||||
|
onProgress: (downloadedBytes: Long, totalBytes: Long?) -> Unit,
|
||||||
|
onSuccess: (localFileUri: String, totalBytes: Long?) -> Unit,
|
||||||
|
onFailure: (message: String) -> Unit,
|
||||||
|
): DownloadsTaskHandle {
|
||||||
|
onFailure("Downloads are not available on desktop yet.")
|
||||||
|
return NoOpDownloadsTaskHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun removeFile(localFileUri: String?): Boolean = false
|
||||||
|
|
||||||
|
actual fun removePartialFile(destinationFileName: String): Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object DownloadsLiveStatusPlatform {
|
||||||
|
actual fun onItemsChanged(items: List<DownloadItem>) = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object DownloadsClock {
|
||||||
|
actual fun nowEpochMs(): Long = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.nuvio.app.features.home
|
||||||
|
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
|
||||||
|
internal actual object HomeCatalogSettingsStorage {
|
||||||
|
private const val preferencesName = "nuvio_home_catalog_settings"
|
||||||
|
private const val payloadKey = "catalog_settings_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(payloadKey))
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(payloadKey), payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.nuvio.app.features.home.components
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal actual fun CollectionCardRemoteImage(
|
||||||
|
imageUrl: String,
|
||||||
|
contentDescription: String,
|
||||||
|
modifier: Modifier,
|
||||||
|
contentScale: ContentScale,
|
||||||
|
animateIfPossible: Boolean,
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = imageUrl,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
modifier = modifier,
|
||||||
|
contentScale = contentScale,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.nuvio.app.features.library
|
||||||
|
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
|
||||||
|
internal actual object LibraryStorage {
|
||||||
|
private const val preferencesName = "nuvio_library"
|
||||||
|
private const val payloadKey = "library_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(profileId: Int): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, "${payloadKey}_$profileId")
|
||||||
|
|
||||||
|
actual fun savePayload(profileId: Int, payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, "${payloadKey}_$profileId", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object LibraryClock {
|
||||||
|
actual fun nowEpochMs(): Long = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
package com.nuvio.app.features.mdblist
|
||||||
|
|
||||||
|
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 com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
|
||||||
|
internal actual object MdbListSettingsStorage {
|
||||||
|
private const val preferencesName = "nuvio_mdblist_settings"
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
syncKeys.forEach { DesktopPreferences.remove(preferencesName, ProfileScopedKey.of(it)) }
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scopedKey(baseKey: String): String = ProfileScopedKey.of(baseKey)
|
||||||
|
|
||||||
|
private fun loadString(key: String): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, scopedKey(key))
|
||||||
|
|
||||||
|
private fun saveString(key: String, value: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, scopedKey(key), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadBoolean(key: String): Boolean? =
|
||||||
|
DesktopPreferences.getBoolean(preferencesName, scopedKey(key))
|
||||||
|
|
||||||
|
private fun saveBoolean(key: String, value: Boolean) {
|
||||||
|
DesktopPreferences.putBoolean(preferencesName, scopedKey(key), value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.nuvio.app.features.notifications
|
||||||
|
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
internal actual object EpisodeReleaseNotificationsStorage {
|
||||||
|
private const val preferencesName = "nuvio_episode_release_notifications"
|
||||||
|
private const val payloadKey = "episode_release_notifications_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(payloadKey))
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(payloadKey), payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object EpisodeReleaseNotificationPlatform {
|
||||||
|
actual suspend fun notificationsAuthorized(): Boolean = true
|
||||||
|
|
||||||
|
actual suspend fun requestAuthorization(): Boolean = true
|
||||||
|
|
||||||
|
actual suspend fun scheduleEpisodeReleaseNotifications(requests: List<EpisodeReleaseNotificationRequest>) = Unit
|
||||||
|
|
||||||
|
actual suspend fun clearScheduledEpisodeReleaseNotifications() = Unit
|
||||||
|
|
||||||
|
actual suspend fun showTestNotification(request: EpisodeReleaseNotificationRequest) = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object EpisodeReleaseNotificationsClock {
|
||||||
|
actual fun isoDateFromEpochMs(epochMs: Long): String =
|
||||||
|
Instant.ofEpochMilli(epochMs)
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toLocalDate()
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,499 @@
|
||||||
|
package com.nuvio.app.features.player
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
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
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
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 com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
object : 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(controller) {
|
||||||
|
onControllerReady(controller)
|
||||||
|
onSnapshot(PlayerPlaybackSnapshot(isLoading = false))
|
||||||
|
onError(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier.background(Color.Black),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Desktop playback is not implemented yet.",
|
||||||
|
color = Color.White,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object DeviceLanguagePreferences {
|
||||||
|
actual fun preferredLanguageCodes(): List<String> =
|
||||||
|
listOfNotNull(Locale.getDefault().toLanguageTag().takeIf { it.isNotBlank() })
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object PlayerSettingsStorage {
|
||||||
|
private const val preferencesName = "nuvio_player_settings"
|
||||||
|
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 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 subtitleOutlineEnabledKey = "subtitle_outline_enabled"
|
||||||
|
private const val subtitleFontSizeSpKey = "subtitle_font_size_sp"
|
||||||
|
private const val subtitleBottomOffsetKey = "subtitle_bottom_offset"
|
||||||
|
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 streamAutoPlayNextEpisodeEnabledKey = "stream_auto_play_next_episode_enabled"
|
||||||
|
private const val streamAutoPlayPreferBingeGroupKey = "stream_auto_play_prefer_binge_group"
|
||||||
|
private const val nextEpisodeThresholdModeKey = "next_episode_threshold_mode"
|
||||||
|
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 val syncKeys = listOf(
|
||||||
|
showLoadingOverlayKey,
|
||||||
|
resizeModeKey,
|
||||||
|
holdToSpeedEnabledKey,
|
||||||
|
holdToSpeedValueKey,
|
||||||
|
preferredAudioLanguageKey,
|
||||||
|
secondaryPreferredAudioLanguageKey,
|
||||||
|
preferredSubtitleLanguageKey,
|
||||||
|
secondaryPreferredSubtitleLanguageKey,
|
||||||
|
subtitleTextColorKey,
|
||||||
|
subtitleOutlineEnabledKey,
|
||||||
|
subtitleFontSizeSpKey,
|
||||||
|
subtitleBottomOffsetKey,
|
||||||
|
streamReuseLastLinkEnabledKey,
|
||||||
|
streamReuseLastLinkCacheHoursKey,
|
||||||
|
decoderPriorityKey,
|
||||||
|
mapDV7ToHevcKey,
|
||||||
|
tunnelingEnabledKey,
|
||||||
|
streamAutoPlayModeKey,
|
||||||
|
streamAutoPlaySourceKey,
|
||||||
|
streamAutoPlaySelectedAddonsKey,
|
||||||
|
streamAutoPlaySelectedPluginsKey,
|
||||||
|
streamAutoPlayRegexKey,
|
||||||
|
streamAutoPlayTimeoutSecondsKey,
|
||||||
|
skipIntroEnabledKey,
|
||||||
|
animeSkipEnabledKey,
|
||||||
|
animeSkipClientIdKey,
|
||||||
|
streamAutoPlayNextEpisodeEnabledKey,
|
||||||
|
streamAutoPlayPreferBingeGroupKey,
|
||||||
|
nextEpisodeThresholdModeKey,
|
||||||
|
nextEpisodeThresholdPercentKey,
|
||||||
|
nextEpisodeThresholdMinutesBeforeEndKey,
|
||||||
|
useLibassKey,
|
||||||
|
libassRenderTypeKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
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 loadPreferredAudioLanguage(): String? = loadString(preferredAudioLanguageKey)
|
||||||
|
|
||||||
|
actual fun savePreferredAudioLanguage(language: String) {
|
||||||
|
saveString(preferredAudioLanguageKey, language)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun loadSecondaryPreferredAudioLanguage(): String? = loadString(secondaryPreferredAudioLanguageKey)
|
||||||
|
|
||||||
|
actual fun saveSecondaryPreferredAudioLanguage(language: String?) {
|
||||||
|
saveNullableString(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?) {
|
||||||
|
saveNullableString(secondaryPreferredSubtitleLanguageKey, language)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun loadSubtitleTextColor(): String? = loadString(subtitleTextColorKey)
|
||||||
|
|
||||||
|
actual fun saveSubtitleTextColor(colorHex: String) {
|
||||||
|
saveString(subtitleTextColorKey, colorHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun loadSubtitleOutlineEnabled(): Boolean? = loadBoolean(subtitleOutlineEnabledKey)
|
||||||
|
|
||||||
|
actual fun saveSubtitleOutlineEnabled(enabled: Boolean) {
|
||||||
|
saveBoolean(subtitleOutlineEnabledKey, 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 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 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 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 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)) }
|
||||||
|
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)) }
|
||||||
|
loadSubtitleOutlineEnabled()?.let { put(subtitleOutlineEnabledKey, encodeSyncBoolean(it)) }
|
||||||
|
loadSubtitleFontSizeSp()?.let { put(subtitleFontSizeSpKey, encodeSyncInt(it)) }
|
||||||
|
loadSubtitleBottomOffset()?.let { put(subtitleBottomOffsetKey, encodeSyncInt(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)) }
|
||||||
|
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)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||||
|
syncKeys.forEach { DesktopPreferences.remove(preferencesName, ProfileScopedKey.of(it)) }
|
||||||
|
|
||||||
|
payload.decodeSyncBoolean(showLoadingOverlayKey)?.let(::saveShowLoadingOverlay)
|
||||||
|
payload.decodeSyncString(resizeModeKey)?.let(::saveResizeMode)
|
||||||
|
payload.decodeSyncBoolean(holdToSpeedEnabledKey)?.let(::saveHoldToSpeedEnabled)
|
||||||
|
payload.decodeSyncFloat(holdToSpeedValueKey)?.let(::saveHoldToSpeedValue)
|
||||||
|
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.decodeSyncBoolean(subtitleOutlineEnabledKey)?.let(::saveSubtitleOutlineEnabled)
|
||||||
|
payload.decodeSyncInt(subtitleFontSizeSpKey)?.let(::saveSubtitleFontSizeSp)
|
||||||
|
payload.decodeSyncInt(subtitleBottomOffsetKey)?.let(::saveSubtitleBottomOffset)
|
||||||
|
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.decodeSyncBoolean(streamAutoPlayNextEpisodeEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeEnabled)
|
||||||
|
payload.decodeSyncBoolean(streamAutoPlayPreferBingeGroupKey)?.let(::saveStreamAutoPlayPreferBingeGroup)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scopedKey(baseKey: String): String = ProfileScopedKey.of(baseKey)
|
||||||
|
|
||||||
|
private fun loadString(key: String): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, scopedKey(key))
|
||||||
|
|
||||||
|
private fun saveString(key: String, value: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, scopedKey(key), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveNullableString(key: String, value: String?) {
|
||||||
|
if (value.isNullOrBlank()) {
|
||||||
|
DesktopPreferences.remove(preferencesName, scopedKey(key))
|
||||||
|
} else {
|
||||||
|
DesktopPreferences.putString(preferencesName, scopedKey(key), value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadBoolean(key: String): Boolean? =
|
||||||
|
DesktopPreferences.getBoolean(preferencesName, scopedKey(key))
|
||||||
|
|
||||||
|
private fun saveBoolean(key: String, value: Boolean) {
|
||||||
|
DesktopPreferences.putBoolean(preferencesName, scopedKey(key), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadInt(key: String): Int? =
|
||||||
|
DesktopPreferences.getInt(preferencesName, scopedKey(key))
|
||||||
|
|
||||||
|
private fun saveInt(key: String, value: Int) {
|
||||||
|
DesktopPreferences.putInt(preferencesName, scopedKey(key), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadFloat(key: String): Float? =
|
||||||
|
DesktopPreferences.getFloat(preferencesName, scopedKey(key))
|
||||||
|
|
||||||
|
private fun saveFloat(key: String, value: Float) {
|
||||||
|
DesktopPreferences.putFloat(preferencesName, scopedKey(key), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadStringSet(key: String): Set<String>? =
|
||||||
|
DesktopPreferences.getStringSet(preferencesName, scopedKey(key))
|
||||||
|
|
||||||
|
private fun saveStringSet(key: String, values: Set<String>) {
|
||||||
|
DesktopPreferences.putStringSet(preferencesName, scopedKey(key), values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun LockPlayerToLandscape() = Unit
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun EnterImmersivePlayerMode() = Unit
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun ManagePlayerPictureInPicture(
|
||||||
|
isPlaying: Boolean,
|
||||||
|
playerSize: IntSize,
|
||||||
|
) = Unit
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun rememberPlayerGestureController(): PlayerGestureController? = null
|
||||||
|
|
@ -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,48 @@
|
||||||
|
package com.nuvio.app.features.plugins
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
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("Plugins are not available in this 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("Plugins are not available in this build."))
|
||||||
|
|
||||||
|
actual suspend fun executeScraper(
|
||||||
|
scraper: PluginScraper,
|
||||||
|
tmdbId: String,
|
||||||
|
mediaType: String,
|
||||||
|
season: Int?,
|
||||||
|
episode: Int?,
|
||||||
|
): Result<List<PluginRuntimeResult>> =
|
||||||
|
Result.failure(UnsupportedOperationException("Plugins are not available in this build."))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.nuvio.app.features.profiles
|
||||||
|
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
|
||||||
|
internal actual object ProfileStorage {
|
||||||
|
private const val preferencesName = "nuvio_profile_cache"
|
||||||
|
private const val payloadKey = "profile_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, payloadKey)
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, payloadKey, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.nuvio.app.features.search
|
||||||
|
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
|
||||||
|
internal actual object SearchHistoryStorage {
|
||||||
|
private const val preferencesName = "nuvio_search_history"
|
||||||
|
private const val payloadKey = "search_history_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(payloadKey))
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(payloadKey), payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package com.nuvio.app.features.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
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 com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import nuvio.composeapp.generated.resources.Res
|
||||||
|
import nuvio.composeapp.generated.resources.mdblist_logo
|
||||||
|
import nuvio.composeapp.generated.resources.rating_tmdb
|
||||||
|
import nuvio.composeapp.generated.resources.trakt_tv_favicon
|
||||||
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
|
||||||
|
internal actual object ThemeSettingsStorage {
|
||||||
|
private const val preferencesName = "nuvio_theme_settings"
|
||||||
|
private const val selectedThemeKey = "selected_theme"
|
||||||
|
private const val amoledEnabledKey = "amoled_enabled"
|
||||||
|
private val syncKeys = listOf(selectedThemeKey, amoledEnabledKey)
|
||||||
|
|
||||||
|
actual fun loadSelectedTheme(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(selectedThemeKey))
|
||||||
|
|
||||||
|
actual fun saveSelectedTheme(themeName: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(selectedThemeKey), themeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun loadAmoledEnabled(): Boolean? =
|
||||||
|
DesktopPreferences.getBoolean(preferencesName, ProfileScopedKey.of(amoledEnabledKey))
|
||||||
|
|
||||||
|
actual fun saveAmoledEnabled(enabled: Boolean) {
|
||||||
|
DesktopPreferences.putBoolean(preferencesName, ProfileScopedKey.of(amoledEnabledKey), enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||||
|
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
|
||||||
|
loadAmoledEnabled()?.let { put(amoledEnabledKey, encodeSyncBoolean(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||||
|
syncKeys.forEach { DesktopPreferences.remove(preferencesName, ProfileScopedKey.of(it)) }
|
||||||
|
|
||||||
|
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
|
||||||
|
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual fun LazyListScope.pluginsSettingsContent() = Unit
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal actual fun integrationLogoPainter(logo: IntegrationLogo): Painter =
|
||||||
|
when (logo) {
|
||||||
|
IntegrationLogo.Tmdb -> painterResource(Res.drawable.rating_tmdb)
|
||||||
|
IntegrationLogo.Trakt -> painterResource(Res.drawable.trakt_tv_favicon)
|
||||||
|
IntegrationLogo.MdbList -> painterResource(Res.drawable.mdblist_logo)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.nuvio.app.features.streams
|
||||||
|
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
|
||||||
|
internal actual object StreamLinkCacheStorage {
|
||||||
|
private const val preferencesName = "nuvio_stream_link_cache"
|
||||||
|
|
||||||
|
actual fun loadEntry(hashedKey: String): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(hashedKey))
|
||||||
|
|
||||||
|
actual fun saveEntry(hashedKey: String, payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(hashedKey), payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun removeEntry(hashedKey: String) {
|
||||||
|
DesktopPreferences.remove(preferencesName, ProfileScopedKey.of(hashedKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual fun epochMs(): Long = System.currentTimeMillis()
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
package com.nuvio.app.features.tmdb
|
||||||
|
|
||||||
|
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 com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
|
||||||
|
internal actual object TmdbSettingsStorage {
|
||||||
|
private const val preferencesName = "nuvio_tmdb_settings"
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
syncKeys.forEach { DesktopPreferences.remove(preferencesName, ProfileScopedKey.of(it)) }
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scopedKey(baseKey: String): String = ProfileScopedKey.of(baseKey)
|
||||||
|
|
||||||
|
private fun loadString(key: String): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, scopedKey(key))
|
||||||
|
|
||||||
|
private fun saveString(key: String, value: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, scopedKey(key), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadBoolean(key: String): Boolean? =
|
||||||
|
DesktopPreferences.getBoolean(preferencesName, scopedKey(key))
|
||||||
|
|
||||||
|
private fun saveBoolean(key: String, value: Boolean) {
|
||||||
|
DesktopPreferences.putBoolean(preferencesName, scopedKey(key), value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.nuvio.app.features.trailer
|
||||||
|
|
||||||
|
actual object TrailerPlaybackResolver {
|
||||||
|
actual suspend fun resolveFromYouTubeUrl(youtubeUrl: String): TrailerPlaybackSource? = null
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
package com.nuvio.app.features.trakt
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
import com.nuvio.app.core.sync.decodeSyncBoolean
|
||||||
|
import com.nuvio.app.core.sync.encodeSyncBoolean
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
import java.time.Instant
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import nuvio.composeapp.generated.resources.Res
|
||||||
|
import nuvio.composeapp.generated.resources.trakt_logo_wordmark
|
||||||
|
import nuvio.composeapp.generated.resources.trakt_tv_favicon
|
||||||
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
|
||||||
|
internal actual object TraktAuthStorage {
|
||||||
|
private const val preferencesName = "nuvio_trakt_auth"
|
||||||
|
private const val payloadKey = "trakt_auth_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(payloadKey))
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(payloadKey), payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object TraktCommentsStorage {
|
||||||
|
private const val preferencesName = "nuvio_trakt_comments"
|
||||||
|
private const val enabledKey = "comments_enabled"
|
||||||
|
private val syncKeys = listOf(enabledKey)
|
||||||
|
|
||||||
|
actual fun loadEnabled(): Boolean? =
|
||||||
|
DesktopPreferences.getBoolean(preferencesName, ProfileScopedKey.of(enabledKey))
|
||||||
|
|
||||||
|
actual fun saveEnabled(enabled: Boolean) {
|
||||||
|
DesktopPreferences.putBoolean(preferencesName, ProfileScopedKey.of(enabledKey), enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||||
|
loadEnabled()?.let { put(enabledKey, encodeSyncBoolean(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||||
|
syncKeys.forEach { DesktopPreferences.remove(preferencesName, ProfileScopedKey.of(it)) }
|
||||||
|
payload.decodeSyncBoolean(enabledKey)?.let(::saveEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object TraktLibraryStorage {
|
||||||
|
private const val preferencesName = "nuvio_trakt_library"
|
||||||
|
private const val payloadKey = "trakt_library_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(payloadKey))
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(payloadKey), payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun traktBrandPainter(asset: TraktBrandAsset): Painter =
|
||||||
|
when (asset) {
|
||||||
|
TraktBrandAsset.Glyph -> painterResource(Res.drawable.trakt_tv_favicon)
|
||||||
|
TraktBrandAsset.Wordmark -> painterResource(Res.drawable.trakt_logo_wordmark)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object TraktPlatformClock {
|
||||||
|
actual fun nowEpochMs(): Long = System.currentTimeMillis()
|
||||||
|
|
||||||
|
actual fun parseIsoDateTimeToEpochMs(value: String): Long? = runCatching {
|
||||||
|
Instant.parse(value).toEpochMilli()
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.nuvio.app.features.watched
|
||||||
|
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
|
||||||
|
actual object WatchedStorage {
|
||||||
|
private const val preferencesName = "nuvio_watched"
|
||||||
|
private const val payloadKey = "watched_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(profileId: Int): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, "${payloadKey}_$profileId")
|
||||||
|
|
||||||
|
actual fun savePayload(profileId: Int, payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, "${payloadKey}_$profileId", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual object WatchedClock {
|
||||||
|
actual fun nowEpochMs(): Long = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package com.nuvio.app.features.watchprogress
|
||||||
|
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
import com.nuvio.app.desktop.DesktopPreferences
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
actual object CurrentDateProvider {
|
||||||
|
actual fun todayIsoDate(): String = LocalDate.now().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object WatchProgressClock {
|
||||||
|
actual fun nowEpochMs(): Long = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object ContinueWatchingPreferencesStorage {
|
||||||
|
private const val preferencesName = "nuvio_continue_watching_preferences"
|
||||||
|
private const val payloadKey = "continue_watching_preferences_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(payloadKey))
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, ProfileScopedKey.of(payloadKey), payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual object ContinueWatchingEnrichmentStorage {
|
||||||
|
private const val preferencesName = "nuvio_cw_enrichment"
|
||||||
|
|
||||||
|
actual fun loadPayload(key: String): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, key)
|
||||||
|
|
||||||
|
actual fun savePayload(key: String, payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, key, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual object ResumePromptStorage {
|
||||||
|
private const val preferencesName = "nuvio_resume_prompt"
|
||||||
|
private const val wasInPlayerKey = "was_in_player"
|
||||||
|
private const val lastPlayerVideoIdKey = "last_player_video_id"
|
||||||
|
|
||||||
|
actual fun loadWasInPlayer(): Boolean =
|
||||||
|
DesktopPreferences.getBoolean(preferencesName, ProfileScopedKey.of(wasInPlayerKey)) ?: false
|
||||||
|
|
||||||
|
actual fun saveWasInPlayer(value: Boolean) {
|
||||||
|
DesktopPreferences.putBoolean(preferencesName, ProfileScopedKey.of(wasInPlayerKey), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun loadLastPlayerVideoId(): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, ProfileScopedKey.of(lastPlayerVideoIdKey))
|
||||||
|
|
||||||
|
actual fun saveLastPlayerVideoId(videoId: String?) {
|
||||||
|
DesktopPreferences.putNullableString(
|
||||||
|
preferencesName,
|
||||||
|
ProfileScopedKey.of(lastPlayerVideoIdKey),
|
||||||
|
videoId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual object WatchProgressStorage {
|
||||||
|
private const val preferencesName = "nuvio_watch_progress"
|
||||||
|
private const val payloadKey = "watch_progress_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(profileId: Int): String? =
|
||||||
|
DesktopPreferences.getString(preferencesName, "${payloadKey}_$profileId")
|
||||||
|
|
||||||
|
actual fun savePayload(profileId: Int, payload: String) {
|
||||||
|
DesktopPreferences.putString(preferencesName, "${payloadKey}_$profileId", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ kermit = "2.0.5"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlin = "2.3.0"
|
kotlin = "2.3.0"
|
||||||
kotlinx-serialization = "1.8.1"
|
kotlinx-serialization = "1.8.1"
|
||||||
|
kotlinx-coroutines = "1.10.2"
|
||||||
ktor = "3.4.1"
|
ktor = "3.4.1"
|
||||||
material3 = "1.10.0-alpha05"
|
material3 = "1.10.0-alpha05"
|
||||||
androidx-media3 = "1.8.0"
|
androidx-media3 = "1.8.0"
|
||||||
|
|
@ -51,9 +52,11 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil"
|
||||||
coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }
|
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-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
|
||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
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-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
|
||||||
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
|
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
|
||||||
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
|
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
|
||||||
|
ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" }
|
||||||
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" }
|
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" }
|
||||||
androidx-media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "androidx-media3" }
|
androidx-media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "androidx-media3" }
|
||||||
androidx-media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "androidx-media3" }
|
androidx-media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "androidx-media3" }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
CURRENT_PROJECT_VERSION=25
|
CURRENT_PROJECT_VERSION=26
|
||||||
MARKETING_VERSION=0.1.0
|
MARKETING_VERSION=0.1.0
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue