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) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-04-19 05:21:12 +03:00
parent ac349665eb
commit 0455d1f8c6
5 changed files with 83 additions and 34 deletions
@@ -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())
}
}
}
}
},
@@ -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<String>,
onScreensChange: (List<String>) -> 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
},
)
}
}
}
}
@@ -67,6 +67,11 @@ class PinnedNavBarState {
DesktopPreferences.pinnedNavItems = _pinnedScreens.value.joinToString(",") { it.typeKey() }
}
fun loadFromList(screens: List<DeckColumnType>) {
_pinnedScreens.value = screens.ifEmpty { DEFAULT_PINNED }
save()
}
fun load() {
val raw = DesktopPreferences.pinnedNavItems
if (raw.isBlank()) {
@@ -47,7 +47,7 @@ data class Workspace(
val iconName: String,
val layoutMode: LayoutMode,
val columns: List<WorkspaceColumn>,
val singlePaneScreen: String? = null,
val singlePaneScreens: List<String> = emptyList(),
) {
data class WorkspaceColumn(
val typeKey: String,
@@ -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<String>) ?: 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