Milan Jurkulak 1 yıl önce
ebeveyn
işleme
3fe82a3f99
35 değiştirilmiş dosya ile 784 ekleme ve 439 silme
  1. 1 1
      .intellijPlatform/self-update.lock
  2. 20 0
      composeApp/composeApp.gradle.kts
  3. 2 2
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/components/video/VideoPlayer.kt
  4. 2 2
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/context/DesktopContext.kt
  5. 0 8
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/extensions/ImageBitmapExt.kt
  6. 3 3
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/AppsMenu.kt
  7. 27 19
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/background/BackgroundImage.kt
  8. 132 0
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/image/GifView.kt
  9. 74 58
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/image/ImageAny.kt
  10. 3 3
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/sliding/base/VisibilityState.kt
  11. 5 5
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/video/VideoView.kt
  12. 1 1
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/data/DesktopConfig.kt
  13. 66 8
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/extensions/ImageBitmapExt.kt
  14. 1 14
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/extensions/LaunchedEffect.kt
  15. 41 0
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/extensions/MimeTypeExt.kt
  16. 0 3
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/helpers/bitmap/Bitmap.kt
  17. 227 194
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/helpers/gif/GifDecoder.kt
  18. 2 2
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/helpers/gif/GifFrame.kt
  19. 50 0
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/helpers/gif/GifImage.kt
  20. 4 0
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/log/Log.kt
  21. 2 1
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/managers/ai/AiManager.kt
  22. 4 4
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/managers/ai/plugins/AiPluginGemini.kt
  23. 1 0
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/managers/ai/plugins/AiPluginOllama.kt
  24. 71 0
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/managers/ai/plugins/AiPluginOpenAi.kt
  25. 10 0
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/managers/ai/plugins/AiPluginPerplexity.kt
  26. 5 9
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/managers/palette/Palette.kt
  27. 5 5
      composeApp/src/desktopMain/kotlin/org/mjdev/desktop/components/appsmenu/AppsMenuWindow.kt
  28. 1 1
      composeApp/src/desktopMain/kotlin/org/mjdev/desktop/components/controlcenter/ControlCenterWindow.kt
  29. 1 1
      composeApp/src/desktopMain/kotlin/org/mjdev/desktop/components/desktoppanel/DesktopPanelWindow.kt
  30. 0 59
      composeApp/src/desktopMain/kotlin/org/mjdev/desktop/components/image/GifView.kt
  31. 6 6
      composeApp/src/desktopMain/kotlin/org/mjdev/desktop/components/main/MainWindow.kt
  32. 2 1
      composeApp/src/desktopMain/kotlin/org/mjdev/desktop/context/DeskptopContext.kt
  33. 1 15
      composeApp/src/desktopMain/kotlin/org/mjdev/desktop/extensions/ImageBitmapExt.kt
  34. 9 9
      composeApp/src/desktopMain/kotlin/org/mjdev/desktop/windows/ChromeWindowState.kt
  35. 5 5
      composeApp/src/desktopMain/kotlin/org/mjdev/desktop/windows/Window.kt

+ 1 - 1
.intellijPlatform/self-update.lock

@@ -1 +1 @@
-2025-06-10
+2025-06-12

+ 20 - 0
composeApp/composeApp.gradle.kts

@@ -184,6 +184,26 @@ kotlin {
                 implementation(libs.gettext.lib)
                 // timeline
                 implementation(libs.jetlime)
+                // html to text
+                implementation("com.github.BartoszJarocki:android-boilerpipe:-SNAPSHOT")
+                // markdown
+                implementation("io.noties.markwon:core:4.6.2")
+                // open ai
+                implementation (platform("com.aallam.openai:openai-client-bom:3.2.0"))
+                implementation ("com.aallam.openai:openai-client")
+                // ktor
+                implementation("io.ktor:ktor-client-core:2.3.9")
+                implementation("io.ktor:ktor-client-cio:2.3.9")
+                implementation("io.ktor:ktor-client-content-negotiation:2.3.9")
+                implementation("io.ktor:ktor-client-logging:2.3.9")
+                implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.9")
+                // okhttp
+                implementation("com.squareup.okhttp3:okhttp:4.12.0")
+                implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
+                implementation("com.squareup.okhttp3:okhttp-urlconnection:4.12.0")
+                implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0")
+                implementation("com.squareup.okhttp3:okhttp-tls:4.12.0")
+//                implementation("com.squareup.okhttp3:okhttp-ws:4.12.0")
                 // notification
                 // implementation("io.github.shadmanadman:knotif:0.56.0")
                 // sensors

+ 2 - 2
composeApp/src/androidMain/kotlin/org/mjdev/desktop/components/video/VideoPlayer.kt

@@ -14,6 +14,7 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.core.net.toUri
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
 
 @Composable
 actual fun VideoPlayer(
@@ -21,7 +22,7 @@ actual fun VideoPlayer(
     videoInfo: VideoInfo,
     state: VideoPlayerState,
     modifier: Modifier,
-) withDesktopContext {
+) = withDesktopContext {
     val time = rememberSaveable(mrl) { mutableIntStateOf(-1) }
     var videoRatio by rememberSaveable(mrl) { mutableFloatStateOf(-1f) }
     Box(modifier = modifier) {
@@ -41,7 +42,6 @@ actual fun VideoPlayer(
                                 mediaPlayer.setDataSource(context, mrl.toUri())
                                 mediaPlayer.setSurface(surface)
                                 mediaPlayer.prepareAsync()
-                                // Play video when the media source is ready for playback.
                                 mediaPlayer.setOnPreparedListener { mediaPlayer ->
                                     if (time.intValue != -1) {
                                         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

+ 2 - 2
composeApp/src/androidMain/kotlin/org/mjdev/desktop/context/DesktopContext.kt

@@ -16,7 +16,6 @@ import org.mjdev.desktop.data.Category
 import org.mjdev.desktop.data.User
 import org.mjdev.desktop.extensions.Compose.isDesign
 import org.mjdev.desktop.helpers.image.ImageLoader.asyncImageLoader
-import org.mjdev.desktop.helpers.persistence.StorageProvider
 import org.mjdev.desktop.interfaces.IApp
 import org.mjdev.desktop.managers.apps.IAppsManager
 import org.mjdev.desktop.managers.connectivity.IConnectivityManager
@@ -33,6 +32,7 @@ import org.mjdev.desktop.managers.os.IOSManager
 import org.mjdev.desktop.managers.theme.IThemeManager
 import org.mjdev.desktop.managers.translations.ITranslator
 import org.mjdev.desktop.managers.ai.plugins.AiPluginGemini
+import org.mjdev.desktop.managers.ai.plugins.AiPluginOpenAi
 import org.mjdev.desktop.managers.ai.stt.STTPluginEmpty
 import org.mjdev.desktop.managers.ai.tts.TTSPluginAndroid
 import org.mjdev.desktop.managers.base.IDelegate
@@ -133,7 +133,7 @@ class DesktopContext(
         IAiManager::class -> AiManager(
             context = this,
             // todo user can configure
-            pluginAI = AiPluginGemini(this@DesktopContext),
+            pluginAI = AiPluginOpenAi(this@DesktopContext),
             pluginTTS = TTSPluginAndroid(this@DesktopContext),
             pluginSTT = STTPluginEmpty(this@DesktopContext)
         )

+ 0 - 8
composeApp/src/androidMain/kotlin/org/mjdev/desktop/extensions/ImageBitmapExt.kt

@@ -11,14 +11,6 @@ import org.mjdev.desktop.context.IDesktopContext
 
 @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
 actual object ImageBitmapExt {
-
-    actual fun ImageBitmap.cut(
-        x: Int,
-        y: Int,
-        width: Int,
-        height: Int
-    ): ImageBitmap = Bitmap.createBitmap(asAndroidBitmap(), x, y, width, height).asImageBitmap()
-
     actual suspend fun IDesktopContext.loadPicture(
         src: Any?
     ): ImageBitmap? = runCatching {

+ 3 - 3
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/AppsMenu.kt

@@ -18,6 +18,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.HorizontalDivider
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -51,7 +52,6 @@ import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContex
 import org.mjdev.desktop.extensions.Compose.sortByRelevance
 import org.mjdev.desktop.extensions.Compose.trimIsNotEmpty
 import org.mjdev.desktop.extensions.LaunchedEffect.flowBlock
-import org.mjdev.desktop.extensions.LaunchedEffect.launchedEffect
 import org.mjdev.desktop.helpers.animation.Animations.AppsMenuEnterAnimation
 import org.mjdev.desktop.helpers.animation.Animations.AppsMenuExitAnimation
 import org.mjdev.desktop.interfaces.IApp
@@ -231,8 +231,8 @@ fun AppsMenu(
             }
         }
     }
-    launchedEffect(appsMenuState.isVisible) { isVisible ->
-        if (!isVisible) {
+    LaunchedEffect(appsMenuState.isVisible) {
+        if (!appsMenuState.isVisible) {
             appsMenuState.categoryState.value = null
         }
     }

+ 27 - 19
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/background/BackgroundImage.kt

@@ -3,6 +3,7 @@ package org.mjdev.desktop.components.background
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -13,7 +14,6 @@ import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color.Companion.Transparent
 import androidx.compose.ui.layout.ContentScale
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.delay
 import org.mjdev.desktop.components.image.ImageAny
 import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
@@ -22,38 +22,35 @@ import org.mjdev.desktop.extensions.Colors.SuperDarkGray
 import org.mjdev.desktop.extensions.Colors.alpha
 import org.mjdev.desktop.extensions.Compose.Crossfade
 import org.mjdev.desktop.extensions.Compose.preview
-import org.mjdev.desktop.extensions.LaunchedEffect.launch
-import org.mjdev.desktop.extensions.LaunchedEffect.launchedEffect
 import org.mjdev.desktop.extensions.MutableStateExt.rememberComputed
 import org.mjdev.desktop.helpers.generic.Queue
 import kotlin.math.min
 import org.jetbrains.compose.ui.tooling.preview.Preview
+import org.mjdev.desktop.log.Log
 
-@Suppress("FunctionName")
+@Suppress("FunctionName", "CoroutineCreationDuringComposition")
 @Composable
 fun BackgroundImage(
     modifier: Modifier = Modifier,
     switchDelay: Long = 60000L,
+    fadeInDuration: Long = 60000L / 8,
+    fadeOutDuration: Long = 60000L / 4,
     backgroundColor: Color = Color.SuperDarkGray,
     images: MutableList<Any> = mutableListOf(),
-    onError: (Throwable) -> Unit = { e -> e.printStackTrace() },
+    onError: (Throwable) -> Unit = { e -> Log.e(e) },
     onChange: (bck: Any?) -> Unit = {}
 ) = withDesktopContext {
     Box(
         modifier = modifier.background(backgroundColor)
     ) {
-        val backgroundQueue by rememberComputed(images, images.size) { Queue(images) }
-        val fadeInDuration by rememberComputed(switchDelay) { switchDelay / 8 }
-        val fadeOutDuration by rememberComputed(switchDelay) { switchDelay / 4 }
-        var currentBackground by remember { mutableStateOf(backgroundQueue.nextOrNull()) }
+        val imagesSize by rememberComputed(images.size) { images.size }
+        val backgroundQueue by rememberComputed(imagesSize) { Queue(images) }
+        var currentBackground: Any by remember { mutableStateOf(backgroundColor) }
         Crossfade(
-            targetState = currentBackground ?: Color.SuperDarkGray,
+            targetState = currentBackground,
             fadeInDuration = fadeInDuration,
             fadeOutDuration = fadeOutDuration
         ) { imageValue ->
-            launch(Dispatchers.Default) {
-                onChange(currentBackground)
-            }
             ImageAny(
                 modifier = modifier,
                 contentScale = ContentScale.Crop,
@@ -61,8 +58,14 @@ fun BackgroundImage(
                 src = imageValue,
                 onFail = onError,
                 colorFilter = ContrastColorFilter(1.5f),
+                onLoading = {
+                    Log.d("Background image loading: $imageValue")
+                },
+                onLoaded = { duration ->
+                    Log.d("Background image loaded: $imageValue, duration: $duration")
+                },
                 onAnimationFinish = {
-                    currentBackground = backgroundQueue.nextOrNull()
+                    Log.d("Background image finished: $imageValue")
                 }
             )
             Box(
@@ -89,11 +92,16 @@ fun BackgroundImage(
                 )
             )
         }
-        launchedEffect(currentBackground) {
-            if (switchDelay > 0) {
-                delay(switchDelay)
-                currentBackground = backgroundQueue.nextOrNull()
-            }
+        LaunchedEffect(imagesSize) {
+            currentBackground = backgroundQueue.nextOrNull() ?: backgroundColor
+        }
+        // todo duration from movie or gif
+        LaunchedEffect(currentBackground) {
+            delay(switchDelay)
+            currentBackground = backgroundQueue.nextOrNull() ?: backgroundColor
+        }
+        LaunchedEffect(currentBackground) {
+            onChange(currentBackground)
         }
     }
 }

+ 132 - 0
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/image/GifView.kt

@@ -0,0 +1,132 @@
+package org.mjdev.desktop.components.image
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.unit.IntSize
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.jetbrains.compose.ui.tooling.preview.Preview
+import org.mjdev.desktop.extensions.Compose.preview
+import org.mjdev.desktop.helpers.gif.GifDecoder
+import org.mjdev.desktop.components.image.GifViewState.Companion.rememberGifViewState
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
+import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.context.LocalDesktopContext
+import org.mjdev.desktop.log.Log
+import kotlin.math.absoluteValue
+
+@Suppress("FunctionName")
+@Composable
+fun GifView(
+    modifier: Modifier = Modifier,
+    src: String = "",
+    state: GifViewState = rememberGifViewState(src),
+    onAnimationFinish: () -> Unit = {},
+    onLoading: () -> Unit = { Log.d("Loading GIF: ${state.src}") },
+    onLoaded: (duration: Long) -> Unit = { Log.d("Loading GIF: ${state.src} is loaded.") },
+    onFail: (error: Throwable) -> Unit = { e -> Log.e("Failed to load GIF: ${state.src}", e) }
+) = withDesktopContext {
+    BoxWithConstraints {
+        Canvas(modifier = modifier) {
+            drawImage(
+                image = state.currentImage ?: ImageBitmap(1, 1),
+                srcSize = IntSize(state.currentImage?.width ?: 0, state.currentImage?.height ?: 0),
+                dstSize = IntSize(constraints.maxWidth, constraints.maxHeight),
+            )
+        }
+        LaunchedEffect(state.currentFrame) {
+//            if(state.isLoading) {
+//                onLoading()
+//            }
+            if (state.currentFrame == 0) {
+                onLoaded(state.duration)
+            }
+            if (state.currentFrame > -1) {
+                delay(state.currentFrameDelay)
+            }
+            if (state.hasNextFrame) {
+                state.nextFrame()
+            } else {
+                state.reset()
+            }
+        }
+    }
+}
+
+@Suppress("RedundantSuspendModifier", "unused", "MemberVisibilityCanBePrivate")
+class GifViewState {
+    var src: String = ""
+        private set
+    var gifDecoder: GifDecoder? = null
+
+    var isLoading by mutableStateOf(true)
+        internal set
+    var currentFrame by mutableIntStateOf(-1)
+        internal set
+    val currentImage: ImageBitmap?
+        get() = gifDecoder?.getFrame(currentFrame)
+    val currentFrameDelay: Long
+        get() = gifDecoder?.getDelay(currentFrame) ?: 0
+    val framesCount
+        get() = gifDecoder?.getFrameCount() ?: 0
+    val hasNextFrame: Boolean
+        get() = currentFrame < framesCount - 1
+    val duration: Long
+        get() = gifDecoder?.getDuration() ?: 0
+
+    suspend fun nextFrame() {
+        if (isLoading || framesCount == 0) return
+        var nextFrame = currentFrame + 1
+        if (nextFrame >= framesCount) nextFrame = 0
+        currentFrame = nextFrame
+    }
+
+    suspend fun prevFrame() {
+        if (isLoading || framesCount == 0) return
+        var prevFrame = currentFrame - 1
+        if (prevFrame < 0) prevFrame = 0
+        currentFrame = prevFrame
+    }
+
+    suspend fun reset() {
+        if (isLoading || framesCount == 0) return
+        currentFrame = 0
+    }
+
+    suspend fun load(src: String) {
+        isLoading = true
+        this.src = src
+        gifDecoder = GifDecoder().apply { from(src) }
+        while (gifDecoder!!.getFrameCount() == 0) {
+            delay(100L)
+        }
+        isLoading = false
+        currentFrame = 0
+    }
+
+    companion object {
+        @Composable
+        fun rememberGifViewState(
+            src: String,
+            context: IDesktopContext = LocalDesktopContext.current,
+            scope: CoroutineScope = context.scope
+        ): GifViewState = remember(src) {
+            GifViewState().apply {
+                scope.launch {
+                    load(src)
+                }
+            }
+        }
+    }
+}
+
+@Suppress("unused")
+@Preview
+@Composable
+fun PreviewGifView() = preview {
+    GifView()
+}

+ 74 - 58
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/image/ImageAny.kt

@@ -31,57 +31,62 @@ import org.mjdev.desktop.icons.image.BrokenImage
 import androidx.compose.foundation.Image as ComposeImage
 import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.video.VideoView
+import org.mjdev.desktop.extensions.Compose.runAsync
 import org.mjdev.desktop.extensions.PathExt.absolutePath
+import org.mjdev.desktop.extensions.MimeTypeExt.isGif
+import org.mjdev.desktop.extensions.MimeTypeExt.isVideo
 
-@Suppress("FunctionName", "UNUSED_PARAMETER")
+@Suppress("FunctionName", "UNUSED_PARAMETER", "ModifierParameter")
 @Composable
 fun ImageAny(
     src: Any? = null,
-    contentDescription: String? = "",
     modifier: Modifier = Modifier,
+    contentDescription: String? = "",
     alignment: Alignment = Alignment.Center,
     contentScale: ContentScale = ContentScale.Fit,
     alpha: Float = DefaultAlpha,
     colorFilter: ColorFilter? = null,
     filterQuality: FilterQuality = DefaultFilterQuality,
-    // todo
-    imageLoader: ImageLoader? = asyncImageLoader(),
+    imageLoader: ImageLoader? = asyncImageLoader(),// todo?
     placeholder: @Composable () -> Unit = {}, // todo
     onLoading: () -> Unit = {},
+    onLoaded: (duration: Long) -> Unit = {},
     onFail: (error: Throwable) -> Unit = {},
     onAnimationFinish: () -> Unit = {}
 ) {
-    // todo from mime
-    val isGif = src.toString().trim().endsWith(".gif")
-    // todo from mime
-    val isVideo = src.toString().trim().let {
-        it.endsWith(".mp4") || it.endsWith(".mpg")
-    }
     when {
-        src == null -> ComposeImage(
-            modifier = modifier,
-            imageVector = BrokenImage,
-            alignment = alignment,
-            alpha = alpha,
-            contentDescription = contentDescription,
-            colorFilter = colorFilter,
-            contentScale = contentScale
-        )
+        src == null -> {
+            runAsync { onLoaded(0L) }
+            ComposeImage(
+                modifier = modifier,
+                imageVector = BrokenImage,
+                alignment = alignment,
+                alpha = alpha,
+                contentDescription = contentDescription,
+                colorFilter = colorFilter,
+                contentScale = contentScale
+            )
+        }
 
-        src is Color -> Box(
-            modifier = modifier.background(src)
-        )
-
-        src is Painter -> ComposeImage(
-            modifier = modifier,
-            painter = src,
-            alignment = alignment,
-            alpha = alpha,
-            contentDescription = contentDescription,
-            colorFilter = colorFilter,
-            contentScale = contentScale
-        )
+        src is Color -> {
+            runAsync { onLoaded(0L) }
+            Box(
+                modifier = modifier.background(src)
+            )
+        }
 
+        src is Painter -> {
+            runAsync { onLoaded(0L) }
+            ComposeImage(
+                modifier = modifier,
+                painter = src,
+                alignment = alignment,
+                alpha = alpha,
+                contentDescription = contentDescription,
+                colorFilter = colorFilter,
+                contentScale = contentScale
+            )
+        }
 //        src is Bitmap -> ComposeImage(
 //            modifier = modifier,
 //            bitmap = src.asComposeImageBitmap(),
@@ -92,42 +97,52 @@ fun ImageAny(
 //            contentScale = contentScale
 //        )
 
-        src is ImageBitmap -> ComposeImage(
-            modifier = modifier,
-            bitmap = src,
-            alignment = alignment,
-            alpha = alpha,
-            contentDescription = contentDescription,
-            colorFilter = colorFilter,
-            contentScale = contentScale
-        )
+        src is ImageBitmap -> {
+            runAsync { onLoaded(0L) }
+            ComposeImage(
+                modifier = modifier,
+                bitmap = src,
+                alignment = alignment,
+                alpha = alpha,
+                contentDescription = contentDescription,
+                colorFilter = colorFilter,
+                contentScale = contentScale
+            )
+        }
+
+        src is ImageVector -> {
+            runAsync { onLoaded(0L) }
+            ComposeImage(
+                modifier = modifier,
+                imageVector = src,
+                alignment = alignment,
+                alpha = alpha,
+                contentDescription = contentDescription,
+                colorFilter = colorFilter,
+                contentScale = contentScale
+            )
+        }
 
-        src is ImageVector -> ComposeImage(
+        src.isGif -> GifView(
             modifier = modifier,
-            imageVector = src,
-            alignment = alignment,
-            alpha = alpha,
-            contentDescription = contentDescription,
-            colorFilter = colorFilter,
-            contentScale = contentScale
+            src = when {
+                src is Path -> src.absolutePath
+                else -> src.toString()
+            },
+            onLoading = onLoading,
+            onLoaded = onLoaded,
+            onAnimationFinish = onAnimationFinish,
+            onFail = onFail
         )
 
-//        isGif -> GifView(
-//            modifier = modifier,
-//            src = when {
-//                src is File -> src.absolutePath
-//                else -> src.toString()
-//            },
-//            onAnimationFinish = onAnimationFinish
-//        )
-
-        isVideo -> VideoView (
+        src.isVideo -> VideoView(
             modifier = modifier,
             src = when {
                 src is Path -> src.absolutePath
                 else -> src.toString()
             },
             onLoading = { onLoading() },
+            onLoaded = onLoaded,
             onFail = onFail
         )
 
@@ -139,6 +154,7 @@ fun ImageAny(
             filterQuality = filterQuality,
             imageLoader = imageLoader ?: asyncImageLoader(),
             onLoading = { onLoading() },
+            onSuccess = { onLoaded(0L) },
             onError = { e -> onFail(e.result.throwable) },
         )
     }

+ 3 - 3
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/sliding/base/VisibilityState.kt

@@ -53,14 +53,14 @@ open class VisibilityState(
     }
 
     open suspend fun show() {
-        println("Show called.")
+//        println("Show called.")
         hideJob?.cancel()
         visibleState.value = true
     }
 
     open suspend fun hide(force: Boolean = false) {
         if (enabledState.value || force) {
-            println("Hide called.")
+//            println("Hide called.")
             hideJob?.cancel()
             if (hideDelay == 0L) {
                 visibleState.value = false
@@ -85,7 +85,7 @@ open class VisibilityState(
     }
 
     open suspend fun updateSize(size: DpSize) {
-        println("Size update: $size")
+//        println("Size update: $size")
         this.sizeState.value = size
     }
 

+ 5 - 5
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/video/VideoView.kt

@@ -25,19 +25,19 @@ fun VideoView(
     state: VideoPlayerState = rememberVideoPlayerState(),
     onFail: (Throwable) -> Unit = { e -> Log.e(e) },
     onLoading: () -> Unit = {},
-    onVideoFinish: () -> Unit = {}
+    onVideoFinish: () -> Unit = {},
+    onLoaded: (duration: Long) -> Unit = {}
 ) = withDesktopContext {
     BoxWithConstraints(
         modifier = modifier,
         contentAlignment = alignment
     ) {
         val url = remember(src) {
-            src.toString().let {
-                if (it.startsWith("http://") || it.startsWith("https://")) it
-                else "file://$it"
+            src.toString().let { s ->
+                if (s.startsWith("http://") || s.startsWith("https://")) s
+                else "file://$s"
             }
         }
-        println("Playing: $url")
         VideoPlayer(
             modifier = Modifier.fillMaxSize(),
             mrl = url,

+ 1 - 1
composeApp/src/commonMain/kotlin/org/mjdev/desktop/data/DesktopConfig.kt

@@ -55,7 +55,7 @@ class DesktopConfig(
 
         private val DEFAULT = DesktopConfig {
 //            addBackground(ProviderSmug())
-            addBackground(ProviderErzvo())
+//            addBackground(ProviderErzvo())
         }
 
         // todo load from user settings

+ 66 - 8
composeApp/src/commonMain/kotlin/org/mjdev/desktop/extensions/ImageBitmapExt.kt

@@ -1,21 +1,79 @@
 package org.mjdev.desktop.extensions
 
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.Paint
 import org.mjdev.desktop.context.IDesktopContext
 
 @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
 expect object ImageBitmapExt {
-
-    fun ImageBitmap.cut(
-        x: Int,
-        y: Int,
-        width: Int,
-        height: Int
-    ): ImageBitmap
-
     suspend fun IDesktopContext.loadPicture(
         src: Any?
     ): ImageBitmap?
 }
 
+var ImageBitmap.pixels: IntArray
+    get() = IntArray(width * height).also { pixels -> readPixels(pixels) }
+    set(value) = writePixels(value, 0, 0, width, height)
+
+fun ImageBitmap.fillRect(
+    x: Int,
+    y: Int,
+    width: Int,
+    height: Int,
+    c: Color
+) {
+    Canvas(this).drawRect(
+        x.toFloat(),
+        y.toFloat(),
+        width.toFloat(),
+        height.toFloat(),
+        Paint().apply { color = c }
+    )
+}
 
+fun ImageBitmap.writePixels(
+    data: IntArray,
+    x: Int,
+    y: Int,
+    width: Int,
+    height: Int
+) {
+    val paint = Paint()
+    val canvas = Canvas(this)
+    var index = 0
+    for (j in 0 until height) {
+        for (i in 0 until width) {
+            val colorInt = data[index++]
+            paint.color = Color(colorInt)
+            canvas.drawRect(
+                (x + i).toFloat(),
+                (y + j).toFloat(),
+                (x + i + 1).toFloat(),
+                (y + j + 1).toFloat(),
+                paint
+            )
+        }
+    }
+}
+
+fun ImageBitmap.cut(
+    x: Int,
+    y: Int,
+    width: Int,
+    height: Int
+): ImageBitmap {
+    val srcPixels = this.pixels
+    val result = ImageBitmap(width, height)
+    val subPixels = IntArray(width * height)
+    for (j in 0 until height) {
+        for (i in 0 until width) {
+            val srcIndex = (y + j) * this.width + (x + i)
+            val dstIndex = j * width + i
+            subPixels[dstIndex] = srcPixels.getOrElse(srcIndex) { 0 }
+        }
+    }
+    result.pixels = subPixels
+    return result
+}

+ 1 - 14
composeApp/src/commonMain/kotlin/org/mjdev/desktop/extensions/LaunchedEffect.kt

@@ -29,23 +29,10 @@ object LaunchedEffect {
     ) = scope.launch(context, start, block)
 
     @Composable
-    fun launchedEffect(
+    fun LaunchedEffect(
         block: suspend CoroutineScope.() -> Unit
     ) = LaunchedEffect(Unit, block)
 
-    @Composable
-    fun <T> launchedEffect(
-        key: T,
-        block: suspend CoroutineScope.(key: T) -> Unit
-    ) = LaunchedEffect(key) { block(key) }
-
-    @Composable
-    fun <T, E> launchedEffect(
-        key1: T,
-        key2: E,
-        block: suspend CoroutineScope.(key1: T, key2: E) -> Unit
-    ) = LaunchedEffect(key1, key2) { block(key1, key2) }
-
     fun runAsync(
         context: CoroutineContext = Dispatchers.Main,
         block: suspend () -> Unit

+ 41 - 0
composeApp/src/commonMain/kotlin/org/mjdev/desktop/extensions/MimeTypeExt.kt

@@ -0,0 +1,41 @@
+package org.mjdev.desktop.extensions
+
+import okio.Path
+import org.mjdev.desktop.extensions.PathExt.absolutePath
+import org.mjdev.desktop.helpers.gif.GifImage
+
+object MimeTypeExt {
+
+    // todo from mime
+    val String.isGif get() = trim().endsWith(".gif")
+
+    // todo from mime
+    val String.isVideo get() = trim().let {
+        it.endsWith(".mp4") || it.endsWith(".mpg")
+    }
+
+    // todo from mime
+    val Path.isGif
+        get() = absolutePath.isGif
+
+    // todo from mime
+    val Path.isVideo
+        get() = absolutePath.isGif
+
+    // todo from mime
+    val Any.isGif get() = when (this) {
+        is String -> this.isGif
+        is Path -> this.isGif
+        is GifImage -> true
+        else -> false
+    }
+
+    // todo from mime
+    val Any.isVideo get() = when (this) {
+        is String -> this.isVideo
+        is Path -> this.isVideo
+        is GifImage -> false
+        else -> false
+    }
+
+}

+ 0 - 3
composeApp/src/commonMain/kotlin/org/mjdev/desktop/helpers/bitmap/Bitmap.kt

@@ -138,9 +138,6 @@
 //        }
 //    }
 //
-//    fun cut(x: Int, y: Int, w: Int, h: Int): Bitmap =
-//        Bitmap(image.getSubimage(x, y, w, h))
-//
 //    companion object {
 //        fun Bitmap.applyCanvas(block: Canvas.() -> Unit): Bitmap {
 //            Canvas(this).apply(block)

+ 227 - 194
composeApp/src/desktopMain/kotlin/org/mjdev/desktop/helpers/gif/GifDecoder.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/helpers/gif/GifDecoder.kt

@@ -8,37 +8,33 @@
 
 package org.mjdev.desktop.helpers.gif
 
-import java.awt.AlphaComposite
-import java.awt.Color
-import java.awt.Dimension
-import java.awt.Rectangle
-import java.awt.image.BufferedImage
-import java.awt.image.DataBufferInt
-import java.io.BufferedInputStream
-import java.io.File
-import java.io.FileInputStream
-import java.io.IOException
-import java.io.InputStream
-import java.net.URL
-
-@Suppress("unused", "DEPRECATION")
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.unit.IntRect
+import okio.BufferedSource
+import okio.IOException
+import okio.Path
+import okio.Path.Companion.toPath
+import okio.buffer
+import okio.source
+import org.mjdev.desktop.extensions.pixels
+import org.mjdev.desktop.extensions.fillRect
+import org.mjdev.desktop.log.Log
+import org.mjdev.desktop.system.Filesystem.source
+
+// todo optimize code, remove unused variables, use better data structures
+@Suppress("MemberVisibilityCanBePrivate", "unused")
 class GifDecoder {
-    companion object {
-        const val STATUS_OK = 0
-        const val STATUS_FORMAT_ERROR = 1
-        const val STATUS_OPEN_ERROR = 2
-    }
-
     private var status: Int = STATUS_OK
     private var width: Int = 0
     private var height: Int = 0
-    private var gctSize: Int = 0
+    private var globalColorTableSize: Int = 0
     private var loopCount: Int = 1
-    private var bgIndex: Int = 0
-    private var bgColor: Int = 0
-    private var lastBgColor: Int = 0
+    private var backgroundIndex: Int = 0
+    private var backgroundColor: Int = 0
+    private var lastBackgroundColor: Int = 0
     private var pixelAspect: Int = 0
-    private var lctSize: Int = 0
+    private var localColorTableSize: Int = 0
     private var ix: Int = 0
     private var iy: Int = 0
     private var iw: Int = 0
@@ -49,62 +45,131 @@ class GifDecoder {
     private var delay: Int = 0
     private var transIndex: Int = 0
     private val maxStackSize = 4096
-
     private var transparency: Boolean = false
-    private var lctFlag: Boolean = false
+    private var localColorTableFlag: Boolean = false
     private var interlace: Boolean = false
-    private var gctFlag: Boolean = false
-
+    private var globalColorTableFlag: Boolean = false
     private var prefix: ShortArray = shortArrayOf()
     private var suffix: ByteArray = byteArrayOf()
     private var pixelStack: ByteArray = byteArrayOf()
     private var pixels: ByteArray = byteArrayOf()
-    private var lastRect: Rectangle = Rectangle()
+    private var lastRect: IntRect = IntRect.Zero
+    private var globalColorTable: IntArray = intArrayOf()
+    private var localColorTable: IntArray = intArrayOf()
+    private var activeColorTable: IntArray = intArrayOf()
+    private var image: ImageBitmap? = null
+    private var lastImage: ImageBitmap? = null
+    private var input: BufferedSource? = null
+    private var block = byteArrayOf()
+    private var frameCount: Int = 0
 
-    private var gct: IntArray = intArrayOf()
-    private var lct: IntArray = intArrayOf()
-    private var act: IntArray = intArrayOf()
+    private val frames = mutableListOf<GifFrame>()
 
-    private var image: BufferedImage? = null
-    private var lastImage: BufferedImage? = null
-    private var input: BufferedInputStream? = null
+    fun getWidth() : Int = width
 
-    private val block = ByteArray(256)
-    private var frames = ArrayList<GifFrame>()
-    private var frameCount: Int = 0
+    fun getHeight() : Int = height
 
-    fun getDelay(n: Int): Int {
+    fun getDuration(): Long = frames.sumOf { f ->
+        f.delay
+    }.times(loopCount).toLong()
+
+    fun getDelay(n: Int): Long {
         delay = -1
         if (n in 0..<frameCount) {
             delay = frames[n].delay
         }
-        return delay
+        return delay.toLong()
     }
 
     fun getFrameCount(): Int = frameCount
 
-    fun getImage(): BufferedImage? = getFrame(0)
+    fun getFrame(n: Int): ImageBitmap? = if (n in 0..<frameCount) frames[n].image
+    else null
 
-    fun getLoopCount(): Int = loopCount
+    fun fromSource(
+        inp: BufferedSource?
+    ): Int {
+        init()
+        if (inp != null) {
+            input = inp
+            readHeader()
+            if (!isErr()) {
+                readContents()
+                if (frameCount < 0) {
+                    status = STATUS_FORMAT_ERROR
+                }
+            } else {
+                status = STATUS_OPEN_ERROR
+            }
+            inp.close()
+        } else {
+            status = STATUS_OPEN_ERROR
+        }
+        return status
+    }
+
+    fun fromFile(
+        path: String
+    ): Int = fromPath(path.toPath())
+
+    fun fromPath(path: Path): Int {
+        status = STATUS_OK
+        try {
+            status = fromSource(source(path).buffer())
+        } catch (e: IOException) {
+            Log.e(e)
+            status = STATUS_OPEN_ERROR
+        }
+        return status
+    }
+
+    fun fromUrl(
+        url: String
+    ): Int {
+        return try {
+            val connection = java.net.URL(url).openConnection()
+            connection.connect()
+            fromSource(connection.getInputStream().source().buffer())
+        } catch (e: Exception) {
+            Log.e(e)
+            status = STATUS_OPEN_ERROR
+            status
+        }
+    }
+
+    fun from(
+        pathOrUrl: String
+    ) : Int {
+        status = when {
+            pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://") -> {
+                fromUrl(pathOrUrl)
+            }
+            else -> {
+                fromFile(pathOrUrl)
+            }
+        }
+        return status
+    }
 
     private fun setPixels() {
-        val dest = (image?.raster?.dataBuffer as DataBufferInt).data ?: intArrayOf()
+        val dest = image?.pixels ?: intArrayOf()
         if (lastDispose > 0) {
             if (lastDispose == 3) {
                 val n = frameCount - 2
                 lastImage = if (n > 0) getFrame(n - 1) else null
             }
             if (lastImage != null) {
-                val prev = (lastImage?.raster?.dataBuffer as DataBufferInt).data ?: intArrayOf()
+                val prev = lastImage?.pixels ?: intArrayOf()
                 System.arraycopy(prev, 0, dest, 0, width * height)
-
                 if (lastDispose == 2) {
-                    val g = image!!.createGraphics()
-                    val c: Color = if (transparency) Color(0, 0, 0, 0) else Color(lastBgColor)
-                    g.color = c
-                    g.composite = AlphaComposite.Src
-                    g.fillRect(lastRect.x, lastRect.y, lastRect.width, lastRect.height)
-                    g.dispose()
+                    image?.fillRect(
+                        lastRect.left,
+                        lastRect.top,
+                        lastRect.width,
+                        lastRect.height,
+                        if (transparency) Color(0, 0, 0, 0)
+                        else Color(lastBackgroundColor)
+                    )
                 }
             }
         }
@@ -122,7 +187,6 @@ class GifDecoder {
                             iLine = 2
                             inc = 4
                         }
-
                         4 -> {
                             iLine = 1
                             inc = 2
@@ -143,73 +207,13 @@ class GifDecoder {
                 var sx = i * iw
                 while (dx < dLim) {
                     val index = pixels[sx++].toInt() and 0xff
-                    val c = act[index]
+                    val c = activeColorTable[index]
                     if (c != 0) dest[dx] = c
                     dx++
                 }
             }
         }
-    }
-
-    fun getFrame(n: Int): BufferedImage? = if (n in 0..<frameCount) frames[n].image else null
-
-    fun getFrameSize(): Dimension = Dimension(width, height)
-
-    fun read(inp: BufferedInputStream?): Int {
-        init()
-        if (inp != null) {
-            input = inp
-            readHeader()
-            if (!err()) {
-                readContents()
-                if (frameCount < 0) {
-                    status = STATUS_FORMAT_ERROR
-                }
-            } else {
-                status = STATUS_OPEN_ERROR
-            }
-            inp.close()
-        } else {
-            status = STATUS_OPEN_ERROR
-        }
-        return status
-    }
-
-    fun read(inp: InputStream?): Int {
-        init()
-        if (inp != null) {
-            input = if (inp !is BufferedInputStream) BufferedInputStream(inp) else inp
-            readHeader()
-            if (!err()) {
-                readContents()
-                if (frameCount < 0) {
-                    status = STATUS_FORMAT_ERROR
-                }
-            } else {
-                status = STATUS_OPEN_ERROR
-            }
-            inp.close()
-        } else {
-            status = STATUS_OPEN_ERROR
-        }
-        return status
-    }
-
-    fun read(name: String): Int {
-        status = STATUS_OK
-        try {
-            val bufferedInputStream = BufferedInputStream(
-                if (name.startsWith("file:") || name.contains(":/"))
-                    URL(name).openStream()
-                else
-                    FileInputStream(File(name))
-            )
-            status = read(bufferedInputStream)
-            bufferedInputStream.close()
-        } catch (e: IOException) {
-            status = STATUS_OPEN_ERROR
-        }
-        return status
+        image?.pixels = dest
     }
 
     private fun decodeImageData() {
@@ -232,7 +236,7 @@ class GifDecoder {
         if (prefix.isEmpty()) prefix = ShortArray(maxStackSize)
         if (suffix.isEmpty()) suffix = ByteArray(maxStackSize)
         if (pixelStack.isEmpty()) pixelStack = ByteArray(maxStackSize + 1)
-        val dataSize: Int = read()
+        val dataSize: Int = readByte()
         clear = 1 shl dataSize
         val endOfInformation: Int = clear + 1
         available = clear + 2
@@ -313,80 +317,75 @@ class GifDecoder {
         for (ii in pi until numPix) pixels[ii] = 0
     }
 
-    private fun err(): Boolean = status != STATUS_OK
+    private fun isErr(): Boolean = status != STATUS_OK
 
     private fun init() {
         status = STATUS_OK
         frameCount = 0
-        frames = arrayListOf()
-        gct = intArrayOf()
-        lct = intArrayOf()
+        frames.clear()
+        globalColorTable = intArrayOf()
+        localColorTable = intArrayOf()
     }
 
-    private fun read(): Int {
+    private fun readByte(): Int {
         val curByte = try {
-            input!!.read()
+            input!!.readByte()
         } catch (e: IOException) {
+            Log.e(e)
             status = STATUS_FORMAT_ERROR
             -1
         }
-        return curByte
+        return curByte.toInt() and 0xff
     }
 
     private fun readBlock(): Int {
-        val blockSize = read()
-        var n = 0
+        val blockSize = readByte()
         if (blockSize > 0) {
             try {
-                var count: Int
-                while (n < blockSize) {
-                    count = input!!.read(block, n, blockSize - n)
-                    if (count == -1) break
-                    n += count
-                }
+                block = input!!.readByteArray(blockSize.toLong())
             } catch (e: IOException) {
-                e.printStackTrace()
-            }
-            if (n < blockSize) {
+                Log.e(e)
                 status = STATUS_FORMAT_ERROR
             }
         }
-        return n
+        return blockSize
     }
 
     private fun readColorTable(colors: Int): IntArray {
-        val bytes = 3 * colors
-        var tab: IntArray? = null
-        val c = ByteArray(bytes)
-        var n = 0
+        var tab: IntArray
         try {
-            n = input!!.read(c)
+            val bytes = 3 * colors
+            val c = input!!.readByteArray(bytes.toLong())
+            if (c.size < bytes) {
+                tab = intArrayOf()
+                status = STATUS_FORMAT_ERROR
+            } else {
+                tab = IntArray(256)
+                var i = 0
+                var j = 0
+                while (i < colors) {
+                    val r = c[j++].toInt() and 0xff
+                    val g = c[j++].toInt() and 0xff
+                    val b = c[j++].toInt() and 0xff
+                    tab[i++] = 0xff000000.toInt() or (r shl 16) or (g shl 8) or b
+                }
+            }
         } catch (e: IOException) {
-            e.printStackTrace()
-        }
-        if (n < bytes) {
+            Log.e(e)
+            tab = intArrayOf()
             status = STATUS_FORMAT_ERROR
-        } else {
-            tab = IntArray(256)
-            var i = 0
-            var j = 0
-            while (i < colors) {
-                val r = c[j++].toInt() and 0xff
-                val g = c[j++].toInt() and 0xff
-                val b = c[j++].toInt() and 0xff
-                tab[i++] = 0xff000000.toInt() or (r shl 16) or (g shl 8) or b
-            }
         }
-        return tab ?: intArrayOf()
+        return tab
     }
 
     private fun readContents() {
         var done = false
-        while (!(done || err())) {
-            when (read()) {
+        while (!(done || isErr())) {
+            val token = readByte()
+            when (token) {
                 0x2C -> readImage()
                 0x21 -> {
-                    when (read()) {
+                    when (readByte()) {
                         0xf9 -> readGraphicControlExt()
                         0xff -> {
                             readBlock()
@@ -400,11 +399,9 @@ class GifDecoder {
                                 skip()
                             }
                         }
-
                         else -> skip()
                     }
                 }
-
                 0x3b -> done = true
                 0x00 -> {}
                 else -> status = STATUS_FORMAT_ERROR
@@ -413,31 +410,31 @@ class GifDecoder {
     }
 
     private fun readGraphicControlExt() {
-        read()
-        val packed = read()
+        readByte()
+        val packed = readByte()
         dispose = (packed and 0x1c) shr 2
         if (dispose == 0) {
             dispose = 1
         }
         transparency = (packed and 1) != 0
         delay = readShort() * 10
-        transIndex = read()
-        read()
+        transIndex = readByte()
+        readByte()
     }
 
     private fun readHeader() {
         val id = StringBuilder()
         for (i in 0 until 6) {
-            id.append(read().toChar())
+            id.append(readByte().toChar())
         }
         if (!id.toString().startsWith("GIF")) {
             status = STATUS_FORMAT_ERROR
             return
         }
         readLSD()
-        if (gctFlag && !err()) {
-            gct = readColorTable(gctSize)
-            bgColor = gct[bgIndex]
+        if (globalColorTableFlag && !isErr()) {
+            globalColorTable = readColorTable(globalColorTableSize)
+            backgroundColor = globalColorTable[backgroundIndex]
         }
     }
 
@@ -446,38 +443,38 @@ class GifDecoder {
         iy = readShort()
         iw = readShort()
         ih = readShort()
-        val packed = read()
-        lctFlag = (packed and 0x80) != 0
+        val packed = readByte()
+        localColorTableFlag = (packed and 0x80) != 0
         interlace = (packed and 0x40) != 0
-        lctSize = 2 shl (packed and 7)
-        if (lctFlag) {
-            lct = readColorTable(lctSize)
-            act = lct
+        localColorTableSize = 2 shl (packed and 7)
+        if (localColorTableFlag) {
+            localColorTable = readColorTable(localColorTableSize)
+            activeColorTable = localColorTable
         } else {
-            act = gct
-            if (bgIndex == transIndex) {
-                bgColor = 0
+            activeColorTable = globalColorTable
+            if (backgroundIndex == transIndex) {
+                backgroundColor = 0
             }
         }
         var save = 0
-        if (act.isEmpty()) {
+        if (activeColorTable.isEmpty()) {
             status = STATUS_FORMAT_ERROR
             return
         }
         if (transparency) {
-            save = act[transIndex]
-            act[transIndex] = 0
+            save = activeColorTable[transIndex]
+            activeColorTable[transIndex] = 0
         }
-        if (err()) return
+        if (isErr()) return
         decodeImageData()
         skip()
-        if (err()) return
+        if (isErr()) return
         frameCount++
-        image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE)
+        image = ImageBitmap(width, height)
         setPixels()
         frames.add(GifFrame(image, delay))
         if (transparency) {
-            act[transIndex] = save
+            activeColorTable[transIndex] = save
         }
         resetFrame()
     }
@@ -485,11 +482,11 @@ class GifDecoder {
     private fun readLSD() {
         width = readShort()
         height = readShort()
-        val packed = read()
-        gctFlag = (packed and 0x80) != 0
-        gctSize = 2 shl (packed and 7)
-        bgIndex = read()
-        pixelAspect = read()
+        val packed = readByte()
+        globalColorTableFlag = (packed and 0x80) != 0
+        globalColorTableSize = 2 shl (packed and 7)
+        backgroundIndex = readByte()
+        pixelAspect = readByte()
     }
 
     private fun readNetscapeExt() {
@@ -500,25 +497,61 @@ class GifDecoder {
                 val b2 = block[2].toInt() and 0xff
                 loopCount = (b2 shl 8) or b1
             }
-        } while (blockSize > 0 && !err())
+        } while (blockSize > 0 && !isErr())
     }
 
-    private fun readShort(): Int = read() or (read() shl 8)
+    private fun readShort(): Int = (readByte() or (readByte() shl 8)) and 0xffff
 
     private fun resetFrame() {
         lastDispose = dispose
-        lastRect = Rectangle(ix, iy, iw, ih)
+        lastRect = IntRect(ix, iy, iw, ih)
         lastImage = image
-        lastBgColor = bgColor
+        lastBackgroundColor = backgroundColor
         dispose = 0
         transparency = false
         delay = 0
-        lct = intArrayOf()
+        localColorTable = intArrayOf()
     }
 
     private fun skip() {
         do {
             readBlock()
-        } while (blockSize > 0 && !err())
+        } while (blockSize > 0 && !isErr())
+    }
+
+    companion object {
+        const val STATUS_OK = 0
+        const val STATUS_FORMAT_ERROR = 1
+        const val STATUS_OPEN_ERROR = 2
+
+        fun fromSource(
+            source: BufferedSource?
+        ): GifDecoder = GifDecoder().apply {
+            fromSource(source)
+        }
+
+        fun fromPath(
+            source: Path
+        ): GifDecoder = GifDecoder().apply {
+            fromPath(source)
+        }
+
+        fun fromUrl(
+            source: String
+        ): GifDecoder = GifDecoder().apply {
+            fromUrl(source)
+        }
+
+        fun fromPathOrUrl(
+            source: String
+        ): GifDecoder = GifDecoder().apply {
+            fromPathOrUrl(source)
+        }
+
+        fun fromFile(
+            source: String
+        ): GifDecoder = GifDecoder().apply {
+            fromFile(source)
+        }
     }
 }

+ 2 - 2
composeApp/src/desktopMain/kotlin/org/mjdev/desktop/helpers/gif/GifFrame.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/helpers/gif/GifFrame.kt

@@ -1,9 +1,9 @@
 package org.mjdev.desktop.helpers.gif
 
-import java.awt.image.BufferedImage
+import androidx.compose.ui.graphics.ImageBitmap
 import kotlin.jvm.JvmField
 
 class GifFrame(
-    @JvmField val image: BufferedImage?,
+    @JvmField val image: ImageBitmap?,
     @JvmField val delay: Int
 )

+ 50 - 0
composeApp/src/commonMain/kotlin/org/mjdev/desktop/helpers/gif/GifImage.kt

@@ -0,0 +1,50 @@
+package org.mjdev.desktop.helpers.gif
+
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.ImageBitmapConfig
+import androidx.compose.ui.graphics.colorspace.ColorSpace
+import androidx.compose.ui.graphics.colorspace.ColorSpaces
+
+@Suppress("MemberVisibilityCanBePrivate")
+class GifImage(
+    val src: String,
+) : ImageBitmap {
+    private val decoder: GifDecoder = GifDecoder.fromPathOrUrl(src)
+
+    var currentFrame: Int = 0
+    val duration: Long get() = decoder.getDuration()
+
+    override val width: Int = decoder.getWidth()
+    override val height: Int = decoder.getHeight()
+    override val hasAlpha: Boolean = true
+    override val colorSpace: ColorSpace = ColorSpaces.Srgb
+    override val config: ImageBitmapConfig = ImageBitmapConfig.Argb8888
+
+    override fun prepareToDraw() {
+        currentFrame = 0
+    }
+
+    override fun readPixels(
+        buffer: IntArray,
+        startX: Int,
+        startY: Int,
+        width: Int,
+        height: Int,
+        bufferOffset: Int,
+        stride: Int
+    ) {
+        decoder.getFrame(currentFrame)?.readPixels(
+            buffer,
+            startX,
+            startY,
+            width,
+            height,
+            bufferOffset,
+            stride
+        )
+    }
+
+    override fun toString(): String {
+        return "GifImage(src='$src')"
+    }
+}

+ 4 - 0
composeApp/src/commonMain/kotlin/org/mjdev/desktop/log/Log.kt

@@ -83,5 +83,9 @@ object Log {
         }
     }
 
+    fun e(message: String, e: Throwable) {
+        e(message)
+        e(e)
+    }
 
 }

+ 2 - 1
composeApp/src/commonMain/kotlin/org/mjdev/desktop/managers/ai/AiManager.kt

@@ -20,13 +20,14 @@ import org.mjdev.desktop.managers.ai.actions.ActionCalculator
 import org.mjdev.desktop.managers.ai.actions.base.ActionsProvider
 import org.mjdev.desktop.managers.ai.actions.ActionCurrentTime
 import org.mjdev.desktop.managers.ai.actions.base.ActionException.ActionNone
+import org.mjdev.desktop.managers.ai.plugins.AiPluginOpenAi
 import org.mjdev.desktop.managers.ai.plugins.base.AIPlugin
 import org.mjdev.desktop.managers.ai.tts.TTSPluginEmpty
 
 @Suppress("MemberVisibilityCanBePrivate", "unused")
 class AiManager(
     val context: IDesktopContext,
-    var pluginAI: AIPlugin = AiPluginGemini(context),
+    var pluginAI: AIPlugin = AiPluginEmpty(context),
     var pluginSTT: STTPlugin = STTPluginEmpty(context),
     var pluginTTS: TTSPlugin = TTSPluginEmpty(context),
     var actions: ActionsProvider = ActionsProvider(context) {

+ 4 - 4
composeApp/src/commonMain/kotlin/org/mjdev/desktop/managers/ai/plugins/AiPluginGemini.kt

@@ -23,12 +23,12 @@ class AiPluginGemini(
     private val aiModel: GeminiModel = GeminiModel.Gemini1_5Pro,
     private val scope: CoroutineScope = context.scope
 ) : AIPlugin {
-    private val key = context.keysManager.loadKey("gemini")
+    private val apiKey = context.keysManager.loadKey("gemini")
     private val generativeModel: GenerativeModel? by lazy {
-        if (key.isNotEmpty()) {
+        if (apiKey.isNotEmpty()) {
             GenerativeModel(
                 modelName = aiModel.model,
-                apiKey = key
+                apiKey = apiKey
             )
         } else null
     }
@@ -36,7 +36,7 @@ class AiPluginGemini(
     override suspend fun ask(question: String): String = scope.async {
         var error: Throwable? = null
         runCatching {
-            if (key.isEmpty()) {
+            if (apiKey.isEmpty()) {
                 throw (Exception("Error: No gemini api key provided, pleas read manual and provide Your api key."))
             } else {
                 generativeModel?.generateContent(

+ 1 - 0
composeApp/src/commonMain/kotlin/org/mjdev/desktop/managers/ai/plugins/AiPluginOllama.kt

@@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope
 import org.mjdev.desktop.context.IDesktopContext
 import org.mjdev.desktop.managers.ai.plugins.base.AIPlugin
 
+// todo
 class AiPluginOllama(
     private val context: IDesktopContext,
     private val scope: CoroutineScope = context.scope

+ 71 - 0
composeApp/src/commonMain/kotlin/org/mjdev/desktop/managers/ai/plugins/AiPluginOpenAi.kt

@@ -0,0 +1,71 @@
+package org.mjdev.desktop.managers.ai.plugins
+
+import kotlinx.coroutines.CoroutineScope
+import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.managers.ai.plugins.base.AIPlugin
+import kotlin.time.Duration.Companion.seconds
+import com.aallam.openai.api.BetaOpenAI
+import com.aallam.openai.api.chat.ChatCompletionChunk
+import com.aallam.openai.api.chat.ChatCompletionRequest
+import com.aallam.openai.api.chat.ChatMessage
+import com.aallam.openai.api.chat.ChatRole
+import com.aallam.openai.api.http.Timeout
+import com.aallam.openai.api.logging.LogLevel
+import com.aallam.openai.api.logging.Logger
+import com.aallam.openai.api.model.ModelId
+import com.aallam.openai.client.OpenAI
+import com.aallam.openai.client.OpenAIConfig
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.first
+import org.mjdev.desktop.log.Log
+
+// todo test
+class AiPluginOpenAi(
+    private val context: IDesktopContext,
+    private val scope: CoroutineScope = context.scope
+) : AIPlugin {
+    private val apiKey = context.keysManager.loadKey("open-ai")
+    private val modelId = ModelId("gpt-3.5-turbo")
+    private val openAI: OpenAI? = runCatching {
+        if (apiKey.isNotEmpty()) OpenAI(
+            OpenAIConfig(
+                token = apiKey,
+                logLevel = LogLevel.All,
+                logger = Logger.Simple,
+                timeout = Timeout(socket = 60.seconds),
+            )
+        ) else {
+            Log.w("OpenAI API key is not set, using empty OpenAI client.")
+            null
+        }
+    }.onFailure { e ->
+        Log.e(e)
+    }.getOrNull()
+
+    @OptIn(BetaOpenAI::class)
+    override suspend fun ask(question: String): String = scope.async {
+        runCatching {
+            openAI?.chatCompletions(
+                ChatCompletionRequest(
+                    model = modelId,
+                    messages = listOf(
+                        ChatMessage(
+                            role = ChatRole.User,
+                            content = question
+                        )
+                    ),
+                    maxTokens = 1000,
+                    temperature = 0.0
+                )
+            )?.first().let { chunk ->
+                if (chunk is ChatCompletionChunk) {
+                    chunk.choices.firstOrNull()?.delta?.content ?: ""
+                } else {
+                    ""
+                }
+            }
+        }.onFailure { e ->
+            Log.e(e)
+        }.getOrDefault("")
+    }.await()
+}

+ 10 - 0
composeApp/src/commonMain/kotlin/org/mjdev/desktop/managers/ai/plugins/AiPluginPerplexity.kt

@@ -0,0 +1,10 @@
+package org.mjdev.desktop.managers.ai.plugins
+
+import org.mjdev.desktop.managers.ai.plugins.base.AIPlugin
+
+// todo
+class AiPluginPerplexity : AIPlugin  {
+    override suspend fun ask(question: String): String {
+        TODO("Not yet implemented")
+    }
+}

+ 5 - 9
composeApp/src/commonMain/kotlin/org/mjdev/desktop/managers/palette/Palette.kt

@@ -19,10 +19,10 @@ import org.mjdev.desktop.extensions.Colors.darker
 import org.mjdev.desktop.extensions.Colors.isLightColor
 import org.mjdev.desktop.extensions.Colors.lighter
 import org.mjdev.desktop.extensions.Colors.nonAlphaValue
-import org.mjdev.desktop.extensions.ImageBitmapExt.cut
 import org.mjdev.desktop.extensions.ImageBitmapExt.loadPicture
 import org.mjdev.desktop.helpers.image.ImageUtils.topMostColor
 import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.extensions.cut
 
 @Suppress("CanBeParameter", "MemberVisibilityCanBePrivate", "unused")
 class Palette(
@@ -103,14 +103,10 @@ class Palette(
                 val cutPercent = 5
                 val cutWidth = (width / 100) * cutPercent
                 val cutHeight = (height / 100) * cutPercent
-                val leftTopPart = image
-                    .cut(0, 0, cutWidth, cutHeight)
-                val rightTopPart = image
-                    .cut(width - cutWidth, 0, cutWidth, cutHeight)
-                val leftBottomPart = image
-                    .cut(0, height - cutHeight, cutWidth, cutHeight)
-                val rightBottomPart = image
-                    .cut(width - cutWidth, height - cutHeight, cutWidth, cutHeight)
+                val leftTopPart = image.cut(0, 0, cutWidth, cutHeight)
+                val rightTopPart = image.cut(width - cutWidth, 0, cutWidth, cutHeight)
+                val leftBottomPart = image.cut(0, height - cutHeight, cutWidth, cutHeight)
+                val rightBottomPart = image.cut(width - cutWidth, height - cutHeight, cutWidth, cutHeight)
                 val colors = PaletteColors(
                     leftTopPart.topMostColor,
                     rightTopPart.topMostColor,

+ 5 - 5
composeApp/src/desktopMain/kotlin/org/mjdev/desktop/components/appsmenu/AppsMenuWindow.kt

@@ -3,6 +3,7 @@ package org.mjdev.desktop.components.appsmenu
 import androidx.compose.animation.EnterTransition
 import androidx.compose.animation.ExitTransition
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.unit.DpOffset
@@ -10,7 +11,6 @@ import androidx.compose.ui.unit.DpSize
 import org.mjdev.desktop.components.appsmenu.AppsMenuState.Companion.rememberAppsMenuState
 import org.mjdev.desktop.data.Category
 import org.mjdev.desktop.extensions.Compose.preview
-import org.mjdev.desktop.extensions.LaunchedEffect.launchedEffect
 import org.mjdev.desktop.extensions.MutableStateExt.clear
 import org.mjdev.desktop.extensions.MutableStateExt.plus
 import org.mjdev.desktop.extensions.MutableStateExt.rememberComputed
@@ -71,8 +71,8 @@ fun AppsMenuWindow(
             panelState.x,
             containerSize.height - (panelState.height + appMenuMinHeight)
         ).apply {
-            println("Menu position: $this")
-            println("Menu size: $size")
+//            println("Menu position: $this")
+//            println("Menu size: $size")
         }
     }
     ChromeWindow(
@@ -135,8 +135,8 @@ fun AppsMenuWindow(
             onActionClick = onActionClick,
             onTooltip = onTooltip
         )
-        launchedEffect(menuState.isVisible) { isVisible ->
-            if (!isVisible) {
+        LaunchedEffect(menuState.isVisible) {
+            if (!menuState.isVisible) {
                 categoryState.value = null
             }
         }

+ 1 - 1
composeApp/src/desktopMain/kotlin/org/mjdev/desktop/components/controlcenter/ControlCenterWindow.kt

@@ -100,7 +100,7 @@ fun ControlCenterWindow(
         onGlobalMouse = {
             onPointerEnter(mouseRange) {
                 runAsync {
-                    println("Pointer enter control center.")
+//                    println("Pointer enter control center.")
                     controlCenterState.showOrFocus()
                 }
             }

+ 1 - 1
composeApp/src/desktopMain/kotlin/org/mjdev/desktop/components/desktoppanel/DesktopPanelWindow.kt

@@ -123,7 +123,7 @@ fun DesktopPanelWindow(
         onGlobalMouse = {
             onPointerEnter(mouseRange) {
                 runAsync {
-                    println("Pointer enter desktop panel.")
+//                    println("Pointer enter desktop panel.")
                     if (menuState.isNotVisible) {
                         panelState.showOrFocus()
                     }

+ 0 - 59
composeApp/src/desktopMain/kotlin/org/mjdev/desktop/components/image/GifView.kt

@@ -1,59 +0,0 @@
-package org.mjdev.desktop.components.image
-
-import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.runtime.*
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.toComposeImageBitmap
-import androidx.compose.ui.unit.IntSize
-import org.mjdev.desktop.extensions.Compose.preview
-import org.mjdev.desktop.helpers.gif.GifDecoder
-import kotlinx.coroutines.delay
-import java.awt.image.BufferedImage
-import java.io.File
-import java.io.FileInputStream
-
-@Suppress("FunctionName")
-@Composable
-fun GifView(
-    modifier: Modifier = Modifier,
-    src: String = "",
-    onAnimationFinish: () -> Unit = {}
-) = BoxWithConstraints {
-    val file by remember(src) { mutableStateOf(File(src)) }
-    val streamData by remember(file) { mutableStateOf(FileInputStream(file)) }
-    val gifDecoder by remember(streamData) { mutableStateOf(GifDecoder().apply { read(streamData) }) }
-    val framesCount by remember(gifDecoder) { mutableStateOf(gifDecoder.getFrameCount()) }
-    var currentFrame by remember(gifDecoder) { mutableStateOf(0) }
-    var decodedImage by remember { mutableStateOf<BufferedImage?>(null) }
-    Canvas(
-        modifier = modifier
-    ) {
-        decodedImage?.toComposeImageBitmap()?.let { img ->
-            drawImage(
-                image = img,
-                srcSize = IntSize(img.width, img.height),
-                dstSize = IntSize(constraints.maxWidth, constraints.maxHeight),
-            )
-        }
-    }
-    LaunchedEffect(currentFrame, decodedImage) {
-        decodedImage = gifDecoder.getFrame(currentFrame)
-        delay(gifDecoder.getDelay(currentFrame).toLong())
-        var nextFrame = currentFrame + 1
-        if (nextFrame > framesCount) {
-            nextFrame = 0
-            onAnimationFinish()
-        }
-        currentFrame = nextFrame
-    }
-}
-
-// todo
-@Suppress("unused")
-@Preview
-@Composable
-fun PreviewGifView() = preview {
-    GifView()
-}

+ 6 - 6
composeApp/src/desktopMain/kotlin/org/mjdev/desktop/components/main/MainWindow.kt

@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -25,7 +26,6 @@ import org.mjdev.desktop.components.tooltip.rememberTooltipState
 import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
 import org.mjdev.desktop.extensions.Compose.isDesign
 import org.mjdev.desktop.extensions.Compose.preview
-import org.mjdev.desktop.extensions.LaunchedEffect.launchedEffect
 import org.mjdev.desktop.extensions.MutableStateExt.rememberCalculated
 import org.mjdev.desktop.helpers.system.shell.Shell
 import org.mjdev.desktop.log.Log
@@ -60,7 +60,7 @@ fun MainWindow() = withDesktopContext {
         else max(0.dp, panelState.height)
     }
     val onTooltip: (item: Any?) -> Unit = { item ->
-        println("Tooltip: $item")
+//        println("Tooltip: $item")
         tooltipState.show(item)
     }
     DesktopWindow(
@@ -97,7 +97,7 @@ fun MainWindow() = withDesktopContext {
         panelState = panelState,
         menuState = menuState,
         onFocusChange = { focused ->
-            Log.d("panel focus : $focused")
+//            Log.d("panel focus : $focused")
             val menuIsVisible = appsMenuState.isVisible || menuState.isVisible
             if (panelState.enabled) {
                 if (!menuIsVisible && !focused) {
@@ -124,7 +124,7 @@ fun MainWindow() = withDesktopContext {
         onTooltip = onTooltip,
         controlCenterState = controlCenterState,
         onFocusChange = { focused ->
-            Log.d("control center focus : $focused")
+//            Log.d("control center focus : $focused")
             if (!focused) {
                 runAsync {
                     controlCenterState.hide()
@@ -162,8 +162,8 @@ fun MainWindow() = withDesktopContext {
             Log.i("App ended.")
         }
     }
-    launchedEffect(menuState.isVisible) { isVisible ->
-        appsMenuState.isVisible = isVisible
+    LaunchedEffect(menuState.isVisible) {
+        appsMenuState.isVisible = menuState.isVisible
     }
 }
 

+ 2 - 1
composeApp/src/desktopMain/kotlin/org/mjdev/desktop/context/DeskptopContext.kt

@@ -32,6 +32,7 @@ import org.mjdev.desktop.log.Log
 import org.mjdev.desktop.managers.ai.AiManager
 import org.mjdev.desktop.managers.ai.IAiManager
 import org.mjdev.desktop.managers.ai.plugins.AiPluginGemini
+import org.mjdev.desktop.managers.ai.plugins.AiPluginOpenAi
 import org.mjdev.desktop.managers.ai.stt.STTPluginEmpty
 import org.mjdev.desktop.managers.ai.tts.TTSPluginSwift
 import org.mjdev.desktop.managers.apps.IAppsManager
@@ -247,7 +248,7 @@ class DesktopContext(
         IAiManager::class -> AiManager(
             context = this,
             // todo user can configure
-            pluginAI = AiPluginGemini(this@DesktopContext),
+            pluginAI = AiPluginOpenAi(this@DesktopContext),
             pluginTTS = TTSPluginSwift(this@DesktopContext),
             pluginSTT = STTPluginEmpty(this@DesktopContext)
         )

+ 1 - 15
composeApp/src/desktopMain/kotlin/org/mjdev/desktop/extensions/ImageBitmapExt.kt

@@ -2,26 +2,13 @@ package org.mjdev.desktop.extensions
 
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.asComposeImageBitmap
-import androidx.compose.ui.graphics.asSkiaBitmap
-import androidx.compose.ui.graphics.toComposeImageBitmap
 import coil3.request.ImageRequest
 import coil3.toBitmap
 import okio.Path
-import org.jetbrains.skiko.toBufferedImage
 import org.mjdev.desktop.context.IDesktopContext
 
 @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
 actual object ImageBitmapExt {
-
-    actual fun ImageBitmap.cut(
-        x: Int,
-        y: Int,
-        width: Int,
-        height: Int
-    ): ImageBitmap = asSkiaBitmap().toBufferedImage().apply {
-        getSubimage(x, y, width, height)
-    }.toComposeImageBitmap()
-
     actual suspend fun IDesktopContext.loadPicture(
         src: Any?
     ): ImageBitmap? = runCatching {
@@ -36,5 +23,4 @@ actual object ImageBitmapExt {
                 imageLoader?.execute(req)?.image?.toBitmap()?.asComposeImageBitmap()
             }
     }.getOrNull()
-
-}
+}

+ 9 - 9
composeApp/src/desktopMain/kotlin/org/mjdev/desktop/windows/ChromeWindowState.kt

@@ -13,7 +13,7 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import org.mjdev.desktop.components.sliding.base.VisibilityState
-import org.mjdev.desktop.extensions.LaunchedEffect.launchedEffect
+import org.mjdev.desktop.extensions.LaunchedEffect.LaunchedEffect
 import org.mjdev.desktop.extensions.MutableStateExt.rememberState
 import org.mjdev.desktop.log.Log
 import java.awt.Window
@@ -80,7 +80,7 @@ open class ChromeWindowState(
 
     override var position: DpOffset = position
         set(value) {
-            Log.i("Setting position of window : ${window?.name}")
+//            Log.i("Setting position of window : ${window?.name}")
             field = value
             if (isCreated) {
                 scope.launch {
@@ -91,7 +91,7 @@ open class ChromeWindowState(
 
     override var size: DpSize = size
         set(value) {
-            Log.i("Setting size of window : ${window?.name}, $size")
+//            Log.i("Setting size of window : ${window?.name}, $size")
             val oldSize = field
             field = value
             if (isCreated) {
@@ -126,7 +126,7 @@ open class ChromeWindowState(
     }
 
     suspend fun moveBy(x: Dp, y: Dp) = runCatching {
-        Log.i("moveBy called ($x, $y)")
+//        Log.i("moveBy called ($x, $y)")
         setPosition(
             DpOffset(
                 position.x - x,
@@ -136,12 +136,12 @@ open class ChromeWindowState(
     }.onFailure { e -> Log.e(e) }
 
     suspend fun setSize(size: DpSize) = runCatching {
-        Log.i("Setting window size : ${window?.name} to $size")
+//        Log.i("Setting window size : ${window?.name} to $size")
         window?.setSizeSafely(size, WindowPlacement.Floating)
     }.onFailure { e -> Log.e(e) }
 
     suspend fun setPosition(position: DpOffset) = runCatching {
-        Log.i("Setting window position : ${window?.name} to $position")
+//        Log.i("Setting window position : ${window?.name} to $position")
         window?.setPosition(
             WindowPosition.Absolute(position.x, position.y),
             WindowPlacement.Floating
@@ -150,7 +150,7 @@ open class ChromeWindowState(
 
     override suspend fun focus() {
         runCatching {
-            Log.i("Requesting window focus : ${window?.name}")
+//            Log.i("Requesting window focus : ${window?.name}")
             window?.toFront()
             if (!isFocused) {
                 window?.requestFocus()
@@ -207,8 +207,8 @@ open class ChromeWindowState(
             visible: Boolean,
             wnState: MutableState<Boolean>,
             state: ChromeWindowState
-        ) = launchedEffect(wnState.value) { currentState ->
-            if (wnState.value == currentState && !visible) {
+        ) = LaunchedEffect {
+            if (wnState.value == wnState.value && !visible) {
                 if (state.closeAction == WindowCloseAction.CLOSE) {
                     wnState.value = false
                 }

+ 5 - 5
composeApp/src/desktopMain/kotlin/org/mjdev/desktop/windows/Window.kt

@@ -208,7 +208,7 @@ fun Window.align(alignment: Alignment) {
     val location = alignment.align(size, screenSize, LayoutDirection.Ltr)
     val x = screenBounds.x + screenInsets.left + location.x
     val y = screenBounds.y + screenInsets.top + location.y
-    Log.i("Window location (aligned) set to : ($x, $y)")
+//    Log.i("Window location (aligned) set to : ($x, $y)")
     setLocation(x, y)
 }
 
@@ -218,7 +218,7 @@ fun Window.setPositionImpl(
 ) = when (position) {
     WindowPosition.PlatformDefault -> {
         val loc = platformDefaultPosition()
-        Log.i("Window location (platform) set to : (${loc.x}, ${loc.y})")
+//        Log.i("Window location (platform) set to : (${loc.x}, ${loc.y})")
         location = loc
     }
 
@@ -227,7 +227,7 @@ fun Window.setPositionImpl(
     }
 
     is WindowPosition.Absolute -> {
-        Log.i("Window location (absolute) set to : $position")
+//        Log.i("Window location (absolute) set to : $position")
         val x = try {
             position.x.value.roundToInt()
         } catch (e: Exception) {
@@ -238,7 +238,7 @@ fun Window.setPositionImpl(
         } catch (e: Exception) {
             0
         }
-        Log.i("Window location (absolute) set to : ($x,$y)")
+//        Log.i("Window location (absolute) set to : ($x,$y)")
         setLocation(x, y)
     }
 }
@@ -304,7 +304,7 @@ fun Window.setSizeImpl(size: DpSize) {
     }
     val finalWidth = if (isWidthSpecified) width else computedPreferredSize!!.width
     val finalHeight = if (isHeightSpecified) height else computedPreferredSize!!.height
-    Log.i("Window $name size set to : $finalWidth x  $finalHeight.")
+//    Log.i("Window $name size set to : $finalWidth x  $finalHeight.")
     setSize(finalWidth, finalHeight)
     revalidate()
 }