This commit is contained in:
2026-10-18 04:03:44 +07:00
parent 9c5419b9bf
commit e85d899fd7
7 changed files with 160 additions and 59 deletions
+71 -44
View File
@@ -94,7 +94,7 @@ async function refreshPreview() {
}
}
const channels = ['main', 'music'] as const
const channel = 'music' as const
const playlist = computed(() =>
state.value?.catalog.playlists.find((p) => p.id === state.value?.channels.music.playlistId),
)
@@ -296,6 +296,7 @@ onUnmounted(() => {
:draft="planDraft"
:is-editing="isPlanEditing"
:is-busy="isBusy || isPlanSaving"
:can-play-main="state.outputOpen && !!state.channels.main.source"
:can-edit="planStatus.key === state.catalog.key && planStatus.editable"
:notice="planStatus.key === state.catalog.key ? planStatus.notice : ''"
@edit="editPlan"
@@ -311,7 +312,49 @@ onUnmounted(() => {
@clear="planDraft = []"
@update-step="(index, patch) => Object.assign(planDraft[index]!, patch)"
@command="send"
/>
>
<template #main-controls>
<label for="position-main">Позиция, секунды</label>
<n-input-number
v-model:value="positions.main"
:input-props="{ id: 'position-main' }"
:min="0"
:max="state.channels.main.duration ?? undefined"
/>
<n-button
:disabled="
!state.outputOpen || !state.channels.main.source || isBusy || isPlanSaving
"
@click="send({ action: 'seek', channel: 'main', position: positions.main ?? 0 })"
>Перемотать</n-button
>
<label for="volume-main">Громкость, %</label>
<n-input-number
:value="state.channels.main.volume"
:input-props="{ id: 'volume-main' }"
:min="0"
:max="100"
:disabled="isBusy || isPlanSaving"
@update:value="send({ action: 'volume', channel: 'main', volume: $event ?? 0 })"
/>
<p role="status">
{{ statusLabels[state.channels.main.status] }} ·
{{ state.channels.main.position.toFixed(1) }} /
{{ state.channels.main.duration?.toFixed(1) ?? '—' }} с
</p>
<n-alert v-if="state.channels.main.error" type="error">{{
state.channels.main.error
}}</n-alert>
<details>
<summary>Программные замеры запуска</summary>
<p>{{ metrics('main') }}</p>
<p>
Приём команды → подтверждение проигрывателя. Физический звук и первый кадр
проектора здесь не измеряются.
</p>
</details>
</template>
</ScreenPlanPanel>
<p role="status">{{ sceneLabels[state.scene.status] }}</p>
<p v-if="state.timer.durationMs > 0" role="status" class="timer-status">
{{ state.timer.label }} · {{ timerDisplay }} · {{ timerLabels[state.timer.status] }}
@@ -361,51 +404,36 @@ onUnmounted(() => {
aria-label="Выключить весь звук"
@update:value="send({ action: 'mute', enabled: $event })"
/></label>
<n-card
v-for="channel in channels"
:key="channel"
:title="channel === 'main' ? 'Вступление и видео' : 'Музыка'"
>
<n-card title="Музыка">
<n-space vertical>
<n-select
v-if="channel === 'main'"
:value="state.channels.main.source?.id ?? null"
:options="state.catalog.media.map((s) => ({ label: s.name, value: s.id }))"
placeholder="Выберите медиа"
:value="state.channels.music.playlistId"
:options="state.catalog.playlists.map((p) => ({ label: p.name, value: p.id }))"
placeholder="Выберите плейлист"
:disabled="isBusy"
:input-props="{ 'aria-label': 'Медиа показа' }"
@update:value="send({ action: 'select', channel: 'main', sourceId: $event })"
:input-props="{ 'aria-label': 'Плейлист' }"
@update:value="send({ action: 'playlist', channel: 'music', playlistId: $event })"
/>
<template v-else>
<n-select
:value="state.channels.music.playlistId"
:options="state.catalog.playlists.map((p) => ({ label: p.name, value: p.id }))"
placeholder="Выберите плейлист"
:disabled="isBusy"
:input-props="{ 'aria-label': 'Плейлист' }"
@update:value="send({ action: 'playlist', channel: 'music', playlistId: $event })"
/>
<n-select
:value="state.channels.music.source ? state.channels.music.trackIndex : null"
:options="
(playlist?.tracks ?? []).map((t, index) => ({
label: t.name || `Трек ${index + 1}`,
value: index,
}))
"
placeholder="Выберите трек"
:disabled="isBusy"
:input-props="{ 'aria-label': 'Трек' }"
@update:value="send({ action: 'track', channel: 'music', trackIndex: $event })"
/>
<label
>Повторять плейлист
<n-switch
:value="state.channels.music.loop"
aria-label="Повторять плейлист"
@update:value="send({ action: 'loop', channel: 'music', enabled: $event })"
/></label>
</template>
<n-select
:value="state.channels.music.source ? state.channels.music.trackIndex : null"
:options="
(playlist?.tracks ?? []).map((t, index) => ({
label: t.name || `Трек ${index + 1}`,
value: index,
}))
"
placeholder="Выберите трек"
:disabled="isBusy"
:input-props="{ 'aria-label': 'Трек' }"
@update:value="send({ action: 'track', channel: 'music', trackIndex: $event })"
/>
<label
>Повторять плейлист
<n-switch
:value="state.channels.music.loop"
aria-label="Повторять плейлист"
@update:value="send({ action: 'loop', channel: 'music', enabled: $event })"
/></label>
<n-space>
<n-button
:disabled="!state.outputOpen || !state.channels[channel].source || isBusy"
@@ -433,7 +461,6 @@ onUnmounted(() => {
>Повторить с начала</n-button
>
<n-button
v-if="channel === 'music'"
:disabled="!state.outputOpen || !state.channels.music.source || isBusy"
@click="send({ action: 'next', channel: 'music' })"
>Следующий трек</n-button
+10 -5
View File
@@ -9,6 +9,7 @@ defineProps<{
draft: ScreenStep[]
isEditing: boolean
isBusy: boolean
canPlayMain: boolean
canEdit: boolean
notice: string
}>()
@@ -150,32 +151,36 @@ const modeOptions = Object.entries(modeLabels).map(([mode, label]) => ({
</template>
<template v-if="currentIndex === index && (step.mode === 2 || step.mode === 4)">
<n-button
:disabled="isBusy"
:disabled="isBusy || !canPlayMain"
@click="emit('command', { action: 'play', channel: 'main' })"
>Запуск</n-button
>
<n-button
:disabled="isBusy"
:disabled="isBusy || !canPlayMain"
@click="emit('command', { action: 'pause', channel: 'main' })"
>Пауза</n-button
>
<n-button
:disabled="isBusy"
:disabled="isBusy || !canPlayMain"
@click="emit('command', { action: 'resume', channel: 'main' })"
>Продолжить</n-button
>
<n-button
:disabled="isBusy"
:disabled="isBusy || !canPlayMain"
@click="emit('command', { action: 'stop', channel: 'main' })"
>Стоп</n-button
>
<n-button
:disabled="isBusy"
:disabled="isBusy || !canPlayMain"
@click="emit('command', { action: 'replay', channel: 'main' })"
>Повтор</n-button
>
</template>
</n-space>
<slot
v-if="currentIndex === index && (step.mode === 2 || step.mode === 4)"
name="main-controls"
/>
</n-space>
<n-space>
<n-button