From df55025386e2ed552223ec591ab2c1465dc9f10c Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Sun, 12 Apr 2026 02:23:54 +0300 Subject: [PATCH 1/2] feat(sample): migrate to Nucleus plugin with GraalVM native image support - Replace compose.desktop with nucleus.application DSL - Add GraalVM configuration (BellSoft JDK 25, native image) - Migrate reachability-metadata.json to new unified format - Configure NSIS packaging with proper app name and metadata - Force dark theme only in sample app --- gradle/libs.versions.toml | 2 + .../native-image/native-image.properties | 1 - .../native-image/reachability-metadata.json | 63 +++++++++++-------- sample/composeApp/build.gradle.kts | 45 ++++++++----- .../kotlin/sample/app/theme/Theme.kt | 26 +------- .../src/jvmMain/kotlin/sample/app/main.kt | 5 +- 6 files changed, 73 insertions(+), 69 deletions(-) delete mode 100644 mediaplayer/src/jvmMain/resources/META-INF/native-image/native-image.properties diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c9e66f60..e2ff11c0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ compose-ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.r compose-ui-tooling-preview = { module = "org.jetbrains.compose.components:components-ui-tooling-preview", version.ref = "compose" } compose-material-icons-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version = "1.7.3" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-runtime-ktx" } +nucleus-graalvm-runtime = { module = "io.github.kdroidfilter:nucleus.graalvm-runtime", version = "1.9.1" } [plugins] @@ -57,3 +58,4 @@ vannitktech-maven-publish = {id = "com.vanniktech.maven.publish", version = "0.3 dokka = { id = "org.jetbrains.dokka" , version = "2.2.0"} detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +nucleus = { id = "io.github.kdroidfilter.nucleus", version = "1.9.1" } diff --git a/mediaplayer/src/jvmMain/resources/META-INF/native-image/native-image.properties b/mediaplayer/src/jvmMain/resources/META-INF/native-image/native-image.properties deleted file mode 100644 index a2208431..00000000 --- a/mediaplayer/src/jvmMain/resources/META-INF/native-image/native-image.properties +++ /dev/null @@ -1 +0,0 @@ -Args = -H:IncludeResources=composemediaplayer/native/.* diff --git a/mediaplayer/src/jvmMain/resources/META-INF/native-image/reachability-metadata.json b/mediaplayer/src/jvmMain/resources/META-INF/native-image/reachability-metadata.json index 47ddcdb8..48a43ced 100644 --- a/mediaplayer/src/jvmMain/resources/META-INF/native-image/reachability-metadata.json +++ b/mediaplayer/src/jvmMain/resources/META-INF/native-image/reachability-metadata.json @@ -1,26 +1,39 @@ -[ - { - "type": "io.github.kdroidfilter.composemediaplayer.linux.LinuxNativeBridge", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, - { - "type": "io.github.kdroidfilter.composemediaplayer.mac.MacNativeBridge", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, - { - "type": "io.github.kdroidfilter.composemediaplayer.windows.WindowsNativeBridge", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, - { - "type": "java.lang.Runnable", - "methods": [ - { "name": "run", "parameterTypes": [] } +{ + "reflection": [ + { + "type": "io.github.kdroidfilter.composemediaplayer.linux.LinuxNativeBridge", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true, + "jniAccessible": true + }, + { + "type": "io.github.kdroidfilter.composemediaplayer.mac.MacNativeBridge", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true, + "jniAccessible": true + }, + { + "type": "io.github.kdroidfilter.composemediaplayer.windows.WindowsNativeBridge", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true, + "jniAccessible": true + }, + { + "type": "java.lang.Runnable", + "methods": [ + { + "name": "run", + "parameterTypes": [] + } + ] + } + ], + "resources": [ + { + "glob": "composemediaplayer/native/**" + } ] - } -] +} diff --git a/sample/composeApp/build.gradle.kts b/sample/composeApp/build.gradle.kts index 6743f480..ff1a8710 100644 --- a/sample/composeApp/build.gradle.kts +++ b/sample/composeApp/build.gradle.kts @@ -1,7 +1,8 @@ @file:OptIn(ExperimentalWasmDsl::class) +import io.github.kdroidfilter.nucleus.desktop.application.dsl.CompressionLevel import org.apache.tools.ant.taskdefs.condition.Os -import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import io.github.kdroidfilter.nucleus.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig @@ -10,6 +11,7 @@ plugins { alias(libs.plugins.compose.compiler) alias(libs.plugins.compose) alias(libs.plugins.android.application) + alias(libs.plugins.nucleus) } @@ -81,6 +83,7 @@ kotlin { jvmMain.dependencies { implementation(compose.desktop.currentOs) + implementation(libs.nucleus.graalvm.runtime) } webMain.dependencies { implementation(libs.kotlinx.browser) @@ -106,24 +109,32 @@ dependencies { debugImplementation(libs.compose.ui.tooling) } -compose.desktop { - application { - mainClass = "sample.app.MainKt" - - nativeDistributions { - targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "sample" - packageVersion = "1.0.0" - linux { - modules("jdk.security.auth", "jdk.accessibility") - } - macOS { - jvmArgs( - "-Dapple.awt.application.appearance=system" - ) - } +nucleus.application { + mainClass = "sample.app.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Nsis, TargetFormat.Deb) + packageName = "Compose Media Player" + description = "A Kotlin Multiplatform media player built with Compose" + vendor = "KDroidFilter" + cleanupNativeLibs = true + packageVersion = "1.0.0" + compressionLevel = CompressionLevel.Maximum + windows { + shortcut = true } } + + graalvm { + isEnabled = true + imageName = "compose-media-player" + javaLanguageVersion = 25 + jvmVendor = JvmVendorSpec.BELLSOFT + buildArgs.addAll( + "-H:+AddAllCharsets", + "-Djava.awt.headless=false" + ) + } } diff --git a/sample/composeApp/src/commonMain/kotlin/sample/app/theme/Theme.kt b/sample/composeApp/src/commonMain/kotlin/sample/app/theme/Theme.kt index c55191b6..cc36d611 100644 --- a/sample/composeApp/src/commonMain/kotlin/sample/app/theme/Theme.kt +++ b/sample/composeApp/src/commonMain/kotlin/sample/app/theme/Theme.kt @@ -1,9 +1,7 @@ package sample.app.theme -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color @@ -33,34 +31,12 @@ private val DarkScheme = darkColorScheme( outlineVariant = Color(0xFF49454F), ) -private val LightScheme = lightColorScheme( - primary = Color(0xFF5B4FC4), - onPrimary = Color.White, - primaryContainer = Color(0xFFE8DEFF), - onPrimaryContainer = Color(0xFF1A0063), - secondary = Color(0xFF006A6A), - onSecondary = Color.White, - secondaryContainer = Color(0xFF9CF1F0), - onSecondaryContainer = Color(0xFF002020), - tertiary = Color(0xFF8C4A3B), - onTertiary = Color.White, - tertiaryContainer = Color(0xFFFFDAD4), - onTertiaryContainer = Color(0xFF3A0905), - background = Color(0xFFFCF8FF), - onBackground = Color(0xFF1C1B1F), - surface = Color(0xFFFCF8FF), - onSurface = Color(0xFF1C1B1F), - surfaceVariant = Color(0xFFE8E0F0), - onSurfaceVariant = Color(0xFF49454F), -) - @Composable fun AppTheme( - darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit, ) { MaterialTheme( - colorScheme = if (darkTheme) DarkScheme else LightScheme, + colorScheme = DarkScheme, content = content, ) } diff --git a/sample/composeApp/src/jvmMain/kotlin/sample/app/main.kt b/sample/composeApp/src/jvmMain/kotlin/sample/app/main.kt index d79e8647..9c6ae04a 100644 --- a/sample/composeApp/src/jvmMain/kotlin/sample/app/main.kt +++ b/sample/composeApp/src/jvmMain/kotlin/sample/app/main.kt @@ -4,7 +4,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState -fun main() { +import io.github.kdroidfilter.nucleus.graalvm.GraalVmInitializer + +fun main() { + GraalVmInitializer.initialize() application { val windowState = rememberWindowState(width = 720.dp, height = 1000.dp) Window( From df581e3604e92b189c05d2811052105a64fb5ed6 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Sun, 12 Apr 2026 03:17:10 +0300 Subject: [PATCH 2/2] fix(sample): prevent player disposal on window resize and screen navigation Hoist rememberVideoPlayerState() above BoxWithConstraints in App to prevent player state disposal when resizing crosses the 600dp breakpoint between RailLayout and BarLayout. Also add DisposableEffect to pause playback when leaving PlayerScreen and allow resuming when returning. --- .../src/commonMain/kotlin/sample/app/App.kt | 19 +++++++++++-------- .../kotlin/sample/app/player/PlayerScreen.kt | 14 ++++++++++++-- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/sample/composeApp/src/commonMain/kotlin/sample/app/App.kt b/sample/composeApp/src/commonMain/kotlin/sample/app/App.kt index 97ec0ef0..7e5c6673 100644 --- a/sample/composeApp/src/commonMain/kotlin/sample/app/App.kt +++ b/sample/composeApp/src/commonMain/kotlin/sample/app/App.kt @@ -25,6 +25,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp +import io.github.kdroidfilter.composemediaplayer.VideoPlayerState +import io.github.kdroidfilter.composemediaplayer.rememberVideoPlayerState import sample.app.feed.FeedScreen import sample.app.gallery.GalleryScreen import sample.app.player.PlayerScreen @@ -40,14 +42,15 @@ private enum class Screen(val label: String, val icon: ImageVector) { fun App() { AppTheme { var currentScreen by remember { mutableStateOf(Screen.Player) } + val playerState = rememberVideoPlayerState() BoxWithConstraints(modifier = Modifier.fillMaxSize()) { val useRail = maxWidth >= 600.dp if (useRail) { - RailLayout(currentScreen, onScreenChange = { currentScreen = it }) + RailLayout(currentScreen, onScreenChange = { currentScreen = it }, playerState = playerState) } else { - BarLayout(currentScreen, onScreenChange = { currentScreen = it }) + BarLayout(currentScreen, onScreenChange = { currentScreen = it }, playerState = playerState) } } } @@ -55,7 +58,7 @@ fun App() { // Compact: bottom NavigationBar @Composable -private fun BarLayout(current: Screen, onScreenChange: (Screen) -> Unit) { +private fun BarLayout(current: Screen, onScreenChange: (Screen) -> Unit, playerState: VideoPlayerState) { Scaffold( bottomBar = { NavigationBar { @@ -70,13 +73,13 @@ private fun BarLayout(current: Screen, onScreenChange: (Screen) -> Unit) { } }, ) { padding -> - ScreenContent(current, Modifier.fillMaxSize().padding(padding)) + ScreenContent(current, Modifier.fillMaxSize().padding(padding), playerState) } } // Medium+: side NavigationRail @Composable -private fun RailLayout(current: Screen, onScreenChange: (Screen) -> Unit) { +private fun RailLayout(current: Screen, onScreenChange: (Screen) -> Unit, playerState: VideoPlayerState) { Row(modifier = Modifier.fillMaxSize()) { NavigationRail { Spacer(Modifier.weight(1f)) @@ -90,14 +93,14 @@ private fun RailLayout(current: Screen, onScreenChange: (Screen) -> Unit) { } Spacer(Modifier.weight(1f)) } - ScreenContent(current, Modifier.weight(1f).fillMaxHeight()) + ScreenContent(current, Modifier.weight(1f).fillMaxHeight(), playerState) } } @Composable -private fun ScreenContent(screen: Screen, modifier: Modifier) { +private fun ScreenContent(screen: Screen, modifier: Modifier, playerState: VideoPlayerState) { when (screen) { - Screen.Player -> PlayerScreen(modifier) + Screen.Player -> PlayerScreen(modifier, playerState) Screen.Gallery -> GalleryScreen(modifier) Screen.Feed -> FeedScreen(modifier) } diff --git a/sample/composeApp/src/commonMain/kotlin/sample/app/player/PlayerScreen.kt b/sample/composeApp/src/commonMain/kotlin/sample/app/player/PlayerScreen.kt index e75bf1c4..f5416960 100644 --- a/sample/composeApp/src/commonMain/kotlin/sample/app/player/PlayerScreen.kt +++ b/sample/composeApp/src/commonMain/kotlin/sample/app/player/PlayerScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.material3.Snackbar import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -73,8 +74,17 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable -fun PlayerScreen(modifier: Modifier = Modifier) { - val playerState = rememberVideoPlayerState() +fun PlayerScreen(modifier: Modifier = Modifier, playerState: VideoPlayerState = rememberVideoPlayerState()) { + // Pause when leaving the screen, resume when coming back + DisposableEffect(playerState) { + val wasPlaying = playerState.isPlaying + onDispose { + if (playerState.isPlaying) { + playerState.pause() + } + } + } + val scope = rememberCoroutineScope() var videoUrl by remember { mutableStateOf(SAMPLE_VIDEOS.first().second) }