feat(desktop): workspace system with save/switch/restore (v1c)
Add workspace presets for different usage modes. Each workspace stores layout mode + column configuration. Switching destroys current layout and rebuilds from saved config (no background state). - Add Workspace data model with WorkspaceColumn - Add WorkspaceManager with save/load/switch/add/delete - Add WorkspaceIcons registry for Material icon resolution - Add WorkspaceBar to AppDrawer footer with workspace chips - Add DeckState.loadFromWorkspace() for column rebuilding - Add workspaces persistence to DesktopPreferences - Default workspace: "Social" (Home + Notifications + DMs) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,14 @@ object DesktopPreferences {
|
|||||||
prefs.put(KEY_LAYOUT_MODE, value)
|
prefs.put(KEY_LAYOUT_MODE, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val KEY_WORKSPACES = "workspaces"
|
||||||
|
|
||||||
|
var workspaces: String
|
||||||
|
get() = prefs.get(KEY_WORKSPACES, "")
|
||||||
|
set(value) {
|
||||||
|
prefs.put(KEY_WORKSPACES, value)
|
||||||
|
}
|
||||||
|
|
||||||
private const val KEY_PINNED_NAV_ITEMS = "pinned_nav_items"
|
private const val KEY_PINNED_NAV_ITEMS = "pinned_nav_items"
|
||||||
|
|
||||||
var pinnedNavItems: String
|
var pinnedNavItems: String
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ import com.vitorpamplona.amethyst.desktop.ui.deck.DeckState
|
|||||||
import com.vitorpamplona.amethyst.desktop.ui.deck.PinnedNavBarState
|
import com.vitorpamplona.amethyst.desktop.ui.deck.PinnedNavBarState
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.deck.SinglePaneLayout
|
import com.vitorpamplona.amethyst.desktop.ui.deck.SinglePaneLayout
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.deck.SinglePaneState
|
import com.vitorpamplona.amethyst.desktop.ui.deck.SinglePaneState
|
||||||
|
import com.vitorpamplona.amethyst.desktop.ui.deck.WorkspaceManager
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.media.LocalAwtWindow
|
import com.vitorpamplona.amethyst.desktop.ui.media.LocalAwtWindow
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.media.LocalIsImmersiveFullscreen
|
import com.vitorpamplona.amethyst.desktop.ui.media.LocalIsImmersiveFullscreen
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.media.LocalWindowState
|
import com.vitorpamplona.amethyst.desktop.ui.media.LocalWindowState
|
||||||
@@ -192,6 +193,7 @@ fun main() {
|
|||||||
var replyToNote by remember { mutableStateOf<com.vitorpamplona.quartz.nip01Core.core.Event?>(null) }
|
var replyToNote by remember { mutableStateOf<com.vitorpamplona.quartz.nip01Core.core.Event?>(null) }
|
||||||
val deckScope = rememberCoroutineScope()
|
val deckScope = rememberCoroutineScope()
|
||||||
val deckState = remember { DeckState(deckScope).also { it.load() } }
|
val deckState = remember { DeckState(deckScope).also { it.load() } }
|
||||||
|
val workspaceManager = remember { WorkspaceManager(deckScope).also { it.load() } }
|
||||||
val accountManager = remember { AccountManager.create() }
|
val accountManager = remember { AccountManager.create() }
|
||||||
val accountState by accountManager.accountState.collectAsState()
|
val accountState by accountManager.accountState.collectAsState()
|
||||||
var showAppDrawer by remember { mutableStateOf(false) }
|
var showAppDrawer by remember { mutableStateOf(false) }
|
||||||
@@ -446,6 +448,7 @@ fun main() {
|
|||||||
App(
|
App(
|
||||||
layoutMode = layoutMode,
|
layoutMode = layoutMode,
|
||||||
deckState = deckState,
|
deckState = deckState,
|
||||||
|
workspaceManager = workspaceManager,
|
||||||
accountManager = accountManager,
|
accountManager = accountManager,
|
||||||
showComposeDialog = showComposeDialog,
|
showComposeDialog = showComposeDialog,
|
||||||
showAppDrawer = showAppDrawer,
|
showAppDrawer = showAppDrawer,
|
||||||
@@ -477,6 +480,7 @@ fun main() {
|
|||||||
fun App(
|
fun App(
|
||||||
layoutMode: LayoutMode,
|
layoutMode: LayoutMode,
|
||||||
deckState: DeckState,
|
deckState: DeckState,
|
||||||
|
workspaceManager: WorkspaceManager,
|
||||||
accountManager: AccountManager,
|
accountManager: AccountManager,
|
||||||
showComposeDialog: Boolean,
|
showComposeDialog: Boolean,
|
||||||
showAppDrawer: Boolean,
|
showAppDrawer: Boolean,
|
||||||
@@ -699,6 +703,7 @@ fun App(
|
|||||||
MainContent(
|
MainContent(
|
||||||
layoutMode = layoutMode,
|
layoutMode = layoutMode,
|
||||||
deckState = deckState,
|
deckState = deckState,
|
||||||
|
workspaceManager = workspaceManager,
|
||||||
singlePaneState = singlePaneState,
|
singlePaneState = singlePaneState,
|
||||||
pinnedNavBarState = pinnedNavBarState,
|
pinnedNavBarState = pinnedNavBarState,
|
||||||
relayManager = relayManager,
|
relayManager = relayManager,
|
||||||
@@ -736,6 +741,10 @@ fun App(
|
|||||||
emptySet()
|
emptySet()
|
||||||
},
|
},
|
||||||
pinnedNavBarState = pinnedNavBarState,
|
pinnedNavBarState = pinnedNavBarState,
|
||||||
|
workspaceManager = workspaceManager,
|
||||||
|
onSwitchWorkspace = { ws ->
|
||||||
|
deckState.loadFromWorkspace(ws.columns)
|
||||||
|
},
|
||||||
onSelectScreen = { type ->
|
onSelectScreen = { type ->
|
||||||
when (layoutMode) {
|
when (layoutMode) {
|
||||||
LayoutMode.DECK -> {
|
LayoutMode.DECK -> {
|
||||||
@@ -773,6 +782,7 @@ fun App(
|
|||||||
fun MainContent(
|
fun MainContent(
|
||||||
layoutMode: LayoutMode,
|
layoutMode: LayoutMode,
|
||||||
deckState: DeckState,
|
deckState: DeckState,
|
||||||
|
workspaceManager: WorkspaceManager,
|
||||||
singlePaneState: SinglePaneState,
|
singlePaneState: SinglePaneState,
|
||||||
pinnedNavBarState: PinnedNavBarState,
|
pinnedNavBarState: PinnedNavBarState,
|
||||||
relayManager: DesktopRelayConnectionManager,
|
relayManager: DesktopRelayConnectionManager,
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import androidx.compose.material3.TextField
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -216,6 +217,8 @@ private class AppDrawerState {
|
|||||||
fun AppDrawer(
|
fun AppDrawer(
|
||||||
openColumnTypes: Set<String>,
|
openColumnTypes: Set<String>,
|
||||||
pinnedNavBarState: PinnedNavBarState,
|
pinnedNavBarState: PinnedNavBarState,
|
||||||
|
workspaceManager: WorkspaceManager,
|
||||||
|
onSwitchWorkspace: (Workspace) -> Unit,
|
||||||
onSelectScreen: (DeckColumnType) -> Unit,
|
onSelectScreen: (DeckColumnType) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -306,8 +309,18 @@ fun AppDrawer(
|
|||||||
if (state.awaitingHashtag) {
|
if (state.awaitingHashtag) {
|
||||||
HashtagInputSection(state, onSelectScreen, onDismiss)
|
HashtagInputSection(state, onSelectScreen, onDismiss)
|
||||||
} else {
|
} else {
|
||||||
|
Box(Modifier.weight(1f)) {
|
||||||
DrawerGrid(state, openColumnTypes, pinnedNavBarState, onSelectScreen, onDismiss)
|
DrawerGrid(state, openColumnTypes, pinnedNavBarState, onSelectScreen, onDismiss)
|
||||||
}
|
}
|
||||||
|
// Workspace bar at the bottom
|
||||||
|
WorkspaceBar(
|
||||||
|
workspaceManager = workspaceManager,
|
||||||
|
onSwitchWorkspace = { ws ->
|
||||||
|
onSwitchWorkspace(ws)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -528,3 +541,47 @@ private fun HashtagInputSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WorkspaceBar(
|
||||||
|
workspaceManager: WorkspaceManager,
|
||||||
|
onSwitchWorkspace: (Workspace) -> Unit,
|
||||||
|
) {
|
||||||
|
val workspaces by workspaceManager.workspaces.collectAsState()
|
||||||
|
val activeIndex by workspaceManager.activeIndex.collectAsState()
|
||||||
|
|
||||||
|
androidx.compose.material3.HorizontalDivider()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Workspaces",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspaces.forEachIndexed { index, ws ->
|
||||||
|
val isActive = index == activeIndex
|
||||||
|
androidx.compose.material3.FilterChip(
|
||||||
|
selected = isActive,
|
||||||
|
onClick = {
|
||||||
|
if (!isActive) {
|
||||||
|
val switched = workspaceManager.switchTo(index)
|
||||||
|
if (switched != null) onSwitchWorkspace(switched)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text(ws.name, style = MaterialTheme.typography.labelSmall) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
WorkspaceIcons.resolve(ws.iconName),
|
||||||
|
contentDescription = ws.name,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -256,6 +256,20 @@ class DeckState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadFromWorkspace(workspaceColumns: List<Workspace.WorkspaceColumn>) {
|
||||||
|
val loaded =
|
||||||
|
workspaceColumns.mapNotNull { col ->
|
||||||
|
val entry = mapOf("type" to col.typeKey, "param" to col.param)
|
||||||
|
val type = parseColumnType(entry) ?: return@mapNotNull null
|
||||||
|
DeckColumn(
|
||||||
|
type = type,
|
||||||
|
width = col.width.coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_columns.value = loaded.ifEmpty { DEFAULT_COLUMNS }
|
||||||
|
_focusedColumnIndex.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MIN_COLUMN_WIDTH = 300f
|
const val MIN_COLUMN_WIDTH = 300f
|
||||||
const val MAX_COLUMN_WIDTH = 800f
|
const val MAX_COLUMN_WIDTH = 800f
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.desktop.ui.deck
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Bookmark
|
||||||
|
import androidx.compose.material.icons.filled.Chat
|
||||||
|
import androidx.compose.material.icons.filled.Code
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.Explore
|
||||||
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
|
import androidx.compose.material.icons.filled.Groups
|
||||||
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material.icons.filled.MenuBook
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material.icons.filled.SportsEsports
|
||||||
|
import androidx.compose.material.icons.filled.Star
|
||||||
|
import androidx.compose.material.icons.filled.Work
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import com.vitorpamplona.amethyst.desktop.LayoutMode
|
||||||
|
|
||||||
|
data class Workspace(
|
||||||
|
val id: String =
|
||||||
|
java.util.UUID
|
||||||
|
.randomUUID()
|
||||||
|
.toString(),
|
||||||
|
val name: String,
|
||||||
|
val iconName: String,
|
||||||
|
val layoutMode: LayoutMode,
|
||||||
|
val columns: List<WorkspaceColumn>,
|
||||||
|
val singlePaneScreen: String? = null,
|
||||||
|
) {
|
||||||
|
data class WorkspaceColumn(
|
||||||
|
val typeKey: String,
|
||||||
|
val param: String? = null,
|
||||||
|
val width: Float = 400f,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
object WorkspaceIcons {
|
||||||
|
private val icons: Map<String, ImageVector> =
|
||||||
|
mapOf(
|
||||||
|
"Groups" to Icons.Default.Groups,
|
||||||
|
"Edit" to Icons.Default.Edit,
|
||||||
|
"MenuBook" to Icons.Default.MenuBook,
|
||||||
|
"Home" to Icons.Default.Home,
|
||||||
|
"Chat" to Icons.Default.Chat,
|
||||||
|
"Search" to Icons.Default.Search,
|
||||||
|
"SportsEsports" to Icons.Default.SportsEsports,
|
||||||
|
"Bookmark" to Icons.Default.Bookmark,
|
||||||
|
"Explore" to Icons.Default.Explore,
|
||||||
|
"Person" to Icons.Default.Person,
|
||||||
|
"Star" to Icons.Default.Star,
|
||||||
|
"Favorite" to Icons.Default.Favorite,
|
||||||
|
"Work" to Icons.Default.Work,
|
||||||
|
"Code" to Icons.Default.Code,
|
||||||
|
)
|
||||||
|
|
||||||
|
val availableNames: List<String> = icons.keys.sorted()
|
||||||
|
|
||||||
|
fun resolve(name: String): ImageVector = icons[name] ?: Icons.Default.Home
|
||||||
|
}
|
||||||
+225
@@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.desktop.ui.deck
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import com.vitorpamplona.amethyst.desktop.DesktopPreferences
|
||||||
|
import com.vitorpamplona.amethyst.desktop.LayoutMode
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class WorkspaceManager(
|
||||||
|
private val saveScope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
private val _workspaces = MutableStateFlow(listOf(DEFAULT_WORKSPACE))
|
||||||
|
val workspaces: StateFlow<List<Workspace>> = _workspaces.asStateFlow()
|
||||||
|
|
||||||
|
private val _activeIndex = MutableStateFlow(0)
|
||||||
|
val activeIndex: StateFlow<Int> = _activeIndex.asStateFlow()
|
||||||
|
|
||||||
|
val activeWorkspace: Workspace
|
||||||
|
get() = _workspaces.value.getOrElse(_activeIndex.value) { _workspaces.value.first() }
|
||||||
|
|
||||||
|
private var saveJob: Job? = null
|
||||||
|
|
||||||
|
fun switchTo(index: Int): Workspace? {
|
||||||
|
if (index !in _workspaces.value.indices) return null
|
||||||
|
_activeIndex.value = index
|
||||||
|
scheduleSave()
|
||||||
|
return activeWorkspace
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveCurrentColumns(columns: List<DeckColumn>) {
|
||||||
|
_workspaces.update { wsList ->
|
||||||
|
wsList.mapIndexed { idx, ws ->
|
||||||
|
if (idx == _activeIndex.value) {
|
||||||
|
ws.copy(
|
||||||
|
columns =
|
||||||
|
columns.map { col ->
|
||||||
|
Workspace.WorkspaceColumn(
|
||||||
|
typeKey = col.type.typeKey(),
|
||||||
|
param =
|
||||||
|
when (col.type) {
|
||||||
|
is DeckColumnType.Profile -> col.type.pubKeyHex
|
||||||
|
is DeckColumnType.Thread -> col.type.noteId
|
||||||
|
is DeckColumnType.Hashtag -> col.type.tag
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
width = col.width,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ws
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduleSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addWorkspace(workspace: Workspace) {
|
||||||
|
if (_workspaces.value.size >= MAX_WORKSPACES) return
|
||||||
|
_workspaces.update { it + workspace }
|
||||||
|
scheduleSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateWorkspace(workspace: Workspace) {
|
||||||
|
_workspaces.update { wsList ->
|
||||||
|
wsList.map { if (it.id == workspace.id) workspace else it }
|
||||||
|
}
|
||||||
|
scheduleSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteWorkspace(id: String) {
|
||||||
|
if (_workspaces.value.size <= 1) return
|
||||||
|
val idx = _workspaces.value.indexOfFirst { it.id == id }
|
||||||
|
_workspaces.update { it.filter { ws -> ws.id != id } }
|
||||||
|
if (_activeIndex.value >= _workspaces.value.size) {
|
||||||
|
_activeIndex.value = _workspaces.value.size - 1
|
||||||
|
}
|
||||||
|
if (idx == _activeIndex.value || _activeIndex.value >= _workspaces.value.size) {
|
||||||
|
_activeIndex.value = 0
|
||||||
|
}
|
||||||
|
scheduleSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleSave() {
|
||||||
|
saveJob?.cancel()
|
||||||
|
saveJob =
|
||||||
|
saveScope.launch {
|
||||||
|
delay(SAVE_DEBOUNCE_MS)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save() {
|
||||||
|
try {
|
||||||
|
val data =
|
||||||
|
mapOf(
|
||||||
|
"activeIndex" to _activeIndex.value,
|
||||||
|
"workspaces" to
|
||||||
|
_workspaces.value.map { ws ->
|
||||||
|
mapOf(
|
||||||
|
"id" to ws.id,
|
||||||
|
"name" to ws.name,
|
||||||
|
"iconName" to ws.iconName,
|
||||||
|
"layoutMode" to ws.layoutMode.name,
|
||||||
|
"singlePaneScreen" to ws.singlePaneScreen,
|
||||||
|
"columns" to
|
||||||
|
ws.columns.map { col ->
|
||||||
|
mapOf(
|
||||||
|
"typeKey" to col.typeKey,
|
||||||
|
"param" to col.param,
|
||||||
|
"width" to col.width,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DesktopPreferences.workspaces = mapper.writeValueAsString(data)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("WorkspaceManager: failed to save: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load() {
|
||||||
|
try {
|
||||||
|
val json = DesktopPreferences.workspaces
|
||||||
|
if (json.isBlank()) return
|
||||||
|
val data: Map<String, Any?> = mapper.readValue(json)
|
||||||
|
val activeIdx = (data["activeIndex"] as? Number)?.toInt() ?: 0
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val wsList = data["workspaces"] as? List<Map<String, Any?>> ?: return
|
||||||
|
val loaded =
|
||||||
|
wsList.mapNotNull { entry ->
|
||||||
|
try {
|
||||||
|
val name = entry["name"] as? String ?: return@mapNotNull null
|
||||||
|
val iconName = entry["iconName"] as? String ?: "Home"
|
||||||
|
val layoutMode =
|
||||||
|
try {
|
||||||
|
LayoutMode.valueOf(entry["layoutMode"] as? String ?: "DECK")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LayoutMode.DECK
|
||||||
|
}
|
||||||
|
val singlePaneScreen = entry["singlePaneScreen"] as? String
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val columns =
|
||||||
|
(entry["columns"] as? List<Map<String, Any?>>)?.map { col ->
|
||||||
|
Workspace.WorkspaceColumn(
|
||||||
|
typeKey = col["typeKey"] as? String ?: "home",
|
||||||
|
param = col["param"] as? String,
|
||||||
|
width = (col["width"] as? Number)?.toFloat() ?: 400f,
|
||||||
|
)
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
Workspace(
|
||||||
|
id =
|
||||||
|
entry["id"] as? String ?: java.util.UUID
|
||||||
|
.randomUUID()
|
||||||
|
.toString(),
|
||||||
|
name = name,
|
||||||
|
iconName = iconName,
|
||||||
|
layoutMode = layoutMode,
|
||||||
|
columns = columns,
|
||||||
|
singlePaneScreen = singlePaneScreen,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loaded.isNotEmpty()) {
|
||||||
|
_workspaces.value = loaded
|
||||||
|
_activeIndex.value = activeIdx.coerceIn(loaded.indices)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("WorkspaceManager: failed to load: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_WORKSPACES = 9
|
||||||
|
private const val SAVE_DEBOUNCE_MS = 500L
|
||||||
|
private val mapper = jacksonObjectMapper()
|
||||||
|
|
||||||
|
val DEFAULT_WORKSPACE =
|
||||||
|
Workspace(
|
||||||
|
id = "default-social",
|
||||||
|
name = "Social",
|
||||||
|
iconName = "Groups",
|
||||||
|
layoutMode = LayoutMode.DECK,
|
||||||
|
columns =
|
||||||
|
listOf(
|
||||||
|
Workspace.WorkspaceColumn("home"),
|
||||||
|
Workspace.WorkspaceColumn("notifications"),
|
||||||
|
Workspace.WorkspaceColumn("messages"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user