refactor(desktop): pin/unpin syncs to active workspace

PinnedNavBarState now takes WorkspaceManager reference. Pin/unpin
actions update the active workspace's singlePaneScreens list,
making sidebar customization and workspace editing the same action.

- PinnedNavBarState.syncToWorkspace() updates active workspace on pin/unpin
- PinnedNavBarState.loadFromWorkspace() loads from active workspace
- Remove separate DesktopPreferences.pinnedNavItems persistence
- Workspace is the single source of truth for nav bar screens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-04-19 05:40:28 +03:00
parent 0455d1f8c6
commit 200c5227fc
2 changed files with 29 additions and 36 deletions
@@ -540,7 +540,7 @@ fun App(
initialTorSettings: com.vitorpamplona.amethyst.commons.tor.TorSettings,
) {
val singlePaneState = remember { SinglePaneState() }
val pinnedNavBarState = remember { PinnedNavBarState().also { it.load() } }
val pinnedNavBarState = remember { PinnedNavBarState(workspaceManager).also { it.loadFromWorkspace() } }
// Always reload from prefs — after key() rebuild, prefs have the latest saved settings
var torSettings by remember {
@@ -795,17 +795,12 @@ fun App(
}
LayoutMode.SINGLE_PANE -> {
// 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())
}
}
// Load nav bar from workspace + navigate to first screen
pinnedNavBarState.loadFromWorkspace()
val firstKey =
ws.singlePaneScreens.firstOrNull() ?: "home"
val type = DeckState.parseColumnTypeFromKey(firstKey)
if (type != null) singlePaneState.navigate(type)
}
}
},
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.desktop.ui.deck
import com.vitorpamplona.amethyst.desktop.DesktopPreferences
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -28,9 +27,11 @@ import kotlinx.coroutines.flow.update
/**
* Manages which screens are pinned to the navigation sidebar.
* Persists to DesktopPreferences as CSV of typeKey strings.
* Pin/unpin syncs to the active workspace's singlePaneScreens.
*/
class PinnedNavBarState {
class PinnedNavBarState(
private val workspaceManager: WorkspaceManager? = null,
) {
private val _pinnedScreens = MutableStateFlow(DEFAULT_PINNED)
val pinnedScreens: StateFlow<List<DeckColumnType>> = _pinnedScreens.asStateFlow()
@@ -40,13 +41,13 @@ class PinnedNavBarState {
if (isPinned(type)) return
if (!isPinnable(type)) return
_pinnedScreens.update { it + type }
save()
syncToWorkspace()
}
fun unpin(type: DeckColumnType) {
if (!isUnpinnable(type)) return
_pinnedScreens.update { current -> current.filter { it.typeKey() != type.typeKey() } }
save()
syncToWorkspace()
}
fun move(
@@ -60,34 +61,32 @@ class PinnedNavBarState {
mutable.add(toIndex, item)
mutable.toList()
}
save()
}
fun save() {
DesktopPreferences.pinnedNavItems = _pinnedScreens.value.joinToString(",") { it.typeKey() }
syncToWorkspace()
}
fun loadFromList(screens: List<DeckColumnType>) {
_pinnedScreens.value = screens.ifEmpty { DEFAULT_PINNED }
save()
}
fun load() {
val raw = DesktopPreferences.pinnedNavItems
if (raw.isBlank()) {
_pinnedScreens.value = DEFAULT_PINNED
return
fun loadFromWorkspace() {
val ws = workspaceManager?.activeWorkspace ?: return
if (ws.singlePaneScreens.isNotEmpty()) {
val screens =
ws.singlePaneScreens.mapNotNull { key ->
PINNABLE_SCREENS.find { it.typeKey() == key }
}
_pinnedScreens.value = screens.ifEmpty { DEFAULT_PINNED }
}
val keys = raw.split(",").filter { it.isNotBlank() }
val screens =
keys.mapNotNull { key ->
PINNABLE_SCREENS.find { it.typeKey() == key }
}
_pinnedScreens.value = screens.ifEmpty { DEFAULT_PINNED }
}
private fun syncToWorkspace() {
val wm = workspaceManager ?: return
val ws = wm.activeWorkspace
val updated = ws.copy(singlePaneScreens = _pinnedScreens.value.map { it.typeKey() })
wm.updateWorkspace(updated)
}
companion object {
// Only object types are pinnable (no parameterized types like Hashtag, Editor)
val PINNABLE_SCREENS: List<DeckColumnType> =
LAUNCHABLE_SCREENS.filter { !it.requiresInput() && it !is DeckColumnType.Editor }
@@ -106,7 +105,6 @@ class PinnedNavBarState {
DeckColumnType.Settings,
)
// These screens cannot be unpinned
private val ALWAYS_PINNED = setOf("home", "settings")
fun isPinnable(type: DeckColumnType): Boolean = PINNABLE_SCREENS.any { it.typeKey() == type.typeKey() }