فهرست منبع

control panel design fixes

control panel design fixes
milan jurkulák 2 سال پیش
والد
کامیت
2b8250638a
40فایلهای تغییر یافته به همراه614 افزوده شده و 325 حذف شده
  1. 0 1
      src/main/kotlin/eu/mjdev/desktop/Main.kt
  2. 11 3
      src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppActions.kt
  3. 1 1
      src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsList.kt
  4. 8 1
      src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenu.kt
  5. 2 2
      src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenuApp.kt
  6. 4 2
      src/main/kotlin/eu/mjdev/desktop/components/background/BackgroundImage.kt
  7. 49 0
      src/main/kotlin/eu/mjdev/desktop/components/blur/BlurPanel.kt
  8. 16 9
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/ControlCenter.kt
  9. 23 2
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/base/ControlCenterPage.kt
  10. 65 60
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/AIPage.kt
  11. 4 1
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/MainSettingsPage.kt
  12. 77 2
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/WifiSettingsPage.kt
  13. 8 5
      src/main/kotlin/eu/mjdev/desktop/components/custom/Clock.kt
  14. 3 9
      src/main/kotlin/eu/mjdev/desktop/components/custom/VerticalScrollableBox.kt
  15. 61 53
      src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanel.kt
  16. 1 1
      src/main/kotlin/eu/mjdev/desktop/components/file/FolderIcon.kt
  17. 1 0
      src/main/kotlin/eu/mjdev/desktop/components/file/FolderView.kt
  18. 7 1
      src/main/kotlin/eu/mjdev/desktop/components/greeter/Greeter.kt
  19. 1 0
      src/main/kotlin/eu/mjdev/desktop/components/surface/Surface.kt
  20. 17 17
      src/main/kotlin/eu/mjdev/desktop/data/App.kt
  21. 1 1
      src/main/kotlin/eu/mjdev/desktop/data/Theme.kt
  22. 4 3
      src/main/kotlin/eu/mjdev/desktop/data/User.kt
  23. 37 0
      src/main/kotlin/eu/mjdev/desktop/extensions/Compose.kt
  24. 11 7
      src/main/kotlin/eu/mjdev/desktop/extensions/Custom.kt
  25. 24 0
      src/main/kotlin/eu/mjdev/desktop/extensions/FlippedModifier.kt
  26. 15 14
      src/main/kotlin/eu/mjdev/desktop/helpers/internal/KeyEventHandler.kt
  27. 0 10
      src/main/kotlin/eu/mjdev/desktop/helpers/system/DBus.kt
  28. 7 1
      src/main/kotlin/eu/mjdev/desktop/helpers/system/Environment.kt
  29. 38 28
      src/main/kotlin/eu/mjdev/desktop/helpers/system/Shell.kt
  30. 2 2
      src/main/kotlin/eu/mjdev/desktop/helpers/system/UserDirs.kt
  31. 3 5
      src/main/kotlin/eu/mjdev/desktop/managers/artificialintelligence/actions/base/ActionProviderScope.kt
  32. 11 2
      src/main/kotlin/eu/mjdev/desktop/managers/dbus/DBusManager.kt
  33. 2 2
      src/main/kotlin/eu/mjdev/desktop/managers/os/linux/OsRelease.kt
  34. 2 2
      src/main/kotlin/eu/mjdev/desktop/managers/theme/base/ThemeManagerStub.kt
  35. 36 30
      src/main/kotlin/eu/mjdev/desktop/managers/theme/linux/ThemeManagerLinux.kt
  36. 3 3
      src/main/kotlin/eu/mjdev/desktop/managers/window/WindowsManager.kt
  37. 42 44
      src/main/kotlin/eu/mjdev/desktop/provider/DesktopProvider.kt
  38. 9 0
      src/main/kotlin/eu/mjdev/desktop/windows/ChromeWindowState.kt
  39. 8 1
      src/main/kotlin/eu/mjdev/desktop/windows/WindowFocusHelper.kt
  40. BIN
      src/main/resources/images/blur4.png

+ 0 - 1
src/main/kotlin/eu/mjdev/desktop/Main.kt

@@ -12,7 +12,6 @@ fun main(
     exitProcessOnExit = true
 ) {
     // net devices  : ls /sys/class/net
-    // wifi list    : nmcli -t -f ALL dev wifi
     // wave mon     : apt install wavemon
     // connect gui  : nmtui
     // eth settings : iwconfig

+ 11 - 3
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppActions.kt

@@ -15,8 +15,10 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
 import eu.mjdev.desktop.components.icon.ShapedIcon
+import eu.mjdev.desktop.extensions.Compose.runAsync
 import eu.mjdev.desktop.icons.Icons
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
+import kotlinx.coroutines.Dispatchers
 
 @Composable
 fun AppActions(
@@ -40,7 +42,9 @@ fun AppActions(
             onClick = {
                 onActionClick()
                 // todo : dialog
-                api.restart()
+                runAsync(Dispatchers.IO) {
+                    api.restart()
+                }
             }
         )
         ShapedIcon(
@@ -51,7 +55,9 @@ fun AppActions(
             onClick = {
                 onActionClick()
                 // todo : dialog
-                api.suspend()
+                runAsync(Dispatchers.IO) {
+                    api.suspend()
+                }
             }
         )
         ShapedIcon(
@@ -62,7 +68,9 @@ fun AppActions(
             onClick = {
                 onActionClick()
                 // todo : dialog
-                api.logOut()
+                runAsync(Dispatchers.IO) {
+                    api.logOut()
+                }
             }
         )
     }

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsList.kt

@@ -54,7 +54,7 @@ fun AppsList(
                     AppsMenuApp(
                         app = item as App,
                         showDivider = (items.size - 1) > idx,
-                        dividerColor =  textColor.alpha(0.3f),
+                        dividerColor = textColor.alpha(0.3f),
                         onClick = {
                             onAppClick(item)
                         },

+ 8 - 1
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenu.kt

@@ -16,6 +16,7 @@ import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.WindowPosition
+import eu.mjdev.desktop.components.blur.BlurPanel
 import eu.mjdev.desktop.components.sliding.base.VisibilityState
 import eu.mjdev.desktop.components.sliding.base.VisibilityState.Companion.rememberVisibilityState
 import eu.mjdev.desktop.components.user.UserAvatar
@@ -87,7 +88,9 @@ fun AppsMenu(
         )
     }
     val windowState: ChromeWindowState = rememberChromeWindowState(position = position)
-    globalKeyEventHandler(menuState.isVisible && menuState.enabled) {
+    globalKeyEventHandler(
+        isEnabled = { menuState.isVisible && menuState.enabled }
+    ) {
         onEscape {
             menuState.hide()
             true
@@ -136,6 +139,10 @@ fun AppsMenu(
                     windowState.requestFocus()
                 }
         ) {
+            BlurPanel(
+                modifier = Modifier.fillMaxSize()
+                    .clip(RoundedCornerShape(24.dp))
+            )
             Box(
                 modifier = Modifier
                     .fillMaxSize()

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenuApp.kt

@@ -60,14 +60,14 @@ fun AppsMenuApp(
                 Modifier.padding(4.dp).fillMaxWidth()
             ) {
                 TextAny(
-                    text = (app?.name ?: app?.fullAppName ?: "").ifEmpty { "-" },
+                    text = (app?.name ?: app?.fullAppName.orEmpty()).ifEmpty { "-" },
                     color = textColor,
                     singleLine = true,
                     fontSize = titleTextSize,
                     fontWeight = FontWeight.Bold
                 )
                 TextAny(
-                    text = (app?.comment ?: "").ifEmpty { "-" },
+                    text = (app?.comment.orEmpty()).ifEmpty { "-" },
                     color = textColor,
                     singleLine = true,
                     fontSize = commentTextSize,

+ 4 - 2
src/main/kotlin/eu/mjdev/desktop/components/background/BackgroundImage.kt

@@ -5,9 +5,11 @@ import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.*
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import eu.mjdev.desktop.components.image.ImageAny
 import eu.mjdev.desktop.extensions.Compose.Crossfade
+import eu.mjdev.desktop.extensions.Compose.SuperDarkGray
 import eu.mjdev.desktop.extensions.Compose.launchedEffect
 import eu.mjdev.desktop.helpers.internal.Queue
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
@@ -31,11 +33,11 @@ fun BackgroundImage(
         modifier = modifier.background(backgroundColor)
     ) {
         Crossfade(
-            targetState = currentBackground,
+            targetState = currentBackground ?: Color.SuperDarkGray,
             fadeInDuration = fadeInDuration,
             fadeOutDuration = fadeOutDuration
         ) { value ->
-            api.scope.launch(Dispatchers.IO) {
+            scope.launch(Dispatchers.IO) {
                 onChange(currentBackground)
             }
             ImageAny(

+ 49 - 0
src/main/kotlin/eu/mjdev/desktop/components/blur/BlurPanel.kt

@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.blur
+
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.fillMaxSize
+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.draw.blur
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import eu.mjdev.desktop.components.image.ImageAny
+import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
+
+// todo real blur
+@Composable
+fun BlurPanel(
+    modifier: Modifier = Modifier,
+    resourceId: String = "blur4.png",
+    contentAlignment: Alignment = Alignment.TopStart,
+    propagateMinConstraints: Boolean = false,
+    blurRadius: Dp = 20.dp,
+    alpha: Float = 0.5f,
+    content: @Composable BoxScope.() -> Unit = {}
+) = withDesktopScope {
+    BoxWithConstraints(
+        modifier = modifier,
+        contentAlignment = contentAlignment,
+        propagateMinConstraints = propagateMinConstraints
+    ) {
+        ImageAny(
+            modifier = modifier.fillMaxSize().alpha(alpha).blur(blurRadius),
+            src = painterResource("images/$resourceId"),
+            contentScale = if (maxHeight > maxWidth) ContentScale.FillHeight else ContentScale.FillWidth
+        )
+        content()
+    }
+}

+ 16 - 9
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/ControlCenter.kt

@@ -12,7 +12,10 @@ import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.Divider
 import androidx.compose.material.Icon
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -21,6 +24,7 @@ import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.WindowPosition
+import eu.mjdev.desktop.components.blur.BlurPanel
 import eu.mjdev.desktop.components.controlcenter.base.PageHeader
 import eu.mjdev.desktop.components.sliding.SlidingMenu
 import eu.mjdev.desktop.components.sliding.base.VisibilityState
@@ -31,6 +35,7 @@ import eu.mjdev.desktop.extensions.Compose.onRightClick
 import eu.mjdev.desktop.extensions.Compose.rememberCalculated
 import eu.mjdev.desktop.extensions.Compose.rememberComputed
 import eu.mjdev.desktop.extensions.Compose.rememberState
+import eu.mjdev.desktop.extensions.Compose.runAsync
 import eu.mjdev.desktop.helpers.animation.Animations.ControlCenterEnterAnimation
 import eu.mjdev.desktop.helpers.animation.Animations.ControlCenterExitAnimation
 import eu.mjdev.desktop.helpers.internal.KeyEventHandler.Companion.globalKeyEventHandler
@@ -38,15 +43,12 @@ import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 import eu.mjdev.desktop.windows.ChromeWindow
 import eu.mjdev.desktop.windows.ChromeWindowState
 import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberChromeWindowState
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
 
 @Suppress("FunctionName")
 @Composable
 fun ControlCenter(
     pagerState: MutableState<Int> = rememberState(0),
     controlCenterState: VisibilityState = rememberVisibilityState(),
-    scope: CoroutineScope = rememberCoroutineScope(),
     enterAnimation: EnterTransition = ControlCenterEnterAnimation,
     exitAnimation: ExitTransition = ControlCenterExitAnimation,
     onFocusChange: ChromeWindowState.(Boolean) -> Unit = {},
@@ -71,7 +73,9 @@ fun ControlCenter(
         }
     }
     val windowState: ChromeWindowState = rememberChromeWindowState(position = position, size = size)
-    globalKeyEventHandler(controlCenterState.isVisible && controlCenterState.enabled) {
+    globalKeyEventHandler(
+        isEnabled = { controlCenterState.isVisible && controlCenterState.enabled }
+    ) {
         onEscape {
             controlCenterState.hide()
             true
@@ -101,9 +105,12 @@ fun ControlCenter(
                     thickness = controlCenterDividerWidth
                 )
             } else {
+                BlurPanel(
+                    modifier = Modifier.fillMaxHeight()
+                        .matchParentSize()
+                )
                 Box(
-                    modifier = Modifier
-                        .fillMaxHeight()
+                    modifier = Modifier.fillMaxHeight()
                         .wrapContentSize()
                         .background(
                             backgroundColor.copy(alpha = backgroundAlpha)
@@ -142,12 +149,12 @@ fun ControlCenter(
                                             }
                                             .onMousePress {
                                                 onLeftClick {
-                                                    scope.launch {
+                                                    runAsync {
                                                         pagerState.value = idx
                                                     }
                                                 }
                                                 onRightClick {
-                                                    scope.launch {
+                                                    runAsync {
                                                         pagerState.value = idx
                                                     }
                                                     onContextMenuClick()

+ 23 - 2
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/base/ControlCenterPage.kt

@@ -1,8 +1,12 @@
 package eu.mjdev.desktop.components.controlcenter.base
 
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Settings
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
 import eu.mjdev.desktop.components.controlcenter.base.ControlCenterPageScope.Companion.withControlCenterPageScope
 import eu.mjdev.desktop.provider.DesktopProvider
@@ -15,8 +19,25 @@ class ControlCenterPage(
     val saver: IControlCenterPageDataSaver? = null,
     val cache: PageCache = PageCache(saver),
     val showHeader: Boolean = true,
+//    val scrollState: ScrollState = ScrollState(0),
     val content: @Composable ControlCenterPageScope.() -> Unit = {},
 ) {
     @Composable
-    fun render() = withControlCenterPageScope(cache = cache) { content() }
-}
+    fun render() = withControlCenterPageScope(
+        cache = cache
+    ) {
+        Row(
+            modifier = Modifier.fillMaxSize()
+        ) {
+            Box(
+                modifier = Modifier.weight(1f),
+            ) {
+                content()
+            }
+//            VerticalScrollbar(
+//                modifier = Modifier.fillMaxHeight(),
+//                adapter = rememberScrollbarAdapter(scrollState)
+//            )
+        }
+    }
+}

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

@@ -16,17 +16,24 @@ import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.LocalClipboardManager
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
+import eu.mjdev.desktop.components.button.TransparentButton
 import eu.mjdev.desktop.components.controlcenter.base.ControlCenterPage
 import eu.mjdev.desktop.components.controlcenter.base.ControlCenterPageScope.Companion.remember
 import eu.mjdev.desktop.components.input.SelectableOutlineEditText
 import eu.mjdev.desktop.components.text.TextAny
 import eu.mjdev.desktop.extensions.Compose.rememberState
+import eu.mjdev.desktop.extensions.Compose.scrollWithAnimToLast
+import eu.mjdev.desktop.extensions.Compose.verticalTouchScrollable
+import eu.mjdev.desktop.extensions.Custom.replaceLast
+import eu.mjdev.desktop.extensions.Modifier.clipCircle
+import eu.mjdev.desktop.helpers.compose.rememberForeverLazyListState
 import eu.mjdev.desktop.icons.Icons
 
-// todo copy only selected text to clipboard
 @OptIn(ExperimentalFoundationApi::class)
 @Suppress("FunctionName")
 fun AIPage() = ControlCenterPage(
@@ -34,7 +41,8 @@ fun AIPage() = ControlCenterPage(
     name = "Assistant",
     condition = { ai.isAvailable() }
 ) {
-    val clipboardManager = LocalClipboardManager.current
+    val scrollState = rememberForeverLazyListState("Assistant")
+    val clipboardManager: ClipboardManager = LocalClipboardManager.current
     val questionsList = remember { mutableStateListOf<Pair<String, String>>() }
     val request = rememberState("")
     val iconSend = androidx.compose.runtime.remember {
@@ -47,13 +55,14 @@ fun AIPage() = ControlCenterPage(
         if (what.isNotEmpty()) {
             questionsList.add(Pair(what, ""))
             request.value = ""
+            scrollState.scrollWithAnimToLast(scope)
             api.ai.ask(what) { _, res ->
                 res.replace("* **", "")
                     .replace("**", "")
                     .trim()
                     .let { resx ->
-                        questionsList.removeIf { it.first == what }
-                        questionsList.add(Pair(what, resx))
+                        questionsList.replaceLast(Pair(what, resx))
+                        scrollState.scrollWithAnimToLast(scope)
                         talk(resx)
                     }
             }
@@ -61,7 +70,6 @@ fun AIPage() = ControlCenterPage(
     }
     Box(
         modifier = Modifier.fillMaxSize()
-            .background(backgroundColor)
     ) {
         Row {
             Column(
@@ -74,7 +82,8 @@ fun AIPage() = ControlCenterPage(
                         .background(
                             Color.Black.copy(alpha = 0.3f),
                             RoundedCornerShape(8.dp)
-                        )
+                        ).verticalTouchScrollable(scrollState),
+                    state = scrollState
                 ) {
                     itemsIndexed(questionsList) { idx, textData ->
                         Column(
@@ -86,60 +95,15 @@ fun AIPage() = ControlCenterPage(
                                 )
                                 .padding(4.dp),
                         ) {
-                            Box(
-                                Modifier.padding(4.dp)
-                                    .fillMaxWidth()
-                                    .background(
-                                        Color.White.copy(alpha = 0.1f),
-                                        RoundedCornerShape(8.dp)
-                                    )
-                                    .padding(8.dp)
-                            ) {
-                                TextAny(
-                                    modifier = Modifier.fillMaxWidth().padding(end = 24.dp, top = 6.dp),
-                                    text = "${idx + 1}. ${textData.first}",
-                                    color = Color.White,
-                                    textSelectionEnabled = true
-                                )
-                                Icon(
-                                    modifier = Modifier.padding(4.dp)
-                                        .size(24.dp)
-                                        .align(Alignment.TopEnd)
-                                        .onClick {
-                                            clipboardManager.setText(AnnotatedString(textData.first))
-                                        },
-                                    imageVector = Icons.CopyToClipboard,
-                                    tint = Color.White.copy(alpha = 0.9f),
-                                    contentDescription = ""
-                                )
-                            }
-                            Box(
-                                modifier = Modifier.padding(4.dp)
-                                    .fillMaxWidth()
-                                    .background(
-                                        Color.White.copy(alpha = 0.1f),
-                                        RoundedCornerShape(8.dp)
-                                    )
-                                    .padding(8.dp),
-                            ) {
-                                TextAny(
-                                    modifier = Modifier.fillMaxWidth().padding(end = 24.dp, top = 6.dp),
-                                    text = textData.second,
-                                    color = Color.White.copy(alpha = 0.9f),
-                                    textSelectionEnabled = true
-                                )
-                                Icon(
-                                    modifier = Modifier.padding(4.dp)
-                                        .size(24.dp)
-                                        .align(Alignment.TopEnd)
-                                        .onClick {
-                                            clipboardManager.setText(AnnotatedString(textData.second))
-                                        },
-                                    imageVector = Icons.CopyToClipboard,
-                                    tint = Color.White.copy(alpha = 0.9f),
-                                    contentDescription = ""
-                                )
-                            }
+                            TextBlock(
+                                idx = idx,
+                                text = textData.first,
+                                clipboardManager = clipboardManager
+                            )
+                            TextBlock(
+                                text = textData.second,
+                                clipboardManager = clipboardManager
+                            )
                         }
                     }
                 }
@@ -197,6 +161,47 @@ fun AIPage() = ControlCenterPage(
     }
 }
 
+@Composable
+fun TextBlock(
+    idx: Int = -1,
+    text: String = "",
+    clipboardManager: ClipboardManager = LocalClipboardManager.current
+) {
+    Box(
+        Modifier.padding(4.dp)
+            .fillMaxWidth()
+            .background(
+                Color.White.copy(alpha = 0.1f),
+                RoundedCornerShape(8.dp)
+            )
+            .padding(8.dp)
+    ) {
+        TextAny(
+            modifier = Modifier.fillMaxWidth()
+                .padding(end = 24.dp),
+            text = "${if (idx >= 0) "${idx + 1}. " else ""}${text}",
+            color = Color.White,
+            textAlign = TextAlign.Start,
+            textSelectionEnabled = true
+        )
+        TransparentButton(
+            modifier = Modifier.size(32.dp)
+                .clipCircle()
+                .align(Alignment.TopEnd),
+            onClick = {
+                clipboardManager.setText(AnnotatedString(text))
+            },
+        ) {
+            Icon(
+                modifier = Modifier.size(24.dp),
+                imageVector = Icons.CopyToClipboard,
+                tint = Color.White.copy(alpha = 0.9f),
+                contentDescription = ""
+            )
+        }
+    }
+}
+
 @Preview
 @Composable
 fun AIPagePreview() = AIPage().render()

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

@@ -16,6 +16,7 @@ import eu.mjdev.desktop.components.custom.DateTime
 import eu.mjdev.desktop.components.custom.PowerBlock
 import eu.mjdev.desktop.components.user.UserAvatar
 import eu.mjdev.desktop.extensions.ColorUtils.alpha
+import eu.mjdev.desktop.extensions.Compose.runAsync
 import eu.mjdev.desktop.extensions.Modifier.rectShadow
 import eu.mjdev.desktop.helpers.compose.Orientation
 
@@ -96,7 +97,9 @@ fun MainSettingsPage() = ControlCenterPage(
                     iconTintColor = iconsTintColor,
                     onPowerButtonClick = {
                         // todo : dialog
-                        api.logOut()
+                        runAsync {
+                            api.logOut()
+                        }
                     }
                 )
             }

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

@@ -1,22 +1,97 @@
 package eu.mjdev.desktop.components.controlcenter.pages
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Wifi
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
 import eu.mjdev.desktop.components.controlcenter.base.ControlCenterPage
+import eu.mjdev.desktop.components.text.TextAny
+import eu.mjdev.desktop.extensions.Compose.launchedEffect
+import eu.mjdev.desktop.extensions.Compose.rememberState
+import eu.mjdev.desktop.helpers.system.Shell
+import eu.mjdev.desktop.helpers.system.Shell.Companion.CMD_NMCLI
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
 
+// nmcli -t -f ALL dev wifi
+// nmcli device show
 @Suppress("FunctionName")
 fun WifiSettingsPage() = ControlCenterPage(
     icon = Icons.Filled.Wifi,
     name = "Wifi",
     condition = { connectionManager.isWifiAdapterAvailable }
 ) {
+    var lastSeen by rememberState(0L)
+    val list by flow<List<List<String>>> {
+        Shell.executeAndReadLines(
+            CMD_NMCLI, "-t", " -f", "ALL", "dev", "wifi"
+        ).map { ws ->
+            println(ws)
+            ws.split(":")
+        }
+    }.flowOn(Dispatchers.IO).collectAsState(emptyList())
     Box(
         modifier = Modifier.fillMaxSize()
+    ) {
+        LazyColumn(
+            modifier = Modifier.fillMaxWidth()
+        ) {
+            list.forEach { wr ->
+                item {
+                    WifiRow(wr)
+                }
+            }
+        }
+    }
+    launchedEffect(lastSeen) {
+        lastSeen = System.currentTimeMillis()
+        delay(500L)
+    }
+}
+
+// 0  : AP[n]
+// 1  : ssid
+// 2  : ? 57692D737472656C
+// 3  : IPV6 or IP
+// 4  : ? Infra
+// 5  : freq type
+// 6  : speed
+// 7  : freq
+// 8  : level in db
+// 9  : level semi-graph
+// 10 : encryption
+// 11 : ?
+// 12 : device
+// 14 : ?
+// 15 : connected [*  || ""]
+// 16 : config point
+@Composable
+fun WifiRow(item: List<String>) = Row(
+    modifier = Modifier.fillMaxWidth(),
+) {
+    TextAny(
+        modifier = Modifier.weight(1f),
+        text = item.getOrNull(1).orEmpty(),
+        color = Color.White,
+        textAlign = TextAlign.Start,
+        textSelectionEnabled = true
+    )
+    TextAny(
+        modifier = Modifier.wrapContentWidth(),
+        text = item.getOrNull(9).orEmpty(),
+        color = Color.White,
+        textAlign = TextAlign.Start,
+        textSelectionEnabled = true
     )
 }
 

+ 8 - 5
src/main/kotlin/eu/mjdev/desktop/components/custom/Clock.kt

@@ -5,7 +5,10 @@ import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.onClick
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -15,11 +18,11 @@ import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.sp
 import eu.mjdev.desktop.components.text.TextWithShadow
 import eu.mjdev.desktop.extensions.Compose.rememberState
+import eu.mjdev.desktop.extensions.Compose.runAsync
 import eu.mjdev.desktop.extensions.Custom.ParsedList
 import eu.mjdev.desktop.extensions.Custom.dateFlow
 import eu.mjdev.desktop.extensions.Custom.timeFlow
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
-import kotlinx.coroutines.launch
 
 // todo remove params
 @OptIn(ExperimentalFoundationApi::class)
@@ -40,7 +43,7 @@ fun Clock(
     val time = timeFlow.value
     var lastTalk by rememberState("")
     val talk: () -> Unit = {
-        scope.launch {
+        runAsync {
             lastTalk = time
             val parsed = ParsedList(time, ":")
             ai.talk(
@@ -65,7 +68,7 @@ fun Clock(
                 fontWeight = timeTextWeight,
                 fontSize = timeTextSize,
                 color = timeTextColor,
-               singleLine = true
+                singleLine = true
             )
             if (showDate) TextWithShadow(
                 text = dateFlow.value,
@@ -73,7 +76,7 @@ fun Clock(
                 fontWeight = dateTextWeight,
                 fontSize = dateTextSize,
                 color = dateTextColor,
-               singleLine = true
+                singleLine = true
             )
         }
         LaunchedEffect(time) {

+ 3 - 9
src/main/kotlin/eu/mjdev/desktop/components/custom/VerticalScrollableBox.kt

@@ -8,7 +8,6 @@
 
 package eu.mjdev.desktop.components.custom
 
-import androidx.compose.foundation.gestures.detectVerticalDragGestures
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.lazy.LazyListState
@@ -16,23 +15,18 @@ import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.pointer.pointerInput
 import eu.mjdev.desktop.extensions.Compose.isLandscapeMode
+import eu.mjdev.desktop.extensions.Compose.verticalTouchScrollable
 
 @Composable
 fun VerticalScrollableBox(
     modifier: Modifier = Modifier,
     contentAlignment: Alignment = Alignment.TopStart,
     propagateMinConstraints: Boolean = isLandscapeMode(),
-    state: LazyListState? = rememberLazyListState(),
+    state: LazyListState = rememberLazyListState(),
     content: @Composable BoxScope.() -> Unit = {}
 ) = Box(
-    modifier = modifier
-        .pointerInput(Unit) {
-            detectVerticalDragGestures { _, dragAmount ->
-                state?.dispatchRawDelta(-dragAmount)
-            }
-        },
+    modifier = modifier.verticalTouchScrollable(state),
     contentAlignment = contentAlignment,
     propagateMinConstraints = propagateMinConstraints,
 ) {

+ 61 - 53
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanel.kt

@@ -23,6 +23,7 @@ import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.WindowPosition
+import eu.mjdev.desktop.components.blur.BlurPanel
 import eu.mjdev.desktop.components.desktoppanel.applets.DesktopMenuIcon
 import eu.mjdev.desktop.components.desktoppanel.applets.DesktopPanelDateTime
 import eu.mjdev.desktop.components.desktoppanel.applets.DesktopPanelFavoriteApps
@@ -86,7 +87,9 @@ fun DesktopPanel(
         position = position, size = panelSize
     )
     panelState.updateSize(panelSize - DpSize(0.dp, tooltipState.tooltipHeight))
-    globalKeyEventHandler(panelState.isVisible && panelState.enabled) {
+    globalKeyEventHandler(
+        isEnabled = { panelState.isVisible && panelState.enabled }
+    ) {
         onEscape {
             panelState.hide()
             true
@@ -140,70 +143,75 @@ fun DesktopPanel(
                             modifier = Modifier.fillMaxWidth()
                                 .height(tooltipState.tooltipHeight)
                         )
-                        Column(
+                        BlurPanel(
                             modifier = Modifier.fillMaxWidth()
                                 .wrapContentHeight()
-                                .background(
-                                    brush = Brush.verticalGradient(
-                                        colors = listOf(
-                                            backgroundColor.alpha(0.3f),
-                                            backgroundColor.alpha(0.7f),
-                                            backgroundColor.alpha(0.8f),
-                                        )
-                                    )
-                                ).topShadow(
-                                    color = borderColor.alpha(0.3f),
-                                    blur = 4.dp
-                                ).onPlaced(panelState::onPlaced),
                         ) {
-                            Divider(
+                            Column(
                                 modifier = Modifier.fillMaxWidth()
-                                    .height(2.dp),
-                                color = borderColor,
-                                thickness = 2.dp
-                            )
-                            Box(
-                                modifier = Modifier.fillMaxWidth()
-                                    .padding(top = 4.dp)
-                                    .padding(theme.panelContentPadding)
+                                    .wrapContentHeight()
+                                    .background(
+                                        brush = Brush.verticalGradient(
+                                            colors = listOf(
+                                                backgroundColor.alpha(0.3f),
+                                                backgroundColor.alpha(0.7f),
+                                                backgroundColor.alpha(0.8f),
+                                            )
+                                        )
+                                    ).topShadow(
+                                        color = borderColor.alpha(0.3f),
+                                        blur = 4.dp
+                                    ).onPlaced(panelState::onPlaced),
                             ) {
-                                if (showMenuIcon) {
-                                    DesktopMenuIcon(
-                                        modifier = Modifier.align(Alignment.CenterStart),
+                                Divider(
+                                    modifier = Modifier.fillMaxWidth()
+                                        .height(2.dp),
+                                    color = borderColor,
+                                    thickness = 2.dp
+                                )
+                                Box(
+                                    modifier = Modifier.fillMaxWidth()
+                                        .padding(top = 4.dp)
+                                        .padding(theme.panelContentPadding)
+                                ) {
+                                    if (showMenuIcon) {
+                                        DesktopMenuIcon(
+                                            modifier = Modifier.align(Alignment.CenterStart),
+                                            iconColor = borderColor,
+                                            iconBackgroundColor = iconsTintColor,
+                                            iconSize = iconSize,
+                                            iconPadding = iconPadding,
+                                            iconOuterPadding = iconOuterPadding,
+                                            onTooltip = { item -> tooltipState.show(item) },
+                                            onClick = { onMenuIconClicked() },
+                                            onContextMenuClick = onMenuIconContextMenuClicked
+                                        )
+                                    }
+                                    DesktopPanelFavoriteApps(
+                                        modifier = Modifier.align(Alignment.Center),
                                         iconColor = borderColor,
                                         iconBackgroundColor = iconsTintColor,
+                                        iconColorRunning = iconsTintColor.lighter(0.3f),
                                         iconSize = iconSize,
                                         iconPadding = iconPadding,
                                         iconOuterPadding = iconOuterPadding,
                                         onTooltip = { item -> tooltipState.show(item) },
-                                        onClick = { onMenuIconClicked() },
-                                        onContextMenuClick = onMenuIconContextMenuClicked
-                                    )
-                                }
-                                DesktopPanelFavoriteApps(
-                                    modifier = Modifier.align(Alignment.Center),
-                                    iconColor = borderColor,
-                                    iconBackgroundColor = iconsTintColor,
-                                    iconColorRunning = iconsTintColor.lighter(0.3f),
-                                    iconSize = iconSize,
-                                    iconPadding = iconPadding,
-                                    iconOuterPadding = iconOuterPadding,
-                                    onTooltip = { item -> tooltipState.show(item) },
-                                    onAppClick = { app -> onAppClick(app) },
-                                    onContextMenuClick = onAppContextMenuClick
-                                )
-                                DesktopPanelTray(
-                                    modifier = Modifier.align(Alignment.CenterEnd)
-                                        .padding(end = 16.dp),
-                                ) {
-                                    DesktopPanelLanguage(
-                                        onTooltip = { item -> tooltipState.show(item) },
-                                        onClick = onLanguageClick
-                                    )
-                                    DesktopPanelDateTime(
-                                        onTooltip = { item -> tooltipState.show(item) },
-                                        onClick = onClockClick
+                                        onAppClick = { app -> onAppClick(app) },
+                                        onContextMenuClick = onAppContextMenuClick
                                     )
+                                    DesktopPanelTray(
+                                        modifier = Modifier.align(Alignment.CenterEnd)
+                                            .padding(end = 16.dp),
+                                    ) {
+                                        DesktopPanelLanguage(
+                                            onTooltip = { item -> tooltipState.show(item) },
+                                            onClick = onLanguageClick
+                                        )
+                                        DesktopPanelDateTime(
+                                            onTooltip = { item -> tooltipState.show(item) },
+                                            onClick = onClockClick
+                                        )
+                                    }
                                 }
                             }
                         }

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/file/FolderIcon.kt

@@ -64,7 +64,7 @@ fun FolderIcon(
             TextWithShadow(
                 modifier = Modifier.width(computedSize.width)
                     .padding(start = 4.dp),
-                text = customName ?: path.name ?: "",
+                text = (customName ?: path.name).orEmpty(),
                 color = textColor,
                 fontWeight = FontWeight.Bold,
                 overflow = Ellipsis,

+ 1 - 0
src/main/kotlin/eu/mjdev/desktop/components/file/FolderView.kt

@@ -36,6 +36,7 @@ import kotlin.math.max
 // ShellFolderManager
 // X11GraphicsEnvironment
 
+// todo orientation
 @Suppress("FunctionName")
 @Composable
 fun FolderView(

+ 7 - 1
src/main/kotlin/eu/mjdev/desktop/components/greeter/Greeter.kt

@@ -19,9 +19,11 @@ import eu.mjdev.desktop.components.user.UserAvatar
 import eu.mjdev.desktop.data.User
 import eu.mjdev.desktop.extensions.ColorUtils.alpha
 import eu.mjdev.desktop.extensions.Compose.rememberState
+import eu.mjdev.desktop.extensions.Compose.runAsync
 import eu.mjdev.desktop.helpers.compose.Orientation
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 import eu.mjdev.desktop.windows.ChromeWindow
+import kotlinx.coroutines.flow.collectLatest
 
 // todo : all users
 @Suppress("FunctionName", "unused")
@@ -31,7 +33,11 @@ fun Greeter() = withDesktopScope {
     var isUserLoggedIn by rememberState(user.isLoggedIn)
     var passwordVisible by rememberState(false)
     val onDone: (password: String) -> Unit = { password ->
-        isUserLoggedIn = user.login(api, password)
+        runAsync {
+            user.login(api, password).collectLatest { logState ->
+                isUserLoggedIn = logState
+            }
+        }
     }
     ChromeWindow(
         position = WindowPosition.Aligned(Alignment.Center),

+ 1 - 0
src/main/kotlin/eu/mjdev/desktop/components/surface/Surface.kt

@@ -182,6 +182,7 @@ private fun SurfaceImpl(
     }
 }
 
+// todo elevation
 @Composable
 fun surfaceColorAtElevation(
     color: Color,

+ 17 - 17
src/main/kotlin/eu/mjdev/desktop/data/App.kt

@@ -20,31 +20,31 @@ class App(
 
     val file: File get() = desktopFile.file
 
-    val fileName
+    val fileName: String
         get() = desktopFile.fileName
-    val fullAppName
+    val fullAppName: String
         get() = desktopFile.fullAppName
-    val type
+    val type: DesktopFile.DesktopEntryType
         get() = desktopFile.desktopSection?.Type ?: DesktopFile.DesktopEntryType.Application
-    val version
-        get() = desktopFile.desktopSection?.Version ?: ""
-    val name
+    val version: String
+        get() = desktopFile.desktopSection?.Version.orEmpty()
+    val name: String
         get() = desktopFile.desktopSection?.Name ?: fullAppName
-    val comment
+    val comment: String
         get() = desktopFile.desktopSection?.Comment ?: fullAppName
-    val path
-        get() = desktopFile.desktopSection?.Path ?: ""
-    val exec
-        get() = desktopFile.desktopSection?.Exec ?: ""
-    val iconName
+    val path: String
+        get() = desktopFile.desktopSection?.Path.orEmpty()
+    val exec: String
+        get() = desktopFile.desktopSection?.Exec.orEmpty()
+    val iconName: String
         get() = desktopFile.desktopSection?.Icon ?: fullAppName
-    val categories
-        get() = desktopFile.desktopSection?.Categories.ifEmptyCategories { listOf(Category.UNCATEGORIZED) }
-    val notifyOnStart
+    val categories: List<String>
+        get() = desktopFile.desktopSection?.Categories.orEmpty().ifEmptyCategories { listOf(Category.UNCATEGORIZED) }
+    val notifyOnStart: Boolean
         get() = desktopFile.desktopSection?.NotifyOnStart ?: false
-    val runInTerminal
+    val runInTerminal: Boolean
         get() = desktopFile.desktopSection?.RunInTerminal ?: false
-    val windowClass
+    val windowClass: String
         get() = desktopFile.desktopSection?.StartupWMClass.orEmpty().ifEmpty { fullAppName }
 
     val cmd

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/data/Theme.kt

@@ -112,7 +112,7 @@ class Theme(
             controlCenterDividerWidth = 4.dp,
             controlCenterIconColor = Color.White,
             controlCenterIconSize = DpSize(32.dp, 32.dp),
-            controlCenterBackgroundAlpha = 0.8f,
+            controlCenterBackgroundAlpha = 0.6f,
 
             appMenuMinWidth = 480.dp,
             appMenuMinHeight = 640.dp,

+ 4 - 3
src/main/kotlin/eu/mjdev/desktop/data/User.kt

@@ -5,6 +5,7 @@ import eu.mjdev.desktop.helpers.system.Shell
 import eu.mjdev.desktop.helpers.system.UserDirs
 import eu.mjdev.desktop.icons.Icons
 import eu.mjdev.desktop.provider.DesktopProvider
+import kotlinx.coroutines.flow.Flow
 import java.io.File
 
 @Suppress("MemberVisibilityCanBePrivate", "unused")
@@ -45,9 +46,9 @@ class User(
         return userName
     }
 
-    fun login(
+    suspend fun login(
         api: DesktopProvider, password: String
-    ): Boolean = api.login(userName, password)
+    ): Flow<Boolean> = api.login(userName, password)
 
     companion object {
         val Nobody = User(":x:-1:-1:::")
@@ -75,4 +76,4 @@ class User(
                 User(it)
             }
     }
-}
+}

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

@@ -6,7 +6,9 @@ import androidx.compose.animation.*
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.gestures.detectVerticalDragGestures
 import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.relocation.BringIntoViewResponder
 import androidx.compose.foundation.relocation.bringIntoViewResponder
 import androidx.compose.material.ButtonDefaults
@@ -41,10 +43,12 @@ import coil3.svg.SvgDecoder
 import eu.mjdev.desktop.helpers.compose.FocusHelper
 import eu.mjdev.desktop.provider.DesktopProvider
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
+import eu.mjdev.desktop.provider.DesktopScope
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import kotlin.coroutines.CoroutineContext
 
 object Compose {
 
@@ -445,6 +449,39 @@ object Compose {
         initial: DpSize = DpSize.Zero
     ) = remember { mutableStateOf(initial) }
 
+    fun LazyListState.scrollWithAnimTo(
+        idx: Int,
+        scope: CoroutineScope
+    ) = scope.launch {
+        animateScrollToItem(idx)
+    }
+
+    fun LazyListState.scrollWithAnimToLast(
+        scope: CoroutineScope
+    ) = scrollWithAnimTo(layoutInfo.totalItemsCount, scope)
+
+    fun DesktopScope.runAsync(
+        context: CoroutineContext = Dispatchers.Main,
+        block: suspend () -> Unit
+    ) = scope.launch(context) { block() }
+
+    // todo
+    fun Modifier.verticalTouchScrollable(
+        state: LazyListState
+    ) = this then pointerInput(Unit) {
+        detectVerticalDragGestures { _, dragAmount ->
+            println("drag ammount : $dragAmount")
+            state.dispatchRawDelta(-dragAmount)
+        }
+    }
+
+//    @Composable
+//    fun runAsync(
+//        context: CoroutineContext = Dispatchers.Main,
+//        scope: CoroutineScope = rememberCoroutineScope(),
+//        block: suspend () -> Unit
+//    ) = scope.launch(context) { block() }
+
 //    Toolkit.getDefaultToolkit().addAWTEventListener({ event ->
 //        }, AWTEvent.MOUSE_EVENT_MASK or AWTEvent.FOCUS_EVENT_MASK
 //    )

+ 11 - 7
src/main/kotlin/eu/mjdev/desktop/extensions/Custom.kt

@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.State
 import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.snapshots.SnapshotStateList
 import androidx.compose.ui.graphics.NativePaint
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.DpSize
@@ -23,7 +24,7 @@ import java.util.*
 import java.util.Locale
 import kotlin.jvm.optionals.getOrNull
 
-@Suppress("MemberVisibilityCanBePrivate", "FunctionName")
+@Suppress("MemberVisibilityCanBePrivate", "FunctionName", "unused")
 object Custom {
     val ProcessHandle.command: String
         get() = info().command().getOrNull().orEmpty()
@@ -52,6 +53,10 @@ object Custom {
                     block != Character.UnicodeBlock.SPECIALS
         }
 
+    fun <T> SnapshotStateList<T>.replaceLast(data: T) {
+        this[this.size - 1] = data
+    }
+
     fun File.listFiles(
         ext: String? = null
     ): List<File> = if (exists()) listFiles().let { list ->
@@ -111,16 +116,16 @@ object Custom {
     val File.dirsOnly
         get() = listFiles()?.filter { it.isDirectory }?.toList() ?: emptyList<File>()
 
-    val File.lines
+    val File.lines: List<String>
         get() = runCatching { if (exists()) readLines(Charsets.UTF_8) else null }.getOrNull() ?: emptyList()
 
-    val File.text
-        get() = runCatching { if (exists()) readText(Charsets.UTF_8) else null }.getOrNull() ?: ""
+    val File.text: String
+        get() = runCatching { if (exists()) readText(Charsets.UTF_8) else null }.getOrNull().orEmpty()
 
     val File.textAsLocale: Locale
         get() = if (exists()) readTextAsLocale() else Locale.ENGLISH
 
-    val File.desktopFiles
+    val File.desktopFiles: List<DesktopFile>
         get() = listDesktopFiles()
 
 //    inline fun <T, K> distinctList(
@@ -255,5 +260,4 @@ object Custom {
     fun MutableState<Boolean>.toggle() {
         value = !value
     }
-
-}
+}

+ 24 - 0
src/main/kotlin/eu/mjdev/desktop/extensions/FlippedModifier.kt

@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.extensions
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.DrawModifier
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.scale
+
+class FlippedModifier : DrawModifier {
+    override fun ContentDrawScope.draw() {
+        scale(1f, -1f) {
+            this@draw.drawContent()
+        }
+    }
+}
+
+fun Modifier.flipped() = this.then(FlippedModifier())

+ 15 - 14
src/main/kotlin/eu/mjdev/desktop/helpers/internal/KeyEventHandler.kt

@@ -10,7 +10,7 @@ package eu.mjdev.desktop.helpers.internal
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
-import androidx.compose.ui.input.key.*
+import androidx.compose.ui.input.key.Key
 import eu.mjdev.desktop.extensions.Custom.isPrintable
 import java.awt.AWTEvent
 import java.awt.Toolkit
@@ -19,7 +19,7 @@ import java.awt.event.KeyEvent as AwtKeyEvent
 
 @Suppress("CanBeParameter", "unused", "MemberVisibilityCanBePrivate")
 class KeyEventHandler(
-    private val event: AwtKeyEvent,
+    private val isEnabled: () -> Boolean = { true },
     private val block: KeyEventHandler.() -> Unit
 ) {
     private val listeners = mutableListOf<KeyEventListener>()
@@ -30,6 +30,12 @@ class KeyEventHandler(
 
     init {
         block()
+    }
+
+    fun onEvent(
+        event: AwtKeyEvent
+    ) {
+        if (!isEnabled()) return
         val code = event.keyCode
         val char = parseCharacter(event)
         val isKeyUp = event.id == AwtKeyEvent.KEY_RELEASED
@@ -72,7 +78,6 @@ class KeyEventHandler(
     class KeyEventListener(val key: Key?, val block: (Char) -> Boolean)
 
     class GlobalKeyListener(
-        val enabled: Boolean = true,
         val onKey: (event: AwtKeyEvent) -> Unit
     ) : AWTEventListener {
         init {
@@ -85,26 +90,22 @@ class KeyEventHandler(
 
         override fun eventDispatched(event: AWTEvent?) {
             (event as? AwtKeyEvent)?.also { kev ->
-                if (enabled && !event.isConsumed) onKey(kev)
+                if (!event.isConsumed) onKey(kev)
             }
         }
     }
 
     companion object {
-        fun keyEventHandler(
-            event: AwtKeyEvent,
-            block: KeyEventHandler.() -> Unit
-        ) = KeyEventHandler(event, block)
-
         @Composable
         fun globalKeyEventHandler(
-            enabled: Boolean = true,
-            block: KeyEventHandler.() -> Unit
+            isEnabled: () -> Boolean = { true },
+            block: KeyEventHandler.() -> Unit,
         ) {
-            val globalHandler = GlobalKeyListener(enabled) { event ->
-                keyEventHandler(event, block)
-            }
             DisposableEffect(Unit) {
+                val handler = KeyEventHandler(isEnabled, block)
+                val globalHandler = GlobalKeyListener { event ->
+                    handler.onEvent(event)
+                }
                 onDispose {
                     globalHandler.dispose()
                 }

+ 0 - 10
src/main/kotlin/eu/mjdev/desktop/helpers/system/DBus.kt

@@ -1,10 +0,0 @@
-package eu.mjdev.desktop.helpers.system
-
-class DBus {
-    fun updateEnvironment(): String =
-        Shell.executeAndRead("dbus-update-activation-environment")
-
-    // todo
-    fun send(vararg args: String): String =
-        Shell.executeAndRead("dbus-send", *args)
-}

+ 7 - 1
src/main/kotlin/eu/mjdev/desktop/helpers/system/Environment.kt

@@ -1,9 +1,11 @@
 package eu.mjdev.desktop.helpers.system
 
+import eu.mjdev.desktop.helpers.system.Shell.Companion.CMD_ENV
 import eu.mjdev.desktop.managers.theme.linux.ThemeManagerLinux.Companion.THEME_MJDEV
 
+@Suppress("unused")
 class Environment(
-    val data: List<String> = Shell.executeAndReadLines("env")
+    val data: List<String> = Shell.executeAndReadLines(CMD_ENV)
 ) : HashMap<String, String>() {
     init {
         putAll(System.getenv())
@@ -12,6 +14,10 @@ class Environment(
         put(GTK_THEME, THEME_MJDEV)
     }
 
+    fun toTypedArray() = map { p ->
+        "${p.key}=${p.value}"
+    }.toTypedArray()
+
     companion object {
         const val DBUS_SESSION_BUS_ADDRESS = "DBUS_SESSION_BUS_ADDRESS"
         const val XDG_CURRENT_DESKTOP = "XDG_CURRENT_DESKTOP"

+ 38 - 28
src/main/kotlin/eu/mjdev/desktop/helpers/system/Shell.kt

@@ -4,41 +4,52 @@ import eu.mjdev.desktop.data.App
 import eu.mjdev.desktop.extensions.Custom.consoleOutput
 import eu.mjdev.desktop.helpers.exception.ErrorException.Companion.error
 import eu.mjdev.desktop.helpers.exception.SuccessException.Companion.success
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 
-@Suppress("MemberVisibilityCanBePrivate", "unused")
+@Suppress("MemberVisibilityCanBePrivate", "unused", "RedundantSuspendModifier")
 class Shell(
-    block: ShellScope.() -> Unit
+    val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+    block: suspend ShellScope.() -> Unit
 ) {
     val environment by lazy { Environment() }
 
     init {
-        ShellScope(this).apply(block)
+        scope.launch(Dispatchers.IO) {
+            with(ShellScope(this@Shell)) {
+                block()
+            }
+        }
     }
 
     class ShellScope(
         val shell: Shell,
         val environment: Environment = shell.environment
     ) {
-        fun autoStartApps() = shell.autoStartApps()
+        suspend fun autoStartApps() = shell.autoStartApps()
 
-        fun startApp(
+        suspend fun startApp(
             app: App,
             onStarted: () -> Unit = {},
             onStopped: (e: Throwable) -> Unit = {},
         ) = shell.startApp(app, onStarted, onStopped)
 
-        fun executeAndRead(
+        suspend fun executeAndRead(
             cmd: String,
             vararg args: String
         ): String = executeAndRead(cmd, *args)
 
-        fun executeAndReadLines(
+        suspend fun executeAndReadLines(
             cmd: String,
             vararg args: String
         ): List<String> = executeAndReadLines(cmd, *args)
     }
 
-    fun autoStartApps() = ProcessBuilder("dex", "-a").apply {
+    suspend fun autoStartApps() = ProcessBuilder(
+        CMD_DEX,
+        "-a"
+    ).apply {
         environment().putAll(environment)
     }.also { processBuilder ->
         runCatching {
@@ -49,12 +60,12 @@ class Shell(
         }
     }
 
-    fun startApp(
+    suspend fun startApp(
         app: App,
         onStarted: () -> Unit = {},
         onStopped: (e: Throwable) -> Unit = {},
     ) = ProcessBuilder(
-        "dex",
+        CMD_DEX,
         "-w",
         app.desktopFile.absolutePath
     ).apply {
@@ -78,28 +89,27 @@ class Shell(
     }
 
     companion object {
+
+        const val CMD_NMCLI = "nmcli"
+        const val CMD_ENV = "env"
+        const val CMD_DEX = "dex"
+
         fun executeAndRead(
             cmd: String,
             vararg args: String
-        ): String = Runtime.getRuntime().exec(
-            mutableListOf<String>().apply {
-                add(cmd)
-                addAll(args)
-            }.toTypedArray()
-        ).apply {
-            waitFor()
-        }.inputReader().readText()
+        ): String = runCatching {
+            Runtime.getRuntime().exec(arrayOf(cmd) + args).apply { waitFor() }
+        }.onFailure { e ->
+            e.printStackTrace()
+        }.getOrNull()?.inputReader()?.readText().orEmpty()
 
         fun executeAndReadLines(
             cmd: String,
-            vararg args: String
-        ): List<String> = Runtime.getRuntime().exec(
-            mutableListOf<String>().apply {
-                add(cmd)
-                addAll(args)
-            }.toTypedArray()
-        ).apply {
-            waitFor()
-        }.inputReader().readLines()
+            vararg args: String,
+        ): List<String> = runCatching {
+            Runtime.getRuntime().exec(arrayOf(cmd) + args).apply { waitFor() }
+        }.onFailure { e ->
+            e.printStackTrace()
+        }.getOrNull()?.inputReader()?.readLines() ?: emptyList()
     }
-}
+}

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/helpers/system/UserDirs.kt

@@ -20,7 +20,7 @@ class UserDirs(
         configFileContent.map { line ->
             line.split("=").let { pair -> Pair(pair[0], pair.getOrNull(1)) }
         }.forEach {
-            put(it.first, File(it.second ?: ""))
+            put(it.first, File(it.second.orEmpty()))
         }
     }
 
@@ -51,4 +51,4 @@ class UserDirs(
         const val XDG_PICTURES_DIR = "XDG_PICTURES_DIR"
         const val XDG_VIDEOS_DIR = "XDG_VIDEOS_DIR"
     }
-}
+}

+ 3 - 5
src/main/kotlin/eu/mjdev/desktop/managers/artificialintelligence/actions/base/ActionProviderScope.kt

@@ -8,16 +8,15 @@
 
 package eu.mjdev.desktop.managers.artificialintelligence.actions.base
 
-import eu.mjdev.desktop.provider.DesktopProvider
 import eu.mjdev.desktop.managers.artificialintelligence.actions.base.ActionException.ActionFail
 import eu.mjdev.desktop.managers.artificialintelligence.actions.base.ActionException.ActionSuccess
+import eu.mjdev.desktop.provider.DesktopProvider
 
 class ActionProviderScope(
     private val api: DesktopProvider
 ) {
     // todo actions
-
-    fun open(
+    suspend fun open(
         what: String
     ): ActionException = try {
         api.open(what)
@@ -25,5 +24,4 @@ class ActionProviderScope(
     } catch (e: Exception) {
         ActionFail(e)
     }
-
-}
+}

+ 11 - 2
src/main/kotlin/eu/mjdev/desktop/managers/dbus/DBusManager.kt

@@ -8,9 +8,18 @@
 
 package eu.mjdev.desktop.managers.dbus
 
+import eu.mjdev.desktop.helpers.system.Shell
+
 // todo
-@Suppress("unused")
+@Suppress("unused", "RedundantSuspendModifier")
 class DBusManager {
+    suspend fun updateEnvironment(): String =
+        Shell.executeAndRead("dbus-update-activation-environment")
+
+    // todo
+    suspend fun send(vararg args: String): String =
+        Shell.executeAndRead("dbus-send", *args)
+
 //    val connection by lazy { DBusConnection.getConnection(DBusConnection.DBusBusType.SESSION) }
 //    val settingsInterface by lazy {
 //        connection.getRemoteObject(
@@ -36,4 +45,4 @@ class DBusManager {
 //    interface SettingsInterface : DBusInterface {
 //        fun Read(namespace: String, key: String): Variant<*>
 //    }
-}
+}

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/managers/os/linux/OsRelease.kt

@@ -23,7 +23,7 @@ class OsRelease(
         configFileContent.map {
             it.split("=").let { pair -> Pair(pair[0], pair.getOrNull(1)) }
         }.forEach {
-            put(it.first, it.second ?: "")
+            put(it.first, it.second.orEmpty())
         }
     }
 
@@ -69,4 +69,4 @@ class OsRelease(
         const val UBUNTU_CODENAME = "UBUNTU_CODENAME"
         const val LOGO = "LOGO"
     }
-}
+}

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/managers/theme/base/ThemeManagerStub.kt

@@ -12,9 +12,9 @@ import eu.mjdev.desktop.provider.DesktopProvider
 
 @Suppress("unused")
 open class ThemeManagerStub(
-    private val api: DesktopProvider,
+    val api: DesktopProvider,
 ) {
     open fun createFromPalette() {
         // no op
     }
-}
+}

+ 36 - 30
src/main/kotlin/eu/mjdev/desktop/managers/theme/linux/ThemeManagerLinux.kt

@@ -316,7 +316,7 @@ class ThemeManagerLinux(
 
     fun setColorScheme(
         schemeName: String
-    ) {
+    ) = api.runAsync {
         Shell.executeAndRead(
             "gsettings",
             "set",
@@ -328,41 +328,47 @@ class ThemeManagerLinux(
 
     fun setGTKTheme(
         themeName: String = THEME_YARU
-    ) = Shell.executeAndRead(
-        "gsettings",
-        "set",
-        "org.gnome.desktop.interface",
-        "gtk-theme",
-        themeName
-    )
-
+    ) = api.runAsync {
+        Shell.executeAndRead(
+            "gsettings",
+            "set",
+            "org.gnome.desktop.interface",
+            "gtk-theme",
+            themeName
+        )
+    }
 
-    fun getGTKTheme(): String = Shell.executeAndRead(
-        "gsettings",
-        "get",
-        "org.gnome.desktop.interface",
-        "gtk-theme"
-    ).replace("'", "").trim()
+//    fun getGTKTheme(): String = runAsync { Shell.executeAndRead(
+//        "gsettings",
+//        "get",
+//        "org.gnome.desktop.interface",
+//        "gtk-theme"
+//    ).replace("'", "").trim()
+//    }
 
     fun setIconTheme(
         themeName: String = THEME_YARU
-    ) = Shell.executeAndRead(
-        "gsettings",
-        "set",
-        "org.gnome.desktop.interface",
-        "icon-theme",
-        themeName
-    )
+    ) = api.runAsync {
+        Shell.executeAndRead(
+            "gsettings",
+            "set",
+            "org.gnome.desktop.interface",
+            "icon-theme",
+            themeName
+        )
+    }
 
     fun setSoundTheme(
         themeName: String = THEME_YARU
-    ) = Shell.executeAndRead(
-        "gsettings",
-        "set",
-        "org.gnome.desktop.sound",
-        "gtk-theme",
-        themeName
-    )
+    ) = api.runAsync {
+        Shell.executeAndRead(
+            "gsettings",
+            "set",
+            "org.gnome.desktop.sound",
+            "gtk-theme",
+            themeName
+        )
+    }
 
     fun setDarkColorScheme() =
         setColorScheme(COLOR_SCHEME_PREFER_DARK)
@@ -391,4 +397,4 @@ class ThemeManagerLinux(
             }
         }
     }
-}
+}

+ 3 - 3
src/main/kotlin/eu/mjdev/desktop/managers/window/WindowsManager.kt

@@ -141,11 +141,11 @@ class WindowsManager(
 //            if (id == 0L) continue
 //            Window(id).also { window ->
 //                val pid = getWindowPid(window) ?: 0L
-////                val name = getWindowName(window) ?: ""
-//                val cls = getWindowClass(window) ?: ""
+////                val name = getWindowName(window).orEmpty()
+//                val cls = getWindowClass(window).orEmpty()
 //                val desk = getWindowDesktop(window) ?: 0L
 //                val process = ProcessHandle.allProcesses().toList().firstOrNull { it.pid() == pid }
-////                val icon = getWindowIconName(window) ?: ""
+////                val icon = getWindowIconName(window).orEmpty()
 //                windowList.add(
 //                    SystemWindow(
 //                        id,

+ 42 - 44
src/main/kotlin/eu/mjdev/desktop/provider/DesktopProvider.kt

@@ -6,8 +6,6 @@ import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import coil3.ImageLoader
 import eu.mjdev.desktop.components.controlcenter.pages.*
-import eu.mjdev.desktop.data.App
-import eu.mjdev.desktop.data.DesktopFile
 import eu.mjdev.desktop.data.User
 import eu.mjdev.desktop.extensions.Compose.asyncImageLoader
 import eu.mjdev.desktop.helpers.internal.Palette
@@ -23,15 +21,19 @@ import eu.mjdev.desktop.managers.processes.ProcessManager
 import eu.mjdev.desktop.managers.theme.themeManager
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.launch
 import java.awt.Desktop
 import java.awt.GraphicsDevice
 import java.awt.GraphicsEnvironment
 import java.awt.Toolkit
 import java.io.File
 import java.net.URI
+import kotlin.coroutines.CoroutineContext
 import kotlin.system.exitProcess
 
-@Suppress("unused", "MemberVisibilityCanBePrivate", "SameParameterValue")
+@Suppress("unused", "MemberVisibilityCanBePrivate", "SameParameterValue", "RedundantSuspendModifier")
 class DesktopProvider(
     val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
     val args: List<String> = emptyList(),
@@ -139,100 +141,106 @@ class DesktopProvider(
 //        }
     }
 
-    fun openMail(emailAddress: String) = runCatching {
+    fun runAsync(
+        context: CoroutineContext = Dispatchers.IO,
+        block: suspend () -> Unit
+    ) = scope.launch(context) { block() }
+
+    suspend fun openMail(emailAddress: String) = runCatching {
         desktopUtils.mail(URI.create("mailto:$emailAddress"))
     }
 
-    fun openBrowser(url: String) = runCatching {
+    suspend fun openBrowser(url: String) = runCatching {
         desktopUtils.browse(URI.create(url))
     }
 
-    fun openDirectoryForFile(path: String) = runCatching {
+    suspend fun openDirectoryForFile(path: String) = runCatching {
         desktopUtils.browseFileDirectory(File(path))
     }
 
-    fun moveToTrash(file: File) = runCatching {
+    suspend fun moveToTrash(file: File) = runCatching {
         desktopUtils.moveToTrash(file)
     }
 
-    fun moveToTrash(filePath: String) = runCatching {
+    suspend fun moveToTrash(filePath: String) = runCatching {
         moveToTrash(File(filePath))
     }
 
     // todo, may be need another function
-    fun open(what: String) {
+    suspend fun open(what: String) {
         Shell.executeAndRead("xdg-open", what)
     }
 
-    fun openFileInAssociated(file: File) = runCatching {
+    suspend fun openFileInAssociated(file: File) = runCatching {
         desktopUtils.open(file)
     }
 
-    fun openFileInAssociated(filePath: String) = runCatching {
+    suspend fun openFileInAssociated(filePath: String) = runCatching {
         openFileInAssociated(File(filePath))
     }
 
-//    fun runScript(script: String): Any = runCatching {
-//        scriptEngine.eval(script)
-//    }
-
-//    fun runScript(file: File): Any = runCatching {
-//        scriptEngine.eval(FileReader(file))
-//    }
-
-    fun beep() = runCatching {
+    suspend fun beep() = runCatching {
         toolkit.beep()
     }
 
-    fun sync() = runCatching {
+    suspend fun sync() = runCatching {
         toolkit.sync()
     }
 
-//    fun createCustomCursor(cursor: Image, hotSpot: Point, name: String): Cursor? = runCatching {
+//    suspend fun createCustomCursor(cursor: Image, hotSpot: Point, name: String): Cursor? = runCatching {
 //        toolkit.createCustomCursor(cursor, hotSpot, name)
 //    }.getOrNull()
 
-//    fun createCustomCursor(cursor: Bitmap, hotSpot: Point, name: String): Cursor? = runCatching {
+//    suspend fun createCustomCursor(cursor: Bitmap, hotSpot: Point, name: String): Cursor? = runCatching {
 //        toolkit.createCustomCursor(cursor.image, hotSpot, name)
 //    }.getOrNull()
 
-//    fun createCustomCursor(cursor: ImageBitmap, hotSpot: Point, name: String): Cursor? = runCatching {
+//    suspend fun createCustomCursor(cursor: ImageBitmap, hotSpot: Point, name: String): Cursor? = runCatching {
 //        toolkit.createCustomCursor(cursor.toAwtImage(), hotSpot, name)
 //    }.getOrNull()
 
-    // todo
-//    fun createCustomCursor(cursor: ImageVector, hotSpot: Point, name: String) =
+//    suspend fun createCustomCursor(cursor: ImageVector, hotSpot: Point, name: String) =
 //        toolkit.createCustomCursor(cursor, hotSpot, name)
 
     @Suppress("UNUSED_PARAMETER")
-    fun login(
+    suspend fun login(
         user: String,
         password: String
-    ): Boolean {
+    ): Flow<Boolean> = flow {
         // todo
-        return true
+        emit(true)
     }
 
-    fun lock() {
+    suspend fun lock() {
         Shell.executeAndRead("/usr/bin/loginctl", "lock-sessions")
     }
 
-    fun logOut() {
+    suspend fun logOut() {
         exitProcess(0)
     }
 
-    fun suspend() {
+    suspend fun suspend() {
         Shell.executeAndRead("/usr/bin/systemctl", "suspend")
     }
 
-    fun shutdown() {
+    suspend fun shutdown() {
         Shell.executeAndRead("/usr/sbin/halt", "--poweroff")
     }
 
-    fun restart() {
+    suspend fun restart() {
         Shell.executeAndRead("/usr/sbin/halt", "--reboot")
     }
 
+//    suspend fun Desktop.startApp(app: App) {
+//        startApp(app.desktopFile)
+//    }
+
+//    suspend fun Desktop.startApp(
+//        desktopFile: DesktopFile?
+//    ) = withContext(Dispatchers.IO) {
+//        open(desktopFile?.file)
+//    }
+
     companion object {
         private val CONTROL_CENTER_PAGES = listOf(
             MainSettingsPage(),
@@ -265,15 +273,5 @@ class DesktopProvider(
                 isDebug = isDebug || args.contains("-d")
             )
         }
-
-        fun Desktop.startApp(app: App) {
-            startApp(app.desktopFile)
-        }
-
-        fun Desktop.startApp(
-            desktopFile: DesktopFile?
-        ) {
-            open(desktopFile?.file)
-        }
     }
 }

+ 9 - 0
src/main/kotlin/eu/mjdev/desktop/windows/ChromeWindowState.kt

@@ -12,6 +12,7 @@ import eu.mjdev.desktop.extensions.Compose.launchedEffect
 import eu.mjdev.desktop.extensions.Compose.rememberCalculated
 import eu.mjdev.desktop.extensions.Compose.rememberState
 
+@Suppress("MemberVisibilityCanBePrivate")
 class ChromeWindowState(
     position: WindowPosition = WindowPosition.Aligned(Alignment.Center),
     size: DpSize = DpSize(Dp.Unspecified, Dp.Unspecified),
@@ -23,7 +24,15 @@ class ChromeWindowState(
     var onOpened: MutableList<ChromeWindowState.() -> Unit> = mutableStateListOf()
     var onClosed: MutableList<ChromeWindowState.() -> Unit> = mutableStateListOf()
 
+    private val focusState: MutableState<Boolean> = mutableStateOf(false)
+    var isFocused: Boolean
+        get() = focusState.value
+        internal set(value) {
+            focusState.value = value
+        }
+
     val focusHelper: WindowFocusHelper = WindowFocusHelper { _, focus ->
+        isFocused = focus
         this@ChromeWindowState.onFocusChange.forEach {
             it.invoke(this@ChromeWindowState, focus)
         }

+ 8 - 1
src/main/kotlin/eu/mjdev/desktop/windows/WindowFocusHelper.kt

@@ -35,7 +35,14 @@ class WindowFocusHelper(
     override fun windowStateChanged(e: WindowEvent?) {
         when (e?.newState) {
             null -> Unit
-            WINDOW_CLOSED -> window?.let { unregister(it) }
+            WINDOW_CLOSED -> window?.let {
+                onFocusChange(window, false)
+                unregister(it)
+            }
+
+            WINDOW_GAINED_FOCUS -> onFocusChange(window, true)
+            WINDOW_ICONIFIED -> onFocusChange(window, false)
+            WINDOW_DEICONIFIED -> onFocusChange(window, true)
             WINDOW_ACTIVATED -> onFocusChange(window, true)
             WINDOW_DEACTIVATED -> onFocusChange(window, false)
         }

BIN
src/main/resources/images/blur4.png