fix(desktop): audit fixes for deck layout
Critical:
- GlobalFeed now renders global feed (was identical to HomeFeed)
- DeckState uses atomic _columns.update{} instead of non-atomic .value=
- lastKnownWidth marked @Volatile for thread safety
Medium:
- save() debounced 500ms via coroutine (was sync disk I/O per drag frame)
- Width redistribution fills gaps after clamping on column remove
- Hashtag column pre-fills search query
- ColumnNavigationState.stack exposed as StateFlow not MutableStateFlow
- Settings column deduped (focuses existing instead of adding duplicate)
- Exceptions logged in save/load instead of silently swallowed
Low:
- Thread column icon fixed (was Home, now Article)
- Drag divider hit target widened to 12dp (visual stays 4dp)
- Horizontal scroll added for column overflow
- Auto-fit columns on first composition / window resize
- Column shortcuts limited to actual column count
- Deterministic default column IDs
- Overlay fallback shows message instead of blank
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -141,7 +141,8 @@ fun main() =
|
||||
)
|
||||
var showComposeDialog by remember { mutableStateOf(false) }
|
||||
var replyToNote by remember { mutableStateOf<com.vitorpamplona.quartz.nip01Core.core.Event?>(null) }
|
||||
val deckState = remember { DeckState().also { it.load() } }
|
||||
val deckScope = rememberCoroutineScope()
|
||||
val deckState = remember { DeckState(deckScope).also { it.load() } }
|
||||
var showAddColumnDialog by remember { mutableStateOf(false) }
|
||||
var layoutMode by remember {
|
||||
mutableStateOf(
|
||||
@@ -179,7 +180,13 @@ fun main() =
|
||||
} else {
|
||||
KeyShortcut(Key.Comma, ctrl = true)
|
||||
},
|
||||
onClick = { deckState.addColumn(DeckColumnType.Settings) },
|
||||
onClick = {
|
||||
if (deckState.hasColumnOfType(DeckColumnType.Settings)) {
|
||||
deckState.focusExistingColumn(DeckColumnType.Settings)
|
||||
} else {
|
||||
deckState.addColumn(DeckColumnType.Settings)
|
||||
}
|
||||
},
|
||||
)
|
||||
Separator()
|
||||
Item(
|
||||
@@ -305,7 +312,8 @@ fun main() =
|
||||
Key.Eight,
|
||||
Key.Nine,
|
||||
)
|
||||
columnKeys.forEachIndexed { i, key ->
|
||||
val columnCount = deckState.columns.value.size
|
||||
columnKeys.take(columnCount).forEachIndexed { i, key ->
|
||||
Item(
|
||||
"Column ${i + 1}",
|
||||
shortcut =
|
||||
@@ -617,7 +625,13 @@ fun MainContent(
|
||||
LayoutMode.DECK -> {
|
||||
DeckSidebar(
|
||||
onAddColumn = onShowAddColumnDialog,
|
||||
onOpenSettings = { deckState.addColumn(DeckColumnType.Settings) },
|
||||
onOpenSettings = {
|
||||
if (deckState.hasColumnOfType(DeckColumnType.Settings)) {
|
||||
deckState.focusExistingColumn(DeckColumnType.Settings)
|
||||
} else {
|
||||
deckState.addColumn(DeckColumnType.Settings)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
@@ -151,6 +151,7 @@ fun FeedScreen(
|
||||
account: AccountState.LoggedIn? = null,
|
||||
nwcConnection: com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm? = null,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null,
|
||||
initialFeedMode: FeedMode? = null,
|
||||
onCompose: () -> Unit = {},
|
||||
onNavigateToProfile: (String) -> Unit = {},
|
||||
onNavigateToThread: (String) -> Unit = {},
|
||||
@@ -170,7 +171,7 @@ fun FeedScreen(
|
||||
}
|
||||
val events by eventState.items.collectAsState()
|
||||
var replyToEvent by remember { mutableStateOf<Event?>(null) }
|
||||
var feedMode by remember { mutableStateOf(DesktopPreferences.feedMode) }
|
||||
var feedMode by remember { mutableStateOf(initialFeedMode ?: DesktopPreferences.feedMode) }
|
||||
var followedUsers by remember { mutableStateOf<Set<String>>(emptySet()) }
|
||||
var zapsByEvent by remember { mutableStateOf<Map<String, List<ZapReceipt>>>(emptyMap()) }
|
||||
// Track reaction event IDs per target event to deduplicate
|
||||
|
||||
@@ -78,6 +78,7 @@ fun SearchScreen(
|
||||
localCache: DesktopLocalCache,
|
||||
relayManager: DesktopRelayConnectionManager,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null,
|
||||
initialQuery: String = "",
|
||||
onNavigateToProfile: (String) -> Unit,
|
||||
onNavigateToThread: (String) -> Unit,
|
||||
onNavigateToHashtag: (String) -> Unit = {},
|
||||
@@ -86,6 +87,13 @@ fun SearchScreen(
|
||||
val scope = rememberCoroutineScope()
|
||||
val searchState = remember { SearchBarState(localCache, scope) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
// Pre-fill initial query (e.g., hashtag column)
|
||||
LaunchedEffect(initialQuery) {
|
||||
if (initialQuery.isNotBlank()) {
|
||||
searchState.updateSearchText(initialQuery)
|
||||
}
|
||||
}
|
||||
val relayStatuses by relayManager.relayStatuses.collectAsState()
|
||||
|
||||
// Collect state from SearchBarState
|
||||
|
||||
+1
-1
@@ -133,6 +133,6 @@ fun DeckColumnType.icon(): ImageVector =
|
||||
DeckColumnType.Chess -> Icons.Default.Extension
|
||||
DeckColumnType.Settings -> Icons.Default.Settings
|
||||
is DeckColumnType.Profile -> Icons.Default.Person
|
||||
is DeckColumnType.Thread -> Icons.Default.Home
|
||||
is DeckColumnType.Thread -> Icons.AutoMirrored.Filled.Article
|
||||
is DeckColumnType.Hashtag -> Icons.Default.Tag
|
||||
}
|
||||
|
||||
+13
-2
@@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.desktop.chess.ChessScreen
|
||||
import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.FeedMode
|
||||
import com.vitorpamplona.amethyst.desktop.ui.BookmarksScreen
|
||||
import com.vitorpamplona.amethyst.desktop.ui.FeedScreen
|
||||
import com.vitorpamplona.amethyst.desktop.ui.NotificationsScreen
|
||||
@@ -56,10 +57,11 @@ import com.vitorpamplona.amethyst.desktop.ui.chats.DmSendTracker
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class ColumnNavigationState {
|
||||
private val _stack = MutableStateFlow<List<DesktopScreen>>(emptyList())
|
||||
val stack = _stack
|
||||
val stack: kotlinx.coroutines.flow.StateFlow<List<DesktopScreen>> = _stack.asStateFlow()
|
||||
|
||||
fun push(screen: DesktopScreen) {
|
||||
_stack.value = _stack.value + screen
|
||||
@@ -180,6 +182,7 @@ internal fun RootContent(
|
||||
account = account,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
initialFeedMode = FeedMode.FOLLOWING,
|
||||
onCompose = onShowComposeDialog,
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
onNavigateToThread = onNavigateToThread,
|
||||
@@ -249,6 +252,7 @@ internal fun RootContent(
|
||||
account = account,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
initialFeedMode = FeedMode.GLOBAL,
|
||||
onCompose = onShowComposeDialog,
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
onNavigateToThread = onNavigateToThread,
|
||||
@@ -320,6 +324,7 @@ internal fun RootContent(
|
||||
localCache = localCache,
|
||||
relayManager = relayManager,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
initialQuery = "#${columnType.tag}",
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
onNavigateToThread = onNavigateToThread,
|
||||
)
|
||||
@@ -374,6 +379,12 @@ internal fun OverlayContent(
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
else -> {
|
||||
androidx.compose.material3.Text(
|
||||
"Unsupported screen type",
|
||||
style = androidx.compose.material3.MaterialTheme.typography.bodyMedium,
|
||||
color = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-2
@@ -21,15 +21,18 @@
|
||||
package com.vitorpamplona.amethyst.desktop.ui.deck
|
||||
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -70,7 +73,22 @@ fun DeckLayout(
|
||||
val availableWidthDp = with(density) { constraints.maxWidth.toDp().value }
|
||||
deckState.setAvailableWidth(availableWidthDp)
|
||||
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
// Auto-fit columns on first composition or when available width changes significantly
|
||||
LaunchedEffect(availableWidthDp, columns.size) {
|
||||
val dividers = (columns.size - 1) * DeckState.DIVIDER_WIDTH
|
||||
val totalColumnWidth = columns.sumOf { it.width.toDouble() }.toFloat()
|
||||
val diff = kotlin.math.abs(totalColumnWidth + dividers - availableWidthDp)
|
||||
if (diff > 20f && columns.isNotEmpty()) {
|
||||
deckState.fitColumnsToWidth(availableWidthDp)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
) {
|
||||
columns.forEachIndexed { index, column ->
|
||||
if (index > 0) {
|
||||
DraggableDivider(
|
||||
@@ -108,7 +126,7 @@ private fun DraggableDivider(onDrag: (Float) -> Unit) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.width(4.dp)
|
||||
.width(12.dp)
|
||||
.fillMaxHeight()
|
||||
.pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)))
|
||||
.pointerInput(Unit) {
|
||||
@@ -117,6 +135,7 @@ private fun DraggableDivider(onDrag: (Float) -> Unit) {
|
||||
onDrag(dragAmount.x / density)
|
||||
}
|
||||
},
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center,
|
||||
) {
|
||||
VerticalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
|
||||
+114
-68
@@ -23,19 +23,29 @@ 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 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 DeckState {
|
||||
class DeckState(
|
||||
private val saveScope: CoroutineScope,
|
||||
) {
|
||||
private val _columns = MutableStateFlow(DEFAULT_COLUMNS)
|
||||
val columns: StateFlow<List<DeckColumn>> = _columns.asStateFlow()
|
||||
|
||||
private val _focusedColumnIndex = MutableStateFlow(0)
|
||||
val focusedColumnIndex: StateFlow<Int> = _focusedColumnIndex.asStateFlow()
|
||||
|
||||
@Volatile
|
||||
private var lastKnownWidth: Float = 0f
|
||||
|
||||
private var saveJob: Job? = null
|
||||
|
||||
fun setAvailableWidth(width: Float) {
|
||||
lastKnownWidth = width
|
||||
}
|
||||
@@ -45,71 +55,95 @@ class DeckState {
|
||||
afterIndex: Int? = null,
|
||||
) {
|
||||
val col = DeckColumn(type = type)
|
||||
_columns.value =
|
||||
if (afterIndex != null && afterIndex < _columns.value.size) {
|
||||
_columns.value.toMutableList().apply { add(afterIndex + 1, col) }
|
||||
_columns.update { current ->
|
||||
if (afterIndex != null && afterIndex < current.size) {
|
||||
current.toMutableList().apply { add(afterIndex + 1, col) }
|
||||
} else {
|
||||
_columns.value + col
|
||||
current + col
|
||||
}
|
||||
}
|
||||
// Auto-fit all columns to available width when known
|
||||
if (lastKnownWidth > 0f) {
|
||||
fitColumnsToWidth(lastKnownWidth)
|
||||
val width = lastKnownWidth
|
||||
if (width > 0f) {
|
||||
fitColumnsToWidth(width)
|
||||
} else {
|
||||
save()
|
||||
scheduleSave()
|
||||
}
|
||||
}
|
||||
|
||||
fun hasColumnOfType(type: DeckColumnType): Boolean = _columns.value.any { it.type == type }
|
||||
|
||||
fun focusExistingColumn(type: DeckColumnType) {
|
||||
val idx = _columns.value.indexOfFirst { it.type == type }
|
||||
if (idx >= 0) focusColumn(idx)
|
||||
}
|
||||
|
||||
fun removeColumn(id: String) {
|
||||
if (_columns.value.size <= 1) return
|
||||
val removed = _columns.value.find { it.id == id } ?: return
|
||||
val remaining = _columns.value.filter { it.id != id }
|
||||
// Redistribute removed column's width evenly across remaining columns
|
||||
val extra = removed.width / remaining.size
|
||||
_columns.value =
|
||||
remaining.map {
|
||||
it.copy(width = (it.width + extra).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH))
|
||||
_columns.update { current ->
|
||||
if (current.size <= 1) return
|
||||
val removed = current.find { it.id == id } ?: return
|
||||
val remaining = current.filter { it.id != id }
|
||||
val extra = removed.width / remaining.size
|
||||
val result =
|
||||
remaining.map {
|
||||
it.copy(width = (it.width + extra).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH))
|
||||
}
|
||||
// Fix gaps: if total is less than available, redistribute remainder
|
||||
val width = lastKnownWidth
|
||||
if (width > 0f) {
|
||||
val dividers = (result.size - 1) * DIVIDER_WIDTH
|
||||
val totalUsed = result.sumOf { it.width.toDouble() }.toFloat()
|
||||
val deficit = width - dividers - totalUsed
|
||||
if (deficit > 1f) {
|
||||
val perColumn = deficit / result.size
|
||||
return@update result.map {
|
||||
it.copy(width = (it.width + perColumn).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_focusedColumnIndex.value >= _columns.value.size) {
|
||||
_focusedColumnIndex.value = _columns.value.size - 1
|
||||
result
|
||||
}
|
||||
save()
|
||||
_focusedColumnIndex.update { idx ->
|
||||
idx.coerceAtMost(_columns.value.size - 1)
|
||||
}
|
||||
scheduleSave()
|
||||
}
|
||||
|
||||
fun moveColumn(
|
||||
fromIndex: Int,
|
||||
toIndex: Int,
|
||||
) {
|
||||
val list = _columns.value.toMutableList()
|
||||
if (fromIndex !in list.indices || toIndex !in list.indices) return
|
||||
val item = list.removeAt(fromIndex)
|
||||
list.add(toIndex, item)
|
||||
_columns.value = list
|
||||
save()
|
||||
_columns.update { current ->
|
||||
if (fromIndex !in current.indices || toIndex !in current.indices) return
|
||||
current.toMutableList().apply {
|
||||
val item = removeAt(fromIndex)
|
||||
add(toIndex, item)
|
||||
}
|
||||
}
|
||||
scheduleSave()
|
||||
}
|
||||
|
||||
fun updateColumnWidth(
|
||||
id: String,
|
||||
width: Float,
|
||||
) {
|
||||
_columns.value =
|
||||
_columns.value.map {
|
||||
_columns.update { current ->
|
||||
current.map {
|
||||
if (it.id == id) it.copy(width = width.coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)) else it
|
||||
}
|
||||
save()
|
||||
}
|
||||
scheduleSave()
|
||||
}
|
||||
|
||||
fun expandColumn(
|
||||
id: String,
|
||||
availableWidth: Float,
|
||||
) {
|
||||
val cols = _columns.value
|
||||
val target = cols.find { it.id == id } ?: return
|
||||
val others = cols.filter { it.id != id }
|
||||
// Shrink others to minimum, give the rest to the target
|
||||
val othersMin = others.size * MIN_COLUMN_WIDTH
|
||||
val dividerWidth = (cols.size - 1) * DIVIDER_WIDTH
|
||||
val maxForTarget = (availableWidth - othersMin - dividerWidth).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)
|
||||
_columns.value =
|
||||
_columns.update { cols ->
|
||||
if (cols.find { it.id == id } == null) return
|
||||
val othersMin = (cols.size - 1) * MIN_COLUMN_WIDTH
|
||||
val dividerWidth = (cols.size - 1) * DIVIDER_WIDTH
|
||||
val maxForTarget = (availableWidth - othersMin - dividerWidth).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)
|
||||
cols.map {
|
||||
if (it.id == id) {
|
||||
it.copy(width = maxForTarget)
|
||||
@@ -117,7 +151,8 @@ class DeckState {
|
||||
it.copy(width = MIN_COLUMN_WIDTH)
|
||||
}
|
||||
}
|
||||
save()
|
||||
}
|
||||
scheduleSave()
|
||||
}
|
||||
|
||||
fun resizePair(
|
||||
@@ -126,24 +161,21 @@ class DeckState {
|
||||
delta: Float,
|
||||
availableWidth: Float,
|
||||
) {
|
||||
val cols = _columns.value
|
||||
val left = cols.find { it.id == leftId } ?: return
|
||||
val right = cols.find { it.id == rightId } ?: return
|
||||
// Compute max total allowed (available minus other columns and dividers)
|
||||
val otherWidth = cols.filter { it.id != leftId && it.id != rightId }.sumOf { it.width.toDouble() }.toFloat()
|
||||
val dividerWidth = (cols.size - 1) * DIVIDER_WIDTH
|
||||
val maxPairWidth = availableWidth - otherWidth - dividerWidth
|
||||
var newLeft = (left.width + delta).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)
|
||||
var newRight = (right.width - delta).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)
|
||||
// Ensure pair doesn't exceed available space
|
||||
if (newLeft + newRight > maxPairWidth) {
|
||||
if (delta > 0) {
|
||||
newLeft = (maxPairWidth - newRight).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)
|
||||
} else {
|
||||
newRight = (maxPairWidth - newLeft).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)
|
||||
_columns.update { cols ->
|
||||
val left = cols.find { it.id == leftId } ?: return
|
||||
val right = cols.find { it.id == rightId } ?: return
|
||||
val otherWidth = cols.filter { it.id != leftId && it.id != rightId }.sumOf { it.width.toDouble() }.toFloat()
|
||||
val dividerWidth = (cols.size - 1) * DIVIDER_WIDTH
|
||||
val maxPairWidth = availableWidth - otherWidth - dividerWidth
|
||||
var newLeft = (left.width + delta).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)
|
||||
var newRight = (right.width - delta).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)
|
||||
if (newLeft + newRight > maxPairWidth) {
|
||||
if (delta > 0) {
|
||||
newLeft = (maxPairWidth - newRight).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)
|
||||
} else {
|
||||
newRight = (maxPairWidth - newLeft).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)
|
||||
}
|
||||
}
|
||||
}
|
||||
_columns.value =
|
||||
cols.map {
|
||||
when (it.id) {
|
||||
leftId -> it.copy(width = newLeft)
|
||||
@@ -151,17 +183,19 @@ class DeckState {
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
save()
|
||||
}
|
||||
scheduleSave()
|
||||
}
|
||||
|
||||
fun fitColumnsToWidth(availableWidth: Float) {
|
||||
val cols = _columns.value
|
||||
if (cols.isEmpty()) return
|
||||
val dividers = (cols.size - 1) * DIVIDER_WIDTH
|
||||
val usable = availableWidth - dividers
|
||||
val perColumn = (usable / cols.size).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)
|
||||
_columns.value = cols.map { it.copy(width = perColumn) }
|
||||
save()
|
||||
_columns.update { cols ->
|
||||
if (cols.isEmpty()) return
|
||||
val dividers = (cols.size - 1) * DIVIDER_WIDTH
|
||||
val usable = availableWidth - dividers
|
||||
val perColumn = (usable / cols.size).coerceIn(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)
|
||||
cols.map { it.copy(width = perColumn) }
|
||||
}
|
||||
scheduleSave()
|
||||
}
|
||||
|
||||
fun focusColumn(index: Int) {
|
||||
@@ -170,6 +204,15 @@ class DeckState {
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleSave() {
|
||||
saveJob?.cancel()
|
||||
saveJob =
|
||||
saveScope.launch {
|
||||
delay(SAVE_DEBOUNCE_MS)
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
fun save() {
|
||||
try {
|
||||
val data =
|
||||
@@ -188,7 +231,8 @@ class DeckState {
|
||||
)
|
||||
}
|
||||
DesktopPreferences.deckColumns = mapper.writeValueAsString(data)
|
||||
} catch (_: Exception) {
|
||||
} catch (e: Exception) {
|
||||
println("DeckState: failed to save columns: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,20 +251,22 @@ class DeckState {
|
||||
if (loaded.isNotEmpty()) {
|
||||
_columns.value = loaded
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
} catch (e: Exception) {
|
||||
println("DeckState: failed to load columns: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MIN_COLUMN_WIDTH = 300f
|
||||
const val MAX_COLUMN_WIDTH = 800f
|
||||
const val DIVIDER_WIDTH = 4f
|
||||
const val DIVIDER_WIDTH = 12f
|
||||
private const val SAVE_DEBOUNCE_MS = 500L
|
||||
|
||||
val DEFAULT_COLUMNS =
|
||||
listOf(
|
||||
DeckColumn(type = DeckColumnType.HomeFeed),
|
||||
DeckColumn(type = DeckColumnType.Notifications),
|
||||
DeckColumn(type = DeckColumnType.Messages),
|
||||
DeckColumn(id = "default-home", type = DeckColumnType.HomeFeed),
|
||||
DeckColumn(id = "default-notifications", type = DeckColumnType.Notifications),
|
||||
DeckColumn(id = "default-messages", type = DeckColumnType.Messages),
|
||||
)
|
||||
|
||||
private val mapper = jacksonObjectMapper()
|
||||
|
||||
Reference in New Issue
Block a user