up
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
||||
import type { State } from '../types/screen'
|
||||
import { MediaPlayer, waitUntil } from '../utils/media-player'
|
||||
import { useScreenTimer } from '../composables/useScreenTimer'
|
||||
import ResultsTable from './ResultsTable.vue'
|
||||
import { formatTimer } from '../utils/presentation'
|
||||
|
||||
const audio = ref<HTMLAudioElement | null>(null)
|
||||
const video = ref<HTMLVideoElement | null>(null)
|
||||
@@ -13,6 +14,25 @@ const surface = ref<HTMLElement | null>(null)
|
||||
const state = shallowRef<State | null>(null)
|
||||
const imageError = ref(false)
|
||||
const timerDisplay = useScreenTimer(() => state.value)
|
||||
const introPosition = ref(0)
|
||||
const introDuration = ref<number | null>(null)
|
||||
const introProgress = computed(() => introDuration.value ? introPosition.value / introDuration.value : 0)
|
||||
const introTime = computed(() => `${formatTimer(Math.floor(introPosition.value) * 1000)} / ${introDuration.value === null ? '—' : formatTimer(Math.floor(introDuration.value) * 1000)}`)
|
||||
const introBars = [24, 38, 18, 46, 58, 32, 68, 44, 26, 54, 72, 40, 60, 30, 48, 64, 36, 76, 52, 28, 44, 62, 34, 56, 70, 42, 22, 50, 66, 38, 58, 74, 46, 30, 54, 40, 68, 48, 26, 60, 36, 72, 52, 32, 46, 62, 28, 18]
|
||||
function resetIntroProgress() {
|
||||
introPosition.value = 0
|
||||
introDuration.value = null
|
||||
}
|
||||
function updateIntroProgress() {
|
||||
const media = audio.value
|
||||
if (!media || state.value?.scene.mode !== 2 || media.getAttribute('src') !== state.value.channels.main.source?.url) {
|
||||
resetIntroProgress()
|
||||
return
|
||||
}
|
||||
introDuration.value = Number.isFinite(media.duration) && media.duration > 0 ? media.duration : null
|
||||
introPosition.value = introDuration.value && Number.isFinite(media.currentTime)
|
||||
? Math.max(0, Math.min(media.currentTime, introDuration.value)) : 0
|
||||
}
|
||||
const bridge = window.screenBridge!
|
||||
let player: MediaPlayer | undefined
|
||||
let disposed = false
|
||||
@@ -90,8 +110,11 @@ function apply(value: State) {
|
||||
if (disposed) return
|
||||
if (value.revision <= revision) return
|
||||
revision = value.revision
|
||||
if (value.session !== state.value?.session || value.channels.main.source?.url !== state.value?.channels.main.source?.url || value.scene.mode !== 2)
|
||||
resetIntroProgress()
|
||||
state.value = value
|
||||
player?.apply(value)
|
||||
updateIntroProgress()
|
||||
const token = `${value.session}:${value.scene.revision}`
|
||||
if (token !== sceneToken) {
|
||||
sceneToken = token
|
||||
@@ -145,6 +168,31 @@ onUnmounted(() => {
|
||||
<p v-else-if="state.scene.mode === 2 && !state.channels.main.source" class="message">
|
||||
Вступление не задано
|
||||
</p>
|
||||
<section v-else-if="state.scene.mode === 2" class="intro-recording" aria-label="Аудиозапись введения">
|
||||
<h1>Введение</h1>
|
||||
<div
|
||||
class="intro-waveform"
|
||||
role="progressbar"
|
||||
aria-label="Прогресс введения"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
:aria-valuenow="introDuration === null ? undefined : Math.round(introProgress * 100)"
|
||||
:aria-valuetext="introTime"
|
||||
>
|
||||
<svg viewBox="0 0 480 80" preserveAspectRatio="none" aria-hidden="true">
|
||||
<defs>
|
||||
<mask id="intro-waveform-mask" maskUnits="userSpaceOnUse" x="0" y="0" width="480" height="80">
|
||||
<rect v-for="(height, index) in introBars" :key="index" :x="index * 10 + 2" :y="(80 - height) / 2" width="6" :height="height" rx="3" fill="white" />
|
||||
</mask>
|
||||
</defs>
|
||||
<g mask="url(#intro-waveform-mask)" fill="currentColor">
|
||||
<rect width="480" height="80" opacity="0.25" />
|
||||
<rect :width="480 * introProgress" height="80" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="intro-time">{{ introTime }}</p>
|
||||
</section>
|
||||
<p v-else-if="state.scene.mode === 3" class="message">Успехов</p>
|
||||
<p v-else-if="state.scene.mode === 4 && !state.channels.main.source" class="message">
|
||||
Видео закрывашки не задано
|
||||
@@ -161,7 +209,18 @@ onUnmounted(() => {
|
||||
</section>
|
||||
<p v-else-if="state.scene.mode === 8" class="message text">{{ state.scene.text }}</p>
|
||||
</div>
|
||||
<audio ref="audio" data-channel="main-audio" preload="auto" />
|
||||
<audio
|
||||
ref="audio"
|
||||
data-channel="main-audio"
|
||||
preload="auto"
|
||||
@timeupdate="updateIntroProgress"
|
||||
@loadedmetadata="updateIntroProgress"
|
||||
@durationchange="updateIntroProgress"
|
||||
@seeked="updateIntroProgress"
|
||||
@pause="updateIntroProgress"
|
||||
@ended="updateIntroProgress"
|
||||
@emptied="resetIntroProgress"
|
||||
/>
|
||||
<video
|
||||
v-show="state?.scene.mode === 4 && state.channels.main.source?.kind === 'video'"
|
||||
ref="video"
|
||||
@@ -229,6 +288,37 @@ video {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.intro-recording {
|
||||
background: rgb(17 17 17 / 78%);
|
||||
border-radius: clamp(20px, 3vw, 48px);
|
||||
box-sizing: border-box;
|
||||
max-width: 1100px;
|
||||
padding: clamp(20px, 4vw, 64px);
|
||||
width: 82vw;
|
||||
}
|
||||
|
||||
.intro-recording h1 {
|
||||
font-size: clamp(24px, min(4vw, 8vh), 64px);
|
||||
font-weight: 500;
|
||||
margin: 0 0 clamp(16px, 4vh, 48px);
|
||||
}
|
||||
|
||||
.intro-waveform {
|
||||
height: clamp(48px, 14vh, 160px);
|
||||
}
|
||||
|
||||
.intro-waveform svg {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.intro-time {
|
||||
font-size: clamp(18px, min(2.5vw, 5vh), 36px);
|
||||
font-variant-numeric: tabular-nums;
|
||||
margin: clamp(16px, 3vh, 32px) 0 0;
|
||||
}
|
||||
|
||||
.timer h1 {
|
||||
font-size: clamp(28px, 5vw, 80px);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user