feat(desktop): collapsible info panel in chess deck column
Hide GameInfo/MoveHistory/Actions panel by default in deck view with a chevron toggle to expand. Add Chess to AddColumnDialog. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+196
-172
@@ -39,6 +39,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
@@ -90,6 +92,7 @@ fun ChessScreen(
|
|||||||
relayManager: DesktopRelayConnectionManager,
|
relayManager: DesktopRelayConnectionManager,
|
||||||
account: AccountState.LoggedIn,
|
account: AccountState.LoggedIn,
|
||||||
onBack: () -> Unit = {},
|
onBack: () -> Unit = {},
|
||||||
|
compactMode: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val viewModel =
|
val viewModel =
|
||||||
@@ -274,6 +277,7 @@ fun ChessScreen(
|
|||||||
},
|
},
|
||||||
onResign = { viewModel.resign(gameState.startEventId) },
|
onResign = { viewModel.resign(gameState.startEventId) },
|
||||||
isSpectatorOverride = isSpectating,
|
isSpectatorOverride = isSpectating,
|
||||||
|
compactMode = compactMode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -593,6 +597,7 @@ private fun DesktopChessGameLayout(
|
|||||||
onMoveMade: (from: String, to: String, san: String) -> Unit,
|
onMoveMade: (from: String, to: String, san: String) -> Unit,
|
||||||
onResign: () -> Unit,
|
onResign: () -> Unit,
|
||||||
isSpectatorOverride: Boolean? = null,
|
isSpectatorOverride: Boolean? = null,
|
||||||
|
compactMode: Boolean = false,
|
||||||
) {
|
) {
|
||||||
// Collect state flows to trigger recomposition on changes
|
// Collect state flows to trigger recomposition on changes
|
||||||
val currentPosition by gameState.currentPosition.collectAsState()
|
val currentPosition by gameState.currentPosition.collectAsState()
|
||||||
@@ -604,12 +609,13 @@ private fun DesktopChessGameLayout(
|
|||||||
val opponentPubkey = gameState.opponentPubkey
|
val opponentPubkey = gameState.opponentPubkey
|
||||||
val isSpectator = isSpectatorOverride ?: gameState.isSpectator
|
val isSpectator = isSpectatorOverride ?: gameState.isSpectator
|
||||||
|
|
||||||
|
var showInfoPanel by remember { mutableStateOf(!compactMode) }
|
||||||
|
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
) {
|
) {
|
||||||
// Calculate board size based on available space
|
// Calculate board size based on available space
|
||||||
// Leave room for the info panel (300dp + 24dp spacing)
|
val infoPanelWidth = if (showInfoPanel) 300.dp + 24.dp else 0.dp
|
||||||
val infoPanelWidth = 300.dp + 24.dp
|
|
||||||
val availableWidth = maxWidth - infoPanelWidth
|
val availableWidth = maxWidth - infoPanelWidth
|
||||||
val availableHeight = maxHeight
|
val availableHeight = maxHeight
|
||||||
// Board should fit within available space, maintaining square aspect ratio
|
// Board should fit within available space, maintaining square aspect ratio
|
||||||
@@ -617,11 +623,11 @@ private fun DesktopChessGameLayout(
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
horizontalArrangement = Arrangement.spacedBy(if (showInfoPanel) 24.dp else 0.dp),
|
||||||
) {
|
) {
|
||||||
// Left side: Chess board
|
// Left side: Chess board + toggle
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxHeight(),
|
modifier = Modifier.weight(1f).fillMaxHeight(),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
InteractiveChessBoard(
|
InteractiveChessBoard(
|
||||||
@@ -633,188 +639,206 @@ private fun DesktopChessGameLayout(
|
|||||||
positionVersion = moveHistory.size,
|
positionVersion = moveHistory.size,
|
||||||
onMoveMade = onMoveMade,
|
onMoveMade = onMoveMade,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Toggle button anchored to top-end of board area
|
||||||
|
IconButton(
|
||||||
|
onClick = { showInfoPanel = !showInfoPanel },
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
if (showInfoPanel) {
|
||||||
|
Icons.AutoMirrored.Filled.KeyboardArrowRight
|
||||||
|
} else {
|
||||||
|
Icons.AutoMirrored.Filled.KeyboardArrowLeft
|
||||||
|
},
|
||||||
|
contentDescription = if (showInfoPanel) "Hide info panel" else "Show info panel",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right side: Game info, moves, controls
|
// Right side: Game info, moves, controls (collapsible)
|
||||||
Column(
|
if (showInfoPanel) {
|
||||||
modifier =
|
Column(
|
||||||
Modifier
|
modifier =
|
||||||
.width(300.dp)
|
Modifier
|
||||||
.fillMaxHeight()
|
.width(300.dp)
|
||||||
.verticalScroll(rememberScrollState()),
|
.fillMaxHeight()
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
.verticalScroll(rememberScrollState()),
|
||||||
) {
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
// Extract human-readable game name
|
|
||||||
val gameName =
|
|
||||||
remember(startEventId) {
|
|
||||||
ChessGameNameGenerator.extractDisplayName(startEventId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game info card
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
) {
|
||||||
Column(
|
// Extract human-readable game name
|
||||||
modifier = Modifier.padding(16.dp),
|
val gameName =
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
remember(startEventId) {
|
||||||
) {
|
ChessGameNameGenerator.extractDisplayName(startEventId)
|
||||||
// Show readable game name if available
|
|
||||||
if (gameName != null) {
|
|
||||||
Text(
|
|
||||||
gameName,
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
"Game Info",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
// Game info card
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Card(
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
UserAvatar(
|
// Show readable game name if available
|
||||||
userHex = opponentPubkey,
|
if (gameName != null) {
|
||||||
pictureUrl = opponentPicture,
|
Text(
|
||||||
size = 48.dp,
|
gameName,
|
||||||
)
|
style = MaterialTheme.typography.titleLarge,
|
||||||
Column {
|
fontWeight = FontWeight.Bold,
|
||||||
if (isSpectator) {
|
color = MaterialTheme.colorScheme.primary,
|
||||||
Text(
|
)
|
||||||
"Spectating",
|
} else {
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
Text(
|
||||||
fontWeight = FontWeight.Bold,
|
"Game Info",
|
||||||
color = MaterialTheme.colorScheme.tertiary,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
fontWeight = FontWeight.Bold,
|
||||||
} else {
|
)
|
||||||
Text(
|
}
|
||||||
"vs $opponentName",
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
Row(
|
||||||
)
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
Text(
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
"You play ${if (playerColor == com.vitorpamplona.quartz.nip64Chess.Color.WHITE) "White" else "Black"}",
|
) {
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
UserAvatar(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
userHex = opponentPubkey,
|
||||||
)
|
pictureUrl = opponentPicture,
|
||||||
|
size = 48.dp,
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
if (isSpectator) {
|
||||||
|
Text(
|
||||||
|
"Spectating",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
"vs $opponentName",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"You play ${if (playerColor == com.vitorpamplona.quartz.nip64Chess.Color.WHITE) "White" else "Black"}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use currentPosition to derive turn (triggers recomposition on move)
|
||||||
|
val currentTurn = currentPosition.activeColor
|
||||||
|
|
||||||
|
if (isSpectator) {
|
||||||
|
Text(
|
||||||
|
"${if (currentTurn == com.vitorpamplona.quartz.nip64Chess.Color.WHITE) "White" else "Black"}'s turn",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val isYourTurn = currentTurn == playerColor
|
||||||
|
Text(
|
||||||
|
if (isYourTurn) "Your turn" else "Opponent's turn",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = if (isYourTurn) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
color =
|
||||||
|
if (isYourTurn) {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move history card
|
||||||
|
if (moveHistory.isNotEmpty()) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Move History",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Format moves as numbered pairs
|
||||||
|
val moveText =
|
||||||
|
moveHistory
|
||||||
|
.chunked(2)
|
||||||
|
.mapIndexed { index, pair ->
|
||||||
|
"${index + 1}. ${pair.joinToString(" ")}"
|
||||||
|
}.joinToString("\n")
|
||||||
|
|
||||||
|
Text(
|
||||||
|
moveText,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game controls card (only for participants, not spectators)
|
||||||
|
if (!isSpectator) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Actions",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onResign,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text("Resign")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Use currentPosition to derive turn (triggers recomposition on move)
|
// Spectator info card
|
||||||
val currentTurn = currentPosition.activeColor
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
if (isSpectator) {
|
colors =
|
||||||
Text(
|
CardDefaults.cardColors(
|
||||||
"${if (currentTurn == com.vitorpamplona.quartz.nip64Chess.Color.WHITE) "White" else "Black"}'s turn",
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.5f),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val isYourTurn = currentTurn == playerColor
|
|
||||||
Text(
|
|
||||||
if (isYourTurn) "Your turn" else "Opponent's turn",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
fontWeight = if (isYourTurn) FontWeight.Bold else FontWeight.Normal,
|
|
||||||
color =
|
|
||||||
if (isYourTurn) {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move history card
|
|
||||||
if (moveHistory.isNotEmpty()) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
"Move History",
|
modifier = Modifier.padding(16.dp),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
fontWeight = FontWeight.Bold,
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
)
|
|
||||||
|
|
||||||
// Format moves as numbered pairs
|
|
||||||
val moveText =
|
|
||||||
moveHistory
|
|
||||||
.chunked(2)
|
|
||||||
.mapIndexed { index, pair ->
|
|
||||||
"${index + 1}. ${pair.joinToString(" ")}"
|
|
||||||
}.joinToString("\n")
|
|
||||||
|
|
||||||
Text(
|
|
||||||
moveText,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game controls card (only for participants, not spectators)
|
|
||||||
if (!isSpectator) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"Actions",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
)
|
|
||||||
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = onResign,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
) {
|
||||||
Text("Resign")
|
Icon(Icons.Default.Visibility, contentDescription = null)
|
||||||
|
Text(
|
||||||
|
"Watching game - spectator mode",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Spectator info card
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors =
|
|
||||||
CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.5f),
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.Visibility, contentDescription = null)
|
|
||||||
Text(
|
|
||||||
"Watching game - spectator mode",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game ID (small footer)
|
// Game ID (small footer)
|
||||||
Text(
|
Text(
|
||||||
"Game: ${startEventId.take(16)}...",
|
"Game: ${startEventId.take(16)}...",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -56,6 +56,7 @@ private val COLUMN_OPTIONS =
|
|||||||
DeckColumnType.Bookmarks,
|
DeckColumnType.Bookmarks,
|
||||||
DeckColumnType.GlobalFeed,
|
DeckColumnType.GlobalFeed,
|
||||||
DeckColumnType.MyProfile,
|
DeckColumnType.MyProfile,
|
||||||
|
DeckColumnType.Chess,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+1
@@ -276,6 +276,7 @@ internal fun RootContent(
|
|||||||
relayManager = relayManager,
|
relayManager = relayManager,
|
||||||
account = account,
|
account = account,
|
||||||
onBack = {},
|
onBack = {},
|
||||||
|
compactMode = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user