milan jurkulák před 2 roky
rodič
revize
cff1ad7719

+ 5 - 1
.gitignore

@@ -42,4 +42,8 @@ src/main/resources/dependencies/linux/usr/share/backgrounds
 /src/main/resources/dependencies/linux/usr/share/backgrounds/
 src/main/resources/keys
 /src/main/resources/keys
-/src/main/resources/keys/
+/src/main/resources/keys/
+
+### out ###
+/out
+/out/

+ 9 - 0
build.gradle.kts

@@ -67,8 +67,17 @@ allprojects {
         implementation(libs.coil.compose.core)
         implementation(libs.coil.compose)
         implementation(libs.coil.mp)
+        implementation(libs.coil.svg)
 //        implementation(libs.coil.gif)
         implementation(libs.coil.network.okhttp)
+        // video vlc
+        implementation("uk.co.caprica:vlcj:4.8.2")
+        // media player
+        implementation("io.github.khubaibkhan4:mediaplayer-kmp:1.0.9")
+        // music
+        implementation(libs.korau)
+        // gif
+//        implementation(libs.accompanist.drawablepainter)
         // glide
 //        implementation(libs.glide)
 //        implementation(libs.landscapist.glide)

+ 2 - 2
gradle.properties

@@ -1,3 +1,3 @@
-org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -Dprism.order=sw
 kotlin.code.style=official
-gemini_api_key=
+gemini_api_key=

+ 6 - 3
gradle/libs.versions.toml

@@ -18,6 +18,7 @@ materialKolor = "1.7.0"
 coil3 = "3.0.0-alpha06"
 ktor = "3.0.0-wasm2"
 kotlinxCoroutinesSwing = "1.8.0"
+korau = "4.0.10"
 #lanscapist = "2.3.8"
 #generativeai = "0.5.0-1.0.0-wasm"
 #compose-webview = "1.9.20"
@@ -37,8 +38,11 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3"
 coil-compose-core = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "coil3" }
 coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil3" }
 coil-mp = { module = "io.coil-kt.coil3:coil", version.ref = "coil3" }
-coil-gif = { module = "io.coil-kt.coil3:coil-gif", version = "2.2.7" }
-
+coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3" }
+coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3" }
+korau = { module = "com.soywiz.korlibs.korau:korau", version.ref = "korau" }
+#accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version = "0.35.0-alpha" }
+#coil-gif = { module = "io.coil-kt.coil3:coil-gif", version = "2.2.7" }
 #landscapist-glide = { module = "com.github.skydoves:landscapist-glide", version.ref = "lanscapist" }
 #landscapist-palette = { module = "com.github.skydoves:landscapist-palette", version.ref = "lanscapist" }
 #font-icons-awesome = { module = "br.com.devsrsouza.compose.icons:font-awesome-desktop", version = "1.1.0" }
@@ -49,7 +53,6 @@ coil-gif = { module = "io.coil-kt.coil3:coil-gif", version = "2.2.7" }
 #decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" }
 #decompose-extensions-compose = { module = "com.arkivanov.decompose:extensions-compose", version.ref = "decompose" }
 #google-generativeai = { module = "dev.shreyaspatil.generativeai:generativeai-google-jvm", version.ref = "generativeai" }
-
 #// open ai
 #//        implementation("com.aallam.openai:openai-client:3.8.2")
 #// byte buffer

+ 3 - 3
src/main/kotlin/eu/mjdev/dadb/DadbDiscover.kt

@@ -9,7 +9,7 @@ class AdbDiscover(
     coroutineScope: CoroutineScope? = null,
     discoverDelay: Long = 1000L,
     onAdded: suspend (Dadb) -> Unit,
-    onRemoved: suspend (Dadb) -> Unit,
+//    onRemoved: suspend (Dadb) -> Unit,
 ) {
     val devices = mutableMapOf<String, Dadb>()
 
@@ -33,12 +33,12 @@ class AdbDiscover(
     companion object {
         fun adbDevicesHandler(
             coroutineScope: CoroutineScope? = null,
-            onRemoved: suspend (Dadb) -> Unit = {},
+//            onRemoved: suspend (Dadb) -> Unit = {},
             onAdded: suspend (Dadb) -> Unit = {},
         ) = AdbDiscover(
             coroutineScope = coroutineScope,
             onAdded = onAdded,
-            onRemoved = onRemoved
+//            onRemoved = onRemoved
         )
     }
 }

+ 8 - 4
src/main/kotlin/eu/mjdev/desktop/Main.kt

@@ -8,6 +8,7 @@ import eu.mjdev.desktop.components.appsmenu.AppsMenu
 import eu.mjdev.desktop.components.controlcenter.ControlCenter
 import eu.mjdev.desktop.components.desktoppanel.DesktopPanel
 import eu.mjdev.desktop.components.sliding.VisibilityState.Companion.rememberVisibilityState
+import eu.mjdev.desktop.extensions.Compose.asyncImageLoader
 import eu.mjdev.desktop.provider.DesktopProvider
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
 import eu.mjdev.desktop.windows.MainWindow
@@ -20,16 +21,16 @@ fun main() = application(
     val panelState = rememberVisibilityState()
     val menuState = rememberVisibilityState()
 
-    val handleMenuFocus: (Boolean) -> Unit = { focus ->
+    val handleMenuFocus: (Boolean) -> Unit = {
         if (!menuState.isWindowFocus) {
             menuState.hide()
         }
     }
 
-    val handlePanelFocus: (Boolean) -> Unit = { focus ->
+    val handlePanelFocus: (Boolean) -> Unit = {
     }
 
-    val handleControlCenterFocus: (Boolean) -> Unit = { focus ->
+    val handleControlCenterFocus: (Boolean) -> Unit = {
         if (!controlCenterState.isWindowFocus) {
             controlCenterState.hide()
         }
@@ -37,7 +38,10 @@ fun main() = application(
 
     MaterialTheme {
         CompositionLocalProvider(
-            LocalDesktop provides DesktopProvider(scope)
+            LocalDesktop provides DesktopProvider(
+                scope = scope,
+                imageLoader = asyncImageLoader()
+            ),
         ) {
             MainWindow(
                 panelState = panelState,

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/AIPage.kt

@@ -10,8 +10,8 @@ import eu.mjdev.desktop.components.controlcenter.ControlCenterPage
 @Suppress("FunctionName")
 fun AIPage() = ControlCenterPage(
     icon = Icons.Filled.Campaign,
-    name = "Bluetooth"
-) { backgroundColor ->
+    name = "AI"
+) {
     Box(
         modifier = Modifier.fillMaxSize()
     ) {

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/BluetoothSettingsPage.kt

@@ -11,7 +11,7 @@ import eu.mjdev.desktop.components.controlcenter.ControlCenterPage
 fun BluetoothSettingsPage() = ControlCenterPage(
     icon = Icons.Filled.Bluetooth,
     name = "Bluetooth"
-) { backgroundColor ->
+) {
     Box(
         modifier = Modifier.fillMaxSize()
     )

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/Devices.kt

@@ -10,8 +10,8 @@ import eu.mjdev.desktop.components.controlcenter.ControlCenterPage
 @Suppress("FunctionName")
 fun DevicesPage() = ControlCenterPage(
     icon = Icons.Filled.MobileFriendly,
-    name = "Bluetooth"
-) { backgroundColor ->
+    name = "Connected devices"
+) {
     Box(
         modifier = Modifier.fillMaxSize()
     ) {

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/DisplaySettingsPage.kt

@@ -11,7 +11,7 @@ import eu.mjdev.desktop.components.controlcenter.ControlCenterPage
 fun DisplaySettingsPage() = ControlCenterPage(
     icon = Icons.Filled.ScreenshotMonitor,
     name = "Display"
-) { backgroundColor ->
+) {
     Box(
         modifier = Modifier.fillMaxSize()
     )

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/SoundSettingsPage.kt

@@ -11,7 +11,7 @@ import eu.mjdev.desktop.components.controlcenter.ControlCenterPage
 fun SoundSettingsPage() = ControlCenterPage(
     icon = Icons.Filled.Headphones,
     name = "Sound"
-) { backgroundColor ->
+) {
     Box(
         modifier = Modifier.fillMaxSize()
     )

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/WifiSettingsPage.kt

@@ -11,7 +11,7 @@ import eu.mjdev.desktop.components.controlcenter.ControlCenterPage
 fun WifiSettingsPage() = ControlCenterPage(
     icon = Icons.Filled.Wifi,
     name = "Wifi"
-) { backgroundColor ->
+) {
     Box(
         modifier = Modifier.fillMaxSize()
     )

+ 19 - 2
src/main/kotlin/eu/mjdev/desktop/components/image/ImageAny.kt

@@ -10,12 +10,17 @@ import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQ
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.layout.ContentScale
 import coil3.compose.AsyncImage
+import eu.mjdev.desktop.components.video.VideoPlayerView
+import eu.mjdev.desktop.extensions.Compose.asyncImageLoader
+import eu.mjdev.desktop.provider.DesktopProvider
+import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
 import androidx.compose.foundation.Image as ComposeImage
 
 @Suppress("FunctionName")
 @Composable
 fun ImageAny(
     src: Any?,
+    api: DesktopProvider = LocalDesktop.current,
     contentDescription: String? = "",
     modifier: Modifier = Modifier,
     alignment: Alignment = Alignment.Center,
@@ -26,7 +31,20 @@ fun ImageAny(
     onLoading: () -> Unit = {},
     onFail: (error: Throwable) -> Unit = {}
 ) {
+    val isVideo = src.toString().trim().let {
+        it.endsWith(".gif") || it.endsWith(".mp4") || it.endsWith(".mpg")
+    }
     when {
+        isVideo -> VideoPlayerView(
+            modifier = modifier,
+            src = src,
+            alignment = alignment,
+            alpha = alpha,
+            contentDescription = contentDescription,
+            colorFilter = colorFilter,
+            contentScale = contentScale
+        )
+
         src is ImageVector -> ComposeImage(
             modifier = modifier,
             imageVector = src,
@@ -43,6 +61,7 @@ fun ImageAny(
             contentDescription = contentDescription,
             contentScale = contentScale,
             filterQuality = filterQuality,
+            imageLoader = api.imageLoader ?: asyncImageLoader(),
             onError = {
                 onFail(it.result.throwable)
             },
@@ -52,5 +71,3 @@ fun ImageAny(
         )
     }
 }
-
-

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/components/menu/ContextMenu.kt

@@ -20,8 +20,8 @@ fun ContextMenu(
     backgroundColor: Color = Color.SuperDarkGray,
     textColor: Color = Color.White,
     contextMenuState: ContextMenuState,
-    onShow: () -> Unit = {},
-    onHide: () -> Unit = {},
+//    onShow: () -> Unit = {},
+//    onHide: () -> Unit = {},
     onMenuItemClick: (item: String) -> Unit = {}
 ) = listOf(
     contextMenuState.menuRenderCount,

+ 176 - 0
src/main/kotlin/eu/mjdev/desktop/components/video/VideoPlayerDirect.kt

@@ -0,0 +1,176 @@
+package eu.mjdev.desktop.components.video
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asComposeImageBitmap
+import androidx.compose.ui.graphics.drawscope.scale
+import org.jetbrains.skia.Bitmap
+import uk.co.caprica.vlcj.factory.discovery.NativeDiscovery
+import uk.co.caprica.vlcj.player.base.MediaPlayer
+import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter
+import uk.co.caprica.vlcj.player.base.State
+import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
+import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormat
+import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormatCallback
+import uk.co.caprica.vlcj.player.embedded.videosurface.callback.RenderCallback
+import uk.co.caprica.vlcj.player.embedded.videosurface.callback.format.RV32BufferFormat
+import java.lang.Float.min
+import java.nio.ByteBuffer
+
+@Composable
+fun VideoPlayerDirect(
+    modifier: Modifier = Modifier,
+    state: VlcjVideoPlayerState = remember { VlcjVideoPlayerState() },
+    url: String
+) {
+    var frameCounter = 0L
+    var lastTs = 0L
+    NativeDiscovery().discover()
+    DisposableEffect(state) {
+        val eventListener = object : MediaPlayerEventAdapter() {
+            override fun playing(mediaPlayer: MediaPlayer) {
+                println("Playing: ${mediaPlayer.media().info().textTracks()}")
+            }
+
+            override fun timeChanged(mediaPlayer: MediaPlayer, newTime: Long) {
+                state.time = newTime
+            }
+
+            override fun positionChanged(mediaPlayer: MediaPlayer, newPosition: Float) {
+                //println("Position: $newPosition")
+                state.progress = newPosition
+            }
+        }
+        state.mediaPlayer.events().addMediaPlayerEventListener(eventListener)
+        onDispose {
+            state.mediaPlayer.events().removeMediaPlayerEventListener(eventListener)
+            state.mediaPlayer.release()
+        }
+    }
+    var frameTime: Long by remember { mutableStateOf(0L) }
+    LaunchedEffect(url) {
+        println("Sideffect started")
+        state.mediaPlayer.media()?.start(url)
+        state.mediaPlayer.subpictures().setTrack(-1)
+        while (true) {
+            withFrameMillis {
+                frameTime = it
+            }
+        }
+    }
+    return Canvas(modifier) {
+        state.internalState.updateComposeImage(frameTime)?.let { image ->
+            val scaleW = size.width / image.width.toFloat()
+            val scaleH = size.height / image.height.toFloat()
+            //val frame = renderState.frame // needed to enforce high fps redrawing
+            //val fts = frameTime
+            scale(scale = min(scaleH, scaleW), pivot = Offset.Zero) {
+                drawImage(image)
+            }
+        }
+        frameCounter++
+        if (frameCounter % 60L == 0L) {
+            val time = System.currentTimeMillis()
+            val delta = time - lastTs
+            println("FPS: ${1000 / (delta / 60)}")
+            lastTs = time
+        }
+    }
+}
+
+interface VideoPlayerState {
+    val time: Long //Timestamp milliseconds
+    val progress: Float //from 0 to 1 -> 0 is start and 1 is end
+    val aspectRatio: Float
+    fun seek(position: Float)
+    fun togglePause()
+}
+
+class VlcjVideoPlayerState : VideoPlayerState {
+    internal val internalState = RenderState()
+    override var time: Long = 0L
+        internal set
+    override var progress: Float by mutableStateOf(0f)
+        internal set
+    val mediaPlayer: MediaPlayer get() = internalState.mediaPlayerComponent.mediaPlayer()
+    override val aspectRatio: Float get() = internalState.aspectRatio
+
+    override fun seek(position: Float) {
+        mediaPlayer.controls().setPosition(position)
+    }
+
+    override fun togglePause() {
+        val state = mediaPlayer.media().info().state()
+        if (state == State.PLAYING) {
+            mediaPlayer.controls().pause()
+        } else {
+            mediaPlayer.controls().play()
+        }
+    }
+
+    init {
+        println("New VideoPlayerState")
+    }
+}
+
+internal class RenderState {
+    var aspectRatio: Float by mutableStateOf(1f)
+        private set
+
+    var currentBuffer: ByteBuffer? = null
+
+    private var buffer: ByteArray = ByteArray(0)
+    private var bufferBitmap: Bitmap = Bitmap()
+    private var composeImage: ImageBitmap? = null
+
+    init {
+        println("New RenderState")
+    }
+
+    fun updateComposeImage(frameTime: Long): ImageBitmap? {
+        currentBuffer?.let {
+            it.get(buffer)
+            it.rewind()
+            bufferBitmap.installPixels(buffer)
+            return composeImage
+        }
+        return null
+    }
+
+    private val bufferFormatCallback = object : BufferFormatCallback {
+        override fun getBufferFormat(sourceWidth: Int, sourceHeight: Int): BufferFormat {
+            println("Size: $sourceWidth x $sourceHeight")
+            bufferBitmap = Bitmap().also {
+                it.allocN32Pixels(sourceWidth, sourceHeight, true)
+            }
+            composeImage = bufferBitmap.asComposeImageBitmap()
+            buffer = ByteArray(sourceWidth * sourceHeight * 4)
+            //renderCallback.setBuffer(IntArray(sourceWidth * sourceHeight * 4))
+            with(bufferBitmap) {
+                aspectRatio = if (width <= 0 || height <= 0) 1f else width.toFloat() / height.toFloat()
+                println("Set aspectRatio to: $aspectRatio")
+            }
+            return RV32BufferFormat(sourceWidth, sourceHeight)
+        }
+
+        override fun allocatedBuffers(buffers: Array<out ByteBuffer>) {
+            currentBuffer = buffers.firstOrNull()
+        }
+    }
+    private val renderCallback = RenderCallback { _, nativeBuffers, _ ->
+        //currentBuffer = nativeBuffers.first()
+    }
+    val mediaPlayerComponent = CallbackMediaPlayerComponent(
+        null,
+        null,
+        null,
+        true,
+        null,
+        renderCallback,
+        bufferFormatCallback,
+        null
+    )
+}

+ 46 - 0
src/main/kotlin/eu/mjdev/desktop/components/video/VideoPlayerView.kt

@@ -0,0 +1,46 @@
+package eu.mjdev.desktop.components.video
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.layout.ContentScale
+import java.io.File
+
+@Suppress("UNUSED_PARAMETER")
+@Composable
+fun VideoPlayerView(
+    modifier: Modifier = Modifier,
+    src: Any?,
+    alignment: Alignment,
+    alpha: Float,
+    contentDescription: String?,
+    colorFilter: ColorFilter?,
+    contentScale: ContentScale,
+    onFail: (Throwable) -> Unit = { e -> e.printStackTrace() }
+) = Box(
+    modifier = modifier,
+    contentAlignment = Alignment.Center
+) {
+    val mediaUrl = when (src) {
+        is String -> src
+        is File -> src.absolutePath
+        else -> src.toString()
+    }
+    VideoPlayerDirect(
+        modifier = Modifier.alpha(alpha).align(alignment),
+        url = mediaUrl
+    )
+//    runCatching {
+//        DesktopWebView(
+//            modifier = Modifier.alpha(alpha).align(alignment),
+//            url = mediaUrl
+//        ) {
+//            println("Loading Video: $it")
+//        }
+//    }.onFailure { e->
+//        onFail(e)
+//    }
+}

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/extensions/Compose.kt

@@ -100,7 +100,7 @@ object Compose {
         .build()
 
     @Composable
-    fun AsyncImageLoader(
+    fun asyncImageLoader(
         imageLoaderContext: PlatformContext = imageLoaderContext(),
         imageLoaderMemoryCache: MemoryCache = imageLoaderMemoryCache()
     ) = ImageLoader.Builder(imageLoaderContext)

+ 3 - 1
src/main/kotlin/eu/mjdev/desktop/provider/DesktopProvider.kt

@@ -5,6 +5,7 @@ import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
+import coil3.ImageLoader
 import eu.mjdev.desktop.components.controlcenter.ControlCenterPage
 import eu.mjdev.desktop.components.controlcenter.pages.*
 import eu.mjdev.desktop.helpers.ResourceStream
@@ -21,7 +22,8 @@ import javax.script.ScriptEngineManager
 
 @Suppress("unused", "MemberVisibilityCanBePrivate", "PrivatePropertyName", "SameParameterValue")
 class DesktopProvider(
-    private val scope: CoroutineScope? = null
+    val scope: CoroutineScope? = null,
+    val imageLoader: ImageLoader? = null
 ) {
     private val __currentUser: User by lazy { User.load() }
     private val _currentUser: MutableState<User> = mutableStateOf(__currentUser)

+ 2 - 0
src/main/kotlin/eu/mjdev/desktop/provider/providers/AppsProvider.kt

@@ -26,6 +26,8 @@ class AppsProvider(
     private val allAppsDesktopFilesDir by lazy { localShareDir?.resolve("applications") }
     private val autostartDesktopFilesDir by lazy { configDir?.resolve("autoStart") }
     private val backgroundFilesDir by lazy { localShareDir?.resolve("backgrounds") }
+    // todo
+//    private val userBackgroundFilesDir by lazy { homeDir?.resolve("pictures") }
 
     private val menusDir by lazy { configDir?.resolve("menus") }
 

+ 3 - 3
src/main/kotlin/eu/mjdev/desktop/windows/MainWindow.kt

@@ -31,7 +31,7 @@ fun MainWindow(
     menuState: VisibilityState,
     api: DesktopProvider = LocalDesktop.current,
     palette: Palette = rememberPalette(api.currentUser.theme.backgroundColor),
-    onDesktopMenuShow: () -> Unit = {},
+//    onDesktopMenuShow: () -> Unit = {},
     contextMenuState: ContextMenuState = rememberContextMenuState("item1", "item2"),
     content: @Composable BoxScope.() -> Unit = {}
 ) = FullScreenWindow {
@@ -72,8 +72,8 @@ fun MainWindow(
     ContextMenu(
         contextMenuState = contextMenuState,
         onMenuItemClick = { item -> log { "Context menu : $item" } },
-        onShow = { onDesktopMenuShow() },
-        onHide = {}
+//        onShow = { onDesktopMenuShow() },
+//        onHide = {}
     )
 //    launchedEffect {
 //        runCatching {