Permette di specificare manualmente l'host del WebSocket tramite il parametro ?wsHost=[host:port], utile per scenari di sviluppo con WSL2 o quando si accede da dispositivi remoti. - Aggiunge parsing del parametro wsHost in DisplayPage e ControllerPage - Mantiene fallback automatico a location.host se non specificato - Migliora diagnostica con log della URL WebSocket effettiva
392 lines
11 KiB
Vue
392 lines
11 KiB
Vue
<template>
|
|
<section class="display-page">
|
|
<div class="campo">
|
|
<span v-if="state.order">
|
|
<!-- Ordine visualizzazione: home / guest -->
|
|
<div class="hea home">
|
|
<span :style="{ 'float': 'left' }">
|
|
{{ state.sp.nomi.home }}
|
|
<span class="serv-slot">
|
|
<img v-show="state.sp.servHome" src="/serv.png" width="25" />
|
|
</span>
|
|
<span v-if="state.visuForm" class="score-inline">{{ state.sp.punt.home }}</span>
|
|
</span>
|
|
<span class="mr3" :style="{ 'float': 'right' }">set {{ state.sp.set.home }}</span>
|
|
</div>
|
|
|
|
<div class="hea guest">
|
|
<span :style="{ 'float': 'right' }">
|
|
<span v-if="state.visuForm" class="score-inline">{{ state.sp.punt.guest }}</span>
|
|
<span class="serv-slot">
|
|
<img v-show="!state.sp.servHome" src="/serv.png" width="25" />
|
|
</span>
|
|
{{ state.sp.nomi.guest }}
|
|
</span>
|
|
<span class="ml3" :style="{ 'float': 'left' }">set {{ state.sp.set.guest }}</span>
|
|
</div>
|
|
|
|
<span v-if="state.visuForm">
|
|
<div class="col form home">
|
|
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]" :key="'hf'+x">
|
|
{{ state.sp.form.home[x] }}
|
|
</div>
|
|
</div>
|
|
<div class="col form guest">
|
|
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]" :key="'gf'+x">
|
|
{{ state.sp.form.guest[x] }}
|
|
</div>
|
|
</div>
|
|
</span>
|
|
<span v-else>
|
|
<div class="punteggio-container">
|
|
<div class="col punt home">{{ state.sp.punt.home }}</div>
|
|
<div class="col punt guest">{{ state.sp.punt.guest }}</div>
|
|
</div>
|
|
</span>
|
|
</span>
|
|
|
|
<span v-else>
|
|
<!-- Ordine visualizzazione: guest / home -->
|
|
<div class="hea guest">
|
|
<span :style="{ 'float': 'left' }">
|
|
{{ state.sp.nomi.guest }}
|
|
<span class="serv-slot">
|
|
<img v-show="!state.sp.servHome" src="/serv.png" width="25" />
|
|
</span>
|
|
<span v-if="state.visuForm" class="score-inline">{{ state.sp.punt.guest }}</span>
|
|
</span>
|
|
<span class="mr3" :style="{ 'float': 'right' }">set {{ state.sp.set.guest }}</span>
|
|
</div>
|
|
|
|
<div class="hea home">
|
|
<span :style="{ 'float': 'right' }">
|
|
<span v-if="state.visuForm" class="score-inline">{{ state.sp.punt.home }}</span>
|
|
<span class="serv-slot">
|
|
<img v-show="state.sp.servHome" src="/serv.png" width="25" />
|
|
</span>
|
|
{{ state.sp.nomi.home }}
|
|
</span>
|
|
<span class="ml3" :style="{ 'float': 'left' }">set {{ state.sp.set.home }}</span>
|
|
</div>
|
|
|
|
<span v-if="state.visuForm">
|
|
<div class="col form guest">
|
|
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]" :key="'gf2'+x">
|
|
{{ state.sp.form.guest[x] }}
|
|
</div>
|
|
</div>
|
|
<div class="col form home">
|
|
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]" :key="'hf2'+x">
|
|
{{ state.sp.form.home[x] }}
|
|
</div>
|
|
</div>
|
|
</span>
|
|
<span v-else>
|
|
<div class="punteggio-container">
|
|
<div class="col punt guest">{{ state.sp.punt.guest }}</div>
|
|
<div class="col punt home">{{ state.sp.punt.home }}</div>
|
|
</div>
|
|
</span>
|
|
</span>
|
|
|
|
<div class="striscia" v-if="state.visuStriscia">
|
|
<div>
|
|
<span class="text-bold mr1">{{ state.sp.nomi.home }}</span>
|
|
<div v-for="(h, i) in state.sp.striscia.home" :key="'sh'+i" class="item">
|
|
{{ String(h) }}
|
|
</div>
|
|
</div>
|
|
<div class="guest-striscia">
|
|
<span class="text-bold mr1">{{ state.sp.nomi.guest }}</span>
|
|
<div v-for="(h, i) in state.sp.striscia.guest" :key="'sg'+i" class="item">
|
|
{{ String(h) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Indicatore stato connessione -->
|
|
<div class="connection-status" :class="{ connected: wsConnected, disconnected: !wsConnected }">
|
|
<span class="dot"></span>
|
|
{{ wsConnected ? '' : 'Disconnesso' }}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
name: "DisplayPage",
|
|
data() {
|
|
return {
|
|
ws: null,
|
|
wsConnected: false,
|
|
isConnecting: false,
|
|
reconnectTimeout: null,
|
|
reconnectAttempts: 0,
|
|
maxReconnectDelay: 30000, // Ritardo massimo di riconnessione: 30 secondi
|
|
state: {
|
|
order: true,
|
|
visuForm: false,
|
|
visuStriscia: true,
|
|
modalitaPartita: "3/5",
|
|
sp: {
|
|
striscia: { home: [0], guest: [0] },
|
|
servHome: true,
|
|
punt: { home: 0, guest: 0 },
|
|
set: { home: 0, guest: 0 },
|
|
nomi: { home: "Antoniana", guest: "Guest" },
|
|
form: {
|
|
home: ["1", "2", "3", "4", "5", "6"],
|
|
guest: ["1", "2", "3", "4", "5", "6"],
|
|
},
|
|
storicoServizio: [],
|
|
},
|
|
},
|
|
}
|
|
},
|
|
mounted() {
|
|
this.connectWebSocket()
|
|
// Attiva la modalita fullscreen su dispositivi mobili.
|
|
if (this.isMobile()) {
|
|
try { document.documentElement.requestFullscreen() } catch (e) {}
|
|
}
|
|
|
|
// Gestisce l'HMR di Vite evitando riconnessioni durante la ricarica a caldo.
|
|
if (import.meta.hot) {
|
|
import.meta.hot.on('vite:beforeUpdate', () => {
|
|
// Annulla eventuali tentativi di riconnessione pianificati.
|
|
if (this.reconnectTimeout) {
|
|
clearTimeout(this.reconnectTimeout)
|
|
this.reconnectTimeout = null
|
|
}
|
|
})
|
|
}
|
|
},
|
|
beforeUnmount() {
|
|
// Pulisce il timeout di riconnessione.
|
|
if (this.reconnectTimeout) {
|
|
clearTimeout(this.reconnectTimeout)
|
|
this.reconnectTimeout = null
|
|
}
|
|
|
|
// Chiude il WebSocket con il codice di chiusura appropriato.
|
|
if (this.ws) {
|
|
this.ws.onclose = null // Rimuove il listener per evitare nuove riconnessioni pianificate.
|
|
this.ws.onerror = null
|
|
this.ws.onmessage = null
|
|
this.ws.onopen = null
|
|
|
|
// Usa il codice 1000 (chiusura normale) se la connessione e aperta.
|
|
try {
|
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
this.ws.close(1000, 'Component unmounting')
|
|
} else if (this.ws.readyState === WebSocket.CONNECTING) {
|
|
// Se la connessione e ancora in fase di apertura, chiude direttamente.
|
|
this.ws.close()
|
|
}
|
|
} catch (err) {
|
|
console.error('[Display] Error closing WebSocket:', err)
|
|
}
|
|
this.ws = null
|
|
}
|
|
},
|
|
methods: {
|
|
isMobile() {
|
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
|
},
|
|
connectWebSocket() {
|
|
// Evita connessioni simultanee multiple.
|
|
if (this.isConnecting) {
|
|
console.log('[Display] Already connecting, skipping...')
|
|
return
|
|
}
|
|
|
|
// Chiude la connessione precedente, se presente.
|
|
if (this.ws) {
|
|
this.ws.onclose = null
|
|
this.ws.onerror = null
|
|
this.ws.onmessage = null
|
|
this.ws.onopen = null
|
|
try {
|
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
this.ws.close(1000, 'Reconnecting')
|
|
} else if (this.ws.readyState === WebSocket.CONNECTING) {
|
|
this.ws.close()
|
|
}
|
|
} catch (err) {
|
|
console.error('[Display] Error closing previous WebSocket:', err)
|
|
}
|
|
this.ws = null
|
|
}
|
|
|
|
this.isConnecting = true
|
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
|
|
// Permette di specificare un host WebSocket alternativo via query parameter
|
|
// Utile per scenari WSL2 o development remoto: ?wsHost=192.168.1.100:5173
|
|
const params = new URLSearchParams(location.search)
|
|
const wsHost = params.get('wsHost') || location.host
|
|
const wsUrl = `${protocol}//${wsHost}/ws`
|
|
|
|
console.log('[Display] Connecting to WebSocket:', wsUrl)
|
|
|
|
try {
|
|
this.ws = new WebSocket(wsUrl)
|
|
} catch (err) {
|
|
console.error('[Display] Failed to create WebSocket:', err)
|
|
this.isConnecting = false
|
|
this.scheduleReconnect()
|
|
return
|
|
}
|
|
|
|
this.ws.onopen = () => {
|
|
this.isConnecting = false
|
|
this.wsConnected = true
|
|
this.reconnectAttempts = 0
|
|
console.log('[Display] Connected to server')
|
|
|
|
// Registra il client come display solo con connessione effettivamente aperta.
|
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
try {
|
|
this.ws.send(JSON.stringify({ type: 'register', role: 'display' }))
|
|
} catch (err) {
|
|
console.error('[Display] Failed to register:', err)
|
|
}
|
|
}
|
|
}
|
|
|
|
this.ws.onmessage = (event) => {
|
|
try {
|
|
const msg = JSON.parse(event.data)
|
|
|
|
if (msg.type === 'state') {
|
|
this.state = msg.state
|
|
} else if (msg.type === 'error') {
|
|
console.error('[Display] Server error:', msg.message)
|
|
}
|
|
} catch (e) {
|
|
console.error('[Display] Error parsing message:', e)
|
|
}
|
|
}
|
|
|
|
this.ws.onclose = (event) => {
|
|
this.isConnecting = false
|
|
this.wsConnected = false
|
|
console.log('[Display] Disconnected from server', event.code, event.reason)
|
|
|
|
// Non riconnette durante HMR (codice 1001, "going away")
|
|
// ne in caso di chiusura pulita (codice 1000).
|
|
if (event.code === 1000 || event.code === 1001) {
|
|
console.log('[Display] Clean close, not reconnecting')
|
|
return
|
|
}
|
|
|
|
this.scheduleReconnect()
|
|
}
|
|
|
|
this.ws.onerror = (err) => {
|
|
console.error('[Display] WebSocket error:', err)
|
|
this.isConnecting = false
|
|
this.wsConnected = false
|
|
}
|
|
},
|
|
scheduleReconnect() {
|
|
// Evita pianificazioni multiple della riconnessione.
|
|
if (this.reconnectTimeout) {
|
|
return
|
|
}
|
|
|
|
// Applica backoff esponenziale: 1s, 2s, 4s, 8s, 16s, fino a 30s.
|
|
const delay = Math.min(
|
|
1000 * Math.pow(2, this.reconnectAttempts),
|
|
this.maxReconnectDelay
|
|
)
|
|
this.reconnectAttempts++
|
|
|
|
console.log(`[Display] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`)
|
|
|
|
this.reconnectTimeout = setTimeout(() => {
|
|
this.reconnectTimeout = null
|
|
this.connectWebSocket()
|
|
}, delay)
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.display-page {
|
|
width: 100%;
|
|
height: 100vh;
|
|
background: #000;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.connection-status {
|
|
position: fixed;
|
|
top: 8px;
|
|
right: 8px;
|
|
font-size: 12px;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
z-index: 100;
|
|
transition: opacity 0.5s;
|
|
}
|
|
|
|
.connection-status.connected {
|
|
opacity: 0;
|
|
}
|
|
|
|
.connection-status.disconnected {
|
|
background: rgba(255, 50, 50, 0.8);
|
|
color: white;
|
|
opacity: 1;
|
|
}
|
|
|
|
.dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
}
|
|
|
|
.connected .dot {
|
|
background: #4caf50;
|
|
}
|
|
|
|
.disconnected .dot {
|
|
background: #f44336;
|
|
animation: blink 1s infinite;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.3; }
|
|
}
|
|
|
|
.guest-striscia {
|
|
color: white;
|
|
}
|
|
|
|
.punteggio-container {
|
|
width: 100%;
|
|
display: flex;
|
|
}
|
|
|
|
.punt {
|
|
font-size: 60vh;
|
|
flex: 1;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-height: 50vh;
|
|
min-width: 50vw;
|
|
max-width: 50vw;
|
|
overflow: hidden;
|
|
box-sizing: border-box;
|
|
}
|
|
</style>
|