Sfoglia il codice sorgente

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements
Milan Jurkulak 2 anni fa
parent
commit
d2f0d145b7
27 ha cambiato i file con 625 aggiunte e 388 eliminazioni
  1. 10 7
      src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenu.kt
  2. 12 19
      src/main/kotlin/eu/mjdev/desktop/components/background/BackgroundImage.kt
  3. 19 14
      src/main/kotlin/eu/mjdev/desktop/components/charts/DonutChart.kt
  4. 3 6
      src/main/kotlin/eu/mjdev/desktop/components/desktop/widgets/MemoryChart.kt
  5. 10 3
      src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanel.kt
  6. 3 5
      src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/applets/DesktopPanelFavoriteApps.kt
  7. 39 28
      src/main/kotlin/eu/mjdev/desktop/components/image/ImageAny.kt
  8. 50 42
      src/main/kotlin/eu/mjdev/desktop/data/App.kt
  9. 44 8
      src/main/kotlin/eu/mjdev/desktop/data/DesktopConfig.kt
  10. 8 5
      src/main/kotlin/eu/mjdev/desktop/data/DesktopFile.kt
  11. 2 3
      src/main/kotlin/eu/mjdev/desktop/data/Theme.kt
  12. 9 10
      src/main/kotlin/eu/mjdev/desktop/data/User.kt
  13. 27 0
      src/main/kotlin/eu/mjdev/desktop/extensions/Compose.kt
  14. 121 22
      src/main/kotlin/eu/mjdev/desktop/extensions/Custom.kt
  15. 9 10
      src/main/kotlin/eu/mjdev/desktop/extensions/Locale.kt
  16. 21 0
      src/main/kotlin/eu/mjdev/desktop/helpers/backgrounds/ProviderErzvo.kt
  17. 34 0
      src/main/kotlin/eu/mjdev/desktop/helpers/backgrounds/ProviderLocal.kt
  18. 28 0
      src/main/kotlin/eu/mjdev/desktop/helpers/backgrounds/ProviderSmug.kt
  19. 17 0
      src/main/kotlin/eu/mjdev/desktop/helpers/internal/ImagesProvider.kt
  20. 3 30
      src/main/kotlin/eu/mjdev/desktop/helpers/internal/Palette.kt
  21. 23 16
      src/main/kotlin/eu/mjdev/desktop/helpers/internal/Queue.kt
  22. 2 4
      src/main/kotlin/eu/mjdev/desktop/helpers/system/MemInfo.kt
  23. 5 3
      src/main/kotlin/eu/mjdev/desktop/helpers/system/OsRelease.kt
  24. 2 2
      src/main/kotlin/eu/mjdev/desktop/helpers/system/Shell.kt
  25. 11 8
      src/main/kotlin/eu/mjdev/desktop/helpers/system/UserDirs.kt
  26. 112 135
      src/main/kotlin/eu/mjdev/desktop/provider/AppsProvider.kt
  27. 1 8
      src/main/kotlin/eu/mjdev/desktop/provider/DesktopScope.kt

+ 10 - 7
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenu.kt

@@ -31,6 +31,7 @@ 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.removeLast
+import eu.mjdev.desktop.extensions.Custom.trimIsNotEmpty
 import eu.mjdev.desktop.extensions.Modifier.dropShadow
 import eu.mjdev.desktop.helpers.animation.Animations.AppsMenuEnterAnimation
 import eu.mjdev.desktop.helpers.animation.Animations.AppsMenuExitAnimation
@@ -53,7 +54,7 @@ fun AppsMenu(
     listState: LazyListState = rememberForeverLazyListState("AppsMenu"),
     onFocusChange: ChromeWindowState.(Boolean) -> Unit = {},
     onAppClick: DesktopScope.(App) -> Unit = { app ->
-        startApp(app)
+        app.start()
         searchTextState.clear()
         menuState.hide()
     },
@@ -62,16 +63,18 @@ fun AppsMenu(
     onUserAvatarClick: () -> Unit = {}
 ) = withDesktopScope {
     var category by rememberState("")
+    val appCategories by appCategories.collectAsState(emptyList())
     val items by rememberComputed(searchTextState.value, category) {
         when {
-            searchTextState.value.isNotEmpty() -> appsProvider.allApps
-                .filter { app ->
-                    app.desktopFile?.fileData?.contains(searchTextState.value) == true
-                }
+            searchTextState.trimIsNotEmpty() -> allApps.filter { app ->
+                app.desktopData.contains(searchTextState.value)
+            }
 
-            category.isNotEmpty() -> appCategoriesAndApps[category]?.sortedBy { app ->
+            category.isNotEmpty() -> allApps.filter { app ->
+                app.categories.contains(category)
+            }.sortedBy { app ->
                 app.name
-            } ?: emptyList<App>()
+            }
 
             else -> appCategories
         }

+ 12 - 19
src/main/kotlin/eu/mjdev/desktop/components/background/BackgroundImage.kt

@@ -1,10 +1,5 @@
 package eu.mjdev.desktop.components.background
 
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
 import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
@@ -12,9 +7,10 @@ import androidx.compose.runtime.*
 import androidx.compose.ui.Modifier
 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.launchedEffect
-import eu.mjdev.desktop.extensions.Compose.rememberCalculated
-import eu.mjdev.desktop.helpers.internal.Queue.Companion.mutableQueue
+import eu.mjdev.desktop.extensions.Compose.rememberComputed
+import eu.mjdev.desktop.helpers.internal.Queue
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
@@ -28,20 +24,17 @@ fun BackgroundImage(
     onError: (Throwable) -> Unit = { e -> e.printStackTrace() },
     onChange: (bck: Any?) -> Unit = {}
 ) = withDesktopScope {
-    val switchDelay by remember { api.currentUser.theme.backgroundRotationDelayState }
-    val userBackgrounds by remember { api.currentUser.config.desktopBackgroundsState }
-    val backgrounds by rememberCalculated { api.appsProvider.backgrounds + userBackgrounds }
-    val backgroundQueue = remember { mutableQueue(backgrounds) }
-    var currentBackground: Any? by remember { mutableStateOf(backgroundQueue.value) }
+    val switchDelay = theme.backgroundRotationDelay
+    val backgrounds by rememberComputed(appsProvider.backgrounds.size) { appsProvider.backgrounds }
+    val backgroundQueue = remember { Queue(backgrounds) }
+    var currentBackground by remember { mutableStateOf(backgroundQueue.nextOrNull()) }
     Box(
         modifier = modifier.background(backgroundColor)
     ) {
-        AnimatedContent(
-            currentBackground,
-            transitionSpec = {
-                fadeIn(animationSpec = tween(durationMillis = fadeInDuration.toInt())) togetherWith
-                        fadeOut(animationSpec = tween(durationMillis = fadeOutDuration.toInt()))
-            }
+        Crossfade(
+            targetState = currentBackground,
+            fadeInDuration = fadeInDuration,
+            fadeOutDuration = fadeOutDuration
         ) { value ->
             api.scope.launch {
                 onChange(currentBackground)
@@ -56,7 +49,7 @@ fun BackgroundImage(
         launchedEffect(currentBackground) {
             if (switchDelay > 0) {
                 delay(switchDelay)
-                currentBackground = backgroundQueue.value
+                currentBackground = backgroundQueue.nextOrNull()
             }
         }
     }

+ 19 - 14
src/main/kotlin/eu/mjdev/desktop/components/charts/DonutChart.kt

@@ -3,17 +3,13 @@ package eu.mjdev.desktop.components.charts
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.TweenSpec
 import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.setValue
+import androidx.compose.runtime.*
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.TextStyle
 import com.aay.compose.baseComponents.model.LegendPosition
 import com.aay.compose.donutChart.DonutChart
 import com.aay.compose.donutChart.model.PieChartData
-import eu.mjdev.desktop.extensions.Compose.rememberState
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.isActive
 
@@ -26,14 +22,27 @@ fun DonutChart(
     outerCircularColor: Color = Color.Black,
     innerCircularColor: Color = Color.Black,
     ratioLineColor: Color = Color.Black,
-    animationDuration: Int = 3000,
+    animationDuration: Int = 1,
     refreshTimeout: Long = 5000L,
     animation: AnimationSpec<Float> = TweenSpec(durationMillis = animationDuration),
     legendPosition: LegendPosition = LegendPosition.BOTTOM,
     dataHandler: () -> List<PieChartData> = { emptyList() }
 ) {
-    var data by rememberState(dataHandler())
-    val textStyle by rememberState(TextStyle(color = textColor))
+    var needRefresh by remember { mutableStateOf(0L) }
+    val textStyle = TextStyle(color = textColor)
+    val data by remember(
+        title,
+        textColor,
+        outerCircularColor,
+        innerCircularColor,
+        ratioLineColor,
+        animationDuration,
+        refreshTimeout,
+        animation,
+        legendPosition,
+        dataHandler,
+        needRefresh
+    ) { mutableStateOf(dataHandler()) }
     DonutChart(
         modifier = modifier,
         pieChartData = data,
@@ -44,17 +53,13 @@ fun DonutChart(
         outerCircularColor = outerCircularColor,
         innerCircularColor = innerCircularColor,
         ratioLineColor = ratioLineColor,
-        animation = animation,
+        animation = TweenSpec(1),
         legendPosition = legendPosition
     )
     LaunchedEffect(Unit) {
         while (isActive) {
-            dataHandler().let { newData ->
-                if (data != newData) {
-                    data = newData
-                }
-            }
             delay(refreshTimeout)
+            needRefresh = System.currentTimeMillis()
         }
     }
 }

+ 3 - 6
src/main/kotlin/eu/mjdev/desktop/components/desktop/widgets/MemoryChart.kt

@@ -4,15 +4,12 @@ import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.TweenSpec
 import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import com.aay.compose.baseComponents.model.LegendPosition
 import com.aay.compose.donutChart.model.PieChartData
 import eu.mjdev.desktop.components.charts.DonutChart
 import eu.mjdev.desktop.components.draggable.DraggableView
-import eu.mjdev.desktop.helpers.internal.Palette.Companion.rememberBackgroundColor
-import eu.mjdev.desktop.helpers.internal.Palette.Companion.rememberIconTintColor
 import eu.mjdev.desktop.helpers.system.MemInfo
 import eu.mjdev.desktop.helpers.system.MemInfo.Companion.toReadable
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
@@ -25,7 +22,8 @@ fun MemoryChart(
     mainTitle: String = "Memory",
     usedTitle: String = "Used",
     freeTitle: String = "Free",
-    animation: AnimationSpec<Float> = TweenSpec(durationMillis = 3000),
+    animationDuration: Int = 1000,
+    animation: AnimationSpec<Float> = TweenSpec(durationMillis = animationDuration),
     legendPosition: LegendPosition = LegendPosition.BOTTOM,
     updateTimeOut: Long = 10000,
     dragEnabled: Boolean = true,
@@ -34,8 +32,6 @@ fun MemoryChart(
         modifier = modifier,
         dragEnabled = dragEnabled
     ) {
-        val backgroundColor by rememberBackgroundColor(api)
-        val iconsTintColor by rememberIconTintColor(api)
         DonutChart(
             modifier = modifier,
             title = mainTitle,
@@ -45,6 +41,7 @@ fun MemoryChart(
             ratioLineColor = ratioLineColor,
             refreshTimeout = updateTimeOut,
             animation = animation,
+            animationDuration = animationDuration,
             legendPosition = legendPosition
         ) {
             MemInfo().let { mem ->

+ 10 - 3
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanel.kt

@@ -10,13 +10,18 @@ import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.Orientation.Vertical
 import androidx.compose.foundation.layout.*
 import androidx.compose.material.Divider
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+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.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.onPlaced
-import androidx.compose.ui.unit.*
+import androidx.compose.ui.unit.Dp
+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.desktoppanel.applets.DesktopMenuIcon
 import eu.mjdev.desktop.components.desktoppanel.applets.DesktopPanelDateTime
@@ -65,7 +70,9 @@ fun DesktopPanel(
     onMenuIconClicked: () -> Unit = {},
     onMenuIconContextMenuClicked: () -> Unit = {},
     onFocusChange: ChromeWindowState.(Boolean) -> Unit = {},
-    onAppClick: DesktopScope.(App) -> Unit = { app -> startApp(app) },
+    onAppClick: DesktopScope.(App) -> Unit = { app ->
+        app.start()
+    },
     onAppContextMenuClick: (App) -> Unit = {},
     onLanguageClick: () -> Unit = {},
     onClockClick: () -> Unit = {}

+ 3 - 5
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/applets/DesktopPanelFavoriteApps.kt

@@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.lazy.LazyRow
 import androidx.compose.foundation.lazy.items
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -16,8 +16,7 @@ import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import eu.mjdev.desktop.components.desktoppanel.DesktopPanelIcon
 import eu.mjdev.desktop.data.App
-import eu.mjdev.desktop.extensions.Custom.invalidate
-import eu.mjdev.desktop.managers.ProcessManager.Companion.processManagerListener
+import eu.mjdev.desktop.provider.AppsProvider.Companion.rememberFavoriteApps
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 
 @Suppress("FunctionName")
@@ -34,8 +33,7 @@ fun DesktopPanelFavoriteApps(
     onAppClick: (app: App) -> Unit = {},
     onContextMenuClick: (app: App) -> Unit = {}
 ) = withDesktopScope {
-    val apps = remember(favoriteApps.size) { favoriteApps }
-    processManagerListener { apps.invalidate() }
+    val apps by rememberFavoriteApps()
     Box(
         modifier = modifier
     ) {

+ 39 - 28
src/main/kotlin/eu/mjdev/desktop/components/image/ImageAny.kt

@@ -1,17 +1,15 @@
 package eu.mjdev.desktop.components.image
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.SupervisedUserCircle
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.graphics.DefaultAlpha
-import androidx.compose.ui.graphics.FilterQuality
-import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.graphics.asComposeImageBitmap
+import androidx.compose.ui.graphics.*
 import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQuality
 import androidx.compose.ui.graphics.painter.Painter
 import androidx.compose.ui.graphics.vector.ImageVector
@@ -19,6 +17,9 @@ import androidx.compose.ui.layout.ContentScale
 import coil3.compose.AsyncImage
 import eu.mjdev.desktop.components.video.VideoView
 import eu.mjdev.desktop.extensions.Compose.asyncImageLoader
+import eu.mjdev.desktop.extensions.Custom.flowBlock
+import eu.mjdev.desktop.helpers.internal.ImagesProvider
+import eu.mjdev.desktop.helpers.internal.ImagesProvider.Companion.getOne
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 import org.jetbrains.skia.Bitmap
 import java.io.File
@@ -39,19 +40,27 @@ fun ImageAny(
     onLoading: () -> Unit = {},
     onFail: (error: Throwable) -> Unit = {}
 ) = withDesktopScope {
+    val image = flowBlock {
+        (src as? ImagesProvider).let { provider -> provider?.getOne() ?: src }
+    }.collectAsState(null)
     // todo from mime
-    val isGif = src.toString().trim().endsWith(".gif")
-    val isVideo = src.toString().trim().let {
+    val isGif = image.value.toString().trim().endsWith(".gif")
+    // todo from mime
+    val isVideo = image.value.toString().trim().let {
         it.endsWith(".mp4") || it.endsWith(".mpg")
     }
     when {
-        src == null -> Box(
+        image.value == null -> Box(
             modifier = modifier
         )
 
-        src is Bitmap -> ComposeImage(
+        image.value is Color -> Box(
+            modifier = modifier.background(image.value as Color)
+        )
+
+        image.value is Painter -> ComposeImage(
             modifier = modifier,
-            bitmap = src.asComposeImageBitmap(),
+            painter = image.value as Painter,
             alignment = alignment,
             alpha = alpha,
             contentDescription = contentDescription,
@@ -59,9 +68,9 @@ fun ImageAny(
             contentScale = contentScale
         )
 
-        src is ImageBitmap -> ComposeImage(
+        image.value is Bitmap -> ComposeImage(
             modifier = modifier,
-            bitmap = src,
+            bitmap = (image.value as Bitmap).asComposeImageBitmap(),
             alignment = alignment,
             alpha = alpha,
             contentDescription = contentDescription,
@@ -69,18 +78,9 @@ fun ImageAny(
             contentScale = contentScale
         )
 
-        isGif -> GifView(
-            modifier = modifier,
-            src = when (src) {
-                is File -> src.absolutePath
-                is String -> src
-                else -> src.toString()
-            }
-        )
-
-        isVideo -> VideoView(
+        image.value is ImageBitmap -> ComposeImage(
             modifier = modifier,
-            src = src,
+            bitmap = image.value as ImageBitmap,
             alignment = alignment,
             alpha = alpha,
             contentDescription = contentDescription,
@@ -88,9 +88,9 @@ fun ImageAny(
             contentScale = contentScale
         )
 
-        src is ImageVector -> ComposeImage(
+        image.value is ImageVector -> ComposeImage(
             modifier = modifier,
-            imageVector = src,
+            imageVector = image.value as ImageVector,
             alignment = alignment,
             alpha = alpha,
             contentDescription = contentDescription,
@@ -98,9 +98,20 @@ fun ImageAny(
             contentScale = contentScale
         )
 
-        src is Painter -> ComposeImage(
+        isGif -> GifView(
+            modifier = modifier,
+            src = when {
+                image.value is File -> (image.value as File).absolutePath
+                else -> image.value.toString()
+            }
+        )
+
+        isVideo -> VideoView(
             modifier = modifier,
-            painter = src,
+            src = when {
+                image.value is File -> (image.value as File).absolutePath
+                else -> image.value.toString()
+            },
             alignment = alignment,
             alpha = alpha,
             contentDescription = contentDescription,
@@ -110,7 +121,7 @@ fun ImageAny(
 
         else -> AsyncImage(
             modifier = modifier,
-            model = src,
+            model = image.value,
             contentDescription = contentDescription,
             contentScale = contentScale,
             filterQuality = filterQuality,

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

@@ -2,50 +2,57 @@ package eu.mjdev.desktop.data
 
 import androidx.compose.runtime.*
 import androidx.compose.ui.graphics.Color
-import eu.mjdev.desktop.extensions.Custom.command
-import eu.mjdev.desktop.extensions.Custom.commandLine
 import eu.mjdev.desktop.helpers.exception.EmptyException.Companion.EmptyException
 import eu.mjdev.desktop.helpers.system.Shell
 import eu.mjdev.desktop.provider.DesktopProvider
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 import java.io.File
 
 @Suppress("unused", "MemberVisibilityCanBePrivate")
 class App(
-    val file: File? = null,
-    val desktopFile: DesktopFile? = runCatching { file?.let { f -> DesktopFile(f) } }.getOrNull(),
+    val desktopFile: DesktopFile = DesktopFile.Empty,
     val isFavorite: Boolean = false
 ) {
+    constructor(file: File) : this(DesktopFile(file))
+
+    val file: File get() = desktopFile.file
+
     val fileName
-        get() = desktopFile?.fileName
+        get() = desktopFile.fileName
     val fullAppName
-        get() = desktopFile?.fullAppName ?: ""
+        get() = desktopFile.fullAppName
     val type
-        get() = desktopFile?.desktopSection?.Type ?: ""
+        get() = desktopFile.desktopSection?.Type ?: ""
     val version
-        get() = desktopFile?.desktopSection?.Version ?: ""
+        get() = desktopFile.desktopSection?.Version ?: ""
     val name
-        get() = desktopFile?.desktopSection?.Name ?: ""
+        get() = desktopFile.desktopSection?.Name ?: ""
     val comment
-        get() = desktopFile?.desktopSection?.Comment ?: ""
+        get() = desktopFile.desktopSection?.Comment ?: ""
     val path
-        get() = desktopFile?.desktopSection?.Path ?: ""
+        get() = desktopFile.desktopSection?.Path ?: ""
     val exec
-        get() = desktopFile?.desktopSection?.Exec ?: ""
+        get() = desktopFile.desktopSection?.Exec ?: ""
     val iconName
-        get() = desktopFile?.desktopSection?.Icon ?: ""
+        get() = desktopFile.desktopSection?.Icon ?: ""
     val categories
-        get() = desktopFile?.desktopSection?.Categories.ifEmptyCategories { listOf(Category.UNCATEGORIZED) }
+        get() = desktopFile.desktopSection?.Categories.ifEmptyCategories { listOf(Category.UNCATEGORIZED) }
     val notifyOnStart
-        get() = desktopFile?.desktopSection?.NotifyOnStart ?: false
+        get() = desktopFile.desktopSection?.NotifyOnStart ?: false
     val runInTerminal
-        get() = desktopFile?.desktopSection?.RunInTerminal ?: false
+        get() = desktopFile.desktopSection?.RunInTerminal ?: false
     val windowClass
-        get() = desktopFile?.desktopSection?.StartupWMClass.orEmpty().ifEmpty { fullAppName }
+        get() = desktopFile.desktopSection?.StartupWMClass.orEmpty().ifEmpty { fullAppName }
 
     val cmd
         get() = exec.split(" ").first()
 
+    val desktopData
+        get() = desktopFile.fileData
+
     var iconTint: Color? = null
     val iconBackground: Color = Color.White
 
@@ -66,25 +73,29 @@ class App(
             isStartingState.value = value
         }
 
-    fun start() = runCatching {
-        println("Starting app: $name [$windowClass].")
-        println("dex -w ${desktopFile?.absolutePath}")
-        triggerStart()
-        Shell {
-            startApp(
-                app = this@App,
-                onStarted = {
-                    println("App started app: $name [$windowClass].")
-                    triggerStarted()
-                },
-                onStopped = { e ->
-                    println("App stopped: $name [$windowClass].")
-                    triggerStop(e)
-                }
-            )
+    fun start(
+        scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
+    ) = scope.launch(Dispatchers.IO) {
+        runCatching {
+            println("Starting app: $name [$windowClass].")
+            println("dex -w ${desktopFile.absolutePath}")
+            triggerStart()
+            Shell {
+                startApp(
+                    app = this@App,
+                    onStarted = {
+                        println("App started app: $name [$windowClass].")
+                        triggerStarted()
+                    },
+                    onStopped = { e ->
+                        println("App stopped: $name [$windowClass].")
+                        triggerStop(e)
+                    }
+                )
+            }
+        }.onFailure { e ->
+            triggerStop(e)
         }
-    }.onFailure { e ->
-        triggerStop(e)
     }
 
     fun triggerStart() {
@@ -124,21 +135,18 @@ class App(
     override fun equals(other: Any?): Boolean {
         return when {
             other is App -> {
-                other.file?.name?.contentEquals(file?.name) ?: false
+                other.fileName.contentEquals(file.name)
             }
 
             else -> false
         }
     }
 
-//    fun hasWindow(api: DesktopProvider): Boolean =
-//        api.windowsManager.hasWindow(this)
-
     override fun hashCode(): Int {
-        var result = file?.hashCode() ?: 0
-        result = 31 * result + (desktopFile?.hashCode() ?: 0)
+        var result = file.hashCode()
+        result = 31 * result + desktopFile.hashCode()
         result = 31 * result + isFavorite.hashCode()
-        result = 31 * result + (fileName?.hashCode() ?: 0)
+        result = 31 * result + fileName.hashCode()
         result = 31 * result + fullAppName.hashCode()
         result = 31 * result + type.hashCode()
         result = 31 * result + version.hashCode()

+ 44 - 8
src/main/kotlin/eu/mjdev/desktop/data/DesktopConfig.kt

@@ -1,22 +1,58 @@
 package eu.mjdev.desktop.data
 
-import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.ui.graphics.Color
+import eu.mjdev.desktop.helpers.backgrounds.ProviderErzvo
+import eu.mjdev.desktop.helpers.backgrounds.ProviderSmug
+import eu.mjdev.desktop.helpers.internal.ImagesProvider
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 
-@Suppress("MemberVisibilityCanBePrivate")
+@Suppress("MemberVisibilityCanBePrivate", "unused")
 class DesktopConfig(
-    desktopBackgroundUrls: List<Any> = emptyList()
+    val block: DesktopConfig.() -> Unit = {}
 ) {
-    val desktopBackgroundsState = mutableStateOf(desktopBackgroundUrls.toMutableList())
-    val desktopBackgrounds get() = desktopBackgroundsState.value
+    val desktopBackgroundsState = mutableStateListOf<Any>()
+    val desktopBackgrounds get() = desktopBackgroundsState
+
+    init {
+        block()
+    }
 
     fun addBackground(path: String) {
         desktopBackgrounds.add(path)
     }
 
+    fun addBackground(color: Color) {
+        desktopBackgrounds.add(color)
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    fun addBackground(
+        scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+        provider: suspend () -> Any?,
+    ) = scope.launch {
+        provider().also { bcks ->
+            when (bcks) {
+                null -> Unit
+                is Collection<*> -> desktopBackgrounds.addAll(bcks as Collection<Any>)
+                else -> desktopBackgrounds.add(bcks)
+            }
+        }
+    }
+
+    private fun addBackground(
+        provider: ImagesProvider
+    ) = addBackground {
+        provider.get()
+    }
+
     companion object {
-        val Default = DesktopConfig().apply {
-            // todo add from url or file
-            addBackground("http://erzvo.com/wp-content/uploads/2022/11/22-11-05-10429-Kubo-kvety-2013-1.jpg")
+        val Default = DesktopConfig {
+            addBackground(ProviderSmug())
+            addBackground(ProviderErzvo())
+//            addBackground(ProviderLocal(api))
         }
     }
 }

+ 8 - 5
src/main/kotlin/eu/mjdev/desktop/data/DesktopFile.kt

@@ -3,6 +3,7 @@ package eu.mjdev.desktop.data
 import eu.mjdev.desktop.extensions.Custom.ParsedBoolean
 import eu.mjdev.desktop.extensions.Custom.ParsedList
 import eu.mjdev.desktop.extensions.Custom.ParsedString
+import eu.mjdev.desktop.extensions.Custom.text
 import eu.mjdev.desktop.helpers.system.LinuxIni
 import org.ini4j.Ini
 import org.ini4j.Profile.Section
@@ -11,12 +12,10 @@ import java.io.File
 
 @Suppress("unused", "MemberVisibilityCanBePrivate", "ConstPropertyName", "PropertyName")
 class DesktopFile(
-    var file: File,
+    var file: File = File(""),
     val correctDir: String = "/tmp/.corrected-desktop-files/",
     val correctedFile: File = File(File(correctDir), file.name),
-    var fileData: String = runCatching {
-        (if (correctedFile.exists()) correctedFile else file).readText()
-    }.getOrNull().orEmpty(),
+    var fileData: String = (if (correctedFile.exists()) correctedFile else file).text,
     var content: Ini = (runCatching { LinuxIni(fileData) }.getOrNull() ?: LinuxIni())
 ) {
     var fileName: String = if (correctedFile.exists()) correctedFile.name else file.name
@@ -54,7 +53,7 @@ class DesktopFile(
                     desktopSection?.OnlyShowIn = mutableListOf()
                     writeTo(f)
                     file = f
-                    fileData = file.readText()
+                    fileData = file.text
                     content = Wini(f)
                 }
             }.onFailure { e ->
@@ -258,6 +257,8 @@ class DesktopFile(
     }
 
     companion object {
+        const val EXTENSION: String = "desktop"
+
         const val Prop_Type = "Type"
         const val Prop_Version = "Version"
         const val Prop_Path = "Path"
@@ -294,6 +295,8 @@ class DesktopFile(
         const val Prop_ButtonLayout = "ButtonLayout"
         const val Prop_X_Ubuntu_UseOverlayScrollbars = "X-Ubuntu-UseOverlayScrollbars"
 
+        val Empty = DesktopFile()
+
         fun desktopFile(
             file: File,
             block: DesktopFile.() -> Unit

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

@@ -5,9 +5,9 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
+import eu.mjdev.desktop.extensions.Compose.SuperDarkGray
 import eu.mjdev.desktop.fonts.MaterialIconFont
 import eu.mjdev.desktop.fonts.MaterialSymbolsSharp
-import eu.mjdev.desktop.extensions.Compose.SuperDarkGray
 
 // todo all customizable
 @Suppress("unused", "MemberVisibilityCanBePrivate")
@@ -97,6 +97,7 @@ class Theme(
                 iconSet = MaterialSymbolsSharp,
 
                 backgroundColor = Color.SuperDarkGray,
+                backgroundRotationDelay = 60000,
 
                 panelLocation = PanelLocation.Bottom,
                 panelDividerWidth = 16.dp,
@@ -112,8 +113,6 @@ class Theme(
                 controlCenterIconSize = DpSize(32.dp, 32.dp),
                 controlCenterBackgroundAlpha = 0.8f,
 
-                backgroundRotationDelay = 60000,
-
                 appMenuMinWidth = 480.dp,
                 appMenuMinHeight = 640.dp,
                 appMenuOuterPadding = 2.dp

+ 9 - 10
src/main/kotlin/eu/mjdev/desktop/data/User.kt

@@ -1,5 +1,6 @@
 package eu.mjdev.desktop.data
 
+import eu.mjdev.desktop.extensions.Custom.lines
 import eu.mjdev.desktop.helpers.system.Shell
 import eu.mjdev.desktop.helpers.system.UserDirs
 import eu.mjdev.desktop.icons.Icons
@@ -44,7 +45,7 @@ class User(
     }
 
     fun login(
-        api:DesktopProvider,
+        api: DesktopProvider,
         password: String
     ): Boolean = api.login(userName, password)
 
@@ -63,17 +64,15 @@ class User(
         fun loadPicture(
             homeDir: File,
             picName: String
-        ): File? = File(homeDir, picName).let {
-            if(it.exists()) it else null
+        ): File? = File(homeDir, picName).let { f ->
+            if (f.exists()) f else null
         }
 
         val allUsers: List<User>
-            get() = File("/etc/passwd")
-                .readLines()
-                .filter { t ->
-                    t.contains("/home")
-                }.map {
-                    User(it)
-                }
+            get() = File("/etc/passwd").lines.filter { t ->
+                t.contains("/home")
+            }.map {
+                User(it)
+            }
     }
 }

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

@@ -2,6 +2,8 @@
 
 package eu.mjdev.desktop.extensions
 
+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.layout.PaddingValues
@@ -9,6 +11,7 @@ import androidx.compose.foundation.relocation.BringIntoViewResponder
 import androidx.compose.foundation.relocation.bringIntoViewResponder
 import androidx.compose.material.ButtonDefaults
 import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
@@ -406,6 +409,30 @@ object Compose {
         }
     )
 
+    @Suppress("FunctionName")
+    @Composable
+    fun <S> Crossfade(
+        targetState: S,
+        modifier: Modifier = Modifier,
+        fadeInDuration: Long = 3000L,
+        fadeOutDuration: Long = 5000L,
+        contentAlignment: Alignment = Alignment.TopStart,
+        label: String = "FadeInContent",
+        contentKey: (targetState: S) -> Any? = { it },
+        content: @Composable AnimatedContentScope.(targetState: S) -> Unit
+    ) = AnimatedContent(
+        targetState = targetState,
+        modifier = modifier,
+        transitionSpec = {
+            fadeIn(animationSpec = tween(durationMillis = fadeInDuration.toInt())) togetherWith
+                    fadeOut(animationSpec = tween(durationMillis = fadeOutDuration.toInt()))
+        },
+        contentAlignment = contentAlignment,
+        label = label,
+        contentKey = contentKey,
+        content = content
+    )
+
 //    Toolkit.getDefaultToolkit().addAWTEventListener({ event ->
 //        }, AWTEvent.MOUSE_EVENT_MASK or AWTEvent.FOCUS_EVENT_MASK
 //    )

+ 121 - 22
src/main/kotlin/eu/mjdev/desktop/extensions/Custom.kt

@@ -2,31 +2,34 @@ package eu.mjdev.desktop.extensions
 
 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
 import com.google.gson.Gson
+import eu.mjdev.desktop.data.DesktopFile
+import eu.mjdev.desktop.extensions.Locale.toLocale
 import eu.mjdev.desktop.helpers.streams.ResourceStream
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.launch
 import java.io.BufferedReader
+import java.io.File
 import java.text.DateFormat
 import java.util.*
+import java.util.Locale
 import kotlin.jvm.optionals.getOrNull
 
-@Suppress("FunctionName", "MemberVisibilityCanBePrivate", "unused")
+@Suppress("MemberVisibilityCanBePrivate", "FunctionName")
 object Custom {
-    val Process.handle: ProcessHandle
-        get() = toHandle()
-
     val ProcessHandle.command: String
         get() = info().command().getOrNull().orEmpty()
 
-    val ProcessHandle.commandLine: String
-        get() = info().commandLine().getOrNull().orEmpty()
+//    val ProcessHandle.commandLine: String
+//        get() = info().commandLine().getOrNull().orEmpty()
 
     val Process.consoleOutput: String
         get() {
@@ -36,14 +39,6 @@ object Custom {
             return sj.toString()
         }
 
-    fun BufferedReader.readAvailable(): String = StringBuilder().apply {
-        var ch = read()
-        while (ch != -1) {
-            append(Char(ch))
-            ch = read()
-        }
-    }.toString()
-
     inline fun <reified T> Any?.orElse(
         block: () -> T
     ) = if (this == null) block() else this as T
@@ -57,6 +52,111 @@ object Custom {
                     block != Character.UnicodeBlock.SPECIALS
         }
 
+    fun File.listFiles(
+        ext: String? = null
+    ): List<File> = if (exists()) listFiles().let { list ->
+        when {
+            list == null -> emptyList()
+            ext != null -> list.filter { f -> f.extension == ext }
+            else -> list.toList()
+        }
+    } else emptyList()
+
+    fun File.listFilesFiltered(
+        ext: String? = null,
+        predicate: (File) -> Boolean
+    ) = listFiles(ext).filter(predicate)
+
+    fun File.listFilesOnly(
+        ext: String? = null,
+    ) = listFilesFiltered(ext) { f -> !f.isDirectory }
+
+    fun List<File>.sortedByName() =
+        sortedBy { f -> f.name }
+
+//    fun List<File>.sortedByNameDescending() =
+//        sortedByDescending { f -> f.name }
+
+    inline fun <R> File.listFiles(
+        ext: String? = null,
+        mapper: (File) -> R
+    ): List<R> = listFiles(ext).map(mapper)
+
+//    inline fun <R : Comparable<R>> File.listFilesSortedBy(
+//        ext: String? = null,
+//        crossinline selector: (File) -> R?
+//    ): List<File> = listFiles(ext).sortedBy(selector)
+
+    fun File.listDesktopFiles(
+        ext: String = DesktopFile.EXTENSION
+    ): List<DesktopFile> = listFiles(ext) { f -> DesktopFile(f) }
+
+//    inline fun <reified T : Any> MutableList<T>.addIfNotExists(
+//        element: T,
+//        equal: (e1: T, e2: T) -> Boolean = { e1, e2 -> e1 == e2 }
+//    ) = any { e -> equal(e, element) }.also { contains -> if (!contains) add(element) }
+
+    operator fun File.get(name: String) =
+        resolve(name)
+
+    fun File.readTextAsLocale(): Locale =
+        if (exists()) text.toLocale() else Locale.ENGLISH
+
+    val File.all
+        get() = listFiles()?.toList() ?: emptyList<File>()
+
+    val File.filesOnly
+        get() = listFiles()?.filter { !it.isDirectory }?.toList() ?: emptyList<File>()
+
+    val File.dirsOnly
+        get() = listFiles()?.filter { it.isDirectory }?.toList() ?: emptyList<File>()
+
+    val File.lines
+        get() = runCatching { if (exists()) readLines() else null }.getOrNull() ?: emptyList()
+
+    val File.text
+        get() = runCatching { if (exists()) readText() else null }.getOrNull() ?: ""
+
+    val File.textAsLocale: Locale
+        get() = if (exists()) readTextAsLocale() else Locale.ENGLISH
+
+    val File.desktopFiles
+        get() = listDesktopFiles()
+
+//    inline fun <T, K> distinctList(
+//        vararg lists: List<T>,
+//        selector: (T) -> K
+//    ): List<T> = mutableListOf<T>().apply {
+//        lists.forEach { l -> addAll(l) }
+//    }.distinctBy(selector)
+
+    fun String.notStartsWith(
+        prefix: String,
+        ignoreCase: Boolean = false
+    ) = !startsWith(prefix, ignoreCase)
+
+    fun String.trimIsEmpty() =
+        trim().isEmpty()
+
+//    fun String.trimIsNotEmpty() =
+//        trim().isNotEmpty()
+
+//    fun State<String>.trimIsEmpty() =
+//        value.trim().isEmpty()
+
+    fun State<String>.trimIsNotEmpty() =
+        value.trim().isNotEmpty()
+
+    fun String.trimStartsWith(
+        prefix: String,
+        ignoreCase: Boolean = false
+    ) = trim().startsWith(prefix, ignoreCase)
+
+//    fun String.trimNotStartsWith(
+//        prefix: String,
+//        ignoreCase: Boolean = false
+//    ) = !trim().startsWith(prefix, ignoreCase)
+
     fun ParsedList(
         value: String?,
         delimiter: String = ";"
@@ -78,6 +178,12 @@ object Custom {
         defaultValue: String = ""
     ) = value ?: defaultValue
 
+    fun <T> flowBlock(
+        block: suspend () -> T
+    ): Flow<T> = flow {
+        emit(block())
+    }
+
     fun NativePaint.setMaskFilter(
         blurRadius: Float
     ) {
@@ -119,13 +225,6 @@ object Custom {
             }
         }.collectAsState(initial = "")
 
-    // todo better solution
-    fun <T> SnapshotStateList<T>.invalidate() = apply {
-        val old = toList()
-        clear()
-        addAll(old)
-    }
-
     fun loadKey(key: String): String = runCatching {
         ResourceStream("keys/$key.key").string
     }.getOrNull().orEmpty()

+ 9 - 10
src/main/kotlin/eu/mjdev/desktop/extensions/Locale.kt

@@ -3,14 +3,13 @@ package eu.mjdev.desktop.extensions
 import java.util.Locale
 
 object Locale {
-
-    @Suppress("DEPRECATION")
-    fun String.toLocale(): Locale = this.split("_").let { lc ->
-        when (lc.size) {
-            2 -> Locale(lc[0], lc[1])
-            1 -> Locale(lc[0])
-            else -> Locale.ENGLISH
+    fun String.toLocale(): Locale = runCatching {
+        split("_").let { lc ->
+            when (lc.size) {
+                2 -> Locale(lc[0], lc[1])
+                1 -> Locale(lc[0])
+                else -> Locale.ENGLISH
+            }
         }
-    }
-
-}
+    }.getOrNull() ?: Locale.ENGLISH
+}

+ 21 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/backgrounds/ProviderErzvo.kt

@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.backgrounds
+
+import eu.mjdev.desktop.helpers.internal.ImagesProvider
+
+// todo
+@Suppress("unused")
+class ProviderErzvo(
+    private val loadCount: Int = 10
+) : ImagesProvider {
+    override suspend fun get(): Any {
+        return "http://erzvo.com/wp-content/uploads/2022/11/22-11-05-10429-Kubo-kvety-2013-1.jpg"
+    }
+}

+ 34 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/backgrounds/ProviderLocal.kt

@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.backgrounds
+
+import eu.mjdev.desktop.extensions.Custom.get
+import eu.mjdev.desktop.extensions.Custom.listFilesOnly
+import eu.mjdev.desktop.helpers.internal.ImagesProvider
+import eu.mjdev.desktop.provider.AppsProvider.Companion.DIR_NAME_BACKGROUNDS
+import eu.mjdev.desktop.provider.AppsProvider.Companion.DIR_NAME_DOT_LOCAL
+import eu.mjdev.desktop.provider.AppsProvider.Companion.DIR_NAME_SHARE
+import eu.mjdev.desktop.provider.DesktopProvider
+
+// todo
+@Suppress("unused")
+class ProviderLocal(
+    private val api: DesktopProvider
+) : ImagesProvider {
+    private val localDir
+        get() = api.homeDir[DIR_NAME_DOT_LOCAL]
+    private val localShareDir
+        get() = localDir[DIR_NAME_SHARE]
+    private val backgroundFilesDir
+        get() = localShareDir[DIR_NAME_BACKGROUNDS]
+
+    override suspend fun get(): Any {
+        return backgroundFilesDir.listFilesOnly()
+    }
+}

+ 28 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/backgrounds/ProviderSmug.kt

@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.backgrounds
+
+import eu.mjdev.desktop.helpers.internal.ImagesProvider
+
+// todo
+@Suppress("unused")
+class ProviderSmug(
+    private val loadCount: Int = 10
+) : ImagesProvider {
+    override suspend fun get(): Any {
+        return listOf(
+            "https://wallpapersmug.com/u/c78d6a/rainy-night-of-city-dark.jpeg",
+            "https://wallpapersmug.com/u/db1982/vegeta-artwork.jpg",
+            "https://wallpapersmug.com/u/6d3c26/dark-minimal-mountains.png",
+            "https://wallpapersmug.com/u/4ef517/devil-boy-in-mask-dark.jpg",
+            "https://wallpapersmug.com/u/c3c43f/matrix-code-numbers-green.jpg",
+            "https://wallpapersmug.com/u/ee041b/computer-screen-code-program.jpg"
+        )
+    }
+}

+ 17 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/internal/ImagesProvider.kt

@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.internal
+
+interface ImagesProvider {
+    suspend fun get(): Any?
+
+    companion object {
+        suspend fun ImagesProvider.getOne(): Any? = get().let { if (it is List<*>) it.firstOrNull() else it }
+    }
+}

+ 3 - 30
src/main/kotlin/eu/mjdev/desktop/helpers/internal/Palette.kt

@@ -1,6 +1,7 @@
 package eu.mjdev.desktop.helpers.internal
 
-import androidx.compose.runtime.*
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.graphics.Color
 import eu.mjdev.desktop.extensions.BitmapUtils.cut
 import eu.mjdev.desktop.extensions.BitmapUtils.topMostColor
@@ -98,35 +99,7 @@ class Palette(
         themeName: String = THEME_MJDEV
     ) = gnome.setGTKTheme(themeName)
 
-    fun setSoundTheme (
+    fun setSoundTheme(
         themeName: String = THEME_MJDEV
     ) = gnome.setSoundTheme(themeName)
-
-    companion object {
-        @Composable
-        inline fun <T> rememberPalette(
-            api: DesktopProvider,
-            crossinline calculation: @DisallowComposableCalls () -> T
-        ) = remember(api.palette.backgroundColor) { calculation() }
-
-        @Composable
-        fun rememberBackgroundColor(
-            api: DesktopProvider,
-        ) = rememberPalette(api) { api.currentUser.theme.backgroundColorState }
-
-//        @Composable
-//        fun rememberTextColor(
-//            api: DesktopProvider,
-//        ) = rememberPalette(api) { derivedStateOf { api.palette.textColor } }
-
-//        @Composable
-//        fun rememberBorderColor(
-//            api: DesktopProvider,
-//        ) = rememberPalette(api) { derivedStateOf { api.palette.borderColor } }
-
-        @Composable
-        fun rememberIconTintColor(
-            api: DesktopProvider,
-        ) = rememberPalette(api) { derivedStateOf { api.palette.iconsTintColor } }
-    }
 }

+ 23 - 16
src/main/kotlin/eu/mjdev/desktop/helpers/internal/Queue.kt

@@ -1,24 +1,31 @@
 package eu.mjdev.desktop.helpers.internal
 
 @Suppress("unused")
-class Queue<E> : ArrayList<E>() {
-    private var index = 0
+class Queue<E>(
+    val source: Collection<E>
+) : Iterator<E> {
+    var iterator: Iterator<E> = source.iterator()
 
-    val value: E?
-        get() = runCatching {
-            val ret = this[index] as E?
-            if (index < size) index++
-            if (index >= size) index = 0
-            ret
-        }.onFailure {
-            index = 0
-        }.getOrNull()
+    override fun hasNext(): Boolean = source.isNotEmpty()
 
-    companion object {
-        fun <E> mutableQueue(vararg elements: E) = Queue<E>().apply { addAll(elements) }
+    @Deprecated(
+        message = "Deprecated please use for safety nextOrNull().",
+    )
+    override fun next(): E = when {
+        source.isEmpty() -> throw (IllegalStateException("Source is empty."))
+        iterator.hasNext() -> iterator.next()
+        else -> {
+            iterator = source.iterator()
+            iterator.next()
+        }
+    }
 
-        fun <E> mutableQueue(elements: Collection<E>) = Queue<E>().apply { addAll(elements) }
+    fun nextOrNull(): E? = when {
+        source.isEmpty() -> null
+        iterator.hasNext() -> iterator.next()
+        else -> {
+            iterator = source.iterator()
+            nextOrNull()
+        }
     }
 }
-
-

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

@@ -1,19 +1,17 @@
 package eu.mjdev.desktop.helpers.system
 
+import eu.mjdev.desktop.extensions.Custom.lines
 import java.io.File
-import kotlin.math.roundToInt
 
 @Suppress("MemberVisibilityCanBePrivate")
 class MemInfo(
-    private val info: List<String> = File("/proc/meminfo").readLines(),
+    private val info: List<String> = File("/proc/meminfo").lines,
     val free: Double = info.parseLine("MemAvailable").times(KB),
     val total: Double = info.parseLine("MemTotal").times(KB),
     val used: Double = total - free,
-    val percent: Int = (((free.div(MB) / total.div(MB)) * 100).roundToInt())
 ) {
     companion object {
         const val KB = 1024
-        const val MB = KB * KB
 
         fun List<String>.parseLine(
             filter: String

+ 5 - 3
src/main/kotlin/eu/mjdev/desktop/helpers/system/OsRelease.kt

@@ -1,17 +1,19 @@
 package eu.mjdev.desktop.helpers.system
 
+import eu.mjdev.desktop.extensions.Custom.lines
+import eu.mjdev.desktop.extensions.Custom.notStartsWith
 import java.io.File
 
 @Suppress("unused")
 class OsRelease(
     configFile: File = File("/etc/os-release"),
-    configFileContent: List<String> = configFile.readLines().filter {
-        (!it.startsWith("#")) && (!it.trim().isEmpty())
+    configFileContent: List<String> = configFile.lines.filter { line ->
+        line.trim().let { l -> l.isNotEmpty() && l.notStartsWith("#") }
     }
 ) : HashMap<String, File>() {
     init {
         configFileContent.map {
-            it.split("=").let { Pair(it[0], it.getOrNull(1)) }
+            it.split("=").let { pair -> Pair(pair[0], pair.getOrNull(1)) }
         }.forEach {
             put(it.first, File(it.second ?: ""))
         }

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

@@ -56,7 +56,7 @@ class Shell(
     ) = ProcessBuilder(
         "dex",
         "-w",
-        app.desktopFile?.absolutePath
+        app.desktopFile.absolutePath
     ).apply {
         environment().putAll(environment)
     }.also { processBuilder ->
@@ -100,6 +100,6 @@ class Shell(
             }.toTypedArray()
         ).apply {
             waitFor()
-        }.inputReader().readText().lines()
+        }.inputReader().readLines()
     }
 }

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

@@ -1,21 +1,24 @@
 package eu.mjdev.desktop.helpers.system
 
+import eu.mjdev.desktop.extensions.Custom.lines
+import eu.mjdev.desktop.extensions.Custom.notStartsWith
 import java.io.File
 
+// todo api has directories & hashmap from file
 @Suppress("unused")
 class UserDirs(
     homeDir: File,
     configFile: File = File(homeDir, "/.config/user-dirs.dirs"),
-    configFileContent: List<String> = runCatching {  configFile.readLines().filter {
-        (!it.startsWith("#")) && (!it.trim().isEmpty())
-    }.map {
-        it.replace("\$HOME", homeDir.absolutePath)
-            .replace("\"", "")
-    } }.getOrElse { emptyList() }
+    configFileContent: List<String> =
+        configFile.lines.filter { line ->
+            line.let { l -> l.isNotEmpty() && l.notStartsWith("#") }
+        }.map { s ->
+            s.replace("\$HOME", homeDir.absolutePath).replace("\"", "")
+        }
 ) : HashMap<String, File>() {
     init {
-        configFileContent.map {
-            it.split("=").let { Pair(it[0], it.getOrNull(1)) }
+        configFileContent.map { line ->
+            line.split("=").let { pair -> Pair(pair[0], pair.getOrNull(1)) }
         }.forEach {
             put(it.first, File(it.second ?: ""))
         }

+ 112 - 135
src/main/kotlin/eu/mjdev/desktop/provider/AppsProvider.kt

@@ -1,104 +1,130 @@
 package eu.mjdev.desktop.provider
 
-import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
 import eu.mjdev.desktop.data.App
 import eu.mjdev.desktop.data.Category
-import eu.mjdev.desktop.data.DesktopFile
-import eu.mjdev.desktop.extensions.Custom.invalidate
+import eu.mjdev.desktop.extensions.Custom.desktopFiles
+import eu.mjdev.desktop.extensions.Custom.dirsOnly
+import eu.mjdev.desktop.extensions.Custom.filesOnly
+import eu.mjdev.desktop.extensions.Custom.flowBlock
+import eu.mjdev.desktop.extensions.Custom.get
 import eu.mjdev.desktop.extensions.Custom.jsonToList
-import eu.mjdev.desktop.extensions.Locale.toLocale
-import eu.mjdev.desktop.helpers.exception.SuccessException
+import eu.mjdev.desktop.extensions.Custom.lines
+import eu.mjdev.desktop.extensions.Custom.sortedByName
+import eu.mjdev.desktop.extensions.Custom.text
+import eu.mjdev.desktop.extensions.Custom.textAsLocale
+import eu.mjdev.desktop.extensions.Custom.trimIsEmpty
+import eu.mjdev.desktop.extensions.Custom.trimStartsWith
 import eu.mjdev.desktop.helpers.system.Shell
+import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
 import java.io.File
 import java.util.*
 
-@Suppress("unused", "MemberVisibilityCanBePrivate", "UNNECESSARY_SAFE_CALL")
+@Suppress("unused", "MemberVisibilityCanBePrivate")
 class AppsProvider(
-    val api: DesktopProvider
+    val api: DesktopProvider,
+    val scope: CoroutineScope = api.scope
 ) {
-    val scope
-        get() = api.scope
+    companion object {
+        const val DIR_NAME_ROOT = "/"
+        const val DIR_NAME_USR = "/usr"
+        const val DIR_NAME_VAR = "/var"
+        const val DIR_NAME_DOT_CONFIG = ".config"
+        const val DIR_NAME_DOT_ICONS = ".icons"
+        const val DIR_NAME_DOT_THEMES = ".themes"
+        const val DIR_NAME_DOT_LOCAL = ".local"
+        const val DIR_NAME_SHARE = "share"
+        const val DIR_NAME_APPLICATIONS = "applications"
+        const val DIR_NAME_AUTOSTART = "autoStart"
+        const val DIR_NAME_BACKGROUNDS = "backgrounds"
+        const val DIR_NAME_MENUS = "menus"
+        const val DIR_NAME_LIB = "lib"
+        const val DIR_NAME_FLATPAK = "flatpak"
+        const val DIR_NAME_EXPORTS = "exports"
+        const val DIR_NAME_SNAPD = "snapd"
+        const val DIR_NAME_DESKTOP = "desktop"
+
+        const val FILE_NAME_GNOME_APPS_MENU = "gnome-applications.menu"
+        const val FILE_NAME_BACKGROUND = "background"
+        const val FILE_NAME_MIME_APPS_LIST = "mimeapps.list"
+        const val FILE_NAME_USER_DIRS = "user-dirs.dirs"
+        const val FILE_NAME_USER_DIRS_LOCALE = "user-dirs.locale"
+
+        // todo
+        @Composable
+        fun rememberFavoriteApps(
+            api: DesktopProvider = LocalDesktop.current,
+            appsProvider: AppsProvider = api.appsProvider,
+        ): State<List<App>> {
+//            var processes by mutableStateOf(0)
+//            processManagerListener { processes = size }
+            return appsProvider.favoriteApps.collectAsState(emptyList())
+            // todo
+
+        }
+    }
 
-    private val configDir by lazy { api.homeDir.resolve(".config") }
-    private val iconsDir by lazy { api.homeDir.resolve(".icons") }
-    private val themesDir by lazy { api.homeDir.resolve(".themes") }
-    private val localDir by lazy { api.homeDir.resolve(".local") }
-    private val localShareDir by lazy { localDir?.resolve("share") }
-    private val allAppsDesktopFilesDir by lazy { localShareDir?.resolve("applications") }
-    private val autostartDesktopFilesDir by lazy { configDir?.resolve("autoStart") }
-    private val backgroundFilesDir by lazy { localShareDir?.resolve("backgrounds") }
-    // todo
-//    private val userBackgroundFilesDir by lazy { homeDir?.resolve("pictures") }
+    private val rootDir = File(DIR_NAME_ROOT)
+    private val usrDir = File(DIR_NAME_USR)
+    private val varDir = File(DIR_NAME_VAR)
 
-    private val menusDir by lazy { configDir?.resolve("menus") }
+    private val configDir = api.homeDir[DIR_NAME_DOT_CONFIG]
+    private val iconsDir = api.homeDir[DIR_NAME_DOT_ICONS]
+    private val themesDir = api.homeDir[DIR_NAME_DOT_THEMES]
+    private val localDir = api.homeDir[DIR_NAME_DOT_LOCAL]
+    private val localShareDir = localDir[DIR_NAME_SHARE]
+    private val allAppsDesktopFilesDir = localShareDir[DIR_NAME_APPLICATIONS]
+    private val autostartDesktopFilesDir = configDir[DIR_NAME_AUTOSTART]
 
-    private val gnomeAppsFile by lazy { menusDir?.resolve("gnome-applications.menu") } // xml
-    private val backgroundFile by lazy { configDir?.resolve("background") } // jpg
+    private val backgroundFilesDir by lazy { localShareDir[DIR_NAME_BACKGROUNDS] }
+    // private val userBackgroundFilesDir by lazy { homeDir?.resolve("pictures") } // todo
+
+    private val menusDir = configDir[DIR_NAME_MENUS]
+    private val gnomeAppsFile = menusDir[FILE_NAME_GNOME_APPS_MENU] // xml
+    private val backgroundFile = configDir[FILE_NAME_BACKGROUND] // jpg
 
     private val mimeApps by lazy {
-        runCatching {
-            configDir?.resolve("mimeapps.list")?.readLines()
-        }.getOrNull()
+        configDir[FILE_NAME_MIME_APPS_LIST].lines
     } // <mime>=<desktop.file>
     private val userDirs by lazy {
-        runCatching {
-            configDir?.resolve("user-dirs.dirs")?.readText()
-        }.getOrNull()
+        configDir[FILE_NAME_USER_DIRS].text
     } // <type>="<path>"
     private val userDirsLocale by lazy {
-        runCatching {
-            configDir?.resolve("user-dirs.locale")?.readText()?.toLocale()
-        }.getOrNull()
+        configDir[FILE_NAME_USER_DIRS_LOCALE].textAsLocale
     } //SK_sk
 
     private val menusItems by lazy {
-        runCatching {
-            menusDir?.resolve("gnome-applications.menu")?.readText()
-        }.getOrNull()
+        menusDir[FILE_NAME_GNOME_APPS_MENU].text
     } // xml
 
-    private val iconThemes by lazy { runCatching { iconsDir?.listFiles() }.getOrNull() } // folders
-    private val systemThemes by lazy { runCatching { themesDir?.listFiles() }.getOrNull() } // folders
+    private val iconThemes by lazy { iconsDir.dirsOnly } // folders
+    private val systemThemes by lazy { themesDir.dirsOnly } // folders
 
     private val autoStartDesktopFiles by lazy {
-        runCatching {
-            autostartDesktopFilesDir?.listFiles()?.map { DesktopFile(it) }
-        }.getOrNull() ?: emptyList()
+        autostartDesktopFilesDir.desktopFiles
     }
 
     // todo : probably not all here, need usr and local also
     private val allAppsDesktopFilesLocal by lazy {
-        runCatching {
-            allAppsDesktopFilesDir?.listFiles()?.filter {
-                it.extension == "desktop"
-            }?.map { DesktopFile(it) }
-        }.getOrNull() ?: emptyList()
+        allAppsDesktopFilesDir.desktopFiles
     }
 
     private val allAppsDesktopFilesShared by lazy {
-        runCatching {
-            File("/usr/share/applications").listFiles()?.filter {
-                it.extension == "desktop"
-            }?.map { DesktopFile(it) }
-        }.getOrNull() ?: emptyList()
+        usrDir[DIR_NAME_SHARE][DIR_NAME_APPLICATIONS].desktopFiles
     }
 
     private val allAppsDesktopFilesFlatPack by lazy {
-        runCatching {
-            File("/var/lib/flatpak/exports/share/applications/").listFiles()?.filter {
-                it.extension == "desktop"
-            }?.map { DesktopFile(it) }
-        }.getOrNull() ?: emptyList()
+        varDir[DIR_NAME_LIB][DIR_NAME_FLATPAK][DIR_NAME_EXPORTS][DIR_NAME_SHARE][DIR_NAME_APPLICATIONS].desktopFiles
     }
 
     private val allAppsDesktopFilesSnap by lazy {
-        runCatching {
-            File("/var/lib/snapd/desktop/applications/").listFiles()?.filter {
-                it.extension == "desktop"
-            }?.map { DesktopFile(it) }
-        }.getOrNull() ?: emptyList()
+        varDir[DIR_NAME_LIB][DIR_NAME_SNAPD][DIR_NAME_DESKTOP][DIR_NAME_APPLICATIONS].desktopFiles
     }
 
     private val allAppsDesktopFiles by lazy {
@@ -109,94 +135,45 @@ class AppsProvider(
     }
 
     val currentLocale: Locale
-        get() = userDirsLocale ?: Locale.ENGLISH
+        get() = userDirsLocale
     val autoStartApps
-        get() = autoStartDesktopFiles.map { file -> App(desktopFile = file, file = file.file) }
+        get() = autoStartDesktopFiles.map { file -> App(file) }
     val allApps
-        get() = allAppsDesktopFiles.map { file -> App(desktopFile = file, file = file.file) }
-    val backgrounds
-        get() = runCatching {
-            backgroundFilesDir?.listFiles()?.toList()?.sortedBy { it.name }?.filter { it.isFile }
-        }.getOrNull() ?: emptyList<File>()
-    val appCategories
-        get() = mutableListOf<String>().apply {
-            allApps.forEach { app ->
-                addAll(app.categories.map { c -> provideCategory(c) })
-            }
-        }.distinct().sorted().map { Category(it) }
-    val categoriesAndApps
-        get() = mutableMapOf<String, List<App>>().let { map ->
-            allApps.forEach { app ->
-                app.categories?.forEach { category ->
-                    val cat = provideCategory(category)
-                    if (!map.containsKey(cat)) {
-                        map[cat] = mutableListOf()
-                    }
-                    (map[cat] as MutableList<App>).addIfNotExists(app)
-                }
-            }
-            map
-        }
-
-    val favoriteApps = mutableStateListOf<App>()
+        get() = allAppsDesktopFiles.map { file -> App(file) }
 
-    // todo languages
-    fun provideCategory(category: String): String {
-        return if (
-            category.trim().isEmpty() || category.trim().startsWith("X-", true)
-        ) "Uncategorized" else category
+    val backgrounds by lazy {
+        backgroundFilesDir.filesOnly.sortedByName()
     }
 
-    fun startApp(app: App) = scope.launch(Dispatchers.IO) {
-        app.onStarting {
-            if (favoriteApps.contains(app)) {
-                favoriteApps.invalidate()
-            } else {
-                favoriteApps.add(app)
-            }
-        }.onStarted {
-            favoriteApps.invalidate()
-        }.onStop { result ->
-            if (app.isFavorite) {
-                favoriteApps.invalidate()
-            } else {
-                favoriteApps.remove(app)
-            }
-            when (result) {
-                is SuccessException -> println(result.message)
-                else -> result.printStackTrace()
-            }
-        }.start()
-        favoriteApps.invalidate()
-    }
+    val appCategories = flowBlock {
+        allApps.flatMap { app ->
+            app.categories.map { c ->
+                provideCategory(c)
+            }.distinct().sorted()
+        }.map { c ->
+            Category(c)
+        }
+    }.flowOn(Dispatchers.IO)
 
-    init {
+    val favoriteApps: Flow<List<App>> = flowBlock {
         Shell.executeAndRead(
             "gsettings",
             "get",
             "org.gnome.shell",
             "favorite-apps"
-        ).jsonToList<String>().mapNotNull { deskFileName ->
+        ).jsonToList<String>().asSequence().flatMap { deskFileName ->
             allAppsDesktopFiles.filter { deskFile ->
-                deskFile.file.name?.contentEquals(deskFileName) == true
+                deskFile.fileName.contentEquals(deskFileName)
             }.map { deskFile ->
-                App(
-                    desktopFile = deskFile,
-                    file = deskFile.file,
-                    isFavorite = true
-                )
-            }.firstOrNull()
-        }?.also { favorite ->
-            favoriteApps.addAll(favorite)
-        }
-    }
-
-    companion object {
-        private fun MutableList<App>.addIfNotExists(app: App) {
-            val contains = any { a -> a.fullAppName.contentEquals(app.fullAppName) }
-            if (!contains) {
-                add(app)
+                App(deskFile)
             }
-        }
+        }.toList()
+    }.flowOn(Dispatchers.IO)
+
+    // todo languages
+    fun provideCategory(category: String): String {
+        return if (
+            category.trimIsEmpty() || category.trimStartsWith("X-", true)
+        ) "Uncategorized" else category
     }
 }

+ 1 - 8
src/main/kotlin/eu/mjdev/desktop/provider/DesktopScope.kt

@@ -1,7 +1,6 @@
 package eu.mjdev.desktop.provider
 
 import androidx.compose.runtime.Composable
-import eu.mjdev.desktop.data.App
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
 
 // todo theme user support
@@ -9,7 +8,7 @@ import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
 open class DesktopScope(
     val api: DesktopProvider
 ) {
-    val isDebug: Boolean
+    val isDebug
         get() = api.isDebug
 
     val scope
@@ -78,9 +77,6 @@ open class DesktopScope(
     val appCategories
         get() = appsProvider.appCategories
 
-    val appCategoriesAndApps
-        get() = appsProvider.categoriesAndApps
-
     val allApps
         get() = appsProvider.allApps
 
@@ -120,9 +116,6 @@ open class DesktopScope(
     val panelAutoHideEnabled
         get() = theme.panelHideDelay > 0L
 
-    fun startApp(app: App) =
-        api.appsProvider.startApp(app)
-
     companion object {
         @Composable
         fun withDesktopScope(