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>
|
||||
|
||||
<!-- 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 ══════════════════ -->
|
||||
<template v-if="!isEstesa">
|
||||
@@ -67,8 +69,9 @@
|
||||
<button class="btn btn-ctrl" @click="openCambiTeam()">
|
||||
Cambi
|
||||
</button>
|
||||
<button class="btn btn-ctrl btn-timeout" :class="{ active: state.timeout }" @click="sendAction({ type: 'toggleTimeout' })">
|
||||
⏱ Time Out
|
||||
<button class="btn btn-ctrl btn-timeout" :class="{ active: state.timeout }"
|
||||
@click="state.timeout ? terminaTimeout() : openTimeout()">
|
||||
{{ state.timeout ? '⏱ ' + timeoutCountdown : '⏱ Time Out' }}
|
||||
</button>
|
||||
<button class="btn btn-danger" @click="confirmReset = true">
|
||||
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="speak()">Voce</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -277,6 +281,43 @@
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -304,6 +345,13 @@ export default {
|
||||
formGuest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
isLandscape: window.innerWidth > window.innerHeight,
|
||||
showTimeoutModal: false,
|
||||
timeoutTeamSel: null,
|
||||
timeoutVideoSel: null,
|
||||
spotList: [],
|
||||
spotDurations: {},
|
||||
nowTick: Date.now(),
|
||||
timeoutTicker: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -338,6 +386,17 @@ export default {
|
||||
}
|
||||
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: {
|
||||
squadraVincente(val) {
|
||||
@@ -346,6 +405,12 @@ export default {
|
||||
this.showSetVinto = true
|
||||
}
|
||||
},
|
||||
'state.timeout': {
|
||||
immediate: true,
|
||||
handler(attivo) {
|
||||
attivo ? this.avviaTimeoutTicker() : this.fermaTimeoutTicker()
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this._resizeHandler = () => { this.isLandscape = window.innerWidth > window.innerHeight }
|
||||
@@ -355,6 +420,7 @@ export default {
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('resize', this._resizeHandler)
|
||||
window.removeEventListener('orientationchange', this._resizeHandler)
|
||||
this.fermaTimeoutTicker()
|
||||
},
|
||||
methods: {
|
||||
generaReferto,
|
||||
@@ -458,6 +524,47 @@ export default {
|
||||
: this.servHome ? `${home} a ${guest}` : `${guest} a ${home}`
|
||||
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>
|
||||
@@ -1070,4 +1177,37 @@ export default {
|
||||
padding: 8px 0;
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user