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/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) } 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(