feat(desktop): add single-pane/deck toggle with smart column resizing
- Add LayoutMode enum (SINGLE_PANE/DECK) persisted in DesktopPreferences - SinglePaneLayout: NavigationRail + single content area reusing deck routing - View menu "Deck Layout" toggle with Cmd+Shift+D shortcut - Column resize: redistribute width on add/remove, double-click header to expand - Constrain drag resize to window bounds via BoxWithConstraints - FlowRow headers in FeedScreen/ReadsScreen to wrap on narrow columns Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ object DesktopPreferences {
|
|||||||
private const val KEY_FEED_MODE = "feed_mode"
|
private const val KEY_FEED_MODE = "feed_mode"
|
||||||
private const val KEY_LAST_SCREEN = "last_screen"
|
private const val KEY_LAST_SCREEN = "last_screen"
|
||||||
private const val KEY_DECK_COLUMNS = "deck_columns"
|
private const val KEY_DECK_COLUMNS = "deck_columns"
|
||||||
|
private const val KEY_LAYOUT_MODE = "layout_mode"
|
||||||
|
|
||||||
var feedMode: FeedMode
|
var feedMode: FeedMode
|
||||||
get() {
|
get() {
|
||||||
@@ -61,4 +62,10 @@ object DesktopPreferences {
|
|||||||
set(value) {
|
set(value) {
|
||||||
prefs.put(KEY_DECK_COLUMNS, value)
|
prefs.put(KEY_DECK_COLUMNS, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var layoutMode: String
|
||||||
|
get() = prefs.get(KEY_LAYOUT_MODE, "SINGLE_PANE")
|
||||||
|
set(value) {
|
||||||
|
prefs.put(KEY_LAYOUT_MODE, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ import com.vitorpamplona.amethyst.desktop.ui.deck.DeckColumnType
|
|||||||
import com.vitorpamplona.amethyst.desktop.ui.deck.DeckLayout
|
import com.vitorpamplona.amethyst.desktop.ui.deck.DeckLayout
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.deck.DeckSidebar
|
import com.vitorpamplona.amethyst.desktop.ui.deck.DeckSidebar
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.deck.DeckState
|
import com.vitorpamplona.amethyst.desktop.ui.deck.DeckState
|
||||||
|
import com.vitorpamplona.amethyst.desktop.ui.deck.SinglePaneLayout
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.profile.ProfileInfoCard
|
import com.vitorpamplona.amethyst.desktop.ui.profile.ProfileInfoCard
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.relay.RelayStatusCard
|
import com.vitorpamplona.amethyst.desktop.ui.relay.RelayStatusCard
|
||||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||||
@@ -92,6 +93,11 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
private val isMacOS = System.getProperty("os.name").lowercase().contains("mac")
|
private val isMacOS = System.getProperty("os.name").lowercase().contains("mac")
|
||||||
|
|
||||||
|
enum class LayoutMode {
|
||||||
|
SINGLE_PANE,
|
||||||
|
DECK,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Desktop navigation state — used for in-column navigation (drill-down).
|
* Desktop navigation state — used for in-column navigation (drill-down).
|
||||||
*/
|
*/
|
||||||
@@ -133,6 +139,15 @@ 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 deckState = remember { DeckState().also { it.load() } }
|
val deckState = remember { DeckState().also { it.load() } }
|
||||||
var showAddColumnDialog by remember { mutableStateOf(false) }
|
var showAddColumnDialog by remember { mutableStateOf(false) }
|
||||||
|
var layoutMode by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
try {
|
||||||
|
LayoutMode.valueOf(DesktopPreferences.layoutMode)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LayoutMode.SINGLE_PANE
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Window(
|
Window(
|
||||||
onCloseRequest = ::exitApplication,
|
onCloseRequest = ::exitApplication,
|
||||||
@@ -198,100 +213,117 @@ fun main() =
|
|||||||
}
|
}
|
||||||
Menu("View") {
|
Menu("View") {
|
||||||
Item(
|
Item(
|
||||||
"Add Column",
|
if (layoutMode == LayoutMode.DECK) "\u2713 Deck Layout" else "Deck Layout",
|
||||||
shortcut =
|
shortcut =
|
||||||
if (isMacOS) {
|
if (isMacOS) {
|
||||||
KeyShortcut(Key.T, meta = true)
|
KeyShortcut(Key.D, meta = true, shift = true)
|
||||||
} else {
|
} else {
|
||||||
KeyShortcut(Key.T, ctrl = true)
|
KeyShortcut(Key.D, ctrl = true, shift = true)
|
||||||
},
|
|
||||||
onClick = { showAddColumnDialog = true },
|
|
||||||
)
|
|
||||||
Item(
|
|
||||||
"Close Column",
|
|
||||||
shortcut =
|
|
||||||
if (isMacOS) {
|
|
||||||
KeyShortcut(Key.W, meta = true)
|
|
||||||
} else {
|
|
||||||
KeyShortcut(Key.W, ctrl = true)
|
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
val cols = deckState.columns.value
|
layoutMode =
|
||||||
val idx = deckState.focusedColumnIndex.value
|
if (layoutMode == LayoutMode.DECK) LayoutMode.SINGLE_PANE else LayoutMode.DECK
|
||||||
if (cols.size > 1 && idx in cols.indices) {
|
DesktopPreferences.layoutMode = layoutMode.name
|
||||||
deckState.removeColumn(cols[idx].id)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Item(
|
if (layoutMode == LayoutMode.DECK) {
|
||||||
"Move Column Left",
|
Separator()
|
||||||
shortcut =
|
|
||||||
if (isMacOS) {
|
|
||||||
KeyShortcut(Key.DirectionLeft, meta = true, shift = true)
|
|
||||||
} else {
|
|
||||||
KeyShortcut(Key.DirectionLeft, ctrl = true, shift = true)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
val idx = deckState.focusedColumnIndex.value
|
|
||||||
if (idx > 0) {
|
|
||||||
deckState.moveColumn(idx, idx - 1)
|
|
||||||
deckState.focusColumn(idx - 1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
Item(
|
|
||||||
"Move Column Right",
|
|
||||||
shortcut =
|
|
||||||
if (isMacOS) {
|
|
||||||
KeyShortcut(Key.DirectionRight, meta = true, shift = true)
|
|
||||||
} else {
|
|
||||||
KeyShortcut(Key.DirectionRight, ctrl = true, shift = true)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
val idx = deckState.focusedColumnIndex.value
|
|
||||||
val size = deckState.columns.value.size
|
|
||||||
if (idx < size - 1) {
|
|
||||||
deckState.moveColumn(idx, idx + 1)
|
|
||||||
deckState.focusColumn(idx + 1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
Separator()
|
|
||||||
// Focus column by index (Cmd/Ctrl+1..9)
|
|
||||||
val columnKeys =
|
|
||||||
listOf(
|
|
||||||
Key.One,
|
|
||||||
Key.Two,
|
|
||||||
Key.Three,
|
|
||||||
Key.Four,
|
|
||||||
Key.Five,
|
|
||||||
Key.Six,
|
|
||||||
Key.Seven,
|
|
||||||
Key.Eight,
|
|
||||||
Key.Nine,
|
|
||||||
)
|
|
||||||
columnKeys.forEachIndexed { i, key ->
|
|
||||||
Item(
|
Item(
|
||||||
"Column ${i + 1}",
|
"Add Column",
|
||||||
shortcut =
|
shortcut =
|
||||||
if (isMacOS) {
|
if (isMacOS) {
|
||||||
KeyShortcut(key, meta = true)
|
KeyShortcut(Key.T, meta = true)
|
||||||
} else {
|
} else {
|
||||||
KeyShortcut(key, ctrl = true)
|
KeyShortcut(Key.T, ctrl = true)
|
||||||
},
|
},
|
||||||
onClick = { deckState.focusColumn(i) },
|
onClick = { showAddColumnDialog = true },
|
||||||
)
|
)
|
||||||
}
|
Item(
|
||||||
Separator()
|
"Close Column",
|
||||||
Menu("Add Column...") {
|
shortcut =
|
||||||
Item("Home Feed", onClick = { deckState.addColumn(DeckColumnType.HomeFeed) })
|
if (isMacOS) {
|
||||||
Item("Notifications", onClick = { deckState.addColumn(DeckColumnType.Notifications) })
|
KeyShortcut(Key.W, meta = true)
|
||||||
Item("Messages", onClick = { deckState.addColumn(DeckColumnType.Messages) })
|
} else {
|
||||||
Item("Search", onClick = { deckState.addColumn(DeckColumnType.Search) })
|
KeyShortcut(Key.W, ctrl = true)
|
||||||
Item("Reads", onClick = { deckState.addColumn(DeckColumnType.Reads) })
|
},
|
||||||
Item("Bookmarks", onClick = { deckState.addColumn(DeckColumnType.Bookmarks) })
|
onClick = {
|
||||||
Item("Global Feed", onClick = { deckState.addColumn(DeckColumnType.GlobalFeed) })
|
val cols = deckState.columns.value
|
||||||
Item("Profile", onClick = { deckState.addColumn(DeckColumnType.MyProfile) })
|
val idx = deckState.focusedColumnIndex.value
|
||||||
|
if (cols.size > 1 && idx in cols.indices) {
|
||||||
|
deckState.removeColumn(cols[idx].id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Item(
|
||||||
|
"Move Column Left",
|
||||||
|
shortcut =
|
||||||
|
if (isMacOS) {
|
||||||
|
KeyShortcut(Key.DirectionLeft, meta = true, shift = true)
|
||||||
|
} else {
|
||||||
|
KeyShortcut(Key.DirectionLeft, ctrl = true, shift = true)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
val idx = deckState.focusedColumnIndex.value
|
||||||
|
if (idx > 0) {
|
||||||
|
deckState.moveColumn(idx, idx - 1)
|
||||||
|
deckState.focusColumn(idx - 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Item(
|
||||||
|
"Move Column Right",
|
||||||
|
shortcut =
|
||||||
|
if (isMacOS) {
|
||||||
|
KeyShortcut(Key.DirectionRight, meta = true, shift = true)
|
||||||
|
} else {
|
||||||
|
KeyShortcut(Key.DirectionRight, ctrl = true, shift = true)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
val idx = deckState.focusedColumnIndex.value
|
||||||
|
val size = deckState.columns.value.size
|
||||||
|
if (idx < size - 1) {
|
||||||
|
deckState.moveColumn(idx, idx + 1)
|
||||||
|
deckState.focusColumn(idx + 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Separator()
|
||||||
|
// Focus column by index (Cmd/Ctrl+1..9)
|
||||||
|
val columnKeys =
|
||||||
|
listOf(
|
||||||
|
Key.One,
|
||||||
|
Key.Two,
|
||||||
|
Key.Three,
|
||||||
|
Key.Four,
|
||||||
|
Key.Five,
|
||||||
|
Key.Six,
|
||||||
|
Key.Seven,
|
||||||
|
Key.Eight,
|
||||||
|
Key.Nine,
|
||||||
|
)
|
||||||
|
columnKeys.forEachIndexed { i, key ->
|
||||||
|
Item(
|
||||||
|
"Column ${i + 1}",
|
||||||
|
shortcut =
|
||||||
|
if (isMacOS) {
|
||||||
|
KeyShortcut(key, meta = true)
|
||||||
|
} else {
|
||||||
|
KeyShortcut(key, ctrl = true)
|
||||||
|
},
|
||||||
|
onClick = { deckState.focusColumn(i) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Separator()
|
||||||
|
Menu("Add Column...") {
|
||||||
|
Item("Home Feed", onClick = { deckState.addColumn(DeckColumnType.HomeFeed) })
|
||||||
|
Item("Notifications", onClick = { deckState.addColumn(DeckColumnType.Notifications) })
|
||||||
|
Item("Messages", onClick = { deckState.addColumn(DeckColumnType.Messages) })
|
||||||
|
Item("Search", onClick = { deckState.addColumn(DeckColumnType.Search) })
|
||||||
|
Item("Reads", onClick = { deckState.addColumn(DeckColumnType.Reads) })
|
||||||
|
Item("Bookmarks", onClick = { deckState.addColumn(DeckColumnType.Bookmarks) })
|
||||||
|
Item("Global Feed", onClick = { deckState.addColumn(DeckColumnType.GlobalFeed) })
|
||||||
|
Item("Profile", onClick = { deckState.addColumn(DeckColumnType.MyProfile) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Menu("Help") {
|
Menu("Help") {
|
||||||
@@ -301,6 +333,7 @@ fun main() =
|
|||||||
}
|
}
|
||||||
|
|
||||||
App(
|
App(
|
||||||
|
layoutMode = layoutMode,
|
||||||
deckState = deckState,
|
deckState = deckState,
|
||||||
showComposeDialog = showComposeDialog,
|
showComposeDialog = showComposeDialog,
|
||||||
showAddColumnDialog = showAddColumnDialog,
|
showAddColumnDialog = showAddColumnDialog,
|
||||||
@@ -322,6 +355,7 @@ fun main() =
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun App(
|
fun App(
|
||||||
|
layoutMode: LayoutMode,
|
||||||
deckState: DeckState,
|
deckState: DeckState,
|
||||||
showComposeDialog: Boolean,
|
showComposeDialog: Boolean,
|
||||||
showAddColumnDialog: Boolean,
|
showAddColumnDialog: Boolean,
|
||||||
@@ -425,6 +459,7 @@ fun App(
|
|||||||
}
|
}
|
||||||
|
|
||||||
MainContent(
|
MainContent(
|
||||||
|
layoutMode = layoutMode,
|
||||||
deckState = deckState,
|
deckState = deckState,
|
||||||
relayManager = relayManager,
|
relayManager = relayManager,
|
||||||
localCache = localCache,
|
localCache = localCache,
|
||||||
@@ -466,6 +501,7 @@ fun App(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainContent(
|
fun MainContent(
|
||||||
|
layoutMode: LayoutMode,
|
||||||
deckState: DeckState,
|
deckState: DeckState,
|
||||||
relayManager: DesktopRelayConnectionManager,
|
relayManager: DesktopRelayConnectionManager,
|
||||||
localCache: DesktopLocalCache,
|
localCache: DesktopLocalCache,
|
||||||
@@ -497,27 +533,47 @@ fun MainContent(
|
|||||||
|
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
Row(Modifier.fillMaxSize()) {
|
Row(Modifier.fillMaxSize()) {
|
||||||
DeckSidebar(
|
when (layoutMode) {
|
||||||
onAddColumn = onShowAddColumnDialog,
|
LayoutMode.SINGLE_PANE -> {
|
||||||
onOpenSettings = { deckState.addColumn(DeckColumnType.Settings) },
|
SinglePaneLayout(
|
||||||
)
|
relayManager = relayManager,
|
||||||
|
localCache = localCache,
|
||||||
|
accountManager = accountManager,
|
||||||
|
account = account,
|
||||||
|
nwcConnection = nwcConnection,
|
||||||
|
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||||
|
appScope = appScope,
|
||||||
|
onShowComposeDialog = onShowComposeDialog,
|
||||||
|
onShowReplyDialog = onShowReplyDialog,
|
||||||
|
onZapFeedback = onZapFeedback,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
VerticalDivider()
|
LayoutMode.DECK -> {
|
||||||
|
DeckSidebar(
|
||||||
|
onAddColumn = onShowAddColumnDialog,
|
||||||
|
onOpenSettings = { deckState.addColumn(DeckColumnType.Settings) },
|
||||||
|
)
|
||||||
|
|
||||||
DeckLayout(
|
VerticalDivider()
|
||||||
deckState = deckState,
|
|
||||||
relayManager = relayManager,
|
DeckLayout(
|
||||||
localCache = localCache,
|
deckState = deckState,
|
||||||
accountManager = accountManager,
|
relayManager = relayManager,
|
||||||
account = account,
|
localCache = localCache,
|
||||||
nwcConnection = nwcConnection,
|
accountManager = accountManager,
|
||||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
account = account,
|
||||||
appScope = appScope,
|
nwcConnection = nwcConnection,
|
||||||
onShowComposeDialog = onShowComposeDialog,
|
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||||
onShowReplyDialog = onShowReplyDialog,
|
appScope = appScope,
|
||||||
onZapFeedback = onZapFeedback,
|
onShowComposeDialog = onShowComposeDialog,
|
||||||
modifier = Modifier.weight(1f),
|
onShowReplyDialog = onShowReplyDialog,
|
||||||
)
|
onZapFeedback = onZapFeedback,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snackbar for zap feedback
|
// Snackbar for zap feedback
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.desktop.ui
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -475,16 +477,17 @@ fun FeedScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
// Header with compose button
|
// Header with compose button — wraps on narrow columns
|
||||||
Row(
|
FlowRow(
|
||||||
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
|
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Row(
|
FlowRow(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.desktop.ui
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -259,16 +261,17 @@ fun ReadsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
// Header
|
// Header — wraps on narrow columns
|
||||||
Row(
|
FlowRow(
|
||||||
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
|
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Row(
|
FlowRow(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
+8
-1
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.amethyst.desktop.ui.deck
|
package com.vitorpamplona.amethyst.desktop.ui.deck
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -48,6 +49,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ fun ColumnHeader(
|
|||||||
hasBackStack: Boolean,
|
hasBackStack: Boolean,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
|
onDoubleClick: () -> Unit = {},
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -66,7 +69,11 @@ fun ColumnHeader(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(40.dp)
|
.height(40.dp)
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
.padding(horizontal = 8.dp),
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onDoubleTap = { onDoubleClick() },
|
||||||
|
)
|
||||||
|
}.padding(horizontal = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
if (hasBackStack) {
|
if (hasBackStack) {
|
||||||
|
|||||||
+4
-2
@@ -80,6 +80,7 @@ fun DeckColumnContainer(
|
|||||||
column: DeckColumn,
|
column: DeckColumn,
|
||||||
canClose: Boolean,
|
canClose: Boolean,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
|
onDoubleClickHeader: () -> Unit = {},
|
||||||
relayManager: DesktopRelayConnectionManager,
|
relayManager: DesktopRelayConnectionManager,
|
||||||
localCache: DesktopLocalCache,
|
localCache: DesktopLocalCache,
|
||||||
accountManager: AccountManager,
|
accountManager: AccountManager,
|
||||||
@@ -108,6 +109,7 @@ fun DeckColumnContainer(
|
|||||||
hasBackStack = navStack.isNotEmpty(),
|
hasBackStack = navStack.isNotEmpty(),
|
||||||
onBack = { navState.pop() },
|
onBack = { navState.pop() },
|
||||||
onClose = onClose,
|
onClose = onClose,
|
||||||
|
onDoubleClick = onDoubleClickHeader,
|
||||||
)
|
)
|
||||||
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
@@ -152,7 +154,7 @@ fun DeckColumnContainer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RootContent(
|
internal fun RootContent(
|
||||||
columnType: DeckColumnType,
|
columnType: DeckColumnType,
|
||||||
relayManager: DesktopRelayConnectionManager,
|
relayManager: DesktopRelayConnectionManager,
|
||||||
localCache: DesktopLocalCache,
|
localCache: DesktopLocalCache,
|
||||||
@@ -314,7 +316,7 @@ private fun RootContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun OverlayContent(
|
internal fun OverlayContent(
|
||||||
screen: DesktopScreen,
|
screen: DesktopScreen,
|
||||||
relayManager: DesktopRelayConnectionManager,
|
relayManager: DesktopRelayConnectionManager,
|
||||||
localCache: DesktopLocalCache,
|
localCache: DesktopLocalCache,
|
||||||
|
|||||||
+34
-31
@@ -21,13 +21,12 @@
|
|||||||
package com.vitorpamplona.amethyst.desktop.ui.deck
|
package com.vitorpamplona.amethyst.desktop.ui.deck
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
import androidx.compose.foundation.horizontalScroll
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.VerticalDivider
|
import androidx.compose.material3.VerticalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -37,6 +36,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.input.pointer.PointerIcon
|
import androidx.compose.ui.input.pointer.PointerIcon
|
||||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.desktop.account.AccountManager
|
import com.vitorpamplona.amethyst.desktop.account.AccountManager
|
||||||
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||||
@@ -64,38 +64,41 @@ fun DeckLayout(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val columns by deckState.columns.collectAsState()
|
val columns by deckState.columns.collectAsState()
|
||||||
val scrollState = rememberScrollState()
|
val density = LocalDensity.current
|
||||||
|
|
||||||
Row(
|
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
|
||||||
modifier = modifier.fillMaxSize().horizontalScroll(scrollState),
|
val availableWidthDp = with(density) { constraints.maxWidth.toDp().value }
|
||||||
) {
|
deckState.setAvailableWidth(availableWidthDp)
|
||||||
columns.forEachIndexed { index, column ->
|
|
||||||
if (index > 0) {
|
Row(modifier = Modifier.fillMaxSize()) {
|
||||||
DraggableDivider(
|
columns.forEachIndexed { index, column ->
|
||||||
onDrag = { delta ->
|
if (index > 0) {
|
||||||
val leftCol = columns[index - 1]
|
DraggableDivider(
|
||||||
val rightCol = columns[index]
|
onDrag = { delta ->
|
||||||
deckState.updateColumnWidth(leftCol.id, leftCol.width + delta)
|
val leftCol = columns[index - 1]
|
||||||
deckState.updateColumnWidth(rightCol.id, rightCol.width - delta)
|
val rightCol = columns[index]
|
||||||
},
|
deckState.resizePair(leftCol.id, rightCol.id, delta, availableWidthDp)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckColumnContainer(
|
||||||
|
column = column,
|
||||||
|
canClose = columns.size > 1,
|
||||||
|
onClose = { deckState.removeColumn(column.id) },
|
||||||
|
onDoubleClickHeader = { deckState.expandColumn(column.id, availableWidthDp) },
|
||||||
|
relayManager = relayManager,
|
||||||
|
localCache = localCache,
|
||||||
|
accountManager = accountManager,
|
||||||
|
account = account,
|
||||||
|
nwcConnection = nwcConnection,
|
||||||
|
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||||
|
appScope = appScope,
|
||||||
|
onShowComposeDialog = onShowComposeDialog,
|
||||||
|
onShowReplyDialog = onShowReplyDialog,
|
||||||
|
onZapFeedback = onZapFeedback,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DeckColumnContainer(
|
|
||||||
column = column,
|
|
||||||
canClose = columns.size > 1,
|
|
||||||
onClose = { deckState.removeColumn(column.id) },
|
|
||||||
relayManager = relayManager,
|
|
||||||
localCache = localCache,
|
|
||||||
accountManager = accountManager,
|
|
||||||
account = account,
|
|
||||||
nwcConnection = nwcConnection,
|
|
||||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
|
||||||
appScope = appScope,
|
|
||||||
onShowComposeDialog = onShowComposeDialog,
|
|
||||||
onShowReplyDialog = onShowReplyDialog,
|
|
||||||
onZapFeedback = onZapFeedback,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-2
@@ -34,6 +34,12 @@ class DeckState {
|
|||||||
private val _focusedColumnIndex = MutableStateFlow(0)
|
private val _focusedColumnIndex = MutableStateFlow(0)
|
||||||
val focusedColumnIndex: StateFlow<Int> = _focusedColumnIndex.asStateFlow()
|
val focusedColumnIndex: StateFlow<Int> = _focusedColumnIndex.asStateFlow()
|
||||||
|
|
||||||
|
private var lastKnownWidth: Float = 0f
|
||||||
|
|
||||||
|
fun setAvailableWidth(width: Float) {
|
||||||
|
lastKnownWidth = width
|
||||||
|
}
|
||||||
|
|
||||||
fun addColumn(
|
fun addColumn(
|
||||||
type: DeckColumnType,
|
type: DeckColumnType,
|
||||||
afterIndex: Int? = null,
|
afterIndex: Int? = null,
|
||||||
@@ -45,12 +51,24 @@ class DeckState {
|
|||||||
} else {
|
} else {
|
||||||
_columns.value + col
|
_columns.value + col
|
||||||
}
|
}
|
||||||
save()
|
// Auto-fit all columns to available width when known
|
||||||
|
if (lastKnownWidth > 0f) {
|
||||||
|
fitColumnsToWidth(lastKnownWidth)
|
||||||
|
} else {
|
||||||
|
save()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeColumn(id: String) {
|
fun removeColumn(id: String) {
|
||||||
if (_columns.value.size <= 1) return
|
if (_columns.value.size <= 1) return
|
||||||
_columns.value = _columns.value.filter { it.id != id }
|
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))
|
||||||
|
}
|
||||||
if (_focusedColumnIndex.value >= _columns.value.size) {
|
if (_focusedColumnIndex.value >= _columns.value.size) {
|
||||||
_focusedColumnIndex.value = _columns.value.size - 1
|
_focusedColumnIndex.value = _columns.value.size - 1
|
||||||
}
|
}
|
||||||
@@ -80,6 +98,72 @@ class DeckState {
|
|||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 =
|
||||||
|
cols.map {
|
||||||
|
if (it.id == id) {
|
||||||
|
it.copy(width = maxForTarget)
|
||||||
|
} else {
|
||||||
|
it.copy(width = MIN_COLUMN_WIDTH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resizePair(
|
||||||
|
leftId: String,
|
||||||
|
rightId: String,
|
||||||
|
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.value =
|
||||||
|
cols.map {
|
||||||
|
when (it.id) {
|
||||||
|
leftId -> it.copy(width = newLeft)
|
||||||
|
rightId -> it.copy(width = newRight)
|
||||||
|
else -> it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
fun focusColumn(index: Int) {
|
fun focusColumn(index: Int) {
|
||||||
if (index in _columns.value.indices) {
|
if (index in _columns.value.indices) {
|
||||||
_focusedColumnIndex.value = index
|
_focusedColumnIndex.value = index
|
||||||
@@ -130,6 +214,7 @@ class DeckState {
|
|||||||
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
|
||||||
|
const val DIVIDER_WIDTH = 4f
|
||||||
|
|
||||||
val DEFAULT_COLUMNS =
|
val DEFAULT_COLUMNS =
|
||||||
listOf(
|
listOf(
|
||||||
|
|||||||
+231
@@ -0,0 +1,231 @@
|
|||||||
|
/*
|
||||||
|
* 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.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Article
|
||||||
|
import androidx.compose.material.icons.filled.Bookmark
|
||||||
|
import androidx.compose.material.icons.filled.Email
|
||||||
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.NavigationRail
|
||||||
|
import androidx.compose.material3.NavigationRailItem
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.desktop.DesktopScreen
|
||||||
|
import com.vitorpamplona.amethyst.desktop.account.AccountManager
|
||||||
|
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||||
|
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||||
|
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||||
|
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||||
|
import com.vitorpamplona.amethyst.desktop.ui.ZapFeedback
|
||||||
|
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
private data class NavItem(
|
||||||
|
val type: DeckColumnType,
|
||||||
|
val icon: ImageVector,
|
||||||
|
val label: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val navItems =
|
||||||
|
listOf(
|
||||||
|
NavItem(DeckColumnType.HomeFeed, Icons.Default.Home, "Home"),
|
||||||
|
NavItem(DeckColumnType.Reads, Icons.AutoMirrored.Filled.Article, "Reads"),
|
||||||
|
NavItem(DeckColumnType.Search, Icons.Default.Search, "Search"),
|
||||||
|
NavItem(DeckColumnType.Bookmarks, Icons.Default.Bookmark, "Bookmarks"),
|
||||||
|
NavItem(DeckColumnType.Messages, Icons.Default.Email, "Messages"),
|
||||||
|
NavItem(DeckColumnType.Notifications, Icons.Default.Notifications, "Notifications"),
|
||||||
|
NavItem(DeckColumnType.MyProfile, Icons.Default.Person, "Profile"),
|
||||||
|
NavItem(DeckColumnType.Settings, Icons.Default.Settings, "Settings"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SinglePaneLayout(
|
||||||
|
relayManager: DesktopRelayConnectionManager,
|
||||||
|
localCache: DesktopLocalCache,
|
||||||
|
accountManager: AccountManager,
|
||||||
|
account: AccountState.LoggedIn,
|
||||||
|
nwcConnection: Nip47URINorm?,
|
||||||
|
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator,
|
||||||
|
appScope: CoroutineScope,
|
||||||
|
onShowComposeDialog: () -> Unit,
|
||||||
|
onShowReplyDialog: (com.vitorpamplona.quartz.nip01Core.core.Event) -> Unit,
|
||||||
|
onZapFeedback: (ZapFeedback) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var currentColumnType by remember { mutableStateOf<DeckColumnType>(DeckColumnType.HomeFeed) }
|
||||||
|
val navState = remember { ColumnNavigationState() }
|
||||||
|
val navStack by navState.stack.collectAsState()
|
||||||
|
val currentOverlay = navStack.lastOrNull()
|
||||||
|
|
||||||
|
Row(modifier = modifier.fillMaxSize()) {
|
||||||
|
NavigationRail(
|
||||||
|
modifier = Modifier.width(80.dp).fillMaxHeight(),
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
) {
|
||||||
|
navItems.forEach { item ->
|
||||||
|
NavigationRailItem(
|
||||||
|
selected = currentColumnType == item.type && navStack.isEmpty(),
|
||||||
|
onClick = {
|
||||||
|
currentColumnType = item.type
|
||||||
|
navState.clear()
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
item.icon,
|
||||||
|
contentDescription = item.label,
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
item.label,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalDivider()
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
|
||||||
|
// Show header with back button when navigated into overlay
|
||||||
|
if (navStack.isNotEmpty()) {
|
||||||
|
SinglePaneHeader(
|
||||||
|
title =
|
||||||
|
when (currentOverlay) {
|
||||||
|
is DesktopScreen.UserProfile -> "Profile"
|
||||||
|
is DesktopScreen.Thread -> "Thread"
|
||||||
|
else -> currentColumnType.title()
|
||||||
|
},
|
||||||
|
onBack = { navState.pop() },
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(12.dp),
|
||||||
|
) {
|
||||||
|
if (currentOverlay != null) {
|
||||||
|
OverlayContent(
|
||||||
|
screen = currentOverlay,
|
||||||
|
relayManager = relayManager,
|
||||||
|
localCache = localCache,
|
||||||
|
account = account,
|
||||||
|
nwcConnection = nwcConnection,
|
||||||
|
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||||
|
onShowComposeDialog = onShowComposeDialog,
|
||||||
|
onShowReplyDialog = onShowReplyDialog,
|
||||||
|
onZapFeedback = onZapFeedback,
|
||||||
|
onNavigateToProfile = { navState.push(DesktopScreen.UserProfile(it)) },
|
||||||
|
onNavigateToThread = { navState.push(DesktopScreen.Thread(it)) },
|
||||||
|
onBack = { navState.pop() },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
RootContent(
|
||||||
|
columnType = currentColumnType,
|
||||||
|
relayManager = relayManager,
|
||||||
|
localCache = localCache,
|
||||||
|
accountManager = accountManager,
|
||||||
|
account = account,
|
||||||
|
nwcConnection = nwcConnection,
|
||||||
|
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||||
|
appScope = appScope,
|
||||||
|
onShowComposeDialog = onShowComposeDialog,
|
||||||
|
onShowReplyDialog = onShowReplyDialog,
|
||||||
|
onZapFeedback = onZapFeedback,
|
||||||
|
onNavigateToProfile = { navState.push(DesktopScreen.UserProfile(it)) },
|
||||||
|
onNavigateToThread = { navState.push(DesktopScreen.Thread(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SinglePaneHeader(
|
||||||
|
title: String,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(40.dp)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onBack, modifier = Modifier.size(28.dp)) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user