mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-09-20 14:44:00 +00:00
feat: integrate Nucleus framework for enhanced window management and webview support
This commit is contained in:
parent
d95e1aad70
commit
94903dcc33
@ -20,6 +20,7 @@
|
||||
## Dependencies
|
||||
- `gradle/libs.versions.toml` is the single source of truth for all version pins and library declarations.
|
||||
- Kotlin: `2.3.0`, JVM target: `11` (compile/target compatibility in both `composeApp/build.gradle.kts` and `plugin_interfaces/build.gradle.kts`).
|
||||
- **compose-webview (desktop) dispatcher hijack:** `dev.nucleusframework:composewebview` transitively pulls `nucleus.decorated-window-tao`, which registers a `TaoMainDispatcherFactory` (`loadPriority = 100`) via `META-INF/services` that hijacks `Dispatchers.Main` away from the Swing EDT. This breaks lifecycle's `enforceMainThreadIfNeeded` (window fails with "Method addObserver must be called on the main thread"). The app overrides it with `SwingMainDispatcherFactory` (`composeApp/src/jvmMain/.../core/coroutines/`, `loadPriority = Int.MAX_VALUE`) which pins `Dispatchers.Main` back to the Swing EDT. Keep that factory + its `META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory` resource; the long-term fix is in the webview (make the Tao dispatcher opt-in or lower its priority).
|
||||
|
||||
## Codegen and plugin packaging
|
||||
- JS plugin bundles: `:js_plugin_example:packageDevelopmentPlugin` and `:js_plugin_example:packageProductionPlugin`. Output is `.smplug` files in `js_plugin_example/build/distributions/`.
|
||||
|
||||
@ -33,4 +33,5 @@ plugins {
|
||||
alias(libs.plugins.uniffi) apply false
|
||||
alias(libs.plugins.cargo) apply false
|
||||
alias(libs.plugins.mokkery) apply false
|
||||
alias(libs.plugins.neucleusFramework) apply false
|
||||
}
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
import gobley.gradle.GobleyHost
|
||||
import gobley.gradle.cargo.dsl.jvm
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
import dev.nucleusframework.desktop.application.dsl.TargetFormat
|
||||
import org.jetbrains.compose.reload.gradle.ComposeHotRun
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import java.io.FileInputStream
|
||||
@ -35,6 +35,7 @@ plugins {
|
||||
alias(libs.plugins.uniffi)
|
||||
alias(libs.plugins.cargo)
|
||||
alias(libs.plugins.mokkery)
|
||||
alias(libs.plugins.neucleusFramework)
|
||||
kotlin("plugin.atomicfu") version libs.versions.kotlin
|
||||
}
|
||||
|
||||
@ -220,6 +221,10 @@ kotlin {
|
||||
|
||||
implementation(libs.appdirs)
|
||||
implementation(libs.jna)
|
||||
|
||||
implementation(libs.nucleus.core.runtime)
|
||||
implementation(libs.nucleus.nucleus.application)
|
||||
implementation(libs.nucleus.decorated.window.tao)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -280,56 +285,56 @@ dependencies {
|
||||
debugImplementation(libs.compose.uiTooling)
|
||||
}
|
||||
|
||||
compose.desktop {
|
||||
application {
|
||||
mainClass = "dev.krtirtho.spotube.MainKt"
|
||||
nucleus.application {
|
||||
mainClass = "dev.krtirtho.spotube.MainKt"
|
||||
|
||||
jvmArgs += listOf(
|
||||
"--enable-native-access=ALL-UNNAMED", // need for JNA for compose-webview (wry) to work
|
||||
// --- ADD THESE FOR SPEED & LOW RAM ---
|
||||
"-Xms64m", // Start with a tiny heap (prevents grabbing 500MB upfront)
|
||||
"-Xmx384m", // Cap the max heap (VLC and WebView need a bit of room)
|
||||
"-XX:TieredStopAtLevel=1", // Disables the heavy C2 compiler. Huge startup speedup!
|
||||
"-XX:+UseSerialGC" // The Serial GC is the most efficient for heaps under 512MB
|
||||
jvmArgs += listOf(
|
||||
"--enable-native-access=ALL-UNNAMED", // need for JNA for compose-webview (wry) to work
|
||||
// --- ADD THESE FOR SPEED & LOW RAM ---
|
||||
"-Xms64m", // Start with a tiny heap (prevents grabbing 500MB upfront)
|
||||
"-Xmx384m", // Cap the max heap (VLC and WebView need a bit of room)
|
||||
"-XX:TieredStopAtLevel=1", // Disables the heavy C2 compiler. Huge startup speedup!
|
||||
"-XX:+UseSerialGC" // The Serial GC is the most efficient for heaps under 512MB
|
||||
)
|
||||
|
||||
nativeDistributions {
|
||||
packageName = "dev.krtirtho.spotube"
|
||||
packageVersion = project.findProperty("versionName") as String? ?: "6.0.0"
|
||||
licenseFile = project.file("../LICENSE")
|
||||
|
||||
appResourcesRootDir.set(vlcjBundler.vlcNativesDirectory)
|
||||
|
||||
targetFormats(
|
||||
TargetFormat.Dmg,
|
||||
TargetFormat.Deb,
|
||||
TargetFormat.Rpm,
|
||||
TargetFormat.AppImage,
|
||||
TargetFormat.Msi,
|
||||
TargetFormat.Exe,
|
||||
)
|
||||
|
||||
nativeDistributions {
|
||||
packageName = "dev.krtirtho.spotube"
|
||||
packageVersion = project.findProperty("versionName") as String? ?: "6.0.0"
|
||||
licenseFile = project.file("../LICENSE")
|
||||
modules("jdk.unsupported")
|
||||
|
||||
targetFormats(
|
||||
TargetFormat.Dmg,
|
||||
TargetFormat.Deb,
|
||||
TargetFormat.Rpm,
|
||||
TargetFormat.AppImage,
|
||||
TargetFormat.Msi,
|
||||
TargetFormat.Exe,
|
||||
)
|
||||
|
||||
modules("jdk.unsupported")
|
||||
|
||||
linux {
|
||||
modules("jdk.security.auth")
|
||||
}
|
||||
windows {
|
||||
shortcut = true
|
||||
menu = true
|
||||
dirChooser = true
|
||||
perUserInstall = true
|
||||
}
|
||||
|
||||
macOS {
|
||||
notarization {
|
||||
|
||||
}
|
||||
}
|
||||
linux {
|
||||
modules("jdk.security.auth")
|
||||
}
|
||||
windows {
|
||||
shortcut = true
|
||||
menu = true
|
||||
dirChooser = true
|
||||
perUserInstall = true
|
||||
}
|
||||
|
||||
buildTypes.release.proguard {
|
||||
configurationFiles.from(project.file("proguard-rules.pro"))
|
||||
macOS {
|
||||
notarization {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes.release.proguard {
|
||||
configurationFiles.from(project.file("proguard-rules.pro"))
|
||||
}
|
||||
}
|
||||
|
||||
// Issue with compose plugin
|
||||
|
||||
@ -20,7 +20,7 @@ package dev.krtirtho.spotube.core.webview
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebStorage
|
||||
import android.webkit.WebView
|
||||
import io.github.kdroidfilter.webview.web.WebViewState
|
||||
import dev.nucleusframework.webview.web.WebViewState
|
||||
|
||||
actual fun platformWebviewConfig(webView: WebViewState, pluginId: String?) {
|
||||
val nativeWebView = webView.webView?.nativeWebView as? WebView ?: return
|
||||
|
||||
@ -17,9 +17,9 @@
|
||||
|
||||
package dev.krtirtho.spotube.core.webview
|
||||
|
||||
import io.github.kdroidfilter.webview.web.WebContent
|
||||
import io.github.kdroidfilter.webview.web.WebViewNavigator
|
||||
import io.github.kdroidfilter.webview.cookie.CookieManager
|
||||
import dev.nucleusframework.webview.web.WebContent
|
||||
import dev.nucleusframework.webview.web.WebViewNavigator
|
||||
import dev.nucleusframework.webview.cookie.CookieManager
|
||||
import dev.krtirtho.plugin_interfaces.host_apis.Cookie
|
||||
import dev.krtirtho.spotube.core.di.injectLogger
|
||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
package dev.krtirtho.spotube.core.webview
|
||||
|
||||
import io.github.kdroidfilter.webview.web.WebViewState
|
||||
import dev.nucleusframework.webview.web.WebViewState
|
||||
|
||||
expect fun platformWebviewConfig(webView: WebViewState, pluginId: String?)
|
||||
|
||||
|
||||
@ -51,13 +51,13 @@ import compose.icons.feathericons.ChevronLeft
|
||||
import compose.icons.feathericons.ChevronRight
|
||||
import compose.icons.feathericons.X
|
||||
import dev.krtirtho.spotube.core.tools.user_agents.UserAgents
|
||||
import io.github.kdroidfilter.webview.jsbridge.IJsMessageHandler
|
||||
import io.github.kdroidfilter.webview.jsbridge.JsMessage
|
||||
import io.github.kdroidfilter.webview.jsbridge.rememberWebViewJsBridge
|
||||
import io.github.kdroidfilter.webview.web.WebView
|
||||
import io.github.kdroidfilter.webview.web.WebViewNavigator
|
||||
import io.github.kdroidfilter.webview.web.WebViewState
|
||||
import io.github.kdroidfilter.webview.web.rememberWebViewNavigator
|
||||
import dev.nucleusframework.webview.jsbridge.IJsMessageHandler
|
||||
import dev.nucleusframework.webview.jsbridge.JsMessage
|
||||
import dev.nucleusframework.webview.jsbridge.rememberWebViewJsBridge
|
||||
import dev.nucleusframework.webview.web.WebView
|
||||
import dev.nucleusframework.webview.web.WebViewNavigator
|
||||
import dev.nucleusframework.webview.web.WebViewState
|
||||
import dev.nucleusframework.webview.web.rememberWebViewNavigator
|
||||
|
||||
class PostMessageHandler(
|
||||
private val onMessageReceived: (String) -> Unit = {}
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
package dev.krtirtho.spotube.core.webview
|
||||
|
||||
import io.github.kdroidfilter.webview.web.WebViewState
|
||||
import dev.nucleusframework.webview.web.WebViewState
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import platform.Foundation.NSDate
|
||||
import platform.Foundation.NSHTTPCookieStorage
|
||||
|
||||
@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.window.WindowDraggableArea
|
||||
@ -44,6 +45,10 @@ import dev.krtirtho.spotube.resources.iconsax.FluentMaximize
|
||||
import dev.krtirtho.spotube.resources.iconsax.FluentMinus
|
||||
import dev.krtirtho.spotube.resources.iconsax.FluentSquareMultiple
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.nucleusframework.window.WindowControls
|
||||
import dev.nucleusframework.window.WindowControlsRenderer
|
||||
import dev.nucleusframework.window.WindowDoubleClickAction
|
||||
import dev.nucleusframework.window.windowDragArea
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
|
||||
@ -55,27 +60,27 @@ actual fun ApplicationMainBar(
|
||||
actions: @Composable (RowScope.() -> Unit),
|
||||
backButton: Boolean,
|
||||
) {
|
||||
LocalWindowScope.current.WindowDraggableArea {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
TopAppBar(
|
||||
title = title,
|
||||
actions = {
|
||||
actions()
|
||||
},
|
||||
navigationIcon = {
|
||||
if (backButton) {
|
||||
ApplicationBackButton()
|
||||
}
|
||||
},
|
||||
subtitle = subtitle,
|
||||
modifier = Modifier.padding(end = 125.dp) // To avoid overlap with window buttons
|
||||
)
|
||||
WindowButtons(
|
||||
modifier = Modifier.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.windowDragArea(doubleClickAction = WindowDoubleClickAction.ToggleMaximize)
|
||||
) {
|
||||
TopAppBar(
|
||||
title = title,
|
||||
actions = {
|
||||
actions()
|
||||
},
|
||||
navigationIcon = {
|
||||
if (backButton) {
|
||||
ApplicationBackButton()
|
||||
}
|
||||
},
|
||||
subtitle = subtitle,
|
||||
modifier = Modifier.padding(end = 125.dp) // To avoid overlap with window buttons
|
||||
)
|
||||
LocalWindowScope.current.WindowControls(
|
||||
modifier = Modifier.align(Alignment.TopEnd).heightIn(max = 48.dp),
|
||||
renderer = WindowControlsRenderer.Platform, // default
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,8 @@ package dev.krtirtho.spotube.core.ui.component
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.window.ApplicationScope
|
||||
import dev.nucleusframework.application.NucleusApplicationScope
|
||||
|
||||
val LocalApplicationScope = staticCompositionLocalOf<ApplicationScope> {
|
||||
val LocalApplicationScope = staticCompositionLocalOf< NucleusApplicationScope> {
|
||||
error("No ApplicationScope found in LocalApplicationScope")
|
||||
}
|
||||
@ -18,11 +18,11 @@
|
||||
package dev.krtirtho.spotube.core.ui.component
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.window.FrameWindowScope
|
||||
import androidx.compose.ui.window.WindowState
|
||||
import dev.nucleusframework.application.NucleusDecoratedWindowScope
|
||||
|
||||
// LocalWindow is a LocalComposition that provides access to the current Window instance in the composition hierarchy.
|
||||
val LocalWindowScope = compositionLocalOf<FrameWindowScope> {
|
||||
val LocalWindowScope = compositionLocalOf<NucleusDecoratedWindowScope> {
|
||||
error("No Window found in LocalWindowScope")
|
||||
}
|
||||
val LocalWindowState = compositionLocalOf<WindowState> {
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
package dev.krtirtho.spotube.core.webview
|
||||
|
||||
import dev.krtirtho.spotube.core.paths.Paths
|
||||
import io.github.kdroidfilter.webview.web.WebViewState
|
||||
import dev.nucleusframework.webview.web.WebViewState
|
||||
import okio.FileSystem
|
||||
import okio.Path.Companion.toPath
|
||||
import org.koin.core.context.GlobalContext
|
||||
|
||||
@ -25,9 +25,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.WindowDecoration
|
||||
import androidx.compose.ui.window.application
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
import dev.krtirtho.spotube.core.di.initKoin
|
||||
import dev.krtirtho.spotube.core.newpipe.NewPipeDownloader
|
||||
@ -37,6 +34,9 @@ import dev.krtirtho.spotube.core.ui.component.LocalApplicationScope
|
||||
import dev.krtirtho.spotube.core.ui.component.LocalWindowScope
|
||||
import dev.krtirtho.spotube.core.ui.component.LocalWindowState
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsProvider
|
||||
import dev.nucleusframework.application.DecoratedWindow
|
||||
import dev.nucleusframework.application.NucleusBackend
|
||||
import dev.nucleusframework.application.nucleusApplication
|
||||
import io.github.vinceglb.filekit.FileKit
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -61,7 +61,7 @@ fun main() {
|
||||
NewPipeDownloader.init(KoinPathsProvider.paths)
|
||||
val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
application {
|
||||
nucleusApplication(backend = NucleusBackend.Tao) {
|
||||
val windowState = rememberWindowState(
|
||||
width = 1080.dp,
|
||||
height = 720.dp
|
||||
@ -84,7 +84,7 @@ fun main() {
|
||||
}
|
||||
)
|
||||
|
||||
Window(
|
||||
DecoratedWindow(
|
||||
state = windowState,
|
||||
onCloseRequest = {
|
||||
if (minimizeToTray) {
|
||||
@ -96,12 +96,11 @@ fun main() {
|
||||
}
|
||||
},
|
||||
title = "Spotube",
|
||||
decoration = WindowDecoration.Undecorated(),
|
||||
visible = isWindowVisible,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalApplicationScope provides this@application,
|
||||
LocalWindowScope provides this@Window,
|
||||
LocalApplicationScope provides this@nucleusApplication,
|
||||
LocalWindowScope provides this@DecoratedWindow,
|
||||
LocalWindowState provides windowState
|
||||
) {
|
||||
App()
|
||||
|
||||
@ -47,6 +47,7 @@ materialKolor = "4.1.1"
|
||||
murmurhash = "0.4.2"
|
||||
newpipeextractor = "v0.26.2"
|
||||
newpipeExtractorKmp = "1.3.0"
|
||||
nucleusNucleusApplication = "2.4.4"
|
||||
semver = "2.1.0"
|
||||
vlcj = "4.12.1"
|
||||
vlcjNative = "4.12.0"
|
||||
@ -56,7 +57,7 @@ kotlinStdlib = "2.3.21"
|
||||
runner = "1.7.0"
|
||||
core = "1.7.0"
|
||||
filekit = "0.14.2"
|
||||
compose-webview = "0.1.0-SNAPSHOT"
|
||||
compose-webview = "1.0.1"
|
||||
koin = "4.2.2"
|
||||
multiplatform-nav3-ui = "1.1.1"
|
||||
compose-multiplatform-adaptive = "1.3.0-beta02"
|
||||
@ -119,6 +120,9 @@ material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "m
|
||||
murmurhash = { module = "com.goncalossilva:murmurhash", version.ref = "murmurhash" }
|
||||
newpipe-extractor-kmp = { module = "io.github.yushosei:newpipe-extractor-kmp", version.ref = "newpipeExtractorKmp" }
|
||||
newpipeextractor = { module = "com.github.teamnewpipe:NewPipeExtractor", version.ref = "newpipeextractor" }
|
||||
nucleus-core-runtime = { module = "dev.nucleusframework:nucleus.core-runtime", version.ref = "nucleusNucleusApplication" }
|
||||
nucleus-decorated-window-tao = { module = "dev.nucleusframework:nucleus.decorated-window-tao", version.ref = "nucleusNucleusApplication" }
|
||||
nucleus-nucleus-application = { module = "dev.nucleusframework:nucleus.nucleus-application", version.ref = "nucleusNucleusApplication" }
|
||||
semver = { module = "net.swiftzer.semver:semver", version.ref = "semver" }
|
||||
vlcj = { module = "uk.co.caprica:vlcj", version.ref = "vlcj" }
|
||||
vlcj-natives = { module = "uk.co.caprica:vlcj-natives", version.ref = "vlcjNative" }
|
||||
@ -140,7 +144,7 @@ androidx-core = { group = "androidx.test", name = "core", version.ref = "core" }
|
||||
filekit-core = { group = "io.github.vinceglb", name = "filekit-core", version.ref = "filekit" }
|
||||
filekit-dialogs = { group = "io.github.vinceglb", name = "filekit-dialogs", version.ref = "filekit" }
|
||||
filekit-dialogs-compose = { group = "io.github.vinceglb", name = "filekit-dialogs-compose", version.ref = "filekit" }
|
||||
compose-webview = { module = "io.github.kdroidfilter:composewebview", version.ref = "compose-webview" }
|
||||
compose-webview = { module = "dev.nucleusframework:composewebview", version.ref = "compose-webview" }
|
||||
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
|
||||
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
|
||||
@ -182,4 +186,5 @@ kotlinxAtomicFu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "kotlin
|
||||
uniffi = { id = "dev.gobley.uniffi", version.ref = "uniffi" }
|
||||
cargo = { id = "dev.gobley.cargo", version.ref = "uniffi" }
|
||||
mokkery = { id = "dev.mokkery", version.ref = "mokkery" }
|
||||
vanniktechMavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" }
|
||||
vanniktechMavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" }
|
||||
neucleusFramework = { id = "dev.nucleusframework", version.ref = "nucleusNucleusApplication" }
|
||||
Loading…
Reference in New Issue
Block a user