up
This commit is contained in:
@@ -57,6 +57,12 @@ const host = ref<HostInfo>({
|
||||
preferredDisplayId: null,
|
||||
})
|
||||
const display = ref<number | null>(null)
|
||||
const previewViewport = computed(() => {
|
||||
const selected = host.value.displays.find(item => item.id === display.value)
|
||||
return selected && selected.width > 0 && selected.height > 0
|
||||
? { width: selected.width, height: selected.height }
|
||||
: { width: 1920, height: 1080 }
|
||||
})
|
||||
const isBusy = ref(false)
|
||||
const isCheckingSound = ref(false)
|
||||
async function checkSound() {
|
||||
@@ -283,9 +289,9 @@ onUnmounted(() => {
|
||||
<n-tag
|
||||
role="status"
|
||||
data-testid="display-connection-status"
|
||||
:type="host.displays.length ? 'success' : 'error'"
|
||||
:type="state?.outputOpen ? 'success' : 'default'"
|
||||
>
|
||||
{{ host.displays.length ? 'Подключён' : 'Не подключён' }}
|
||||
{{ state?.outputOpen ? 'Показ открыт' : 'Показ закрыт' }}
|
||||
</n-tag>
|
||||
<n-button
|
||||
v-if="!state?.outputOpen"
|
||||
@@ -400,9 +406,9 @@ onUnmounted(() => {
|
||||
</template>
|
||||
<n-space vertical>
|
||||
<ScreenPlanPanel
|
||||
:game-name="state.catalog.name"
|
||||
:cover="state.catalog.cover"
|
||||
:background="state.catalog.background"
|
||||
:catalog="state.catalog"
|
||||
:results="state.results"
|
||||
:viewport="previewViewport"
|
||||
:steps="state.catalog.steps"
|
||||
:current-index="state.scene.stepIndex"
|
||||
:draft="planDraft"
|
||||
|
||||
+18
-140
@@ -3,7 +3,7 @@ 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 SceneView from './SceneView.vue'
|
||||
import { formatTimer } from '../utils/presentation'
|
||||
|
||||
const audio = ref<HTMLAudioElement | null>(null)
|
||||
@@ -131,12 +131,6 @@ let unsubscribe: (() => void) | undefined
|
||||
let revision = -1
|
||||
let sceneToken = ''
|
||||
let sceneAbort: AbortController | null = null
|
||||
// Новый адрес исключает повторное использование уже декодированной картинки Chromium.
|
||||
function imageURL(url: string) {
|
||||
const result = new URL(url)
|
||||
result.searchParams.set('scene', `${state.value?.session}:${state.value?.scene.revision}`)
|
||||
return result.href
|
||||
}
|
||||
function waitImage(image: HTMLImageElement, signal: AbortSignal) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const finish = (error?: string) => {
|
||||
@@ -240,60 +234,23 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<main ref="surface" class="output" :data-mode="state?.scene.mode">
|
||||
<img
|
||||
v-if="state?.catalog.background"
|
||||
:key="`${state.session}:${state.scene.revision}:background`"
|
||||
class="background"
|
||||
:class="{ failed: imageError }"
|
||||
:src="imageURL(state.catalog.background)"
|
||||
alt=""
|
||||
<SceneView
|
||||
v-if="state"
|
||||
:catalog="state.catalog"
|
||||
:mode="state.scene.mode"
|
||||
:text="state.scene.text"
|
||||
:timer-label="state.timer.label"
|
||||
:timer-display="timerDisplay"
|
||||
:results="state.results"
|
||||
:revision="state.scene.revision"
|
||||
:image-version="`${state.session}:${state.scene.revision}`"
|
||||
:image-error="imageError"
|
||||
:has-main-source="!!state.channels.main.source"
|
||||
:intro-bars="introBars"
|
||||
:intro-progress="introDuration === null ? null : introProgress"
|
||||
:intro-time="introTime"
|
||||
animated-results
|
||||
/>
|
||||
<div v-if="state" class="scene">
|
||||
<template v-if="state.scene.mode === 1">
|
||||
<img
|
||||
v-if="state.catalog.cover"
|
||||
:key="`${state.session}:${state.scene.revision}:cover`"
|
||||
class="cover"
|
||||
:class="{ failed: imageError }"
|
||||
:src="imageURL(state.catalog.cover)"
|
||||
alt="Обложка дела"
|
||||
/>
|
||||
<h1 v-else class="message">{{ state.catalog.name }}</h1>
|
||||
</template>
|
||||
<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="Аудиозапись введения">
|
||||
<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 768 80" preserveAspectRatio="none" aria-hidden="true">
|
||||
<rect v-for="(height, index) in introBars" :key="index" :x="index * 6 + 2" :y="(80 - height) / 2" width="2" :height="height" rx="1" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</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">
|
||||
Видео закрывашки не задано
|
||||
</p>
|
||||
<p v-else-if="state.scene.mode === 5" class="message">Спасибо за игру!</p>
|
||||
<ResultsTable
|
||||
v-else-if="state.scene.mode === 6"
|
||||
:snapshot="state.results"
|
||||
:revision="state.scene.revision"
|
||||
/>
|
||||
<section v-else-if="state.scene.mode === 7" class="timer">
|
||||
<h1>{{ state.timer.label }}</h1>
|
||||
<strong>{{ timerDisplay }}</strong>
|
||||
</section>
|
||||
<p v-else-if="state.scene.mode === 8" class="message text">{{ state.scene.text }}</p>
|
||||
</div>
|
||||
<audio
|
||||
ref="audio"
|
||||
data-channel="main-audio"
|
||||
@@ -328,90 +285,11 @@ onUnmounted(() => {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.scene {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.background,
|
||||
.cover,
|
||||
video {
|
||||
height: 100%;
|
||||
inset: 0;
|
||||
object-fit: contain;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.background {
|
||||
filter: brightness(45%);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.cover,
|
||||
video {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.failed {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.message,
|
||||
.timer,
|
||||
.intro-recording {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: clamp(32px, 7vw, 140px);
|
||||
margin: 0;
|
||||
max-height: 90vh;
|
||||
max-width: 92vw;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.intro-recording {
|
||||
max-width: 1100px;
|
||||
width: 82vw;
|
||||
}
|
||||
|
||||
.intro-waveform {
|
||||
height: clamp(48px, 14vh, 160px);
|
||||
}
|
||||
|
||||
.intro-waveform svg {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(16px, 3vh, 32px);
|
||||
max-width: 92vw;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.timer h1 {
|
||||
font-size: clamp(28px, 5vw, 80px);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timer strong {
|
||||
font-size: clamp(80px, 20vw, 300px);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
import type { ResultsSnapshot } from '../types/screen'
|
||||
|
||||
const props = defineProps<{ snapshot: ResultsSnapshot | null; revision: number }>()
|
||||
const props = defineProps<{ snapshot: ResultsSnapshot | null; revision: number; animated?: boolean }>()
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
let frame = 0
|
||||
let generation = 0
|
||||
@@ -12,7 +12,7 @@ watch(
|
||||
const current = ++generation
|
||||
cancelAnimationFrame(frame)
|
||||
await nextTick()
|
||||
if (current !== generation || !container.value || !props.snapshot) return
|
||||
if (!props.animated || current !== generation || !container.value || !props.snapshot) return
|
||||
const element = container.value
|
||||
const distance = Math.max(0, element.scrollHeight - element.clientHeight)
|
||||
const start = performance.now()
|
||||
@@ -70,18 +70,18 @@ onUnmounted(() => {
|
||||
.results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 90vh;
|
||||
max-width: 96vw;
|
||||
width: 96vw;
|
||||
height: calc(var(--scene-vh, 1vh) * 90);
|
||||
max-width: calc(var(--scene-vw, 1vw) * 96);
|
||||
width: calc(var(--scene-vw, 1vw) * 96);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(28px, 4vw, 72px);
|
||||
font-size: clamp(28px, calc(var(--scene-vw, 1vw) * 4), 72px);
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
font-size: clamp(24px, 3vw, 48px);
|
||||
font-size: clamp(24px, calc(var(--scene-vw, 1vw) * 3), 48px);
|
||||
}
|
||||
|
||||
.results-scroll {
|
||||
@@ -90,7 +90,7 @@ h1 {
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
font-size: clamp(18px, 2.4vw, 48px);
|
||||
font-size: clamp(18px, calc(var(--scene-vw, 1vw) * 2.4), 48px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from 'vue'
|
||||
import type { Catalog, ResultsSnapshot, ScreenMode } from '../types/screen'
|
||||
import ResultsTable from './ResultsTable.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
catalog: Catalog
|
||||
mode: ScreenMode
|
||||
text: string
|
||||
timerLabel: string
|
||||
timerDisplay: string
|
||||
results: ResultsSnapshot | null
|
||||
revision?: number
|
||||
imageVersion?: string
|
||||
imageError?: boolean
|
||||
hasMainSource: boolean
|
||||
introBars?: number[]
|
||||
introProgress?: number | null
|
||||
introTime?: string
|
||||
animatedResults?: boolean
|
||||
viewport?: { width: number; height: number }
|
||||
}>(), {
|
||||
revision: 0,
|
||||
imageVersion: '',
|
||||
imageError: false,
|
||||
introBars: () => Array(128).fill(2),
|
||||
introProgress: null,
|
||||
introTime: '',
|
||||
animatedResults: false,
|
||||
})
|
||||
const failedImages = reactive(new Set<string>())
|
||||
watch(() => [props.imageVersion, props.catalog.cover, props.catalog.background], () => failedImages.clear(), { flush: 'sync' })
|
||||
function imageURL(url: string, version: string) {
|
||||
if (!version) return url
|
||||
const result = new URL(url)
|
||||
result.searchParams.set('scene', version)
|
||||
return result.href
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="scene-view" :data-mode="mode" :style="viewport ? { '--scene-vw': `${viewport.width / 100}px`, '--scene-vh': `${viewport.height / 100}px` } : undefined">
|
||||
<img
|
||||
v-if="catalog.background"
|
||||
:key="`${imageVersion}:background`"
|
||||
class="background"
|
||||
:class="{ failed: imageError || failedImages.has(catalog.background!) }"
|
||||
@error="failedImages.add(catalog.background!)"
|
||||
:src="imageURL(catalog.background, imageVersion)"
|
||||
alt=""
|
||||
/>
|
||||
<div class="scene">
|
||||
<template v-if="mode === 1">
|
||||
<img
|
||||
v-if="catalog.cover"
|
||||
:key="`${imageVersion}:cover`"
|
||||
class="cover"
|
||||
:class="{ failed: imageError || failedImages.has(catalog.cover!) }"
|
||||
@error="failedImages.add(catalog.cover!)"
|
||||
:src="imageURL(catalog.cover, imageVersion)"
|
||||
alt="Обложка дела"
|
||||
/>
|
||||
<h1 v-else class="message">{{ catalog.name }}</h1>
|
||||
</template>
|
||||
<p v-else-if="mode === 2 && !hasMainSource" class="message">
|
||||
Вступление не задано
|
||||
</p>
|
||||
<section v-else-if="mode === 2" class="intro-recording" aria-label="Аудиозапись введения">
|
||||
<div
|
||||
class="intro-waveform"
|
||||
role="progressbar"
|
||||
aria-label="Прогресс введения"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
:aria-valuenow="introProgress === null ? undefined : Math.round(introProgress * 100)"
|
||||
:aria-valuetext="introTime"
|
||||
>
|
||||
<svg viewBox="0 0 768 80" preserveAspectRatio="none" aria-hidden="true">
|
||||
<rect v-for="(height, index) in introBars" :key="index" :x="index * 6 + 2" :y="(80 - height) / 2" width="2" :height="height" rx="1" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
<p v-else-if="mode === 3" class="message">Успехов</p>
|
||||
<p v-else-if="mode === 4 && !hasMainSource" class="message">
|
||||
Видео закрывашки не задано
|
||||
</p>
|
||||
<p v-else-if="mode === 5" class="message">Спасибо за игру!</p>
|
||||
<ResultsTable
|
||||
v-else-if="mode === 6"
|
||||
:snapshot="results"
|
||||
:revision="revision"
|
||||
:animated="animatedResults"
|
||||
/>
|
||||
<section v-else-if="mode === 7" class="timer">
|
||||
<h1>{{ timerLabel }}</h1>
|
||||
<strong>{{ timerDisplay }}</strong>
|
||||
</section>
|
||||
<p v-else-if="mode === 8" class="message text">{{ text }}</p>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scene-view {
|
||||
--scene-vh: 1vh;
|
||||
--scene-vw: 1vw;
|
||||
|
||||
background: var(--brand-background);
|
||||
color: var(--brand-text);
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
line-height: 1.6;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scene {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.background,
|
||||
.cover {
|
||||
height: 100%;
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.background {
|
||||
filter: brightness(45%);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.cover {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.failed {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.message,
|
||||
.timer,
|
||||
.intro-recording {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: clamp(32px, calc(var(--scene-vw) * 7), 140px);
|
||||
margin: 0;
|
||||
max-height: calc(var(--scene-vh) * 90);
|
||||
max-width: calc(var(--scene-vw) * 92);
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.intro-recording {
|
||||
max-width: 1100px;
|
||||
width: calc(var(--scene-vw) * 82);
|
||||
}
|
||||
|
||||
.intro-waveform {
|
||||
height: clamp(48px, calc(var(--scene-vh) * 14), 160px);
|
||||
}
|
||||
|
||||
.intro-waveform svg {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(16px, calc(var(--scene-vh) * 3), 32px);
|
||||
max-width: calc(var(--scene-vw) * 92);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.timer h1 {
|
||||
font-size: clamp(28px, calc(var(--scene-vw) * 5), 80px);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timer strong {
|
||||
font-size: clamp(80px, calc(var(--scene-vw) * 20), 300px);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -2,14 +2,14 @@
|
||||
import { NAlert, NButton, NInput, NInputNumber, NSelect, NSpace } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
import { playbackControl } from '../utils/playbackControl'
|
||||
import type { ChannelState, Intent, ScreenMode, ScreenStep, TimerState } from '../types/screen'
|
||||
import type { Catalog, ChannelState, Intent, ResultsSnapshot, ScreenMode, ScreenStep, TimerState } from '../types/screen'
|
||||
import { modeLabels } from '../utils/presentation'
|
||||
import ScreenStepPreview from './ScreenStepPreview.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
gameName: string
|
||||
cover: string | null
|
||||
background: string | null
|
||||
catalog: Catalog
|
||||
results: ResultsSnapshot | null
|
||||
viewport: { width: number; height: number }
|
||||
steps: ScreenStep[]
|
||||
currentIndex: number | null
|
||||
draft: ScreenStep[]
|
||||
@@ -54,9 +54,9 @@ function timerLabel(index: number) {
|
||||
<div class="plan-step-preview">
|
||||
<ScreenStepPreview
|
||||
:step="step"
|
||||
:game-name="gameName"
|
||||
:cover="cover"
|
||||
:background="background"
|
||||
:catalog="catalog"
|
||||
:results="results"
|
||||
:viewport="viewport"
|
||||
/>
|
||||
</div>
|
||||
<div class="plan-edit-controls">
|
||||
@@ -131,9 +131,9 @@ function timerLabel(index: number) {
|
||||
<div class="plan-step-preview">
|
||||
<ScreenStepPreview
|
||||
:step="step"
|
||||
:game-name="gameName"
|
||||
:cover="cover"
|
||||
:background="background"
|
||||
:catalog="catalog"
|
||||
:results="results"
|
||||
:viewport="viewport"
|
||||
/>
|
||||
</div>
|
||||
<div class="plan-step-content">
|
||||
|
||||
@@ -1,92 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import type { ScreenStep } from '../types/screen'
|
||||
import { formatTimer, modeLabels } from '../utils/presentation'
|
||||
import { computed } from 'vue'
|
||||
import type { Catalog, ResultsSnapshot, ScreenStep } from '../types/screen'
|
||||
import { formatTimer } from '../utils/presentation'
|
||||
import SceneView from './SceneView.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
step: ScreenStep
|
||||
gameName: string
|
||||
cover: string | null
|
||||
background: string | null
|
||||
catalog: Catalog
|
||||
results: ResultsSnapshot | null
|
||||
viewport: { width: number; height: number }
|
||||
}>()
|
||||
const failedImages = reactive(new Set<string>())
|
||||
function previewText(step: ScreenStep) {
|
||||
switch (step.mode) {
|
||||
case 1:
|
||||
return props.gameName
|
||||
case 3:
|
||||
return 'Успехов'
|
||||
case 5:
|
||||
return 'Спасибо за игру!'
|
||||
case 7:
|
||||
return `${step.timerLabel} ${formatTimer(step.timerDurationSeconds * 1000)}`
|
||||
case 8:
|
||||
return step.text
|
||||
default:
|
||||
return modeLabels[step.mode]
|
||||
}
|
||||
}
|
||||
const source = computed(() => props.catalog.media.find(media =>
|
||||
media.kind === (props.step.mode === 2 ? 'audio' : 'video'),
|
||||
))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="step-preview" aria-hidden="true">
|
||||
<img
|
||||
v-if="background && !failedImages.has(background)"
|
||||
:src="background"
|
||||
class="step-preview-background"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
@error="failedImages.add(background!)"
|
||||
/>
|
||||
<img
|
||||
v-if="step.mode === 1 && cover && !failedImages.has(cover)"
|
||||
:src="cover"
|
||||
class="step-preview-cover"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
@error="failedImages.add(cover!)"
|
||||
/>
|
||||
<span v-if="step.mode !== 1 || !cover || failedImages.has(cover)">{{ previewText(step) }}</span>
|
||||
<div class="step-preview" aria-hidden="true" :style="{ aspectRatio: `${viewport.width} / ${viewport.height}` }">
|
||||
<div class="preview-viewport" :style="{ width: `${viewport.width}px`, height: `${viewport.height}px`, transform: `scale(${160 / viewport.width})` }">
|
||||
<SceneView
|
||||
:catalog="catalog"
|
||||
:mode="step.mode"
|
||||
:text="step.text"
|
||||
:timer-label="step.timerLabel"
|
||||
:timer-display="formatTimer(step.timerDurationSeconds * 1000)"
|
||||
:results="results"
|
||||
:has-main-source="!!source"
|
||||
:viewport="viewport"
|
||||
>
|
||||
<video
|
||||
v-if="step.mode === 4 && source"
|
||||
:key="source.url"
|
||||
class="scene-video"
|
||||
:src="source.url"
|
||||
preload="metadata"
|
||||
muted
|
||||
playsinline
|
||||
disablepictureinpicture
|
||||
tabindex="-1"
|
||||
/>
|
||||
</SceneView>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.step-preview {
|
||||
align-items: center;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: var(--brand-background);
|
||||
display: flex;
|
||||
font-size: 10px;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.step-preview img {
|
||||
.preview-viewport {
|
||||
pointer-events: none;
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.scene-video {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
inset: 0;
|
||||
object-fit: contain;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.step-preview img.step-preview-cover {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.step-preview-background {
|
||||
filter: brightness(45%);
|
||||
}
|
||||
|
||||
.step-preview span {
|
||||
-webkit-box-orient: vertical;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
padding: 4px;
|
||||
position: relative;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -124,6 +124,8 @@ export interface Report {
|
||||
export interface DisplayInfo {
|
||||
id: number
|
||||
label: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
export interface AudioOutputInfo {
|
||||
id: string
|
||||
|
||||
Reference in New Issue
Block a user