Make viewers unabler to join before teams are finalized. Viewers are on teams
All checks were successful
Deploy Feature / deploy-feature (push) Successful in 27s
All checks were successful
Deploy Feature / deploy-feature (push) Successful in 27s
This commit is contained in:
58
src/App.vue
58
src/App.vue
@@ -132,12 +132,24 @@
|
|||||||
<section v-if="step === 'game'" class="game-layout">
|
<section v-if="step === 'game'" class="game-layout">
|
||||||
<aside class="scoreboard">
|
<aside class="scoreboard">
|
||||||
<h2>Teams</h2>
|
<h2>Teams</h2>
|
||||||
|
<div v-if="!canControlGame" class="viewer-team-select">
|
||||||
|
<label for="viewer-team">Playing For</label>
|
||||||
|
<select id="viewer-team" v-model="viewerTeamId" class="input">
|
||||||
|
<option value="">Select team</option>
|
||||||
|
<option v-for="team in teams" :key="`viewer-${team.id}`" :value="team.id">
|
||||||
|
{{ team.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="team-list">
|
<div class="team-list">
|
||||||
<button
|
<button
|
||||||
v-for="team in teams"
|
v-for="team in teams"
|
||||||
:key="team.id"
|
:key="team.id"
|
||||||
class="team-score-card"
|
class="team-score-card"
|
||||||
:class="{ active: team.id === currentSelectorId }"
|
:class="{
|
||||||
|
active: team.id === currentSelectorId,
|
||||||
|
viewerTeam: !canControlGame && !!viewerTeamId && team.id === viewerTeamId
|
||||||
|
}"
|
||||||
:style="teamGradient(team)"
|
:style="teamGradient(team)"
|
||||||
@click="awardPoints(team.id)"
|
@click="awardPoints(team.id)"
|
||||||
:disabled="!canAward || !canControlGame"
|
:disabled="!canAward || !canControlGame"
|
||||||
@@ -251,6 +263,9 @@
|
|||||||
@pause="handlePlayerPause"
|
@pause="handlePlayerPause"
|
||||||
@ended="handlePlayerPause"
|
@ended="handlePlayerPause"
|
||||||
></audio>
|
></audio>
|
||||||
|
<button v-if="showEnableAudio" class="primary enable-audio" @click="enableViewerAudio">
|
||||||
|
Tap To Enable Audio
|
||||||
|
</button>
|
||||||
<div v-if="!currentClipUrl" class="player-empty"></div>
|
<div v-if="!currentClipUrl" class="player-empty"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -380,7 +395,10 @@ export default {
|
|||||||
isApplyingRemote: false,
|
isApplyingRemote: false,
|
||||||
queuedRemoteState: null as RealtimeState | null,
|
queuedRemoteState: null as RealtimeState | null,
|
||||||
syncTimer: 0,
|
syncTimer: 0,
|
||||||
playbackSyncInterval: 0
|
playbackSyncInterval: 0,
|
||||||
|
audioUnlocked: true,
|
||||||
|
latestRemoteState: null as RealtimeState | null,
|
||||||
|
viewerTeamId: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
@@ -412,6 +430,9 @@ export default {
|
|||||||
canProceedToSelect() {
|
canProceedToSelect() {
|
||||||
return this.canProceed && this.canControlGame && !!this.normalizedGameIdInput && !this.sessionBusy
|
return this.canProceed && this.canControlGame && !!this.normalizedGameIdInput && !this.sessionBusy
|
||||||
},
|
},
|
||||||
|
showEnableAudio() {
|
||||||
|
return !this.canControlGame && !!this.currentClipUrl && !this.audioUnlocked
|
||||||
|
},
|
||||||
canControlGame() {
|
canControlGame() {
|
||||||
return !this.gameId || this.isHost
|
return !this.gameId || this.isHost
|
||||||
},
|
},
|
||||||
@@ -497,6 +518,7 @@ export default {
|
|||||||
this.gameId = gameId
|
this.gameId = gameId
|
||||||
this.gameIdInput = gameId
|
this.gameIdInput = gameId
|
||||||
this.isHost = true
|
this.isHost = true
|
||||||
|
this.audioUnlocked = true
|
||||||
this.setGameInUrl(gameId)
|
this.setGameInUrl(gameId)
|
||||||
await this.connectSession()
|
await this.connectSession()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -514,9 +536,15 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const existsResponse = await fetch(`${REALTIME_BASE}/sessions/${normalized}`)
|
const existsResponse = await fetch(`${REALTIME_BASE}/sessions/${normalized}`)
|
||||||
if (!existsResponse.ok) throw new Error('Session not found')
|
if (!existsResponse.ok) throw new Error('Session not found')
|
||||||
|
const sessionInfo = (await existsResponse.json()) as { hasState?: boolean }
|
||||||
|
if (!sessionInfo.hasState) {
|
||||||
|
throw new Error('Game is not open yet. Host must press Next: Choose Game first.')
|
||||||
|
}
|
||||||
this.gameId = normalized
|
this.gameId = normalized
|
||||||
this.gameIdInput = normalized
|
this.gameIdInput = normalized
|
||||||
this.isHost = false
|
this.isHost = false
|
||||||
|
this.audioUnlocked = false
|
||||||
|
this.viewerTeamId = ''
|
||||||
this.setGameInUrl(normalized)
|
this.setGameInUrl(normalized)
|
||||||
await this.connectSession()
|
await this.connectSession()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -531,6 +559,9 @@ export default {
|
|||||||
this.gameIdInput = ''
|
this.gameIdInput = ''
|
||||||
this.isHost = false
|
this.isHost = false
|
||||||
this.syncError = ''
|
this.syncError = ''
|
||||||
|
this.audioUnlocked = true
|
||||||
|
this.latestRemoteState = null
|
||||||
|
this.viewerTeamId = ''
|
||||||
this.stopHostPlaybackSync()
|
this.stopHostPlaybackSync()
|
||||||
this.setGameInUrl('')
|
this.setGameInUrl('')
|
||||||
},
|
},
|
||||||
@@ -598,6 +629,7 @@ export default {
|
|||||||
publishState() {
|
publishState() {
|
||||||
if (!this.isHost || !this.socketConnected || !this.socket) return
|
if (!this.isHost || !this.socketConnected || !this.socket) return
|
||||||
if (this.isApplyingRemote) return
|
if (this.isApplyingRemote) return
|
||||||
|
if (this.step === 'setup') return
|
||||||
const payload = {
|
const payload = {
|
||||||
type: 'state:update',
|
type: 'state:update',
|
||||||
state: this.buildRealtimeState()
|
state: this.buildRealtimeState()
|
||||||
@@ -618,6 +650,7 @@ export default {
|
|||||||
this.queuedRemoteState = state
|
this.queuedRemoteState = state
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
this.latestRemoteState = state
|
||||||
const previousClipUrl = this.currentClipUrl
|
const previousClipUrl = this.currentClipUrl
|
||||||
this.isApplyingRemote = true
|
this.isApplyingRemote = true
|
||||||
try {
|
try {
|
||||||
@@ -633,12 +666,24 @@ export default {
|
|||||||
this.currentSelectorId = state.currentSelectorId
|
this.currentSelectorId = state.currentSelectorId
|
||||||
this.lastAwardedTeamId = state.lastAwardedTeamId
|
this.lastAwardedTeamId = state.lastAwardedTeamId
|
||||||
this.isAnswerClip = state.isAnswerClip
|
this.isAnswerClip = state.isAnswerClip
|
||||||
|
if (this.viewerTeamId && !this.teams.some((team) => team.id === this.viewerTeamId)) {
|
||||||
|
this.viewerTeamId = ''
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.isApplyingRemote = false
|
this.isApplyingRemote = false
|
||||||
}
|
}
|
||||||
await nextTick()
|
await nextTick()
|
||||||
this.syncRemotePlayback(previousClipUrl !== this.currentClipUrl, state)
|
this.syncRemotePlayback(previousClipUrl !== this.currentClipUrl, state)
|
||||||
},
|
},
|
||||||
|
async enableViewerAudio() {
|
||||||
|
if (this.canControlGame) return
|
||||||
|
this.audioUnlocked = true
|
||||||
|
this.ensureAudioContext()
|
||||||
|
await nextTick()
|
||||||
|
if (this.latestRemoteState) {
|
||||||
|
this.syncRemotePlayback(false, this.latestRemoteState)
|
||||||
|
}
|
||||||
|
},
|
||||||
getCurrentTileStatus() {
|
getCurrentTileStatus() {
|
||||||
if (!this.currentTileKey) return 'available'
|
if (!this.currentTileKey) return 'available'
|
||||||
return this.tiles[this.currentTileKey]?.status || 'available'
|
return this.tiles[this.currentTileKey]?.status || 'available'
|
||||||
@@ -691,6 +736,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tileStatus === 'playing' || tileStatus === 'guessed') {
|
if (tileStatus === 'playing' || tileStatus === 'guessed') {
|
||||||
|
if (!this.audioUnlocked) return
|
||||||
this.ensureAudioContext()
|
this.ensureAudioContext()
|
||||||
player.play().catch(() => {})
|
player.play().catch(() => {})
|
||||||
}
|
}
|
||||||
@@ -709,7 +755,11 @@ export default {
|
|||||||
setupAudioGraph() {
|
setupAudioGraph() {
|
||||||
const player = this.getPlayer()
|
const player = this.getPlayer()
|
||||||
if (!player) return
|
if (!player) return
|
||||||
|
try {
|
||||||
this.ensureAudioContext()
|
this.ensureAudioContext()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (this.analyser) {
|
if (this.analyser) {
|
||||||
this.analyser.disconnect()
|
this.analyser.disconnect()
|
||||||
@@ -723,7 +773,11 @@ export default {
|
|||||||
if (this.mediaSource) {
|
if (this.mediaSource) {
|
||||||
this.mediaSource.disconnect()
|
this.mediaSource.disconnect()
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
this.mediaSource = context.createMediaElementSource(player)
|
this.mediaSource = context.createMediaElementSource(player)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.mediaElement = player
|
this.mediaElement = player
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -282,6 +282,17 @@ button {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.viewer-team-select {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-team-select label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
.team-score-card {
|
.team-score-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -296,6 +307,10 @@ button {
|
|||||||
outline: 2px solid #ffffff;
|
outline: 2px solid #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.team-score-card.viewerTeam {
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.6) inset;
|
||||||
|
}
|
||||||
|
|
||||||
.team-name {
|
.team-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -539,6 +554,12 @@ audio.hidden-audio {
|
|||||||
color: rgba(255, 255, 255, 0.6);
|
color: rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.enable-audio {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
.end-panel {
|
.end-panel {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user