feat(controller): modal di scelta squadra e video per il time out
Il bottone Time Out apre un modal invece di attivare direttamente l'overlay: si sceglie la squadra che lo ha richiesto e uno dei video in spot/ (durata rilevata lato client via <video preload=metadata>). A time out attivo il bottone diventa "Termina" con countdown, per poterlo interrompere manualmente prima dei 30s.
This commit is contained in:
@@ -7,7 +7,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Banner modalità time out attiva -->
|
<!-- Banner modalità time out attiva -->
|
||||||
<div class="timeout-banner" v-if="state.timeout">⏱ MODALITÀ TIME OUT ATTIVA</div>
|
<div class="timeout-banner" v-if="state.timeout">
|
||||||
|
⏱ TIME OUT {{ state.sp.nomi[state.timeoutTeam] }} — {{ timeoutCountdown }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ══════════════════ MODALITÀ MOBILE ══════════════════ -->
|
<!-- ══════════════════ MODALITÀ MOBILE ══════════════════ -->
|
||||||
<template v-if="!isEstesa">
|
<template v-if="!isEstesa">
|
||||||
@@ -67,8 +69,9 @@
|
|||||||
<button class="btn btn-ctrl" @click="openCambiTeam()">
|
<button class="btn btn-ctrl" @click="openCambiTeam()">
|
||||||
Cambi
|
Cambi
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ctrl btn-timeout" :class="{ active: state.timeout }" @click="sendAction({ type: 'toggleTimeout' })">
|
<button class="btn btn-ctrl btn-timeout" :class="{ active: state.timeout }"
|
||||||
⏱ Time Out
|
@click="state.timeout ? terminaTimeout() : openTimeout()">
|
||||||
|
{{ state.timeout ? '⏱ ' + timeoutCountdown : '⏱ Time Out' }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" @click="confirmReset = true">
|
<button class="btn btn-danger" @click="confirmReset = true">
|
||||||
Reset
|
Reset
|
||||||
@@ -148,7 +151,8 @@
|
|||||||
<button class="btn e-act" @click="sendAction({ type: 'toggleStriscia' })">Striscia {{ state.visuStriscia ? 'ON' : 'OFF' }}</button>
|
<button class="btn e-act" @click="sendAction({ type: 'toggleStriscia' })">Striscia {{ state.visuStriscia ? 'ON' : 'OFF' }}</button>
|
||||||
<button class="btn e-act" @click="speak()">Voce</button>
|
<button class="btn e-act" @click="speak()">Voce</button>
|
||||||
<button class="btn e-act" @click="openConfig()">Config</button>
|
<button class="btn e-act" @click="openConfig()">Config</button>
|
||||||
<button class="btn e-act btn-timeout" :class="{ active: state.timeout }" @click="sendAction({ type: 'toggleTimeout' })">⏱ Time Out</button>
|
<button class="btn e-act btn-timeout" :class="{ active: state.timeout }"
|
||||||
|
@click="state.timeout ? terminaTimeout() : openTimeout()">{{ state.timeout ? '⏱ ' + timeoutCountdown : '⏱ Time Out' }}</button>
|
||||||
<button class="btn e-act e-act--danger" @click="confirmReset = true">Reset</button>
|
<button class="btn e-act e-act--danger" @click="confirmReset = true">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,6 +281,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Finestra avvio time out: squadra + scelta video -->
|
||||||
|
<div class="overlay" v-if="showTimeoutModal" @click.self="showTimeoutModal = false">
|
||||||
|
<div class="dialog dialog-timeout">
|
||||||
|
<div class="dialog-title">Time Out</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Squadra che lo ha richiesto</label>
|
||||||
|
<div class="dialog-buttons">
|
||||||
|
<button class="btn btn-set home-bg" :class="{ 'team-sel-active': timeoutTeamSel === 'home' }"
|
||||||
|
@click="timeoutTeamSel = 'home'">{{ state.sp.nomi.home }}</button>
|
||||||
|
<button class="btn btn-set guest-bg" :class="{ 'team-sel-active': timeoutTeamSel === 'guest' }"
|
||||||
|
@click="timeoutTeamSel = 'guest'">{{ state.sp.nomi.guest }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Video (30s nominali)</label>
|
||||||
|
<div class="timeout-video-list">
|
||||||
|
<label class="timeout-video-option" v-for="file in spotList" :key="file">
|
||||||
|
<input type="radio" name="timeoutVideo" :value="file" v-model="timeoutVideoSel" />
|
||||||
|
{{ file }}
|
||||||
|
<span class="video-duration">{{ formatDurata(spotDurations[file]) }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="timeout-video-option">
|
||||||
|
<input type="radio" name="timeoutVideo" :value="null" v-model="timeoutVideoSel" />
|
||||||
|
Nessun video (overlay "TIME OUT")
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-buttons">
|
||||||
|
<button class="btn btn-cancel" @click="showTimeoutModal = false">Annulla</button>
|
||||||
|
<button class="btn btn-confirm" :disabled="!timeoutTeamSel" @click="avviaTimeout()">Avvia</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -304,6 +345,13 @@ export default {
|
|||||||
formGuest: ["1", "2", "3", "4", "5", "6"],
|
formGuest: ["1", "2", "3", "4", "5", "6"],
|
||||||
},
|
},
|
||||||
isLandscape: window.innerWidth > window.innerHeight,
|
isLandscape: window.innerWidth > window.innerHeight,
|
||||||
|
showTimeoutModal: false,
|
||||||
|
timeoutTeamSel: null,
|
||||||
|
timeoutVideoSel: null,
|
||||||
|
spotList: [],
|
||||||
|
spotDurations: {},
|
||||||
|
nowTick: Date.now(),
|
||||||
|
timeoutTicker: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -338,6 +386,17 @@ export default {
|
|||||||
}
|
}
|
||||||
return hasComplete
|
return hasComplete
|
||||||
},
|
},
|
||||||
|
timeoutRemaining() {
|
||||||
|
if (!this.state.timeout || !this.state.timeoutStartedAt) return 30
|
||||||
|
const trascorsi = (this.nowTick - this.state.timeoutStartedAt) / 1000
|
||||||
|
return Math.max(0, Math.ceil(30 - trascorsi))
|
||||||
|
},
|
||||||
|
timeoutCountdown() {
|
||||||
|
const r = this.timeoutRemaining
|
||||||
|
const m = String(Math.floor(r / 60)).padStart(2, '0')
|
||||||
|
const s = String(r % 60).padStart(2, '0')
|
||||||
|
return `${m}:${s}`
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
squadraVincente(val) {
|
squadraVincente(val) {
|
||||||
@@ -346,6 +405,12 @@ export default {
|
|||||||
this.showSetVinto = true
|
this.showSetVinto = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'state.timeout': {
|
||||||
|
immediate: true,
|
||||||
|
handler(attivo) {
|
||||||
|
attivo ? this.avviaTimeoutTicker() : this.fermaTimeoutTicker()
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this._resizeHandler = () => { this.isLandscape = window.innerWidth > window.innerHeight }
|
this._resizeHandler = () => { this.isLandscape = window.innerWidth > window.innerHeight }
|
||||||
@@ -355,6 +420,7 @@ export default {
|
|||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
window.removeEventListener('resize', this._resizeHandler)
|
window.removeEventListener('resize', this._resizeHandler)
|
||||||
window.removeEventListener('orientationchange', this._resizeHandler)
|
window.removeEventListener('orientationchange', this._resizeHandler)
|
||||||
|
this.fermaTimeoutTicker()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
generaReferto,
|
generaReferto,
|
||||||
@@ -458,6 +524,47 @@ export default {
|
|||||||
: this.servHome ? `${home} a ${guest}` : `${guest} a ${home}`
|
: this.servHome ? `${home} a ${guest}` : `${guest} a ${home}`
|
||||||
this.sendWs({ type: 'speak', text })
|
this.sendWs({ type: 'speak', text })
|
||||||
},
|
},
|
||||||
|
avviaTimeoutTicker() {
|
||||||
|
this.fermaTimeoutTicker()
|
||||||
|
this.nowTick = Date.now()
|
||||||
|
this.timeoutTicker = setInterval(() => { this.nowTick = Date.now() }, 250)
|
||||||
|
},
|
||||||
|
fermaTimeoutTicker() {
|
||||||
|
if (this.timeoutTicker) { clearInterval(this.timeoutTicker); this.timeoutTicker = null }
|
||||||
|
},
|
||||||
|
async openTimeout() {
|
||||||
|
this.timeoutTeamSel = null
|
||||||
|
this.timeoutVideoSel = null
|
||||||
|
this.spotList = []
|
||||||
|
this.spotDurations = {}
|
||||||
|
this.showTimeoutModal = true
|
||||||
|
try {
|
||||||
|
const res = await fetch('/spot/list')
|
||||||
|
this.spotList = await res.json()
|
||||||
|
} catch {
|
||||||
|
this.spotList = []
|
||||||
|
}
|
||||||
|
this.spotList.forEach(file => this.probeDurata(file))
|
||||||
|
},
|
||||||
|
probeDurata(file) {
|
||||||
|
const video = document.createElement('video')
|
||||||
|
video.preload = 'metadata'
|
||||||
|
video.onloadedmetadata = () => {
|
||||||
|
this.spotDurations = { ...this.spotDurations, [file]: video.duration }
|
||||||
|
}
|
||||||
|
video.src = '/spot/' + encodeURIComponent(file)
|
||||||
|
},
|
||||||
|
formatDurata(sec) {
|
||||||
|
return typeof sec === 'number' && isFinite(sec) ? `${Math.round(sec)}s` : '…'
|
||||||
|
},
|
||||||
|
avviaTimeout() {
|
||||||
|
if (!this.timeoutTeamSel) return
|
||||||
|
this.sendAction({ type: 'startTimeout', team: this.timeoutTeamSel, video: this.timeoutVideoSel })
|
||||||
|
this.showTimeoutModal = false
|
||||||
|
},
|
||||||
|
terminaTimeout() {
|
||||||
|
this.sendAction({ type: 'stopTimeout' })
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1070,4 +1177,37 @@ export default {
|
|||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal avvio time out */
|
||||||
|
.dialog-timeout {
|
||||||
|
max-height: 85vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.btn-set.team-sel-active {
|
||||||
|
box-shadow: 0 0 0 3px rgba(255,255,255,0.6) inset;
|
||||||
|
}
|
||||||
|
.timeout-video-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 40vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.timeout-video-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
border: 1px solid rgba(255,255,255,0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.video-duration {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 12px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user