1
0

Adding style for color picking

This commit is contained in:
James Ketr 2025-10-11 12:52:56 -07:00
parent f1a5946045
commit bc5a928da6
6 changed files with 649 additions and 478 deletions

View File

@ -28,13 +28,6 @@
border: 2px dashed #666; /* Visual indicator for drop zone */
}
.MediaControlSpacer.Medium {
width: 11.5em;
height: 8.625em;
/* min-width: 11.5em;
min-height: 8.625em; */
}
.MediaControl {
display: flex;
position: absolute; /* Out of flow */
@ -60,13 +53,6 @@
border: 1px solid black;
}
.MediaControl.Medium {
width: 11.5em;
height: 8.625em;
/* min-width: 11.5em;
min-height: 8.625em; */
}
.MediaControl .Controls {
display: none; /* Hidden by default, shown on hover */
position: absolute;

File diff suppressed because it is too large Load Diff

View File

@ -7,18 +7,20 @@ type PlayerColorProps = { color?: string };
const mapColor = (c?: string) => {
if (!c) return undefined;
const key = c.toLowerCase();
switch (key) {
case "red":
return "R";
case "orange":
return "O";
case "white":
return "W";
case "blue":
return "B";
switch (c.toLowerCase()) {
case 'red':
return 'R'
case 'orange':
return 'O'
case 'white':
return 'W'
case 'blue':
return 'B'
default:
return undefined;
if (['R', 'O', 'W', 'B'].includes(c)) {
return c
}
return undefined
}
};

View File

@ -47,31 +47,12 @@
justify-content: space-around;
}
.PlayerList .Unselected > div:nth-child(2) > div {
justify-content: flex-end;
display: flex;
flex-direction: column;
align-items: center;
margin: 0.25rem;
padding: 0.25rem;
max-width: 8rem;
background-color: #eee;
border-radius: 0.25rem;
}
.PlayerList .Unselected .Self {
border: 1px solid black;
}
.PlayerList .PlayerSelector .PlayerColor {
width: 1em;
height: 1em;
}
.PlayerList .PlayerSelector {
display: inline-flex;
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
flex-direction: row;
align-items: start;
}
.PlayerList .PlayerSelector.MuiList-padding {
@ -91,30 +72,28 @@
.PlayerList .PlayerSelector .PlayerEntry {
display: flex;
flex-direction: column;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
flex: 1 1 0px;
align-items: flex-start;
border: 1px solid rgba(0,0,0,0);
border-radius: 0.25em;
min-width: 11em;
padding: 0 1px;
align-items: stretch;
/* min-width: 10rem; */
padding: 0.25rem;
justify-content: flex-end;
max-width: min-content;
}
.PlayerList .PlayerSelector .PlayerEntry > div:first-child {
/* .PlayerList .PlayerSelector .PlayerEntry > div:first-child {
display: flex;
flex-direction: row;
align-items: center;
align-self: stretch;
margin-bottom: 0.25em;
}
} */
.PlayerList .PlayerEntry[data-selectable=true]:hover {
/* .PlayerList .PlayerEntry[data-selectable=true]:hover {
border-color: rgba(0,0,0,0.5);
cursor: pointer;
}
} */
/* Ensure the inline border color (player color) is visible */
/* inline style borderColor on .PlayerEntry will override background/border defaults */
.PlayerList .Players .PlayerToggle {
min-width: 5em;

View File

@ -1,11 +1,12 @@
import React, { useState, useEffect, useCallback, useContext } from "react";
import Paper from "@mui/material/Paper";
import List from "@mui/material/List";
import "./PlayerList.css";
import { MediaControl, MediaAgent, Peer } from "./MediaControl";
import { PlayerColor } from "./PlayerColor";
import Box from "@mui/material/Box";
import { GlobalContext } from "./GlobalContext";
import React, { useState, useEffect, useCallback, useContext, useMemo } from 'react';
import Paper from '@mui/material/Paper';
import List from '@mui/material/List';
import './PlayerList.css';
import { MediaControl, MediaAgent, Peer } from './MediaControl';
import { PlayerColor } from './PlayerColor';
import Box from '@mui/material/Box';
import { GlobalContext } from './GlobalContext';
import { styles } from './Styles';
type Player = {
name: string;
@ -24,14 +25,17 @@ type Player = {
const PlayerList: React.FC = () => {
const { session, socketUrl, lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
const [players, setPlayers] = useState<Player[] | null>(null);
const [players, setPlayers] = useState<Player[]>([]);
const [player, setPlayer] = useState<Player | null>(null);
const [peers, setPeers] = useState<Record<string, Peer>>({});
const [gameState, setGameState] = useState<string | undefined>(undefined);
useEffect(() => {
console.log("player-list - Mounted - requesting fields");
console.log('player-list - Mounted - requesting fields');
if (sendJsonMessage) {
sendJsonMessage({
type: "get",
fields: ["participants"],
type: 'get',
fields: ['participants'],
});
}
}, [sendJsonMessage]);
@ -64,20 +68,6 @@ const PlayerList: React.FC = () => {
[session]
);
useEffect(() => {
if (!players) {
return;
}
players.forEach((player) => {
console.log("rabbit - player:", {
name: player.name,
live: player.live,
in_peers: peers[player.session_id],
local_or_media: player.local || player.has_media !== false,
});
});
}, [players]);
// Use the WebSocket hook for room events with automatic reconnection
useEffect(() => {
if (!lastJsonMessage) {
@ -85,25 +75,33 @@ const PlayerList: React.FC = () => {
}
const data: any = lastJsonMessage;
switch (data.type) {
case "game-update": {
case 'game-update': {
console.log(`player-list - game-update:`, data.update);
// Track game state if provided
if ('state' in data.update) {
setGameState(data.update.state);
}
// Handle participants list
if ("participants" in data.update && data.update.participants) {
if ('participants' in data.update && data.update.participants) {
const participantsList: Player[] = data.update.participants;
console.log(`player-list - participants:`, participantsList);
participantsList.forEach((player) => {
participantsList.forEach(player => {
player.local = player.session_id === session?.id;
if (player.local) {
setPlayer(player);
}
});
participantsList.sort(sortPlayers);
console.log(`player-list - sorted participants:`, participantsList);
setPlayers(participantsList);
// Initialize peers with remote mute/video state
setPeers((prevPeers) => {
setPeers(prevPeers => {
const updated: Record<string, Peer> = { ...prevPeers };
participantsList.forEach((player) => {
participantsList.forEach(player => {
// Only update remote peers, never overwrite local peer object
if (!player.local && updated[player.session_id]) {
updated[player.session_id] = {
@ -118,9 +116,9 @@ const PlayerList: React.FC = () => {
}
break;
}
case "peer_state_update": {
case 'peer_state_update': {
// Update peer state in peers, but do not override local mute
setPeers((prevPeers) => {
setPeers(prevPeers => {
const updated = { ...prevPeers };
const peerId = data.data?.peer_id || data.peer_id;
if (peerId && updated[peerId]) {
@ -140,62 +138,68 @@ const PlayerList: React.FC = () => {
}
}, [lastJsonMessage, session, sortPlayers, setPeers, setPlayers]);
// Compute which colors are already taken
const availableColors = useMemo(() => {
const assignedColors = new Set(players.filter(p => p.color !== 'unassigned').map(p => p.color));
return ['O', 'R', 'W', 'B'].filter(color => !assignedColors.has(color));
}, [players]);
useEffect(() => {
if (players !== null || !sendJsonMessage) {
if (players.length !== 0 || !sendJsonMessage) {
return;
}
// Request participants list
sendJsonMessage({
type: "get",
fields: ["participants"],
type: 'get',
fields: ['participants'],
});
}, [players, sendJsonMessage]);
return (
<Box sx={{ position: "relative", width: "100%" }}>
<Box sx={{ position: 'relative', width: '100%' }}>
<Paper
className={`player-list Medium`}
className={`PlayerList Medium`}
sx={{
maxWidth: { xs: "100%", sm: 500 },
maxWidth: { xs: '100%', sm: 500 },
p: { xs: 1, sm: 2 },
m: { xs: 0, sm: 2 },
}}
>
<MediaAgent {...{ session, peers, setPeers }} />
<List className="PlayerSelector">
{players?.map((player) => {
const peerObj = peers[player.session_id] || peers[player.name];
return (
<Box
key={player.session_id}
sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}
className={`PlayerEntry ${player.local ? "PlayerSelf" : ""}`}
>
<Box>
<Box style={{ display: "flex-wrap", alignItems: "center", justifyContent: "space-between" }}>
<Box style={{ display: "flex-wrap", alignItems: "center" }}>
<div className="Name">{player.name ? player.name : player.session_id}</div>
{player.protected && (
<div
style={{ marginLeft: 8, fontSize: "0.8em", color: "#a00" }}
title="This name is protected with a password"
>
🔒
</div>
)}
{player.bot_instance_id && (
<div style={{ marginLeft: 8, fontSize: "0.8em", color: "#00a" }} title="This is a bot">
🤖
</div>
)}
<List className="PlayerSelector Players">
{players
.filter(p => p.color && p.color !== 'unassigned')
.map(player => {
const peerObj = peers[player.session_id] || peers[player.name];
const playerStyle = player.color !== 'unassigned' ? styles[player.color] : {};
return (
<Box
key={player.session_id}
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
p: 1,
borderRadius: '0.25rem',
...playerStyle,
}}
className={`PlayerEntry ${player.local ? 'PlayerSelf' : ''}`}
>
<Box
style={{
display: 'flex-wrap',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Box style={{ display: 'flex-wrap', alignItems: 'center' }} className="Name">
{player.bot_instance_id && <>🤖</>}
{player.name ? player.name : player.session_id}
</Box>
{player.name && !player.live && <Box className="NoNetwork"></Box>}
</Box>
{player.name && !player.live && <div className="NoNetwork"></div>}
</Box>
{player.name && player.live && peerObj && (player.local || player.has_media !== false) ? (
<>
{player.name && player.live ? (
<MediaControl
className="Medium"
key={player.session_id}
peer={peerObj}
isSelf={player.local}
@ -203,60 +207,99 @@ const PlayerList: React.FC = () => {
remoteAudioMuted={peerObj?.muted}
remoteVideoOff={peerObj?.video_on === false}
/>
{/* If this is the local player and they haven't picked a color, show a picker */}
{player.local && player.color === "unassigned" && (
<div style={{ marginTop: 8, width: "100%" }}>
<div style={{ marginBottom: 6, fontSize: "0.9em" }}>Pick your color:</div>
<div style={{ display: "flex", gap: 8 }}>
{["orange", "red", "white", "blue"].map((c) => (
<Box
key={c}
sx={{
display: "flex",
alignItems: "center",
gap: 8,
padding: "6px 8px",
borderRadius: 6,
border: "1px solid #ccc",
background: "#fff",
cursor: sendJsonMessage ? "pointer" : "not-allowed",
}}
onClick={() => {
if (!sendJsonMessage) return;
sendJsonMessage({ type: "set", field: "color", value: c[0].toUpperCase() });
}}
>
<PlayerColor color={c} />
</Box>
))}
</div>
</div>
)}
</>
) : player.name && player.live && player.has_media === false ? (
<div
className="Video fade-in"
style={{
background: "#333",
color: "#fff",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
fontSize: "14px",
) : (
<div
className="Video fade-in"
style={{
background: '#333',
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
fontSize: '14px',
}}
>
💬 Chat Only
</div>
)}
</Box>
);
})}
</List>
{gameState === 'lobby' && (
<Paper sx={{ p: 0.5, mt: 0.5, backgroundColor: '#f9f9f9' }}>
<div style={{ marginBottom: 6, fontSize: '0.9em' }}>
{player && player.color !== 'unassigned' ? 'Change' : 'Pick'} your color:
</div>
<Box style={{ display: 'flex', gap: 8 }}>
{availableColors.map(c => {
return (
<Box
key={c}
sx={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
}}
onClick={() => {
sendJsonMessage({ type: 'set', field: 'color', value: c });
}}
>
💬 Chat Only
</div>
) : (
<video className="Video"></video>
)}
</Box>
);
})}
</List>
<PlayerColor color={c} />
</Box>
);
})}
</Box>
</Paper>
)}
<Paper>
<List className="PlayerSelector Observers">
{players
.filter(p => !p.color || p.color === 'unassigned')
.map(player => {
const peerObj = peers[player.session_id] || peers[player.name];
return (
<Box
key={player.session_id}
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}
className={`PlayerEntry ${player.local ? 'PlayerSelf' : ''}`}
data-selectable={player.local && gameState === 'lobby'}
>
<Box
style={{
display: 'flex-wrap',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Box style={{ display: 'flex-wrap', alignItems: 'center' }} className="Name">
{player.bot_instance_id && <>🤖</>}
{player.name ? player.name : player.session_id}
</Box>
{player.name && !player.live && <Box className="NoNetwork"></Box>}
</Box>
{/* Show media control if available */}
{player.name &&
player.live &&
peerObj &&
(player.local || player.has_media !== false) && (
<MediaControl
key={player.session_id}
peer={peerObj}
isSelf={player.local}
sendJsonMessage={player.local ? sendJsonMessage : undefined}
remoteAudioMuted={peerObj?.muted}
remoteVideoOff={peerObj?.video_on === false}
/>
)}
</Box>
);
})}
</List>
</Paper>
</Paper>
</Box>
);

View File

@ -4,18 +4,30 @@ import { orange, lightBlue, red, grey } from '@mui/material/colors';
const styles = {
R: {
color: '#fff',
borderWidth: 1,
borderStyle: 'solid',
borderColor: red[700],
backgroundColor: red[500],
},
O: {
color: '#000',
borderWidth: 1,
borderStyle: 'solid',
borderColor: orange[600],
backgroundColor: orange[500],
},
W: {
color: '#000',
borderWidth: 1,
borderStyle: 'solid',
borderColor: grey[300],
backgroundColor: grey[50],
},
B: {
color: '#fff',
borderWidth: 1,
borderStyle: 'solid',
borderColor: lightBlue[700],
backgroundColor: lightBlue[500],
},
};