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, initialTorSettings: com.vitorpamplona.amethyst.commons.tor.TorSettings,
) { ) {
val singlePaneState = remember { SinglePaneState() } 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 // Always reload from prefs — after key() rebuild, prefs have the latest saved settings
var torSettings by remember { var torSettings by remember {
@@ -795,17 +795,12 @@ fun App(
} }
LayoutMode.SINGLE_PANE -> { LayoutMode.SINGLE_PANE -> {
// Set nav bar screens from workspace // Load nav bar from workspace + navigate to first screen
if (ws.singlePaneScreens.isNotEmpty()) { pinnedNavBarState.loadFromWorkspace()
val screens = val firstKey =
ws.singlePaneScreens.mapNotNull { ws.singlePaneScreens.firstOrNull() ?: "home"
DeckState.parseColumnTypeFromKey(it) val type = DeckState.parseColumnTypeFromKey(firstKey)
} if (type != null) singlePaneState.navigate(type)
if (screens.isNotEmpty()) {
pinnedNavBarState.loadFromList(screens)
singlePaneState.navigate(screens.first())
}
}
} }
} }
}, },
@@ -20,7 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.desktop.ui.deck package com.vitorpamplona.amethyst.desktop.ui.deck
import com.vitorpamplona.amethyst.desktop.DesktopPreferences
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -28,9 +27,11 @@ import kotlinx.coroutines.flow.update
/** /**
* Manages which screens are pinned to the navigation sidebar. * 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) private val _pinnedScreens = MutableStateFlow(DEFAULT_PINNED)
val pinnedScreens: StateFlow<List<DeckColumnType>> = _pinnedScreens.asStateFlow() val pinnedScreens: StateFlow<List<DeckColumnType>> = _pinnedScreens.asStateFlow()
@@ -40,13 +41,13 @@ class PinnedNavBarState {
if (isPinned(type)) return if (isPinned(type)) return
if (!isPinnable(type)) return if (!isPinnable(type)) return
_pinnedScreens.update { it + type } _pinnedScreens.update { it + type }
save() syncToWorkspace()
} }
fun unpin(type: DeckColumnType) { fun unpin(type: DeckColumnType) {
if (!isUnpinnable(type)) return if (!isUnpinnable(type)) return
_pinnedScreens.update { current -> current.filter { it.typeKey() != type.typeKey() } } _pinnedScreens.update { current -> current.filter { it.typeKey() != type.typeKey() } }
save() syncToWorkspace()
} }
fun move( fun move(
@@ -60,34 +61,32 @@ class PinnedNavBarState {
mutable.add(toIndex, item) mutable.add(toIndex, item)
mutable.toList() mutable.toList()
} }
save() syncToWorkspace()
}
fun save() {
DesktopPreferences.pinnedNavItems = _pinnedScreens.value.joinToString(",") { it.typeKey() }
} }
fun loadFromList(screens: List<DeckColumnType>) { fun loadFromList(screens: List<DeckColumnType>) {
_pinnedScreens.value = screens.ifEmpty { DEFAULT_PINNED } _pinnedScreens.value = screens.ifEmpty { DEFAULT_PINNED }
save()
} }
fun load() { fun loadFromWorkspace() {
val raw = DesktopPreferences.pinnedNavItems val ws = workspaceManager?.activeWorkspace ?: return
if (raw.isBlank()) { if (ws.singlePaneScreens.isNotEmpty()) {
_pinnedScreens.value = DEFAULT_PINNED val screens =
return 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 -> private fun syncToWorkspace() {
PINNABLE_SCREENS.find { it.typeKey() == key } val wm = workspaceManager ?: return
} val ws = wm.activeWorkspace
_pinnedScreens.value = screens.ifEmpty { DEFAULT_PINNED } val updated = ws.copy(singlePaneScreens = _pinnedScreens.value.map { it.typeKey() })
wm.updateWorkspace(updated)
} }
companion object { companion object {
// Only object types are pinnable (no parameterized types like Hashtag, Editor)
val PINNABLE_SCREENS: List<DeckColumnType> = val PINNABLE_SCREENS: List<DeckColumnType> =
LAUNCHABLE_SCREENS.filter { !it.requiresInput() && it !is DeckColumnType.Editor } LAUNCHABLE_SCREENS.filter { !it.requiresInput() && it !is DeckColumnType.Editor }
@@ -106,7 +105,6 @@ class PinnedNavBarState {
DeckColumnType.Settings, DeckColumnType.Settings,
) )
// These screens cannot be unpinned
private val ALWAYS_PINNED = setOf("home", "settings") private val ALWAYS_PINNED = setOf("home", "settings")
fun isPinnable(type: DeckColumnType): Boolean = PINNABLE_SCREENS.any { it.typeKey() == type.typeKey() } fun isPinnable(type: DeckColumnType): Boolean = PINNABLE_SCREENS.any { it.typeKey() == type.typeKey() }