mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-31 16:49:28 +00:00
Merge branch 'windows' into dev
This commit is contained in:
commit
128f7322d0
46 changed files with 2681 additions and 184 deletions
|
|
@ -5,8 +5,11 @@ import org.gradle.api.file.RegularFileProperty
|
|||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.InputDirectory
|
||||
import org.gradle.api.tasks.Optional
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.Sync
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.jvm.tasks.Jar
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
|
@ -151,6 +154,21 @@ fun readXcconfigValue(file: File, key: String): String? {
|
|||
|
||||
fun shellQuote(value: String): String = "'${value.replace("'", "'\"'\"'")}'"
|
||||
|
||||
fun cmdQuote(value: String): String = "\"${value.replace("\"", "\"\"")}\""
|
||||
|
||||
fun psSingleQuote(value: String): String = "'${value.replace("'", "''")}'"
|
||||
|
||||
fun semanticVersionSortKey(value: String): String =
|
||||
value.split('.', '-', '_')
|
||||
.joinToString(".") { part ->
|
||||
part.toIntOrNull()?.toString()?.padStart(8, '0') ?: part
|
||||
}
|
||||
|
||||
fun newestDirectory(root: File): File? =
|
||||
root.takeIf(File::exists)
|
||||
?.listFiles(File::isDirectory)
|
||||
?.maxByOrNull { semanticVersionSortKey(it.name) }
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidApplication)
|
||||
|
|
@ -211,6 +229,7 @@ val generateRuntimeConfigs = tasks.register<GenerateRuntimeConfigsTask>("generat
|
|||
}
|
||||
|
||||
val isMacHost = System.getProperty("os.name").contains("mac", ignoreCase = true)
|
||||
val isWindowsHost = System.getProperty("os.name").contains("win", ignoreCase = true)
|
||||
val mpvKitDir = providers.gradleProperty("nuvio.mpvkit.dir")
|
||||
.orElse(rootProject.layout.projectDirectory.dir("MPVKit").asFile.absolutePath)
|
||||
val macosPlayerBridgeSource = layout.projectDirectory.file("src/desktopMain/native/macos/player_bridge.mm")
|
||||
|
|
@ -312,6 +331,204 @@ val buildMacosPlayerBridge = tasks.register<Exec>("buildMacosPlayerBridge") {
|
|||
commandLine(macosPlayerBridgeCommand)
|
||||
}
|
||||
|
||||
val windowsPlayerBridgeArch = when (System.getProperty("os.arch").lowercase()) {
|
||||
"aarch64", "arm64" -> "arm64"
|
||||
"x86" -> "x86"
|
||||
else -> "x64"
|
||||
}
|
||||
val windowsPlayerBridgeSource = layout.projectDirectory.file("src/desktopMain/native/windows/player_bridge.cpp")
|
||||
val windowsPlayerBridgeOutput = layout.buildDirectory.file("native/windows/player_bridge.dll")
|
||||
val windowsPlayerBridgeImportLib = layout.buildDirectory.file("native/windows/player_bridge.lib")
|
||||
val windowsPlayerBridgePdb = layout.buildDirectory.file("native/windows/player_bridge.pdb")
|
||||
val windowsPlayerBridgeObj = layout.buildDirectory.file("native/windows/player_bridge.obj")
|
||||
val windowsPlayerBridgeScript = layout.buildDirectory.file("native/windows/build-player-bridge.bat")
|
||||
val windowsPlayerRuntimeOutput = layout.buildDirectory.dir("native/windows-runtime")
|
||||
if (isWindowsHost) {
|
||||
windowsPlayerBridgeOutput.get().asFile.parentFile.mkdirs()
|
||||
}
|
||||
val windowsWebView2Root = providers.gradleProperty("nuvio.webview2.dir").orNull
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let(::File)
|
||||
?: newestDirectory(File(System.getProperty("user.home"), ".nuget/packages/microsoft.web.webview2"))
|
||||
?: File("__missing_webview2__")
|
||||
val windowsWebView2IncludeDir = File(windowsWebView2Root, "build/native/include")
|
||||
val windowsWebView2NativeDir = File(windowsWebView2Root, "build/native/$windowsPlayerBridgeArch")
|
||||
val windowsWebView2LoaderLib = File(windowsWebView2NativeDir, "WebView2Loader.dll.lib")
|
||||
val windowsWebView2LoaderDll = File(windowsWebView2NativeDir, "WebView2Loader.dll")
|
||||
val windowsLibmpvRuntimeDir = providers.gradleProperty("nuvio.windows.libmpv.runtimeDir").orNull
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let(::File)
|
||||
?: listOf(
|
||||
File("C:/Program Files (x86)/Nuvio/app/native"),
|
||||
File("C:/Program Files/Nuvio/app/native"),
|
||||
).firstOrNull { File(it, "libmpv-2.dll").exists() }
|
||||
val windowsLibmpvDll = providers.gradleProperty("nuvio.windows.libmpv.dll").orNull
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let(::File)
|
||||
?: windowsLibmpvRuntimeDir?.resolve("libmpv-2.dll")
|
||||
?: listOf(
|
||||
File("C:/msys64/ucrt64/bin/libmpv-2.dll"),
|
||||
File("C:/msys64/mingw64/bin/libmpv-2.dll"),
|
||||
).firstOrNull(File::exists)
|
||||
val windowsVsWhere = File("C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe")
|
||||
val windowsVcvarsRelativePath = when (windowsPlayerBridgeArch) {
|
||||
"x86" -> "VC\\Auxiliary\\Build\\vcvars32.bat"
|
||||
"arm64" -> "VC\\Auxiliary\\Build\\vcvarsarm64.bat"
|
||||
else -> "VC\\Auxiliary\\Build\\vcvars64.bat"
|
||||
}
|
||||
val windowsVcvarsPath = providers.gradleProperty("nuvio.windows.vcvars.path").orNull
|
||||
?.takeIf { it.isNotBlank() }
|
||||
val windowsPlayerBridgeJavaHome = providers.systemProperty("java.home").get()
|
||||
val missingWindowsPlayerBridgeInputs = listOfNotNull(
|
||||
"WebView2.h".takeUnless { windowsWebView2IncludeDir.resolve("WebView2.h").exists() },
|
||||
"WebView2Loader.dll.lib".takeUnless { windowsWebView2LoaderLib.exists() },
|
||||
)
|
||||
val missingWindowsPlayerBridgeMessage = """
|
||||
Windows desktop player bridge inputs are missing: ${missingWindowsPlayerBridgeInputs.joinToString()}.
|
||||
Install the Microsoft.Web.WebView2 NuGet package or pass -Pnuvio.webview2.dir=C:/path/to/microsoft.web.webview2/version.
|
||||
libmpv is loaded at runtime; pass -Pnuvio.windows.libmpv.runtimeDir=C:/path/to/mpv-dlls to bundle it.
|
||||
""".trimIndent()
|
||||
val windowsPlayerBridgeCommand = if (missingWindowsPlayerBridgeInputs.isNotEmpty()) {
|
||||
listOf(
|
||||
"cmd",
|
||||
"/c",
|
||||
"echo ${missingWindowsPlayerBridgeMessage.replace("\n", " ")} 1>&2 && exit /b 1",
|
||||
)
|
||||
} else {
|
||||
val sourceFile = windowsPlayerBridgeSource.asFile
|
||||
val outputFile = windowsPlayerBridgeOutput.get().asFile
|
||||
val importLibFile = windowsPlayerBridgeImportLib.get().asFile
|
||||
val pdbFile = windowsPlayerBridgePdb.get().asFile
|
||||
val objFile = windowsPlayerBridgeObj.get().asFile
|
||||
val javaIncludeDir = File(windowsPlayerBridgeJavaHome, "include")
|
||||
val javaWin32IncludeDir = File(javaIncludeDir, "win32")
|
||||
val compileCommand = listOf(
|
||||
"cl",
|
||||
"/nologo",
|
||||
"/EHsc",
|
||||
"/std:c++17",
|
||||
"/LD",
|
||||
"/DUNICODE",
|
||||
"/D_UNICODE",
|
||||
"/DNOMINMAX",
|
||||
"/DWIN32_LEAN_AND_MEAN",
|
||||
"/permissive-",
|
||||
cmdQuote(sourceFile.absolutePath),
|
||||
"/I${cmdQuote(javaIncludeDir.absolutePath)}",
|
||||
"/I${cmdQuote(javaWin32IncludeDir.absolutePath)}",
|
||||
"/I${cmdQuote(windowsWebView2IncludeDir.absolutePath)}",
|
||||
"/Fo${cmdQuote(objFile.absolutePath)}",
|
||||
"/Fd${cmdQuote(pdbFile.absolutePath)}",
|
||||
"/Fe${cmdQuote(outputFile.absolutePath)}",
|
||||
"/link",
|
||||
"/NOLOGO",
|
||||
"/INCREMENTAL:NO",
|
||||
"/IMPLIB:${cmdQuote(importLibFile.absolutePath)}",
|
||||
cmdQuote(windowsWebView2LoaderLib.absolutePath),
|
||||
"Ole32.lib",
|
||||
"User32.lib",
|
||||
"Gdi32.lib",
|
||||
).joinToString(" ")
|
||||
val powershellCompileCommand = compileCommand.replace("\"", "__DQ__")
|
||||
val powershellCommand = """
|
||||
${'$'}ErrorActionPreference = 'Stop'
|
||||
${'$'}dq = [char]34
|
||||
${'$'}vcvars = ${psSingleQuote(windowsVcvarsPath.orEmpty())}
|
||||
if ([string]::IsNullOrWhiteSpace(${'$'}vcvars)) {
|
||||
${'$'}vswhere = ${psSingleQuote(windowsVsWhere.absolutePath)}
|
||||
if (Test-Path -LiteralPath ${'$'}vswhere) {
|
||||
${'$'}vcvars = & ${'$'}vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -find ${psSingleQuote(windowsVcvarsRelativePath)} | Select-Object -First 1
|
||||
}
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace(${'$'}vcvars) -or -not (Test-Path -LiteralPath ${'$'}vcvars)) {
|
||||
Write-Error 'Visual Studio C++ toolchain was not found. Install MSVC or pass -Pnuvio.windows.vcvars.path=C:\path\to\vcvars64.bat.'
|
||||
exit 1
|
||||
}
|
||||
${'$'}vcvars = ([string]${'$'}vcvars).Trim()
|
||||
${'$'}bat = ${psSingleQuote(windowsPlayerBridgeScript.get().asFile.absolutePath)}
|
||||
${'$'}compile = ${psSingleQuote(powershellCompileCommand)}.Replace('__DQ__', ${'$'}dq)
|
||||
${'$'}lines = @(
|
||||
'@echo off',
|
||||
('set {0}VCVARS={1}{0}' -f ${'$'}dq, ${'$'}vcvars),
|
||||
('call {0}%VCVARS%{0} >nul' -f ${'$'}dq),
|
||||
'if errorlevel 1 exit /b %errorlevel%',
|
||||
${'$'}compile,
|
||||
'exit /b %ERRORLEVEL%'
|
||||
)
|
||||
Set-Content -LiteralPath ${'$'}bat -Value ${'$'}lines -Encoding ASCII
|
||||
& cmd.exe /d /c ${'$'}bat
|
||||
${'$'}code = ${'$'}LASTEXITCODE
|
||||
if (${'$'}code -ne 0) { exit ${'$'}code }
|
||||
""".trimIndent()
|
||||
listOf(
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
powershellCommand,
|
||||
)
|
||||
}
|
||||
val buildWindowsPlayerBridge = tasks.register<Exec>("buildWindowsPlayerBridge") {
|
||||
notCompatibleWithConfigurationCache("Builds a host-local player bridge against WebView2 and libmpv for Windows.")
|
||||
enabled = isWindowsHost
|
||||
inputs.file(windowsPlayerBridgeSource)
|
||||
if (windowsWebView2IncludeDir.exists()) {
|
||||
inputs.dir(windowsWebView2IncludeDir)
|
||||
}
|
||||
if (windowsWebView2LoaderLib.exists()) {
|
||||
inputs.file(windowsWebView2LoaderLib)
|
||||
}
|
||||
outputs.file(windowsPlayerBridgeOutput)
|
||||
outputs.file(windowsPlayerBridgeImportLib)
|
||||
outputs.file(windowsPlayerBridgePdb)
|
||||
commandLine(windowsPlayerBridgeCommand)
|
||||
}
|
||||
|
||||
val prepareWindowsPlayerRuntime = tasks.register<Sync>("prepareWindowsPlayerRuntime") {
|
||||
enabled = isWindowsHost
|
||||
into(windowsPlayerRuntimeOutput)
|
||||
if (windowsWebView2LoaderDll.exists()) {
|
||||
from(windowsWebView2LoaderDll)
|
||||
}
|
||||
when {
|
||||
windowsLibmpvRuntimeDir?.exists() == true -> {
|
||||
from(windowsLibmpvRuntimeDir) {
|
||||
include("*.dll")
|
||||
}
|
||||
}
|
||||
windowsLibmpvDll?.exists() == true -> {
|
||||
from(windowsLibmpvDll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val generateWindowsPlayerRuntimeIndex = tasks.register<GenerateNativeRuntimeIndexTask>("generateWindowsPlayerRuntimeIndex") {
|
||||
enabled = isWindowsHost
|
||||
dependsOn(prepareWindowsPlayerRuntime)
|
||||
runtimeDir.set(windowsPlayerRuntimeOutput)
|
||||
indexFile.set(windowsPlayerRuntimeOutput.map { it.file("runtime-files.txt") })
|
||||
}
|
||||
|
||||
abstract class GenerateNativeRuntimeIndexTask : DefaultTask() {
|
||||
@get:InputDirectory
|
||||
abstract val runtimeDir: DirectoryProperty
|
||||
|
||||
@get:OutputFile
|
||||
abstract val indexFile: RegularFileProperty
|
||||
|
||||
@TaskAction
|
||||
fun generate() {
|
||||
val dir = runtimeDir.get().asFile
|
||||
val files = dir
|
||||
.listFiles { file -> file.isFile && file.name != indexFile.get().asFile.name }
|
||||
.orEmpty()
|
||||
.map { it.name }
|
||||
.sorted()
|
||||
indexFile.get().asFile.writeText(files.joinToString(separator = "\n", postfix = "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Jar>().configureEach {
|
||||
if (isMacHost && name == "desktopJar") {
|
||||
dependsOn(buildMacosPlayerBridge)
|
||||
|
|
@ -319,6 +536,43 @@ tasks.withType<Jar>().configureEach {
|
|||
into("native/macos")
|
||||
}
|
||||
}
|
||||
if (isWindowsHost && name == "desktopJar") {
|
||||
dependsOn(buildWindowsPlayerBridge, prepareWindowsPlayerRuntime, generateWindowsPlayerRuntimeIndex)
|
||||
from(windowsPlayerBridgeOutput) {
|
||||
into("native/windows")
|
||||
}
|
||||
from(windowsPlayerRuntimeOutput) {
|
||||
into("native/windows")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isWindowsHost) {
|
||||
val desktopNativePlayerTasks = setOf(
|
||||
"run",
|
||||
"runRelease",
|
||||
"desktopRun",
|
||||
"runDistributable",
|
||||
"runReleaseDistributable",
|
||||
"desktopRunHot",
|
||||
"hotRunDesktop",
|
||||
"hotRunDesktopAsync",
|
||||
"hotDevDesktop",
|
||||
"hotDevDesktopAsync",
|
||||
"createDistributable",
|
||||
"createReleaseDistributable",
|
||||
"createRuntimeImage",
|
||||
"package",
|
||||
"packageDistributionForCurrentOS",
|
||||
"packageMsi",
|
||||
"packageUberJarForCurrentOS",
|
||||
"packageReleaseDistributionForCurrentOS",
|
||||
"packageReleaseMsi",
|
||||
"packageReleaseUberJarForCurrentOS",
|
||||
)
|
||||
tasks.matching { it.name in desktopNativePlayerTasks }.configureEach {
|
||||
dependsOn(buildWindowsPlayerBridge, prepareWindowsPlayerRuntime, generateWindowsPlayerRuntimeIndex)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompilationTask<*>>().configureEach {
|
||||
|
|
@ -446,6 +700,7 @@ compose.desktop {
|
|||
"--add-opens=java.desktop/java.awt=ALL-UNNAMED",
|
||||
"--add-opens=java.desktop/sun.lwawt=ALL-UNNAMED",
|
||||
"--add-opens=java.desktop/sun.lwawt.macosx=ALL-UNNAMED",
|
||||
"--add-opens=java.desktop/sun.awt.windows=ALL-UNNAMED",
|
||||
smokePlayerUrl?.takeIf { it.isNotBlank() }?.let { "-Dnuvio.desktop.smokePlayerUrl=$it" },
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQuality
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.AsyncImagePainter
|
||||
|
||||
@Composable
|
||||
internal actual fun NuvioAsyncImage(
|
||||
model: Any?,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier,
|
||||
placeholder: Painter?,
|
||||
error: Painter?,
|
||||
fallback: Painter?,
|
||||
onLoading: ((AsyncImagePainter.State.Loading) -> Unit)?,
|
||||
onSuccess: ((AsyncImagePainter.State.Success) -> Unit)?,
|
||||
onError: ((AsyncImagePainter.State.Error) -> Unit)?,
|
||||
alignment: Alignment,
|
||||
contentScale: ContentScale,
|
||||
alpha: Float,
|
||||
colorFilter: ColorFilter?,
|
||||
filterQuality: FilterQuality?,
|
||||
clipToBounds: Boolean,
|
||||
desktopImageScaling: NuvioDesktopImageScaling,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
placeholder = placeholder,
|
||||
error = error,
|
||||
fallback = fallback,
|
||||
onLoading = onLoading,
|
||||
onSuccess = onSuccess,
|
||||
onError = onError,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = filterQuality ?: DefaultFilterQuality,
|
||||
clipToBounds = clipToBounds,
|
||||
)
|
||||
}
|
||||
|
|
@ -176,7 +176,9 @@ import com.nuvio.app.features.settings.ThemeSettingsRepository
|
|||
import com.nuvio.app.features.collection.CollectionManagementScreen
|
||||
import com.nuvio.app.features.collection.CollectionEditorScreen
|
||||
import com.nuvio.app.features.collection.CollectionEditorRepository
|
||||
import com.nuvio.app.features.collection.CollectionRepository
|
||||
import com.nuvio.app.features.collection.CollectionSyncService
|
||||
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||
import com.nuvio.app.features.home.HomeCatalogSettingsSyncService
|
||||
import com.nuvio.app.features.collection.FolderDetailScreen
|
||||
import com.nuvio.app.features.collection.FolderDetailRepository
|
||||
|
|
@ -194,6 +196,7 @@ import com.nuvio.app.features.player.PlayerSettingsRepository
|
|||
import com.nuvio.app.features.trakt.TraktAuthRepository
|
||||
import com.nuvio.app.features.trakt.TraktListTab
|
||||
import com.nuvio.app.features.trakt.TraktScrobbleRepository
|
||||
import com.nuvio.app.features.trakt.TraktSettingsRepository
|
||||
import com.nuvio.app.features.updater.AppUpdaterHost
|
||||
import com.nuvio.app.features.updater.rememberAppUpdaterController
|
||||
import com.nuvio.app.features.watched.WatchedRepository
|
||||
|
|
@ -206,10 +209,13 @@ import com.nuvio.app.features.watchprogress.nextUpDismissKey
|
|||
import com.nuvio.app.features.watchprogress.toContinueWatchingItem
|
||||
import com.nuvio.app.features.watching.application.WatchingActions
|
||||
import com.nuvio.app.features.watching.application.WatchingState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import nuvio.composeapp.generated.resources.app_logo_wordmark
|
||||
|
|
@ -350,10 +356,37 @@ private enum class AppGateScreen {
|
|||
Loading,
|
||||
Auth,
|
||||
ProfileSelection,
|
||||
ProfileSwitching,
|
||||
ProfileEdit,
|
||||
Main,
|
||||
}
|
||||
|
||||
private data class PendingProfileSwitch(
|
||||
val profile: NuvioProfile,
|
||||
val syncOnEnter: Boolean,
|
||||
)
|
||||
|
||||
private suspend fun warmProfileBoundRepositories() {
|
||||
withContext(Dispatchers.Default) {
|
||||
AddonRepository.initialize()
|
||||
CollectionRepository.initialize()
|
||||
ContinueWatchingPreferencesRepository.ensureLoaded()
|
||||
DownloadsRepository.ensureLoaded()
|
||||
EpisodeReleaseNotificationsRepository.ensureLoaded()
|
||||
HomeCatalogSettingsRepository.snapshot()
|
||||
LibraryRepository.ensureLoaded()
|
||||
P2pSettingsRepository.ensureLoaded()
|
||||
PlayerSettingsRepository.ensureLoaded()
|
||||
TraktAuthRepository.ensureLoaded()
|
||||
TraktSettingsRepository.ensureLoaded()
|
||||
WatchedRepository.ensureLoaded()
|
||||
WatchProgressRepository.ensureLoaded()
|
||||
CollectionSyncService.startObserving()
|
||||
HomeCatalogSettingsSyncService.startObserving()
|
||||
ProfileSettingsSync.startObserving()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@Preview
|
||||
|
|
@ -416,6 +449,7 @@ fun App() {
|
|||
var editingProfile by remember { mutableStateOf<NuvioProfile?>(null) }
|
||||
var isNewProfile by remember { mutableStateOf(false) }
|
||||
var autoSkipProfileSelection by rememberSaveable { mutableStateOf(false) }
|
||||
var pendingProfileSwitch by remember { mutableStateOf<PendingProfileSwitch?>(null) }
|
||||
|
||||
fun rememberedStartupProfile(profiles: List<NuvioProfile>): NuvioProfile? {
|
||||
val currentProfileState = ProfileRepository.state.value
|
||||
|
|
@ -431,6 +465,12 @@ fun App() {
|
|||
?.takeUnless { it.pinEnabled }
|
||||
}
|
||||
|
||||
fun requestProfileSwitch(profile: NuvioProfile, syncOnEnter: Boolean) {
|
||||
autoSkipProfileSelection = false
|
||||
pendingProfileSwitch = PendingProfileSwitch(profile, syncOnEnter)
|
||||
gateScreen = AppGateScreen.ProfileSwitching.name
|
||||
}
|
||||
|
||||
fun enterProfileGate(profiles: List<NuvioProfile>, syncOnEnter: Boolean) {
|
||||
if (profiles.isEmpty()) {
|
||||
autoSkipProfileSelection = true
|
||||
|
|
@ -439,12 +479,7 @@ fun App() {
|
|||
}
|
||||
|
||||
rememberedStartupProfile(profiles)?.let { profile ->
|
||||
ProfileRepository.selectProfile(profile.profileIndex)
|
||||
if (syncOnEnter) {
|
||||
SyncManager.pullAllForProfile(profile.profileIndex)
|
||||
}
|
||||
gateScreen = AppGateScreen.Main.name
|
||||
autoSkipProfileSelection = false
|
||||
requestProfileSwitch(profile, syncOnEnter)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -455,18 +490,40 @@ fun App() {
|
|||
gateScreen = AppGateScreen.ProfileSelection.name
|
||||
return
|
||||
}
|
||||
ProfileRepository.selectProfile(onlyProfile.profileIndex)
|
||||
if (syncOnEnter) {
|
||||
SyncManager.pullAllForProfile(onlyProfile.profileIndex)
|
||||
}
|
||||
gateScreen = AppGateScreen.Main.name
|
||||
autoSkipProfileSelection = false
|
||||
requestProfileSwitch(onlyProfile, syncOnEnter)
|
||||
} else {
|
||||
gateScreen = AppGateScreen.ProfileSelection.name
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(gateScreen, pendingProfileSwitch) {
|
||||
if (gateScreen == AppGateScreen.ProfileSwitching.name && pendingProfileSwitch == null) {
|
||||
gateScreen = AppGateScreen.Loading.name
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(pendingProfileSwitch) {
|
||||
val request = pendingProfileSwitch ?: return@LaunchedEffect
|
||||
runCatching {
|
||||
ProfileRepository.switchToProfile(request.profile.profileIndex)
|
||||
warmProfileBoundRepositories()
|
||||
if (request.syncOnEnter) {
|
||||
SyncManager.pullAllForProfile(request.profile.profileIndex)
|
||||
}
|
||||
}.onSuccess {
|
||||
pendingProfileSwitch = null
|
||||
autoSkipProfileSelection = false
|
||||
gateScreen = AppGateScreen.Main.name
|
||||
}.onFailure {
|
||||
pendingProfileSwitch = null
|
||||
autoSkipProfileSelection = false
|
||||
gateScreen = AppGateScreen.ProfileSelection.name
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(authState, networkStatusUiState.condition, profileState.profiles) {
|
||||
if (gateScreen == AppGateScreen.ProfileSwitching.name) return@LaunchedEffect
|
||||
|
||||
val cachedProfiles = profileState.profiles
|
||||
val allowOfflineProfileAccess =
|
||||
cachedProfiles.isNotEmpty() &&
|
||||
|
|
@ -519,10 +576,10 @@ fun App() {
|
|||
gateScreen == AppGateScreen.ProfileSelection.name
|
||||
) {
|
||||
rememberedStartupProfile(profileState.profiles)?.let { profile ->
|
||||
ProfileRepository.selectProfile(profile.profileIndex)
|
||||
SyncManager.pullAllForProfile(profile.profileIndex)
|
||||
gateScreen = AppGateScreen.Main.name
|
||||
autoSkipProfileSelection = false
|
||||
requestProfileSwitch(
|
||||
profile = profile,
|
||||
syncOnEnter = authState is AuthState.Authenticated,
|
||||
)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
|
|
@ -531,10 +588,10 @@ fun App() {
|
|||
val onlyProfile = profileState.profiles.first()
|
||||
if (onlyProfile.pinEnabled) return@LaunchedEffect
|
||||
|
||||
ProfileRepository.selectProfile(onlyProfile.profileIndex)
|
||||
SyncManager.pullAllForProfile(onlyProfile.profileIndex)
|
||||
gateScreen = AppGateScreen.Main.name
|
||||
autoSkipProfileSelection = false
|
||||
requestProfileSwitch(
|
||||
profile = onlyProfile,
|
||||
syncOnEnter = authState is AuthState.Authenticated,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -547,15 +604,9 @@ fun App() {
|
|||
},
|
||||
) { currentGate ->
|
||||
when (currentGate) {
|
||||
AppGateScreen.Loading.name -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.nuvio.colors.background),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(color = MaterialTheme.nuvio.colors.accent)
|
||||
}
|
||||
AppGateScreen.Loading.name,
|
||||
AppGateScreen.ProfileSwitching.name -> {
|
||||
AppLaunchOverlay(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
AppGateScreen.Auth.name -> {
|
||||
AuthScreen(modifier = Modifier.fillMaxSize())
|
||||
|
|
@ -568,11 +619,10 @@ fun App() {
|
|||
}
|
||||
ProfileSelectionScreen(
|
||||
onProfileSelected = { profile ->
|
||||
ProfileRepository.selectProfile(profile.profileIndex)
|
||||
if (authState is AuthState.Authenticated) {
|
||||
SyncManager.pullAllForProfile(profile.profileIndex)
|
||||
}
|
||||
gateScreen = AppGateScreen.Main.name
|
||||
requestProfileSwitch(
|
||||
profile = profile,
|
||||
syncOnEnter = authState is AuthState.Authenticated,
|
||||
)
|
||||
},
|
||||
onEditProfile = { profile ->
|
||||
editingProfile = profile
|
||||
|
|
@ -618,18 +668,6 @@ private fun MainAppContent(
|
|||
) {
|
||||
val navController = rememberNavController()
|
||||
val appUpdaterController = rememberAppUpdaterController()
|
||||
remember {
|
||||
EpisodeReleaseNotificationsRepository.ensureLoaded()
|
||||
}
|
||||
remember {
|
||||
CollectionSyncService.startObserving()
|
||||
}
|
||||
remember {
|
||||
HomeCatalogSettingsSyncService.startObserving()
|
||||
}
|
||||
remember {
|
||||
ProfileSettingsSync.startObserving()
|
||||
}
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Home) }
|
||||
|
|
@ -638,6 +676,10 @@ private fun MainAppContent(
|
|||
val searchScrollToTopRequests = remember { MutableSharedFlow<Unit>(extraBufferCapacity = 1) }
|
||||
val libraryScrollToTopRequests = remember { MutableSharedFlow<Unit>(extraBufferCapacity = 1) }
|
||||
val settingsRootActionRequests = remember { MutableSharedFlow<Unit>(extraBufferCapacity = 1) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
warmProfileBoundRepositories()
|
||||
}
|
||||
val currentBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val liquidGlassNativeTabBarEnabled by remember {
|
||||
ThemeSettingsRepository.liquidGlassNativeTabBarEnabled
|
||||
|
|
@ -654,32 +696,14 @@ private fun MainAppContent(
|
|||
var pickerMembership by remember { mutableStateOf<Map<String, Boolean>>(emptyMap()) }
|
||||
var pickerPending by remember { mutableStateOf(false) }
|
||||
var pickerError by remember { mutableStateOf<String?>(null) }
|
||||
val addonsUiState by remember {
|
||||
AddonRepository.initialize()
|
||||
AddonRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val libraryUiState by remember {
|
||||
LibraryRepository.ensureLoaded()
|
||||
LibraryRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val addonsUiState by AddonRepository.uiState.collectAsStateWithLifecycle()
|
||||
val libraryUiState by LibraryRepository.uiState.collectAsStateWithLifecycle()
|
||||
val authState by AuthRepository.state.collectAsStateWithLifecycle()
|
||||
val profileState by ProfileRepository.state.collectAsStateWithLifecycle()
|
||||
val playerSettingsUiState by remember {
|
||||
PlayerSettingsRepository.ensureLoaded()
|
||||
PlayerSettingsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val p2pSettingsUiState by remember {
|
||||
P2pSettingsRepository.ensureLoaded()
|
||||
P2pSettingsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val watchedUiState by remember {
|
||||
WatchedRepository.ensureLoaded()
|
||||
WatchedRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val downloadsUiState by remember {
|
||||
DownloadsRepository.ensureLoaded()
|
||||
DownloadsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val playerSettingsUiState by PlayerSettingsRepository.uiState.collectAsStateWithLifecycle()
|
||||
val p2pSettingsUiState by P2pSettingsRepository.uiState.collectAsStateWithLifecycle()
|
||||
val watchedUiState by WatchedRepository.uiState.collectAsStateWithLifecycle()
|
||||
val downloadsUiState by DownloadsRepository.uiState.collectAsStateWithLifecycle()
|
||||
val networkStatusUiState by remember {
|
||||
NetworkStatusRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
|
|
@ -913,10 +937,7 @@ private fun MainAppContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
val continueWatchingPreferencesUiState by remember {
|
||||
ContinueWatchingPreferencesRepository.ensureLoaded()
|
||||
ContinueWatchingPreferencesRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val continueWatchingPreferencesUiState by ContinueWatchingPreferencesRepository.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(
|
||||
initialHomeReady,
|
||||
|
|
@ -1339,8 +1360,16 @@ private fun MainAppContent(
|
|||
val onProfileSelected: (NuvioProfile) -> Unit = { profile ->
|
||||
profileSwitchLoading = true
|
||||
selectedTab = AppScreenTab.Home
|
||||
ProfileRepository.selectProfile(profile.profileIndex)
|
||||
com.nuvio.app.core.sync.SyncManager.pullAllForProfile(profile.profileIndex)
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
ProfileRepository.switchToProfile(profile.profileIndex)
|
||||
warmProfileBoundRepositories()
|
||||
SyncManager.pullAllForProfile(profile.profileIndex)
|
||||
delay(300)
|
||||
} finally {
|
||||
profileSwitchLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
|
|
@ -1723,10 +1752,7 @@ private fun MainAppContent(
|
|||
hasResolvedVideoId = true
|
||||
}
|
||||
|
||||
val playerSettings by remember {
|
||||
PlayerSettingsRepository.ensureLoaded()
|
||||
PlayerSettingsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val playerSettings by PlayerSettingsRepository.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
fun p2pSentinelUrl(infoHash: String, fileIdx: Int?): String =
|
||||
"torrent://$infoHash${fileIdx?.let { "?index=$it" }.orEmpty()}"
|
||||
|
|
@ -2751,15 +2777,6 @@ private fun MainAppContent(
|
|||
AppLaunchOverlay(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
|
||||
// Auto-dismiss profile switch overlay
|
||||
if (profileSwitchLoading) {
|
||||
LaunchedEffect(Unit) {
|
||||
// Brief loading screen while home refreshes for the new profile
|
||||
kotlinx.coroutines.delay(1200)
|
||||
profileSwitchLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
NuvioFloatingPrompt(
|
||||
visible = resumePromptItem != null,
|
||||
imageUrl = resumePromptItem?.poster ?: resumePromptItem?.imageUrl,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.DefaultAlpha
|
||||
import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import coil3.compose.AsyncImagePainter
|
||||
|
||||
internal enum class NuvioDesktopImageScaling {
|
||||
Auto,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal expect fun NuvioAsyncImage(
|
||||
model: Any?,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
placeholder: Painter? = null,
|
||||
error: Painter? = null,
|
||||
fallback: Painter? = error,
|
||||
onLoading: ((AsyncImagePainter.State.Loading) -> Unit)? = null,
|
||||
onSuccess: ((AsyncImagePainter.State.Success) -> Unit)? = null,
|
||||
onError: ((AsyncImagePainter.State.Error) -> Unit)? = null,
|
||||
alignment: Alignment = Alignment.Center,
|
||||
contentScale: ContentScale = ContentScale.Fit,
|
||||
alpha: Float = DefaultAlpha,
|
||||
colorFilter: ColorFilter? = null,
|
||||
filterQuality: FilterQuality? = null,
|
||||
clipToBounds: Boolean = true,
|
||||
desktopImageScaling: NuvioDesktopImageScaling = NuvioDesktopImageScaling.Auto,
|
||||
)
|
||||
|
|
@ -26,7 +26,6 @@ import androidx.compose.ui.layout.ContentScale
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.features.cloud.CloudLibraryContentType
|
||||
import com.nuvio.app.features.cloud.cloudLibraryDisplayArtworkUrl
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingItem
|
||||
|
|
@ -133,7 +132,7 @@ private fun ContinueWatchingSheetHeader(
|
|||
) {
|
||||
val artwork = item.poster ?: item.imageUrl
|
||||
if (artwork != null) {
|
||||
AsyncImage(
|
||||
NuvioAsyncImage(
|
||||
model = cloudLibraryDisplayArtworkUrl(artwork),
|
||||
contentDescription = item.title,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
|
|
@ -191,7 +190,7 @@ fun NuvioFloatingPrompt(
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
NuvioAsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.format.formatReleaseDateForDisplay
|
||||
import com.nuvio.app.features.home.MetaPreview
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -189,7 +188,7 @@ private fun PosterSheetHeader(
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (item.poster != null) {
|
||||
AsyncImage(
|
||||
NuvioAsyncImage(
|
||||
model = item.poster,
|
||||
contentDescription = item.name,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
|
|
|
|||
|
|
@ -15,8 +15,12 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
|
||||
|
|
@ -28,15 +32,18 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.isDesktop
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.home_view_all
|
||||
import nuvio.composeapp.generated.resources.poster_logo_content_description
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import kotlin.math.abs
|
||||
|
||||
enum class NuvioPosterShape {
|
||||
Poster,
|
||||
|
|
@ -64,6 +71,7 @@ fun <T> NuvioShelfSection(
|
|||
itemContent: @Composable (T) -> Unit,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
val rowState = rememberLazyListState()
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap + NuvioTokens.Space.s2),
|
||||
|
|
@ -78,6 +86,8 @@ fun <T> NuvioShelfSection(
|
|||
)
|
||||
}
|
||||
LazyRow(
|
||||
state = rowState,
|
||||
modifier = Modifier.desktopShelfDragScroll(rowState),
|
||||
contentPadding = rowContentPadding,
|
||||
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
|
||||
) {
|
||||
|
|
@ -97,6 +107,47 @@ fun <T> NuvioShelfSection(
|
|||
}
|
||||
}
|
||||
|
||||
private fun Modifier.desktopShelfDragScroll(
|
||||
state: LazyListState,
|
||||
): Modifier {
|
||||
if (!isDesktop) return this
|
||||
|
||||
return pointerInput(state) {
|
||||
awaitEachGesture {
|
||||
val down = awaitFirstDown(pass = PointerEventPass.Initial)
|
||||
var totalDx = 0f
|
||||
var totalDy = 0f
|
||||
var dragging = false
|
||||
|
||||
while (true) {
|
||||
val event = awaitPointerEvent(pass = PointerEventPass.Initial)
|
||||
val change = event.changes.firstOrNull { it.id == down.id } ?: break
|
||||
if (!change.pressed) break
|
||||
|
||||
val delta = change.position - change.previousPosition
|
||||
totalDx += delta.x
|
||||
totalDy += delta.y
|
||||
|
||||
if (!dragging) {
|
||||
val horizontalDrag =
|
||||
abs(totalDx) > viewConfiguration.touchSlop && abs(totalDx) > abs(totalDy)
|
||||
val verticalDrag =
|
||||
abs(totalDy) > viewConfiguration.touchSlop && abs(totalDy) > abs(totalDx)
|
||||
|
||||
when {
|
||||
verticalDrag -> break
|
||||
horizontalDrag -> dragging = true
|
||||
else -> continue
|
||||
}
|
||||
}
|
||||
|
||||
state.dispatchRawDelta(-delta.x)
|
||||
change.consume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NuvioPosterCard(
|
||||
title: String,
|
||||
|
|
@ -135,7 +186,7 @@ fun NuvioPosterCard(
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
NuvioAsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = title,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
|
|
@ -160,7 +211,7 @@ fun NuvioPosterCard(
|
|||
.padding(horizontal = NuvioTokens.Space.s10, vertical = NuvioTokens.Space.s10),
|
||||
) {
|
||||
if (!bottomLeftLogoUrl.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
NuvioAsyncImage(
|
||||
model = bottomLeftLogoUrl,
|
||||
contentDescription = stringResource(Res.string.poster_logo_content_description, title),
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import androidx.compose.ui.platform.LocalUriHandler
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioIconActionButton
|
||||
import com.nuvio.app.core.ui.NuvioInfoBadge
|
||||
import com.nuvio.app.core.ui.NuvioInputField
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.nuvio.app.core.network.NetworkCondition
|
||||
import com.nuvio.app.core.network.NetworkStatusRepository
|
||||
import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.format.formatReleaseDateForDisplay
|
||||
import com.nuvio.app.core.ui.NuvioBackButton
|
||||
import com.nuvio.app.core.ui.NuvioPosterWatchedOverlay
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioPosterCard
|
||||
import com.nuvio.app.core.ui.NuvioPosterShape
|
||||
import com.nuvio.app.core.ui.NuvioScreenHeader
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.build.AppFeaturePolicy
|
||||
import com.nuvio.app.core.build.TrailerPlaybackMode
|
||||
import com.nuvio.app.core.network.NetworkCondition
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.lerp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import coil3.compose.LocalPlatformContext
|
||||
import coil3.request.ImageRequest
|
||||
import com.nuvio.app.core.i18n.localizedShortMonthName
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import com.nuvio.app.core.ui.landscapePosterHeightForWidth
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import coil3.compose.LocalPlatformContext
|
||||
import coil3.request.ImageRequest
|
||||
import com.nuvio.app.features.details.MetaPerson
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.lerp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioBackButton
|
||||
import com.nuvio.app.features.details.MetaDetails
|
||||
import com.nuvio.app.isIos
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioDesktopImageScaling
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.features.details.MetaDetails
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
|
@ -108,6 +109,7 @@ fun DetailHero(
|
|||
},
|
||||
alignment = if (isTablet) Alignment.TopCenter else Alignment.Center,
|
||||
contentScale = ContentScale.Crop,
|
||||
desktopImageScaling = NuvioDesktopImageScaling.Disabled,
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import androidx.compose.ui.layout.ContentScale
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.features.details.MetaCompany
|
||||
import com.nuvio.app.features.details.MetaDetails
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.nuvio.app.core.build.AppFeaturePolicy
|
||||
import com.nuvio.app.core.format.formatReleaseDateForDisplay
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.features.details.MetaTrailer
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import nuvio.composeapp.generated.resources.detail_tab_trailer
|
||||
|
|
|
|||
|
|
@ -104,19 +104,21 @@ fun HomeScreen(
|
|||
onFirstCatalogRendered: (() -> Unit)? = null,
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
AddonRepository.initialize()
|
||||
CollectionRepository.initialize()
|
||||
ContinueWatchingPreferencesRepository.ensureLoaded()
|
||||
WatchedRepository.ensureLoaded()
|
||||
WatchProgressRepository.ensureLoaded()
|
||||
withContext(Dispatchers.Default) {
|
||||
AddonRepository.initialize()
|
||||
CollectionRepository.initialize()
|
||||
ContinueWatchingPreferencesRepository.ensureLoaded()
|
||||
HomeCatalogSettingsRepository.snapshot()
|
||||
TraktSettingsRepository.ensureLoaded()
|
||||
TraktAuthRepository.ensureLoaded()
|
||||
WatchedRepository.ensureLoaded()
|
||||
WatchProgressRepository.ensureLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
val addonsUiState by AddonRepository.uiState.collectAsStateWithLifecycle()
|
||||
val homeUiState by HomeRepository.uiState.collectAsStateWithLifecycle()
|
||||
val homeSettingsUiState by remember {
|
||||
HomeCatalogSettingsRepository.snapshot()
|
||||
HomeCatalogSettingsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val homeSettingsUiState by HomeCatalogSettingsRepository.uiState.collectAsStateWithLifecycle()
|
||||
val homeListState = rememberLazyListState()
|
||||
val collections by CollectionRepository.collections.collectAsStateWithLifecycle()
|
||||
val continueWatchingPreferences by ContinueWatchingPreferencesRepository.uiState.collectAsStateWithLifecycle()
|
||||
|
|
@ -124,14 +126,8 @@ fun HomeScreen(
|
|||
val watchProgressUiState by WatchProgressRepository.uiState.collectAsStateWithLifecycle()
|
||||
val cloudLibraryUiState by CloudLibraryRepository.uiState.collectAsStateWithLifecycle()
|
||||
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
|
||||
val traktSettingsUiState by remember {
|
||||
TraktSettingsRepository.ensureLoaded()
|
||||
TraktSettingsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val isTraktAuthenticated by remember {
|
||||
TraktAuthRepository.ensureLoaded()
|
||||
TraktAuthRepository.isAuthenticated
|
||||
}.collectAsStateWithLifecycle()
|
||||
val traktSettingsUiState by TraktSettingsRepository.uiState.collectAsStateWithLifecycle()
|
||||
val isTraktAuthenticated by TraktAuthRepository.isAuthenticated.collectAsStateWithLifecycle()
|
||||
var observedOfflineState by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(scrollToTopRequests) {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import androidx.compose.ui.unit.TextUnit
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioProgressBar
|
||||
import com.nuvio.app.core.ui.NuvioShelfSection
|
||||
import com.nuvio.app.core.ui.PosterLandscapeAspectRatio
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioDesktopImageScaling
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.format.formatReleaseDateForDisplay
|
||||
import com.nuvio.app.features.home.MetaPreview
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -181,6 +182,7 @@ fun HomeHeroSection(
|
|||
},
|
||||
alignment = if (layout.isTablet) Alignment.TopCenter else Alignment.Center,
|
||||
contentScale = ContentScale.Crop,
|
||||
desktopImageScaling = NuvioDesktopImageScaling.Disabled,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ import androidx.compose.ui.text.font.FontStyle
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioTokens
|
||||
import com.nuvio.app.core.ui.nuvio
|
||||
import com.nuvio.app.features.debrid.DebridSettingsRepository
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioBackButton
|
||||
import com.nuvio.app.core.ui.nuvioTypeScale
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.compose_player_episode_title_format
|
||||
import nuvio.composeapp.generated.resources.detail_btn_play
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.auth.AuthRepository
|
||||
import com.nuvio.app.core.auth.AuthState
|
||||
import com.nuvio.app.core.ui.NuvioInputField
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
|
@ -62,6 +65,7 @@ object ProfileRepository {
|
|||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val log = Logger.withTag("ProfileRepository")
|
||||
private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true }
|
||||
private val profileSwitchMutex = Mutex()
|
||||
private fun localizedString(resource: StringResource): String = runBlocking { getString(resource) }
|
||||
|
||||
private val _state = MutableStateFlow(ProfileState())
|
||||
|
|
@ -141,7 +145,15 @@ object ProfileRepository {
|
|||
}
|
||||
}
|
||||
|
||||
fun selectProfile(profileIndex: Int) {
|
||||
suspend fun switchToProfile(profileIndex: Int) {
|
||||
profileSwitchMutex.withLock {
|
||||
withContext(Dispatchers.Default) {
|
||||
selectProfile(profileIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectProfile(profileIndex: Int) {
|
||||
activeProfileIndex = profileIndex
|
||||
val selectedProfile = _state.value.profiles.find { it.profileIndex == profileIndex }
|
||||
_state.value = _state.value.copy(
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.auth.AuthRepository
|
||||
import com.nuvio.app.core.auth.AuthState
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -176,7 +176,6 @@ fun ProfileSelectionScreen(
|
|||
} else if (profile.pinEnabled) {
|
||||
pinDialogProfile = profile
|
||||
} else {
|
||||
ProfileRepository.selectProfile(profile.profileIndex)
|
||||
onProfileSelected(profile)
|
||||
}
|
||||
},
|
||||
|
|
@ -217,7 +216,6 @@ fun ProfileSelectionScreen(
|
|||
} else if (profile.pinEnabled) {
|
||||
pinDialogProfile = profile
|
||||
} else {
|
||||
ProfileRepository.selectProfile(profile.profileIndex)
|
||||
onProfileSelected(profile)
|
||||
}
|
||||
},
|
||||
|
|
@ -282,7 +280,6 @@ fun ProfileSelectionScreen(
|
|||
onVerify = { pin -> ProfileRepository.verifyPin(profile.profileIndex, pin) },
|
||||
onVerified = {
|
||||
pinDialogProfile = null
|
||||
ProfileRepository.selectProfile(profile.profileIndex)
|
||||
onProfileSelected(profile)
|
||||
},
|
||||
onDismiss = { pinDialogProfile = null },
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ import androidx.compose.ui.unit.sp
|
|||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioTokens
|
||||
import com.nuvio.app.core.ui.nuvio
|
||||
import com.nuvio.app.isIos
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.network.NetworkCondition
|
||||
import com.nuvio.app.core.format.formatReleaseDateForDisplay
|
||||
import com.nuvio.app.core.ui.NuvioDropdownChip
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import androidx.compose.ui.platform.LocalUriHandler
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioScreen
|
||||
import com.nuvio.app.core.ui.NuvioScreenHeader
|
||||
import com.nuvio.app.features.cloud.PremiumizeCloudLibraryPosterUrl
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioScreen
|
||||
import com.nuvio.app.core.ui.NuvioScreenHeader
|
||||
import com.nuvio.app.core.ui.NuvioSurfaceCard
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.i18n.localizedByteUnit
|
||||
import com.nuvio.app.core.ui.NuvioTokens
|
||||
import com.nuvio.app.core.ui.nuvio
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ import com.nuvio.app.features.downloads.DownloadsRepository
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
|
||||
import com.nuvio.app.features.debrid.DebridProviders
|
||||
import com.nuvio.app.features.debrid.DebridSettingsRepository
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import com.nuvio.app.isIos
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import com.nuvio.app.features.trakt.parseTraktIsoDateTimeToEpochMs
|
|||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.pluralStringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import co.touchlab.kermit.Logger
|
||||
|
||||
@Composable
|
||||
fun computeAirDateBadgeText(
|
||||
|
|
@ -67,46 +66,32 @@ class ReleaseAlertState(
|
|||
val isNewSeasonRelease: Boolean,
|
||||
)
|
||||
|
||||
private const val ReleaseAlertWindowMs = 60L * 24 * 60 * 60 * 1000
|
||||
private val NoReleaseAlertState = ReleaseAlertState(false, false)
|
||||
|
||||
fun calculateReleaseAlertState(
|
||||
seedLastUpdatedEpochMs: Long,
|
||||
seedSeasonNumber: Int?,
|
||||
nextSeasonNumber: Int?,
|
||||
releasedIso: String?,
|
||||
): ReleaseAlertState {
|
||||
if (releasedIso.isNullOrBlank()) return NoReleaseAlertState
|
||||
|
||||
val releaseEpoch = parseReleaseDateToEpochMs(releasedIso)
|
||||
?: return NoReleaseAlertState
|
||||
|
||||
val nowMs = WatchProgressClock.nowEpochMs()
|
||||
if (nowMs < releaseEpoch) return NoReleaseAlertState
|
||||
if (releaseEpoch <= seedLastUpdatedEpochMs) return NoReleaseAlertState
|
||||
if (nowMs - releaseEpoch >= ReleaseAlertWindowMs) return NoReleaseAlertState
|
||||
|
||||
val log = Logger.withTag("ReleaseAlert")
|
||||
log.d {
|
||||
"calculateReleaseAlertState inputs: releasedIso=$releasedIso, " +
|
||||
"releaseEpoch=$releaseEpoch, seedLastUpdatedEpochMs=$seedLastUpdatedEpochMs, " +
|
||||
"seedSeasonNumber=$seedSeasonNumber, nextSeasonNumber=$nextSeasonNumber, nowMs=$nowMs"
|
||||
}
|
||||
|
||||
if (releaseEpoch == null) {
|
||||
log.d { "calculateReleaseAlertState failed: releaseEpoch is null" }
|
||||
return ReleaseAlertState(false, false)
|
||||
}
|
||||
|
||||
val hasAired = nowMs >= releaseEpoch
|
||||
val sixtyDaysMs = 60L * 24 * 60 * 60 * 1000
|
||||
val isReleaseAlert = hasAired &&
|
||||
releaseEpoch > seedLastUpdatedEpochMs &&
|
||||
(nowMs - releaseEpoch) < sixtyDaysMs
|
||||
|
||||
val isNewSeasonRelease = isReleaseAlert &&
|
||||
val isNewSeasonRelease =
|
||||
seedSeasonNumber != null &&
|
||||
nextSeasonNumber != null &&
|
||||
nextSeasonNumber != seedSeasonNumber
|
||||
|
||||
log.d {
|
||||
"calculateReleaseAlertState result: isReleaseAlert=$isReleaseAlert (hasAired=$hasAired, " +
|
||||
"epoch>seed=${releaseEpoch > seedLastUpdatedEpochMs}, ageMs=${nowMs - releaseEpoch}), " +
|
||||
"isNewSeasonRelease=$isNewSeasonRelease"
|
||||
}
|
||||
|
||||
return ReleaseAlertState(
|
||||
isReleaseAlert = isReleaseAlert,
|
||||
isReleaseAlert = true,
|
||||
isNewSeasonRelease = isNewSeasonRelease
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@ package com.nuvio.app
|
|||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.window.Window
|
||||
|
|
@ -48,15 +44,13 @@ fun main() {
|
|||
if (smokePlayerUrl == null) {
|
||||
App()
|
||||
} else {
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
PlatformPlayerSurface(
|
||||
sourceUrl = smokePlayerUrl,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onControllerReady = {},
|
||||
onSnapshot = {},
|
||||
onError = { error = it },
|
||||
onError = {},
|
||||
)
|
||||
error?.let { println("Nuvio desktop player smoke error: $it") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,281 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.DefaultAlpha
|
||||
import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asComposeImageBitmap
|
||||
import androidx.compose.ui.graphics.asSkiaBitmap
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import coil3.BitmapImage
|
||||
import coil3.Image
|
||||
import coil3.PlatformContext
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import coil3.compose.LocalPlatformContext
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.NullRequestDataException
|
||||
import org.jetbrains.skia.Bitmap
|
||||
import org.jetbrains.skia.FilterMipmap
|
||||
import org.jetbrains.skia.FilterMode
|
||||
import org.jetbrains.skia.Image as SkiaImage
|
||||
import org.jetbrains.skia.MipmapMode
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val MinCustomDownscaleRatio = 1.08f
|
||||
private const val MaxDesktopSourceSizePx = 1536
|
||||
private const val MaxScaledBitmapPixels = 1_250_000L
|
||||
|
||||
private val IsWindowsDesktop: Boolean =
|
||||
System.getProperty("os.name")
|
||||
?.startsWith("Windows", ignoreCase = true)
|
||||
?: false
|
||||
|
||||
@Composable
|
||||
internal actual fun NuvioAsyncImage(
|
||||
model: Any?,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier,
|
||||
placeholder: Painter?,
|
||||
error: Painter?,
|
||||
fallback: Painter?,
|
||||
onLoading: ((AsyncImagePainter.State.Loading) -> Unit)?,
|
||||
onSuccess: ((AsyncImagePainter.State.Success) -> Unit)?,
|
||||
onError: ((AsyncImagePainter.State.Error) -> Unit)?,
|
||||
alignment: Alignment,
|
||||
contentScale: ContentScale,
|
||||
alpha: Float,
|
||||
colorFilter: ColorFilter?,
|
||||
filterQuality: FilterQuality?,
|
||||
clipToBounds: Boolean,
|
||||
desktopImageScaling: NuvioDesktopImageScaling,
|
||||
) {
|
||||
val context = LocalPlatformContext.current
|
||||
val effectiveDesktopImageScaling = remember(desktopImageScaling) {
|
||||
if (IsWindowsDesktop) desktopImageScaling else NuvioDesktopImageScaling.Disabled
|
||||
}
|
||||
val requestModel = remember(context, model, effectiveDesktopImageScaling) {
|
||||
if (effectiveDesktopImageScaling == NuvioDesktopImageScaling.Disabled) {
|
||||
model
|
||||
} else {
|
||||
model.withDesktopHighQualitySize(context)
|
||||
}
|
||||
}
|
||||
val transform: (AsyncImagePainter.State) -> AsyncImagePainter.State = remember(
|
||||
placeholder,
|
||||
error,
|
||||
fallback,
|
||||
effectiveDesktopImageScaling,
|
||||
) {
|
||||
{ state ->
|
||||
when (state) {
|
||||
is AsyncImagePainter.State.Loading -> {
|
||||
placeholder?.let { state.copy(painter = it) } ?: state
|
||||
}
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
state.result.image.toScaledBitmapPainter(effectiveDesktopImageScaling)
|
||||
?.let { state.copy(painter = it) }
|
||||
?: state
|
||||
}
|
||||
is AsyncImagePainter.State.Error -> {
|
||||
val fallbackPainter = if (state.result.throwable is NullRequestDataException) {
|
||||
fallback ?: error
|
||||
} else {
|
||||
error
|
||||
}
|
||||
fallbackPainter?.let { state.copy(painter = it) } ?: state
|
||||
}
|
||||
AsyncImagePainter.State.Empty -> state
|
||||
}
|
||||
}
|
||||
}
|
||||
val onState: ((AsyncImagePainter.State) -> Unit)? = remember(onLoading, onSuccess, onError) {
|
||||
if (onLoading == null && onSuccess == null && onError == null) {
|
||||
null
|
||||
} else {
|
||||
{ state ->
|
||||
when (state) {
|
||||
is AsyncImagePainter.State.Loading -> onLoading?.invoke(state)
|
||||
is AsyncImagePainter.State.Success -> onSuccess?.invoke(state)
|
||||
is AsyncImagePainter.State.Error -> onError?.invoke(state)
|
||||
AsyncImagePainter.State.Empty -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = requestModel,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
transform = transform,
|
||||
onState = onState,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = filterQuality ?: FilterQuality.High,
|
||||
clipToBounds = clipToBounds,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Any?.withDesktopHighQualitySize(context: PlatformContext): Any? {
|
||||
if (this == null) return null
|
||||
|
||||
return if (this is ImageRequest) {
|
||||
newBuilder()
|
||||
.size(MaxDesktopSourceSizePx)
|
||||
.build()
|
||||
} else {
|
||||
ImageRequest.Builder(context)
|
||||
.data(this)
|
||||
.size(MaxDesktopSourceSizePx)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Image.toScaledBitmapPainter(desktopImageScaling: NuvioDesktopImageScaling): Painter? {
|
||||
if (desktopImageScaling == NuvioDesktopImageScaling.Disabled) return null
|
||||
|
||||
return (this as? BitmapImage)
|
||||
?.bitmap
|
||||
?.asComposeImageBitmap()
|
||||
?.let { imageBitmap -> ScaledBitmapPainter(imageBitmap) }
|
||||
}
|
||||
|
||||
private class ScaledBitmapPainter(
|
||||
private val image: ImageBitmap,
|
||||
) : Painter() {
|
||||
private var cachedSize: IntSize? = null
|
||||
private var cachedBitmap: ImageBitmap? = null
|
||||
private var alpha: Float = DefaultAlpha
|
||||
private var colorFilter: ColorFilter? = null
|
||||
|
||||
override val intrinsicSize: Size =
|
||||
Size(image.width.toFloat(), image.height.toFloat())
|
||||
|
||||
override fun DrawScope.onDraw() {
|
||||
val drawSize = IntSize(
|
||||
width = size.width.roundToInt().coerceAtLeast(1),
|
||||
height = size.height.roundToInt().coerceAtLeast(1),
|
||||
)
|
||||
if (!shouldUseScaledBitmap(drawSize)) {
|
||||
drawSource(drawSize)
|
||||
return
|
||||
}
|
||||
|
||||
val cacheSize = drawSize.cacheSize()
|
||||
if (cacheSize.pixelCount() > MaxScaledBitmapPixels) {
|
||||
drawSource(drawSize)
|
||||
return
|
||||
}
|
||||
|
||||
val bitmap = scaledBitmap(cacheSize)
|
||||
|
||||
drawImage(
|
||||
image = bitmap,
|
||||
srcOffset = IntOffset.Zero,
|
||||
srcSize = cacheSize,
|
||||
dstOffset = IntOffset.Zero,
|
||||
dstSize = drawSize,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = if (cacheSize == drawSize) FilterQuality.None else FilterQuality.Medium,
|
||||
)
|
||||
}
|
||||
|
||||
override fun applyAlpha(alpha: Float): Boolean {
|
||||
this.alpha = alpha
|
||||
return true
|
||||
}
|
||||
|
||||
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
|
||||
this.colorFilter = colorFilter
|
||||
return true
|
||||
}
|
||||
|
||||
private fun scaledBitmap(size: IntSize): ImageBitmap {
|
||||
cachedBitmap?.let { bitmap ->
|
||||
if (cachedSize == size) return bitmap
|
||||
}
|
||||
return image.scale(size.width, size.height).also { bitmap ->
|
||||
cachedSize = size
|
||||
cachedBitmap = bitmap
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawSource(drawSize: IntSize) {
|
||||
drawImage(
|
||||
image = image,
|
||||
srcOffset = IntOffset.Zero,
|
||||
srcSize = IntSize(image.width, image.height),
|
||||
dstOffset = IntOffset.Zero,
|
||||
dstSize = drawSize,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = FilterQuality.High,
|
||||
)
|
||||
}
|
||||
|
||||
private fun shouldUseScaledBitmap(drawSize: IntSize): Boolean {
|
||||
if (drawSize.pixelCount() > MaxScaledBitmapPixels) return false
|
||||
|
||||
val widthScale = image.width.toFloat() / drawSize.width.toFloat()
|
||||
val heightScale = image.height.toFloat() / drawSize.height.toFloat()
|
||||
return max(widthScale, heightScale) >= MinCustomDownscaleRatio
|
||||
}
|
||||
|
||||
private fun IntSize.cacheSize(): IntSize {
|
||||
val quantum = cacheQuantum()
|
||||
return IntSize(
|
||||
width = width.roundUpTo(quantum),
|
||||
height = height.roundUpTo(quantum),
|
||||
)
|
||||
}
|
||||
|
||||
private fun IntSize.cacheQuantum(): Int {
|
||||
val longestSide = max(width, height)
|
||||
return when {
|
||||
longestSide >= 1200 -> 16
|
||||
longestSide >= 600 -> 8
|
||||
longestSide >= 200 -> 4
|
||||
else -> 2
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.roundUpTo(quantum: Int): Int =
|
||||
((this + quantum - 1) / quantum) * quantum
|
||||
|
||||
private fun IntSize.pixelCount(): Long =
|
||||
width.toLong() * height.toLong()
|
||||
|
||||
private fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap {
|
||||
val image = SkiaImage.makeFromBitmap(asSkiaBitmap())
|
||||
return try {
|
||||
image.scale(width, height)
|
||||
} finally {
|
||||
image.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun SkiaImage.scale(width: Int, height: Int): ImageBitmap {
|
||||
val bitmap = Bitmap()
|
||||
bitmap.allocN32Pixels(width, height)
|
||||
scalePixels(
|
||||
bitmap.peekPixels()!!,
|
||||
FilterMipmap(FilterMode.LINEAR, MipmapMode.LINEAR),
|
||||
false,
|
||||
)
|
||||
return bitmap.asComposeImageBitmap()
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioAsyncImage as AsyncImage
|
||||
import coil3.compose.LocalPlatformContext
|
||||
import coil3.request.ImageRequest
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ actual fun PlatformPlayerSurface(
|
|||
onSnapshot: (PlayerPlaybackSnapshot) -> Unit,
|
||||
onError: (String?) -> Unit,
|
||||
) {
|
||||
if (DesktopHostOs.current == DesktopHostOs.MACOS) {
|
||||
if (DesktopHostOs.current == DesktopHostOs.MACOS || DesktopHostOs.current == DesktopHostOs.WINDOWS) {
|
||||
NativePlayerSurface(
|
||||
sourceUrl = sourceUrl,
|
||||
sourceHeaders = sourceHeaders,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ internal object AwtNativeViewResolver {
|
|||
fun resolveNativeViewPointer(component: Component): Long =
|
||||
when (DesktopHostOs.current) {
|
||||
DesktopHostOs.MACOS -> MacosAwtViewResolver.resolveNativeViewPointer(component)
|
||||
DesktopHostOs.WINDOWS -> WindowsAwtViewResolver.resolveNativeViewPointer(component)
|
||||
else -> error("Native desktop playback is not implemented for ${DesktopHostOs.current}.")
|
||||
}
|
||||
}
|
||||
|
|
@ -48,3 +49,34 @@ private object MacosAwtViewResolver {
|
|||
private fun invokeLong(target: Any, methodName: String): Long =
|
||||
(findMethod(target.javaClass, methodName).invoke(target) as Number).toLong()
|
||||
}
|
||||
|
||||
private object WindowsAwtViewResolver {
|
||||
private val componentPeerField: Field by lazy {
|
||||
Component::class.java.getDeclaredField("peer").apply { isAccessible = true }
|
||||
}
|
||||
|
||||
fun resolveNativeViewPointer(component: Component): Long {
|
||||
val peer = componentPeerField.get(component)
|
||||
?: error("AWT component peer is not ready for native playback.")
|
||||
|
||||
val pointer = invokeLong(peer, "getHWnd")
|
||||
if (pointer == 0L) {
|
||||
error("Windows AWT HWND pointer was zero.")
|
||||
}
|
||||
return pointer
|
||||
}
|
||||
|
||||
private fun findMethod(type: Class<*>, name: String): Method {
|
||||
var current: Class<*>? = type
|
||||
while (current != null) {
|
||||
runCatching {
|
||||
return current.getDeclaredMethod(name).apply { isAccessible = true }
|
||||
}
|
||||
current = current.superclass
|
||||
}
|
||||
error("Method $name was not found on ${type.name}.")
|
||||
}
|
||||
|
||||
private fun invokeLong(target: Any, methodName: String): Long =
|
||||
(findMethod(target.javaClass, methodName).invoke(target) as Number).toLong()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,13 +80,14 @@ internal object NativePlayerBridge {
|
|||
|
||||
private fun loadNativeLibrary() {
|
||||
val platform = DesktopHostOs.current
|
||||
require(platform == DesktopHostOs.MACOS) {
|
||||
require(platform == DesktopHostOs.MACOS || platform == DesktopHostOs.WINDOWS) {
|
||||
"Native desktop playback is not implemented for $platform yet."
|
||||
}
|
||||
|
||||
val libraryName = nativeLibraryName(platform)
|
||||
val platformDir = nativeDirectoryName(platform)
|
||||
findLocalBuildLibrary(platformDir, libraryName)?.let { localLibrary ->
|
||||
copyLocalRuntimeResources(platformDir, localLibrary.parentFile)
|
||||
System.load(localLibrary.absolutePath)
|
||||
return
|
||||
}
|
||||
|
|
@ -98,12 +99,44 @@ internal object NativePlayerBridge {
|
|||
val suffix = libraryName.substringAfter("player_bridge", ".dylib")
|
||||
val file = Files.createTempFile(dir.toPath(), "player-bridge-", suffix).toFile()
|
||||
file.deleteOnExit()
|
||||
extractBundledRuntimeResources(platformDir, dir)
|
||||
input.use { source ->
|
||||
file.outputStream().use { target -> source.copyTo(target) }
|
||||
}
|
||||
System.load(file.absolutePath)
|
||||
}
|
||||
|
||||
private fun extractBundledRuntimeResources(platformDir: String, dir: File) {
|
||||
val runtimeNames = bundledRuntimeResourceNames(platformDir)
|
||||
runtimeNames.forEach { name ->
|
||||
val resource = "/native/$platformDir/$name"
|
||||
val input = NativePlayerBridge::class.java.getResourceAsStream(resource) ?: return@forEach
|
||||
val target = dir.resolve(name)
|
||||
input.use { source ->
|
||||
target.outputStream().use { output -> source.copyTo(output) }
|
||||
}
|
||||
target.deleteOnExit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bundledRuntimeResourceNames(platformDir: String): List<String> {
|
||||
val indexResource = "/native/$platformDir/runtime-files.txt"
|
||||
val indexed = NativePlayerBridge::class.java.getResourceAsStream(indexResource)
|
||||
?.bufferedReader()
|
||||
?.useLines { lines ->
|
||||
lines.map(String::trim)
|
||||
.filter { it.isNotEmpty() && !it.startsWith("#") }
|
||||
.toList()
|
||||
}
|
||||
.orEmpty()
|
||||
if (indexed.isNotEmpty()) return indexed
|
||||
|
||||
return when (platformDir) {
|
||||
"windows" -> listOf("libmpv-2.dll")
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun findLocalBuildLibrary(platformDir: String, libraryName: String): File? {
|
||||
val candidates = listOf(
|
||||
File("composeApp/build/native/$platformDir/$libraryName"),
|
||||
|
|
@ -112,6 +145,21 @@ internal object NativePlayerBridge {
|
|||
return candidates.firstOrNull { it.exists() }
|
||||
}
|
||||
|
||||
private fun copyLocalRuntimeResources(platformDir: String, targetDir: File) {
|
||||
val runtimeDirs = listOf(
|
||||
File("composeApp/build/native/$platformDir-runtime"),
|
||||
File("build/native/$platformDir-runtime"),
|
||||
)
|
||||
runtimeDirs.firstOrNull(File::isDirectory)
|
||||
?.listFiles { file -> file.isFile }
|
||||
?.forEach { runtimeFile ->
|
||||
val target = targetDir.resolve(runtimeFile.name)
|
||||
if (runtimeFile.absolutePath != target.absolutePath) {
|
||||
runCatching { runtimeFile.copyTo(target, overwrite = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun nativeDirectoryName(platform: DesktopHostOs): String =
|
||||
when (platform) {
|
||||
DesktopHostOs.MACOS -> "macos"
|
||||
|
|
@ -163,7 +211,7 @@ internal object NativePlayerBridge {
|
|||
}
|
||||
|
||||
internal fun preloadNativePlayerBridgeAsync() {
|
||||
if (DesktopHostOs.current == DesktopHostOs.MACOS) {
|
||||
if (DesktopHostOs.current == DesktopHostOs.MACOS || DesktopHostOs.current == DesktopHostOs.WINDOWS) {
|
||||
NativePlayerBridge.preloadAsync()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1687
composeApp/src/desktopMain/native/windows/player_bridge.cpp
Normal file
1687
composeApp/src/desktopMain/native/windows/player_bridge.cpp
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2446,7 +2446,12 @@
|
|||
|
||||
const send = (type, value = 0) => {
|
||||
const bridge = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.player;
|
||||
if (bridge) bridge.postMessage({ type, value });
|
||||
if (bridge) {
|
||||
bridge.postMessage({ type, value });
|
||||
return;
|
||||
}
|
||||
const webViewBridge = window.chrome && window.chrome.webview;
|
||||
if (webViewBridge) webViewBridge.postMessage({ type, value });
|
||||
};
|
||||
|
||||
const animationDelay = ms => new Promise(resolve => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQuality
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.AsyncImagePainter
|
||||
|
||||
@Composable
|
||||
internal actual fun NuvioAsyncImage(
|
||||
model: Any?,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier,
|
||||
placeholder: Painter?,
|
||||
error: Painter?,
|
||||
fallback: Painter?,
|
||||
onLoading: ((AsyncImagePainter.State.Loading) -> Unit)?,
|
||||
onSuccess: ((AsyncImagePainter.State.Success) -> Unit)?,
|
||||
onError: ((AsyncImagePainter.State.Error) -> Unit)?,
|
||||
alignment: Alignment,
|
||||
contentScale: ContentScale,
|
||||
alpha: Float,
|
||||
colorFilter: ColorFilter?,
|
||||
filterQuality: FilterQuality?,
|
||||
clipToBounds: Boolean,
|
||||
desktopImageScaling: NuvioDesktopImageScaling,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
placeholder = placeholder,
|
||||
error = error,
|
||||
fallback = fallback,
|
||||
onLoading = onLoading,
|
||||
onSuccess = onSuccess,
|
||||
onError = onError,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = filterQuality ?: DefaultFilterQuality,
|
||||
clipToBounds = clipToBounds,
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue