From 0455d1f8c6b438deebbbc0cd632aaf575e0f9725 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Sun, 19 Apr 2026 05:21:12 +0300 Subject: [PATCH] feat(desktop): single-pane workspaces store nav bar screen list Change Workspace.singlePaneScreen (single string) to singlePaneScreens (list of typeKey strings). First screen is the default. On workspace switch, loads the screen list into PinnedNavBarState and navigates to the first screen. - Add SinglePaneScreensEditor to workspace editor dialog - Add PinnedNavBarState.loadFromList() for workspace-driven nav - Backward compat: load old "singlePaneScreen" format as single-item list - Cmd+Shift+S captures current deck columns as singlePaneScreens Co-Authored-By: Claude Opus 4.6 (1M context) --- .../vitorpamplona/amethyst/desktop/Main.kt | 23 ++++-- .../amethyst/desktop/ui/deck/AppDrawer.kt | 74 +++++++++++++------ .../desktop/ui/deck/PinnedNavBarState.kt | 5 ++ .../amethyst/desktop/ui/deck/Workspace.kt | 2 +- .../desktop/ui/deck/WorkspaceManager.kt | 13 +++- 5 files changed, 83 insertions(+), 34 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index 3a76d9e14..bbac3c026 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -267,6 +267,12 @@ fun main() { iconName = "Star", layoutMode = layoutMode, columns = columns, + singlePaneScreens = + if (layoutMode == LayoutMode.SINGLE_PANE) { + columns.map { it.typeKey } + } else { + emptyList() + }, ) workspaceManager.addWorkspace(ws) } @@ -789,12 +795,17 @@ fun App( } LayoutMode.SINGLE_PANE -> { - val screenKey = - ws.singlePaneScreen - ?: ws.columns.firstOrNull()?.typeKey - ?: "home" - val type = DeckState.parseColumnTypeFromKey(screenKey) - if (type != null) singlePaneState.navigate(type) + // Set nav bar screens from workspace + if (ws.singlePaneScreens.isNotEmpty()) { + val screens = + ws.singlePaneScreens.mapNotNull { + DeckState.parseColumnTypeFromKey(it) + } + if (screens.isNotEmpty()) { + pinnedNavBarState.loadFromList(screens) + singlePaneState.navigate(screens.first()) + } + } } } }, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/AppDrawer.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/AppDrawer.kt index 5a004bba4..0adca7e11 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/AppDrawer.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/AppDrawer.kt @@ -55,6 +55,7 @@ import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.SportsEsports +import androidx.compose.material.icons.filled.Star import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenu @@ -883,7 +884,9 @@ private fun WorkspaceEditorDialog( var columns by remember { mutableStateOf(initial?.columns ?: listOf(Workspace.WorkspaceColumn("home"))) } - var singlePaneScreen by remember { mutableStateOf(initial?.singlePaneScreen) } + var singlePaneScreens by remember { + mutableStateOf(initial?.singlePaneScreens ?: listOf("home")) + } AlertDialog( onDismissRequest = onDismiss, @@ -941,11 +944,11 @@ private fun WorkspaceEditorDialog( if (layoutMode == LayoutMode.DECK) { DeckColumnEditor(columns = columns, onColumnsChange = { columns = it }) } - // Single Pane: screen picker + // Single Pane: nav bar screen selector (first = default) if (layoutMode == LayoutMode.SINGLE_PANE) { - SinglePaneScreenPicker( - selected = singlePaneScreen, - onSelect = { singlePaneScreen = it }, + SinglePaneScreensEditor( + screens = singlePaneScreens, + onScreensChange = { singlePaneScreens = it }, ) } } @@ -963,7 +966,7 @@ private fun WorkspaceEditorDialog( iconName = iconName, layoutMode = layoutMode, columns = columns, - singlePaneScreen = singlePaneScreen, + singlePaneScreens = singlePaneScreens, ), ) }, @@ -1021,29 +1024,52 @@ private fun DeckColumnEditor( } @Composable -private fun SinglePaneScreenPicker( - selected: String?, - onSelect: (String) -> Unit, +private fun SinglePaneScreensEditor( + screens: List, + onScreensChange: (List) -> Unit, ) { - var expanded by remember { mutableStateOf(false) } - val selectedTitle = - selected?.let { DeckState.parseColumnTypeFromKey(it)?.title() } ?: "Select screen" - Column { - Text("Screen", style = MaterialTheme.typography.labelMedium) + Text("Nav Bar Screens", style = MaterialTheme.typography.labelMedium) + Text( + "First screen is the default", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) Spacer(Modifier.height(4.dp)) - TextButton(onClick = { expanded = true }) { Text(selectedTitle) } - DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - LAUNCHABLE_SCREENS.filter { !it.requiresInput() }.forEach { screen -> - DropdownMenuItem( - text = { Text(screen.title()) }, - onClick = { - onSelect(screen.typeKey()) - expanded = false - }, - ) + screens.forEachIndexed { idx, key -> + val displayName = DeckState.parseColumnTypeFromKey(key)?.title() ?: key + Row(verticalAlignment = Alignment.CenterVertically) { + if (idx == 0) { + Icon( + Icons.Default.Star, + "Default", + Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.width(4.dp)) + } + Text(displayName, Modifier.weight(1f)) + IconButton(onClick = { + onScreensChange(screens.toMutableList().apply { removeAt(idx) }) + }) { Icon(Icons.Default.Close, "Remove", Modifier.size(16.dp)) } } } + // Add screen dropdown + var expanded by remember { mutableStateOf(false) } + TextButton(onClick = { expanded = true }) { Text("+ Add Screen") } + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + PinnedNavBarState.PINNABLE_SCREENS + .filter { screen -> screens.none { it == screen.typeKey() } } + .forEach { screen -> + DropdownMenuItem( + text = { Text(screen.title()) }, + onClick = { + onScreensChange(screens + screen.typeKey()) + expanded = false + }, + ) + } + } } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/PinnedNavBarState.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/PinnedNavBarState.kt index fe9252dd4..00d5f5eb9 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/PinnedNavBarState.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/PinnedNavBarState.kt @@ -67,6 +67,11 @@ class PinnedNavBarState { DesktopPreferences.pinnedNavItems = _pinnedScreens.value.joinToString(",") { it.typeKey() } } + fun loadFromList(screens: List) { + _pinnedScreens.value = screens.ifEmpty { DEFAULT_PINNED } + save() + } + fun load() { val raw = DesktopPreferences.pinnedNavItems if (raw.isBlank()) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/Workspace.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/Workspace.kt index 0c3e4f801..0e2d2c62a 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/Workspace.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/Workspace.kt @@ -47,7 +47,7 @@ data class Workspace( val iconName: String, val layoutMode: LayoutMode, val columns: List, - val singlePaneScreen: String? = null, + val singlePaneScreens: List = emptyList(), ) { data class WorkspaceColumn( val typeKey: String, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/WorkspaceManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/WorkspaceManager.kt index d4edbd1e9..2de3e9cf4 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/WorkspaceManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/WorkspaceManager.kt @@ -124,7 +124,7 @@ class WorkspaceManager( "name" to ws.name, "iconName" to ws.iconName, "layoutMode" to ws.layoutMode.name, - "singlePaneScreen" to ws.singlePaneScreen, + "singlePaneScreens" to ws.singlePaneScreens, "columns" to ws.columns.map { col -> mapOf( @@ -162,7 +162,14 @@ class WorkspaceManager( } catch (e: Exception) { LayoutMode.DECK } - val singlePaneScreen = entry["singlePaneScreen"] as? String + + @Suppress("UNCHECKED_CAST") + val singlePaneScreens = + (entry["singlePaneScreens"] as? List) ?: run { + // Backward compat: old format had single "singlePaneScreen" string + val legacy = entry["singlePaneScreen"] as? String + if (legacy != null) listOf(legacy) else emptyList() + } @Suppress("UNCHECKED_CAST") val columns = @@ -183,7 +190,7 @@ class WorkspaceManager( iconName = iconName, layoutMode = layoutMode, columns = columns, - singlePaneScreen = singlePaneScreen, + singlePaneScreens = singlePaneScreens, ) } catch (e: Exception) { null