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.file.DirectoryProperty
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
|
|
@ -164,6 +163,12 @@ kotlin {
|
|||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
jvm("desktop") {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
val iosTargets = listOf(
|
||||
iosArm64(),
|
||||
|
|
@ -203,6 +208,13 @@ kotlin {
|
|||
val commonMain by getting {
|
||||
kotlin.srcDir(generatedRuntimeConfigDir)
|
||||
}
|
||||
val desktopMain by getting {
|
||||
dependencies {
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(libs.ktor.client.java)
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
}
|
||||
}
|
||||
androidMain.dependencies {
|
||||
implementation(libs.compose.uiToolingPreview)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
|
@ -265,6 +277,12 @@ dependencies {
|
|||
debugImplementation(libs.compose.uiTooling)
|
||||
}
|
||||
|
||||
compose.desktop {
|
||||
application {
|
||||
mainClass = "com.nuvio.app.DesktopAppKt"
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude(group = "androidx.media3", module = "media3-exoplayer")
|
||||
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"
|
||||
kotlin = "2.3.0"
|
||||
kotlinx-serialization = "1.8.1"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
ktor = "3.4.1"
|
||||
material3 = "1.10.0-alpha05"
|
||||
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-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-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" }
|
||||
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
|
||||
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-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" }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
CURRENT_PROJECT_VERSION=25
|
||||
CURRENT_PROJECT_VERSION=26
|
||||
MARKETING_VERSION=0.1.0
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue