Files
stream-overlay/controller/index.html
T
2026-06-24 13:00:51 +02:00

540 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>🎾 Tennis Roots Controller</title>
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
background: #0f0f1a;
color: #e0e0e0;
padding: 24px;
min-height: 100vh;
}
h1 {
font-size: 28px;
font-weight: 700;
color: #facc15;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 12px;
}
h1 small {
font-size: 14px;
font-weight: 400;
color: rgba(255,255,255,0.4);
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
max-width: 1100px;
}
.card {
background: linear-gradient(160deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
padding: 18px 20px;
}
.card.full { grid-column: 1 / -1; }
.card h2 {
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: rgba(255,255,255,0.4);
margin-bottom: 14px;
}
.field {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.field label {
min-width: 80px;
font-size: 14px;
font-weight: 500;
color: rgba(255,255,255,0.6);
}
.field input,
.field select {
flex: 1;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 6px;
padding: 8px 12px;
font-size: 15px;
color: #fff;
transition: border-color 0.2s;
}
.field input:focus,
.field select:focus {
outline: none;
border-color: #facc15;
}
.field input::placeholder { color: rgba(255,255,255,0.25); }
.field input.short { flex: 0 0 70px; text-align: center; }
.field .flag-select {
flex: 0 0 70px;
text-align: center;
font-size: 20px;
}
.btn-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 6px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn:hover { filter: brightness(1.15); transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn-primary { background: #3b82f6; color: #fff; }
.btn-success { background: #22c55e; color: #fff; }
.btn-warning { background: #facc15; color: #000; }
.btn-danger { background: #ef4444; color: #fff; }
.btn-ghost { background: rgba(255,255,255,0.08); color: #e0e0e0; }
.btn-sm { padding: 5px 10px; font-size: 12px; }
.score-display {
display: flex;
justify-content: center;
align-items: center;
gap: 24px;
padding: 12px 0;
font-size: 32px;
font-weight: 700;
}
.score-display .vs {
font-size: 18px;
font-weight: 400;
color: rgba(255,255,255,0.25);
}
.score-display .p-score {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.score-display .p-name {
font-size: 16px;
font-weight: 600;
color: rgba(255,255,255,0.7);
}
.score-display .p-points {
font-size: 36px;
font-weight: 800;
color: #facc15;
}
.score-display .p-points.adv { color: #4ade80; }
.score-display .playing-now {
font-size: 12px;
color: #facc15;
letter-spacing: 1px;
}
.sets-display {
display: flex;
justify-content: center;
gap: 32px;
font-size: 15px;
color: rgba(255,255,255,0.5);
margin-bottom: 8px;
}
.sets-display span {
display: flex;
align-items: center;
gap: 12px;
}
.sets-display .set-score {
font-weight: 700;
font-size: 18px;
color: #fff;
}
.sets-display .set-label {
font-size: 12px;
text-transform: uppercase;
color: rgba(255,255,255,0.3);
}
.status-bar {
display: flex;
align-items: center;
gap: 16px;
padding: 8px 0;
font-size: 14px;
color: rgba(255,255,255,0.5);
}
.status-bar .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
}
.status-bar .dot.disconnected { background: #ef4444; }
.status-bar .status-text {
color: #fff;
font-weight: 600;
}
.connection-status {
position: fixed;
top: 24px;
right: 24px;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: rgba(255,255,255,0.5);
}
.connection-status .led {
width: 10px;
height: 10px;
border-radius: 50%;
background: #ef4444;
transition: background 0.3s;
}
.connection-status .led.connected { background: #22c55e; box-shadow: 0 0 8px rgba(34,197,94,0.5); }
@media (max-width: 700px) {
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="connection-status" id="connStatus">
<span class="led" id="connLed"></span>
<span id="connLabel">Disconnesso</span>
</div>
<h1>
🎾 Tennis Roots
<small>Controller</small>
</h1>
<div class="grid">
<!-- STATO IN TEMPO REALE -->
<div class="card full">
<h2>📊 Stato corrente</h2>
<div class="sets-display" id="setsDisplay"></div>
<div class="score-display">
<div class="p-score" id="p1score">
<span class="p-name" id="liveP1Name">P1</span>
<span class="playing-now" id="liveP1Serving"></span>
<span class="p-points" id="liveP1Points">0</span>
</div>
<span class="vs">VS</span>
<div class="p-score" id="p2score">
<span class="p-name" id="liveP2Name">P2</span>
<span class="playing-now" id="liveP2Serving"></span>
<span class="p-points" id="liveP2Points">0</span>
</div>
</div>
<div class="status-bar">
<span class="dot" id="statusDot"></span>
<span class="status-text" id="liveStatus">In attesa...</span>
</div>
</div>
<!-- PUNTEGGIO -->
<div class="card">
<h2>🎾 Punteggio</h2>
<div class="btn-group">
<button class="btn btn-success" onclick="addPoint(1)">🎾 Punto P1</button>
<button class="btn btn-success" onclick="addPoint(2)">🎾 Punto P2</button>
</div>
<div class="btn-group" style="margin-top:8px">
<button class="btn btn-warning" onclick="setDeuce()">⚖️ Deuce</button>
<button class="btn btn-ghost" onclick="resetGame()">🔄 Reset Game</button>
</div>
<div class="btn-group" style="margin-top:8px">
<button class="btn btn-danger" onclick="resetMatch()">🗑️ Reset Match</button>
</div>
</div>
<!-- SERVIZIO -->
<div class="card">
<h2>🎾 Servizio</h2>
<div class="field">
<label>Battuta:</label>
<select id="serverSelect">
<option value="1">P1</option>
<option value="2">P2</option>
</select>
<button class="btn btn-primary btn-sm" onclick="setServer()">Imposta</button>
</div>
</div>
<!-- GIOCATORI -->
<div class="card">
<h2>👤 Giocatori</h2>
<div class="field">
<label>P1 Nome:</label>
<input type="text" id="p1Name" value="J. Sinner" />
</div>
<div class="field">
<label>P1 Bandiera:</label>
<select class="flag-select" id="p1Flag">
<option value="it">🇮🇹</option>
<option value="es">🇪🇸</option>
<option value="us">🇺🇸</option>
<option value="fr">🇫🇷</option>
<option value="gb">🇬🇧</option>
<option value="de">🇩🇪</option>
<option value="au">🇦🇺</option>
<option value="ch">🇨🇭</option>
<option value="rs">🇷🇸</option>
<option value="gr">🇬🇷</option>
<option value="br">🇧🇷</option>
<option value="ar">🇦🇷</option>
</select>
</div>
<div class="field">
<label>P2 Nome:</label>
<input type="text" id="p2Name" value="C. Alcaraz" />
</div>
<div class="field">
<label>P2 Bandiera:</label>
<select class="flag-select" id="p2Flag">
<option value="it">🇮🇹</option>
<option value="es" selected>🇪🇸</option>
<option value="us">🇺🇸</option>
<option value="fr">🇫🇷</option>
<option value="gb">🇬🇧</option>
<option value="de">🇩🇪</option>
<option value="au">🇦🇺</option>
<option value="ch">🇨🇭</option>
<option value="rs">🇷🇸</option>
<option value="gr">🇬🇷</option>
<option value="br">🇧🇷</option>
<option value="ar">🇦🇷</option>
</select>
</div>
<div class="btn-group" style="margin-top:6px">
<button class="btn btn-primary" onclick="updatePlayers()">💾 Salva Giocatori</button>
</div>
</div>
<!-- MATCH INFO -->
<div class="card">
<h2>📋 Info Match</h2>
<div class="field">
<label>Torneo:</label>
<input type="text" id="tournament" value="Wimbledon" />
</div>
<div class="field">
<label>Round:</label>
<input type="text" id="round" value="Finale · Uomini · Singolare" />
</div>
<div class="btn-group" style="margin-top:6px">
<button class="btn btn-primary" onclick="updateMatchInfo()">💾 Salva Info</button>
</div>
</div>
<!-- SCORCIATOIE -->
<div class="card">
<h2>⌨️ Scorciatoie</h2>
<div style="font-size:13px; color:rgba(255,255,255,0.5); line-height:1.8">
<kbd style="background:rgba(255,255,255,0.1); padding:2px 6px; border-radius:3px;">1</kbd> Punto P1 &nbsp;
<kbd style="background:rgba(255,255,255,0.1); padding:2px 6px; border-radius:3px;">2</kbd> Punto P2 &nbsp;
<kbd style="background:rgba(255,255,255,0.1); padding:2px 6px; border-radius:3px;">D</kbd> Deuce &nbsp;
<kbd style="background:rgba(255,255,255,0.1); padding:2px 6px; border-radius:3px;">R</kbd> Reset Game &nbsp;
<kbd style="background:rgba(255,255,255,0.1); padding:2px 6px; border-radius:3px;">M</kbd> Reset Match
</div>
</div>
</div>
<script>
// ---- WebSocket ----
const WS_URL = `ws://${location.host}/ws`;
let ws = null;
let currentState = {};
let reconnectTimer = null;
function connect() {
if (ws && (ws.readyState === 0 || ws.readyState === 1)) return;
ws = new WebSocket(WS_URL);
ws.onopen = () => {
document.getElementById('connLed').className = 'led connected';
document.getElementById('connLabel').textContent = 'Connesso';
};
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === 'STATE_UPDATE') {
currentState = msg.state;
updateUI();
}
} catch (e) {}
};
ws.onclose = () => {
document.getElementById('connLed').className = 'led';
document.getElementById('connLabel').textContent = 'Disconnesso';
scheduleReconnect();
};
ws.onerror = () => ws.close();
}
function scheduleReconnect() {
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(connect, 2000);
}
function send(msg) {
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify(msg));
}
}
// ---- UI Update ----
function pointLabel(state) {
if (state.tiebreak) {
return [String(state.tiebreakPoints[0]), String(state.tiebreakPoints[1])];
}
const p1 = state.points[0];
const p2 = state.points[1];
if (state.deuce) {
if (state.advantage === 1) return ['AD', '40'];
if (state.advantage === 2) return ['40', 'AD'];
return ['40', '40'];
}
const map = ['0', '15', '30', '40'];
return [map[p1] || '40', map[p2] || '40'];
}
function updateUI() {
const s = currentState;
if (!s || !s.player1) return;
// Giocatori
document.getElementById('liveP1Name').textContent = s.player1;
document.getElementById('liveP2Name').textContent = s.player2;
document.getElementById('p1Name').value = s.player1;
document.getElementById('p2Name').value = s.player2;
document.getElementById('p1Flag').value = s.flag1 || 'it';
document.getElementById('p2Flag').value = s.flag2 || 'es';
document.getElementById('serverSelect').value = s.server || 1;
// Punti
const labels = pointLabel(s);
const p1el = document.getElementById('liveP1Points');
const p2el = document.getElementById('liveP2Points');
p1el.textContent = labels[0];
p2el.textContent = labels[1];
p1el.className = 'p-points' + (s.advantage === 1 ? ' adv' : '');
p2el.className = 'p-points' + (s.advantage === 2 ? ' adv' : '');
// Servizio
document.getElementById('liveP1Serving').textContent = s.server === 1 ? '● Serve' : '';
document.getElementById('liveP2Serving').textContent = s.server === 2 ? '● Serve' : '';
// Set
const setsEl = document.getElementById('setsDisplay');
let setsHtml = '';
const setLabels = ['S1', 'S2', 'S3'];
for (let i = 0; i < 3; i++) {
const ss = s.sets[i] || [0, 0];
const active = i === s.currentSet ? ' style="color:#facc15"' : '';
setsHtml += `<span><span class="set-label">${setLabels[i]}</span><span class="set-score"${active}>${ss[0]}${ss[1]}</span></span>`;
}
setsEl.innerHTML = setsHtml;
// Stato
document.getElementById('liveStatus').textContent = s.matchStatus || 'In corso';
document.getElementById('statusDot').style.background = s.matchOver ? '#22c55e' : '#facc15';
// Info match
document.getElementById('tournament').value = s.tournament || '';
document.getElementById('round').value = s.round || '';
}
// ---- Comandi ----
function addPoint(player) {
send({ type: 'COMMAND', command: 'ADD_POINT', player });
}
function resetGame() {
send({ type: 'COMMAND', command: 'RESET_GAME' });
}
function resetMatch() {
if (confirm('Reset totale del match?')) {
send({ type: 'COMMAND', command: 'RESET_MATCH' });
}
}
function setDeuce() {
send({ type: 'COMMAND', command: 'SET_DEUCE' });
}
function setServer() {
const val = parseInt(document.getElementById('serverSelect').value);
send({ type: 'COMMAND', command: 'SET_SERVER', player: val });
}
function updatePlayers() {
send({
type: 'STATE_UPDATE',
state: {
player1: document.getElementById('p1Name').value || 'P1',
player2: document.getElementById('p2Name').value || 'P2',
flag1: document.getElementById('p1Flag').value,
flag2: document.getElementById('p2Flag').value,
}
});
}
function updateMatchInfo() {
send({
type: 'STATE_UPDATE',
state: {
tournament: document.getElementById('tournament').value || 'Torneo',
round: document.getElementById('round').value || '',
}
});
}
// ---- Keyboard shortcuts ----
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
if (e.key === '1') addPoint(1);
if (e.key === '2') addPoint(2);
if (e.key === 'd' || e.key === 'D') setDeuce();
if (e.key === 'r') resetGame();
if (e.key === 'm' || e.key === 'M') resetMatch();
});
// ---- Init ----
connect();
</script>
</body>
</html>