浏览代码

menu improvements

menu improvements
Milan Jurkulak 2 年之前
父节点
当前提交
9ca0dc359e

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

@@ -18,7 +18,7 @@ fun main(
     // eth settings : iwconfig
     MainWindow()
     DisposableEffect(Unit) {
-        println("App started with args: $args")
+        println("App started with args: ${args.toList()}")
         Shell {
             // todo
 //            autoStartApps()

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

@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.appsmenu
+
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import eu.mjdev.desktop.data.App
+import eu.mjdev.desktop.data.Category
+import eu.mjdev.desktop.extensions.ColorUtils.alpha
+import eu.mjdev.desktop.helpers.compose.rememberForeverLazyListState
+import eu.mjdev.desktop.provider.DesktopScope
+import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
+
+@Composable
+fun AppsList(
+    modifier: Modifier = Modifier,
+    category: String = "",
+    listState: LazyListState = rememberForeverLazyListState("AppsMenu"),
+    onCategoryClick: DesktopScope.(Category) -> Unit,
+    onCategoryContextMenuClick: DesktopScope.(Category) -> Unit,
+    onAppClick: DesktopScope.(App) -> Unit,
+    onAppContextMenuClick: DesktopScope.(App) -> Unit,
+    items: List<Any>
+) = withDesktopScope {
+    LazyColumn(
+        modifier = modifier,
+        state = if (category.isEmpty()) listState else rememberLazyListState(),
+    ) {
+        when (items.firstOrNull()) {
+            is Category -> {
+                itemsIndexed(items) { idx, item ->
+                    AppsMenuCategory(
+                        category = item as Category,
+                        showDivider = (items.size - 1) > idx,
+                        dividerColor = textColor.alpha(0.3f),
+                        onClick = { onCategoryClick(item) },
+                        onContextMenuClick = { onCategoryContextMenuClick(item) }
+                    )
+                }
+            }
+
+            is App -> {
+                itemsIndexed(items) { idx, item ->
+                    AppsMenuApp(
+                        app = item as App,
+                        showDivider = (items.size - 1) > idx,
+                        dividerColor =  textColor.alpha(0.3f),
+                        onClick = {
+                            onAppClick(item)
+                        },
+                        onContextMenuClick = {
+                            onAppContextMenuClick(item)
+                        }
+                    )
+                }
+            }
+        }
+    }
+}

+ 14 - 40
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenu.kt

@@ -7,8 +7,7 @@ import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.Divider
 import androidx.compose.runtime.*
@@ -35,6 +34,7 @@ import eu.mjdev.desktop.extensions.Compose.removeLast
 import eu.mjdev.desktop.extensions.Modifier.dropShadow
 import eu.mjdev.desktop.helpers.animation.Animations.AppsMenuEnterAnimation
 import eu.mjdev.desktop.helpers.animation.Animations.AppsMenuExitAnimation
+import eu.mjdev.desktop.helpers.compose.rememberForeverLazyListState
 import eu.mjdev.desktop.helpers.internal.KeyEventHandler.Companion.globalKeyEventHandler
 import eu.mjdev.desktop.provider.DesktopScope
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
@@ -50,6 +50,7 @@ fun AppsMenu(
     enterAnimation: EnterTransition = AppsMenuEnterAnimation,
     exitAnimation: ExitTransition = AppsMenuExitAnimation,
     searchTextState: MutableState<String> = rememberState(""),
+    listState: LazyListState = rememberForeverLazyListState("AppsMenu"),
     onFocusChange: ChromeWindowState.(Boolean) -> Unit = {},
     onAppClick: DesktopScope.(App) -> Unit = { app ->
         startApp(app)
@@ -57,7 +58,7 @@ fun AppsMenu(
         menuState.hide()
     },
     onAppContextMenuClick: DesktopScope.(App) -> Unit = {},
-    onCategoryContextMenuClick: (Category) -> Unit = {},
+    onCategoryContextMenuClick: DesktopScope.(Category) -> Unit = {},
     onUserAvatarClick: () -> Unit = {}
 ) = withDesktopScope {
     var category by rememberState("")
@@ -75,11 +76,6 @@ fun AppsMenu(
             else -> appCategories
         }
     }
-    val onCategoryClick: (Category) -> Unit = remember {
-        { c ->
-            category = c.name
-        }
-    }
     val position by rememberCalculated {
         WindowPosition.Absolute(
             panelState.bounds.x,
@@ -164,41 +160,19 @@ fun AppsMenu(
                         color = borderColor,
                         thickness = 2.dp
                     )
-                    LazyColumn(
+                    AppsList(
                         modifier = Modifier
                             .padding(bottom = 62.dp)
                             .fillMaxSize()
-                            .padding(24.dp)
-                    ) {
-                        when (items.firstOrNull()) {
-                            is Category -> {
-                                items(items) { item ->
-                                    AppsMenuCategory(
-                                        category = item as Category,
-                                        onClick = { onCategoryClick(item) },
-                                        onContextMenuClick = { onCategoryContextMenuClick(item) }
-                                    )
-                                }
-                            }
-
-                            is App -> {
-                                items(items) { item ->
-                                    AppsMenuApp(
-                                        app = item as App,
-                                        backgroundColor = backgroundColor,
-                                        iconTint = textColor,
-                                        textColor = textColor,
-                                        onClick = {
-                                            onAppClick(item)
-                                        },
-                                        onContextMenuClick = {
-                                            onAppContextMenuClick(item)
-                                        }
-                                    )
-                                }
-                            }
-                        }
-                    }
+                            .padding(24.dp),
+                        category = category,
+                        listState = listState,
+                        onCategoryClick = { c -> category = c.name },
+                        onAppClick = onAppClick,
+                        onAppContextMenuClick = onAppContextMenuClick,
+                        onCategoryContextMenuClick = onCategoryContextMenuClick,
+                        items = items
+                    )
                 }
                 Column(
                     modifier = Modifier

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

@@ -1,24 +1,24 @@
 package eu.mjdev.desktop.components.appsmenu
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.Divider
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.*
 import eu.mjdev.desktop.components.fonticon.FontIcon
 import eu.mjdev.desktop.components.text.TextAny
 import eu.mjdev.desktop.data.App
 import eu.mjdev.desktop.extensions.Compose.onLeftClick
 import eu.mjdev.desktop.extensions.Compose.onMousePress
 import eu.mjdev.desktop.extensions.Compose.onRightClick
+import eu.mjdev.desktop.extensions.Modifier.circleBorder
+import eu.mjdev.desktop.helpers.DottedShape
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 
 @Suppress("FunctionName")
@@ -28,35 +28,65 @@ fun AppsMenuApp(
     app: App? = null,
     icon: String? = null,
     iconSize: DpSize = DpSize(32.dp, 32.dp),
-    iconTint: Color = Color.Black,
-    textColor: Color = Color.White,
-    backgroundColor: Color = Color.White,
+    titleTextSize: TextUnit = 16.sp,
+    commentTextSize: TextUnit = 12.sp,
+    showDivider: Boolean = true,
+    dividerPadding: Dp = 48.dp,
+    dividerColor: Color = Color.Black,
     onContextMenuClick: () -> Unit = {},
     onClick: () -> Unit = {},
 ) = withDesktopScope {
-    Row(
+    val appIconName = remember(app, icon) { app?.name ?: icon }
+    val materialIcon = remember(appIconName) { iconSet.iconForName(appIconName) ?: "?".toInt() }
+    Column(
         modifier = modifier.onMousePress {
             onLeftClick { onClick() }
             onRightClick { onContextMenuClick() }
-        },
-        verticalAlignment = Alignment.CenterVertically,
+        }
     ) {
-        val appIconName = remember(app, icon) { app?.name ?: icon }
-        val materialIcon = remember(appIconName) { iconSet.iconForName(appIconName) ?: "?".toInt() }
-        FontIcon(
-            iconId = materialIcon,
-            iconSize = iconSize,
-            iconColor = iconTint,
-            iconBackgroundColor = backgroundColor,
-            outerPadding = PaddingValues(2.dp),
-            innerPadding = PaddingValues(0.dp),
-        )
-        TextAny(
-            modifier = Modifier.padding(start = 4.dp).fillMaxWidth(),
-            text = app?.name ?: "",
-            color = textColor,
-            fontWeight = FontWeight.Bold
-        )
+        Row(
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            FontIcon(
+                modifier = Modifier.circleBorder(1.dp, iconsTintColor),
+                iconId = materialIcon,
+                iconSize = iconSize,
+                iconColor = textColor,
+                iconBackgroundColor = backgroundColor,
+                outerPadding = PaddingValues(2.dp),
+                innerPadding = PaddingValues(0.dp),
+            )
+            Column(
+                Modifier.padding(4.dp).fillMaxWidth()
+            ) {
+                TextAny(
+                    text = app?.name ?: app?.fullAppName ?: "",
+                    color = textColor,
+                    singleLine = true,
+                    fontSize = titleTextSize,
+                    fontWeight = FontWeight.Bold
+                )
+                TextAny(
+                    text = app?.comment ?: "-",
+                    color = textColor,
+                    singleLine = true,
+                    fontSize = commentTextSize,
+                    fontWeight = FontWeight.Bold
+                )
+            }
+        }
+        if (showDivider) {
+            Divider(
+                modifier = Modifier.fillMaxWidth()
+                    .padding(start = dividerPadding)
+                    .height(1.dp)
+                    .background(
+                        color = dividerColor,
+                        shape = DottedShape(5.dp)
+                    ),
+                color = Color.Transparent
+            )
+        }
     }
 }
 

+ 51 - 28
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenuCategory.kt

@@ -1,23 +1,24 @@
 package eu.mjdev.desktop.components.appsmenu
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.Divider
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.*
 import eu.mjdev.desktop.components.fonticon.FontIcon
 import eu.mjdev.desktop.components.text.TextAny
 import eu.mjdev.desktop.data.Category
 import eu.mjdev.desktop.extensions.Compose.onLeftClick
 import eu.mjdev.desktop.extensions.Compose.onMousePress
 import eu.mjdev.desktop.extensions.Compose.onRightClick
+import eu.mjdev.desktop.extensions.Modifier.circleBorder
+import eu.mjdev.desktop.helpers.DottedShape
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 
 @Composable
@@ -25,32 +26,54 @@ fun AppsMenuCategory(
     modifier: Modifier = Modifier,
     category: Category = Category.Empty,
     iconSize: DpSize = DpSize(32.dp, 32.dp),
+    titleTextSize: TextUnit = 16.sp,
+    showDivider: Boolean = true,
+    dividerPadding: Dp = 48.dp,
+    dividerColor: Color = Color.Black,
     onClick: (category: Category) -> Unit = {},
     onContextMenuClick: (category: Category) -> Unit = {}
 ) = withDesktopScope {
-    Row(
-        modifier = modifier.onMousePress {
-            onLeftClick { onClick(category) }
-            onRightClick { onContextMenuClick(category) }
-        },
-        verticalAlignment = Alignment.CenterVertically
+    val categoryName = remember(category) { category.name }
+    val materialIcon = remember(categoryName) { iconSet.iconForName(categoryName) ?: "?".toInt() }
+    Column(
+        modifier = modifier.padding(vertical = 2.dp)
+            .onMousePress {
+                onLeftClick { onClick(category) }
+                onRightClick { onContextMenuClick(category) }
+            }
     ) {
-        val categoryName = remember(category) { category.name }
-        val materialIcon = remember(categoryName) { iconSet.iconForName(categoryName) ?: "?".toInt() }
-        FontIcon(
-            iconId = materialIcon,
-            iconSize = iconSize,
-            iconColor = textColor,
-            iconBackgroundColor = backgroundColor,
-            outerPadding = PaddingValues(2.dp),
-            innerPadding = PaddingValues(0.dp),
-        )
-        TextAny(
-            modifier = Modifier.padding(start = 4.dp).fillMaxWidth(),
-            text = category.name,
-            color = textColor,
-            fontWeight = FontWeight.Bold
-        )
+        Row(
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            FontIcon(
+                modifier = Modifier.circleBorder(1.dp, iconsTintColor),
+                iconId = materialIcon,
+                iconSize = iconSize,
+                iconColor = textColor,
+                iconBackgroundColor = backgroundColor,
+                outerPadding = PaddingValues(2.dp),
+                innerPadding = PaddingValues(0.dp),
+            )
+            TextAny(
+                modifier = Modifier.padding(start = 4.dp).fillMaxWidth(),
+                text = category.name,
+                color = textColor,
+                fontSize = titleTextSize,
+                fontWeight = FontWeight.Bold
+            )
+        }
+        if (showDivider) {
+            Divider(
+                modifier = Modifier.fillMaxWidth()
+                    .padding(start = dividerPadding)
+                    .height(1.dp)
+                    .background(
+                        color = dividerColor,
+                        shape = DottedShape(5.dp)
+                    ),
+                color = Color.Transparent
+            )
+        }
     }
 }
 

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/components/card/FocusableCard.kt

@@ -119,7 +119,7 @@ fun FocusableCard(
             ) {
                 TextAny(
                     modifier = Modifier.padding(titlePadding),
-                    maxLines = 1,
+                    singleLine = true,
                     color = textColor,
                     style = MaterialTheme.typography.bodyMedium,
                     text = item?.toString().orEmpty()
@@ -142,7 +142,7 @@ fun FocusableCard(
             ) {
                 AutoHideEmptyText(
                     modifier = Modifier.padding(titlePadding),
-                    maxLines = 1,
+                    singleLine = true,
                     color = textColor,
                     style = MaterialTheme.typography.bodySmall,
                     text = "" // todo

+ 2 - 4
src/main/kotlin/eu/mjdev/desktop/components/custom/Clock.kt

@@ -65,8 +65,7 @@ fun Clock(
                 fontWeight = timeTextWeight,
                 fontSize = timeTextSize,
                 color = timeTextColor,
-                maxLines = 1,
-                minLines = 1
+               singleLine = true
             )
             if (showDate) TextWithShadow(
                 text = dateFlow.value,
@@ -74,8 +73,7 @@ fun Clock(
                 fontWeight = dateTextWeight,
                 fontSize = dateTextSize,
                 color = dateTextColor,
-                maxLines = 1,
-                minLines = 1
+               singleLine = true
             )
         }
         LaunchedEffect(time) {

+ 2 - 0
src/main/kotlin/eu/mjdev/desktop/components/fonticon/FontIcon.kt

@@ -20,6 +20,7 @@ import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
 @Suppress("FunctionName")
 @Composable
 fun FontIcon(
+    modifier: Modifier = Modifier,
     iconId: Int = 0,
     iconicFont: MaterialIconFont = LocalDesktop.current.currentUser.theme.iconSet,
     iconSize: DpSize = DpSize(32.dp, 32.dp),
@@ -32,6 +33,7 @@ fun FontIcon(
     onRightClick: () -> Unit = {},
     onClick: () -> Unit = {}
 ) = ShapedIcon(
+    modifier = modifier,
     iconSize = iconSize,
     iconColor = iconColor,
     iconBackgroundColor = iconBackgroundColor,

+ 0 - 2
src/main/kotlin/eu/mjdev/desktop/components/input/PasswordTextView.kt

@@ -69,8 +69,6 @@ fun PasswordTextView(
         readOnly = false,
         label = label,
         singleLine = true,
-        maxLines = 1,
-        minLines = 1,
         shape = shape,
         colors = colors,
         placeholder = placeholder,

+ 2 - 1
src/main/kotlin/eu/mjdev/desktop/components/text/AutoHideEmptyText.kt

@@ -30,8 +30,9 @@ fun AutoHideEmptyText(
     lineHeight: TextUnit = TextUnit.Unspecified,
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
+    singleLine: Boolean = false,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
     minLines: Int = 1,
-    maxLines: Int = Int.MAX_VALUE,
     onTextLayout: (TextLayoutResult) -> Unit = {},
     style: TextStyle = LocalTextStyle.current
 ) {

+ 2 - 1
src/main/kotlin/eu/mjdev/desktop/components/text/TextAny.kt

@@ -33,8 +33,9 @@ fun TextAny(
     lineHeight: TextUnit = TextUnit.Unspecified,
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
+    singleLine: Boolean = false,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
     minLines: Int = 1,
-    maxLines: Int = Int.MAX_VALUE,
     onTextLayout: ((TextLayoutResult) -> Unit)? = null,
     style: TextStyle = TextStyle.Default,
     textSelectionEnabled: Boolean = false

+ 3 - 2
src/main/kotlin/eu/mjdev/desktop/components/text/TextWithShadow.kt

@@ -29,8 +29,9 @@ fun TextWithShadow(
     fontSize: TextUnit = 16.sp,
     color: Color = Color.Red,
     overflow: TextOverflow = Ellipsis,
-    minLines: Int = 2,
-    maxLines: Int = 2,
+    singleLine: Boolean = false,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+    minLines: Int = 1,
     text: Any? = "test"
 ) = TextAny(
     modifier = modifier,

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

@@ -12,16 +12,21 @@ import androidx.compose.runtime.*
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawWithCache
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.geometry.CornerRadius
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathEffect
+import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.input.key.*
 import androidx.compose.ui.input.pointer.*
 import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.*
 import coil3.ImageLoader
@@ -373,6 +378,34 @@ object Compose {
     val FocusState.isNotFocused
         get() = !isFocused
 
+    @Composable
+    fun Modifier.dashedBorder(
+        strokeWidth: Dp,
+        color: Color,
+        cornerRadiusDp: Dp
+    ) = composed(
+        factory = {
+            val density = LocalDensity.current
+            val strokeWidthPx = density.run { strokeWidth.toPx() }
+            val cornerRadiusPx = density.run { cornerRadiusDp.toPx() }
+            then(
+                Modifier.drawWithCache {
+                    onDrawBehind {
+                        val stroke = Stroke(
+                            width = strokeWidthPx,
+                            pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
+                        )
+                        drawRoundRect(
+                            color = color,
+                            style = stroke,
+                            cornerRadius = CornerRadius(cornerRadiusPx)
+                        )
+                    }
+                }
+            )
+        }
+    )
+
 //    Toolkit.getDefaultToolkit().addAWTEventListener({ event ->
 //        }, AWTEvent.MOUSE_EVENT_MASK or AWTEvent.FOCUS_EVENT_MASK
 //    )

+ 44 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/DottedShape.kt

@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import kotlin.math.roundToInt
+
+data class DottedShape(
+    val step: Dp,
+) : Shape {
+    override fun createOutline(
+        size: Size,
+        layoutDirection: LayoutDirection,
+        density: Density
+    ) = Outline.Generic(Path().apply {
+        val stepPx = with(density) { step.toPx() }
+        val stepsCount = (size.width / stepPx).roundToInt()
+        val actualStep = size.width / stepsCount
+        val dotSize = Size(width = actualStep / 2, height = size.height)
+        for (i in 0 until stepsCount) {
+            addRect(
+                Rect(
+                    offset = Offset(x = i * actualStep, y = 0f),
+                    size = dotSize
+                )
+            )
+        }
+        close()
+    })
+}

+ 49 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/compose/ListState.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.helpers.compose
+
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.saveable.rememberSaveable
+
+private val SaveMap = mutableMapOf<String, KeyParams>()
+
+private data class KeyParams(
+    val params: String = "",
+    val index: Int,
+    val scrollOffset: Int
+)
+
+@Composable
+fun rememberForeverLazyListState(
+    key: String,
+    params: String = "",
+    initialFirstVisibleItemIndex: Int = 0,
+    initialFirstVisibleItemScrollOffset: Int = 0
+): LazyListState {
+    val scrollState = rememberSaveable(saver = LazyListState.Saver) {
+        var savedValue = SaveMap[key]
+        if (savedValue?.params != params) savedValue = null
+        val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex
+        val savedOffset = savedValue?.scrollOffset ?: initialFirstVisibleItemScrollOffset
+        LazyListState(
+            savedIndex,
+            savedOffset
+        )
+    }
+    DisposableEffect(Unit) {
+        onDispose {
+            val lastIndex = scrollState.firstVisibleItemIndex
+            val lastOffset = scrollState.firstVisibleItemScrollOffset
+            SaveMap[key] = KeyParams(params, lastIndex, lastOffset)
+        }
+    }
+    return scrollState
+}

+ 20 - 4
src/main/kotlin/eu/mjdev/desktop/provider/AppsProvider.kt

@@ -121,17 +121,18 @@ class AppsProvider(
     val appCategories
         get() = mutableListOf<String>().apply {
             allApps.forEach { app ->
-                addAll(app.categories)
+                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 ->
-                    if (!map.containsKey(category)) {
-                        map[category] = mutableListOf()
+                    val cat = provideCategory(category)
+                    if (!map.containsKey(cat)) {
+                        map[cat] = mutableListOf()
                     }
-                    (map[category] as MutableList<App>).add(app)
+                    (map[cat] as MutableList<App>).addIfNotExists(app)
                 }
             }
             map
@@ -139,6 +140,13 @@ class AppsProvider(
 
     val favoriteApps = mutableStateListOf<App>()
 
+    // todo languages
+    fun provideCategory(category: String): String {
+        return if (
+            category.trim().isEmpty() || category.trim().startsWith("X-", true)
+        ) "Uncategorized" else category
+    }
+
     fun startApp(app: App) = scope.launch(Dispatchers.IO) {
         app.onStarting {
             if (favoriteApps.contains(app)) {
@@ -183,4 +191,12 @@ class AppsProvider(
         }
     }
 
+    companion object {
+        private fun MutableList<App>.addIfNotExists(app: App) {
+            val contains = any { a -> a.fullAppName.contentEquals(app.fullAppName) }
+            if (!contains) {
+                add(app)
+            }
+        }
+    }
 }