up
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { NAlert, NButton, NCard, NInput, NSelect, NSpace } from 'naive-ui'
|
||||
import { NAlert, NButton, NCard, NInput, NSelect } from 'naive-ui'
|
||||
import { ref, watch } from 'vue'
|
||||
import { DEFAULT_SERVER, type OnlineIntent, type OnlineState } from '../types/online'
|
||||
|
||||
@@ -9,6 +9,7 @@ const props = defineProps<{
|
||||
isBusy: boolean
|
||||
selectedKey: string | null
|
||||
selectedName: string | null
|
||||
canConnectCurrent: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{ action: [intent: OnlineIntent]; cancel: [] }>()
|
||||
const bridge = window.screenBridge
|
||||
@@ -20,6 +21,7 @@ watch(
|
||||
() => props.state.server,
|
||||
(value) => {
|
||||
server.value = value
|
||||
gameId.value = null
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
@@ -27,6 +29,7 @@ watch(
|
||||
() => props.state.authenticated,
|
||||
() => {
|
||||
password.value = ''
|
||||
if (!props.state.authenticated) gameId.value = null
|
||||
},
|
||||
)
|
||||
function run(intent: OnlineIntent) {
|
||||
@@ -87,8 +90,7 @@ const connectionLabels = {
|
||||
<template v-else>
|
||||
<p>{{ state.server }}</p>
|
||||
<n-alert v-if="!state.remembered" type="warning"
|
||||
>Хранилище секретов ОС недоступно. Вход действует до закрытия приложения; материалы
|
||||
сохраняются.</n-alert
|
||||
>Вход не сохранён: после перезапуска потребуется войти снова.</n-alert
|
||||
>
|
||||
<n-button :disabled="isBusy || state.busy" @click="run({ action: 'logout' })"
|
||||
>Выйти</n-button
|
||||
@@ -113,6 +115,7 @@ const connectionLabels = {
|
||||
>
|
||||
<n-select
|
||||
v-model:value="gameId"
|
||||
clearable
|
||||
:options="
|
||||
state.games.map((g) => ({
|
||||
label: `${g.name} · ${gameStatusLabels.get(g.status) ?? 'Неизвестный статус'}`,
|
||||
@@ -120,56 +123,28 @@ const connectionLabels = {
|
||||
}))
|
||||
"
|
||||
:disabled="isBusy || state.busy"
|
||||
placeholder="Выберите игру для подготовки"
|
||||
placeholder="Выберите игру"
|
||||
:input-props="{ 'aria-label': 'Игра на сервере' }"
|
||||
/>
|
||||
<p v-if="!state.games.length">Список игр пока пуст. Обновите его при доступном сервере.</p>
|
||||
<p v-if="!state.games.length">Нет игр.</p>
|
||||
<n-button
|
||||
:disabled="(gameId === null && !canConnectCurrent) || isBusy || state.busy"
|
||||
@click="run(gameId === null ? { action: 'connect' } : { action: 'connect', gameId })"
|
||||
>Подключить</n-button
|
||||
>
|
||||
<n-button
|
||||
:disabled="gameId === null || isBusy || state.busy"
|
||||
@click="run({ action: 'connect', gameId: gameId! })"
|
||||
>Подключить выбранную игру</n-button
|
||||
>
|
||||
<n-space>
|
||||
<n-button
|
||||
:disabled="gameId === null || isBusy || state.busy"
|
||||
@click="run({ action: 'check', gameId: gameId! })"
|
||||
>Проверить изменения</n-button
|
||||
>
|
||||
<n-button
|
||||
:disabled="gameId === null || isBusy || state.busy"
|
||||
@click="run({ action: 'download', gameId: gameId! })"
|
||||
>Загрузить материалы</n-button
|
||||
>
|
||||
</n-space>
|
||||
<p>
|
||||
Новые материалы загружаются автоматически. Для принудительного обновления файлов по
|
||||
прежним адресам закройте показ и загрузите материалы повторно.
|
||||
</p>
|
||||
<p>
|
||||
Игру можно подключить без предварительной загрузки: недостающие файлы сохранятся
|
||||
автоматически.
|
||||
</p>
|
||||
<p v-if="selectedKey" data-testid="control-game">Игра для управления: {{ selectedName }}</p>
|
||||
<n-button
|
||||
v-if="selectedKey"
|
||||
:disabled="isBusy || state.busy"
|
||||
@click="run({ action: 'connect' })"
|
||||
>Подключить управление этой игрой</n-button
|
||||
@click="run({ action: 'download', gameId: gameId! })"
|
||||
>Загрузить материалы</n-button
|
||||
>
|
||||
</template>
|
||||
<n-button
|
||||
v-if="state.screenId"
|
||||
:disabled="isBusy || state.busy"
|
||||
@click="run({ action: 'results' })"
|
||||
>Обновить снимок результатов</n-button
|
||||
>
|
||||
<p v-if="selectedKey" data-testid="control-game">Текущая игра: {{ selectedName }}</p>
|
||||
<p v-if="state.preparation.active" role="status" data-testid="materials-progress">
|
||||
Подготовка материалов: {{ state.preparation.completed }} /
|
||||
{{ state.preparation.total }} файлов; текущая загрузка
|
||||
{{ (state.preparation.downloadedBytes / 1048576).toFixed(1) }} МиБ.
|
||||
Материалы: {{ state.preparation.completed }}/{{ state.preparation.total }} ·
|
||||
{{ (state.preparation.downloadedBytes / 1048576).toFixed(1) }} МиБ
|
||||
</p>
|
||||
<n-alert v-if="state.preparation.error" type="warning" class="connection-error">
|
||||
{{ state.preparation.error }}. Прежний показ сохраняется до готовности замены.
|
||||
{{ state.preparation.error }}
|
||||
<n-button
|
||||
:disabled="isBusy || state.busy || state.preparation.active"
|
||||
@click="run({ action: 'retry-materials' })"
|
||||
@@ -177,12 +152,9 @@ const connectionLabels = {
|
||||
>
|
||||
</n-alert>
|
||||
<template v-if="state.busy">
|
||||
<p role="status">
|
||||
{{
|
||||
state.progress
|
||||
? `Файлов: ${state.progress.completedFiles}/${state.progress.totalFiles}, загружено ${(state.progress.downloadedBytes / 1048576).toFixed(1)} МиБ`
|
||||
: 'Выполняется запрос…'
|
||||
}}
|
||||
<p v-if="state.progress" role="status">
|
||||
Файлов: {{ state.progress.completedFiles }}/{{ state.progress.totalFiles }} ·
|
||||
{{ (state.progress.downloadedBytes / 1048576).toFixed(1) }} МиБ
|
||||
</p>
|
||||
<n-button v-if="state.canCancel" @click="cancel">Отменить загрузку</n-button>
|
||||
</template>
|
||||
|
||||
@@ -28,6 +28,7 @@ import MusicPlanPanel from './MusicPlanPanel.vue'
|
||||
import { useOnlineConnection } from '../composables/useOnlineConnection'
|
||||
import ScreenPlanPanel from './ScreenPlanPanel.vue'
|
||||
import { useScreenTimer } from '../composables/useScreenTimer'
|
||||
import { playbackControl } from '../utils/playbackControl'
|
||||
|
||||
const {
|
||||
state: onlineState,
|
||||
@@ -63,12 +64,6 @@ const previewURL = computed(() =>
|
||||
? preview.value?.dataURL
|
||||
: null,
|
||||
)
|
||||
const sceneLabels = {
|
||||
idle: 'Окно показа закрыто',
|
||||
pending: 'Подготовка показа',
|
||||
ready: 'Показ готов',
|
||||
error: 'Не удалось подготовить показ',
|
||||
}
|
||||
const timerLabels = {
|
||||
stopped: 'Не запущен',
|
||||
running: 'Идёт',
|
||||
@@ -105,15 +100,18 @@ async function refreshPreview() {
|
||||
}
|
||||
|
||||
const channel = 'music' as const
|
||||
const musicControl = computed(() =>
|
||||
playbackControl(state.value?.channels.music ?? { desired: 'stopped', status: 'idle' }),
|
||||
)
|
||||
const playlist = computed(() =>
|
||||
state.value?.catalog.playlists.find((p) => p.id === state.value?.channels.music.playlistId),
|
||||
)
|
||||
const statusLabels: Record<Playback, string> = {
|
||||
idle: 'Ресурс не выбран',
|
||||
pending: 'Подготовка или выполнение команды',
|
||||
playing: 'Воспроизведение подтверждено',
|
||||
idle: 'Файл не выбран',
|
||||
pending: 'Загрузка…',
|
||||
playing: 'Играет',
|
||||
paused: 'Пауза',
|
||||
stopped: 'Готово, остановлено',
|
||||
stopped: 'Остановлено',
|
||||
ended: 'Завершено',
|
||||
error: 'Ошибка',
|
||||
}
|
||||
@@ -212,18 +210,6 @@ async function refreshHost() {
|
||||
if (display.value === null) display.value = host.value.preferredDisplayId
|
||||
if (!host.value.displays.some((d) => d.id === display.value)) display.value = null
|
||||
}
|
||||
function metrics(channel: Channel) {
|
||||
const values = (state.value?.samples ?? [])
|
||||
.filter(
|
||||
(sample) =>
|
||||
sample.channel === channel && sample.media === state.value?.channels[channel].source?.kind,
|
||||
)
|
||||
.map((sample) => sample.milliseconds)
|
||||
.sort((a, b) => a - b)
|
||||
return values.length
|
||||
? `n=${values.length}, p95 ${values[Math.ceil(values.length * 0.95) - 1]!.toFixed(1)} мс`
|
||||
: 'Замеров пока нет'
|
||||
}
|
||||
onMounted(async () => {
|
||||
await run(async () => {
|
||||
if (!bridge) throw new Error('Откройте пульт в приложении Electron')
|
||||
@@ -254,7 +240,7 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<main class="control">
|
||||
<h1>Вечерний детектив · Локальный проигрыватель</h1>
|
||||
<h1>Вечерний детектив</h1>
|
||||
<n-alert v-if="!bridge" type="warning">Откройте пульт в приложении Electron.</n-alert>
|
||||
<connection-panel
|
||||
:state="onlineState"
|
||||
@@ -264,22 +250,13 @@ onUnmounted(() => {
|
||||
@cancel="cancelOnline"
|
||||
:selected-key="state?.catalog.key ?? null"
|
||||
:selected-name="state?.catalog.name ?? null"
|
||||
:can-connect-current="
|
||||
!!state?.catalog.key && planStatus.key === state.catalog.key && planStatus.editable
|
||||
"
|
||||
/>
|
||||
<n-card v-if="bridge" title="Экран">
|
||||
<n-space vertical>
|
||||
<n-alert v-if="host.notice" type="warning">{{ host.notice }}</n-alert>
|
||||
<p role="status">
|
||||
{{
|
||||
host.sleepBlocked
|
||||
? 'Автоматический сон заблокирован на время показа'
|
||||
: 'Блокировка сна выключена'
|
||||
}}
|
||||
</p>
|
||||
<p>
|
||||
Перед игрой выберите аудиовыход в настройках звука ОС, проверьте громкость и выполните
|
||||
пробный пуск. После подключения HDMI проверьте выход повторно: система может переключить
|
||||
звук на проектор.
|
||||
</p>
|
||||
<n-select
|
||||
v-model:value="display"
|
||||
:options="host.displays.map((d) => ({ label: d.label, value: d.id }))"
|
||||
@@ -295,12 +272,7 @@ onUnmounted(() => {
|
||||
<n-button v-if="state?.outputOpen" @click="run(() => bridge!.closeOutput())"
|
||||
>Закрыть показ</n-button
|
||||
>
|
||||
<ScreenConnectionPanel
|
||||
:state="onlineState"
|
||||
:is-busy="isOnlineBusy"
|
||||
:output-open="state?.outputOpen ?? false"
|
||||
@action="runOnline"
|
||||
/>
|
||||
<ScreenConnectionPanel :state="onlineState" :is-busy="isOnlineBusy" @action="runOnline" />
|
||||
<template v-if="state">
|
||||
<label class="mute"
|
||||
>Выключить весь звук
|
||||
@@ -325,14 +297,10 @@ onUnmounted(() => {
|
||||
: 'Предпросмотр недоступен'
|
||||
}}
|
||||
</div>
|
||||
<p>Кадр реального окна показа, без второго звука.</p>
|
||||
</template>
|
||||
</n-space>
|
||||
</n-card>
|
||||
<template v-if="state">
|
||||
<n-alert v-if="!state.outputOpen" type="info"
|
||||
>Откройте показ, чтобы подготовить медиа и включить воспроизведение.</n-alert
|
||||
>
|
||||
<section class="presentation-grid">
|
||||
<n-card title="План экранов">
|
||||
<n-space vertical>
|
||||
@@ -343,6 +311,9 @@ onUnmounted(() => {
|
||||
:is-editing="isPlanEditing"
|
||||
:is-busy="isBusy || isPlanSaving"
|
||||
:can-play-main="state.outputOpen && !!state.channels.main.source"
|
||||
:main-desired="state.channels.main.desired"
|
||||
:main-status="state.channels.main.status"
|
||||
:timer-status="state.timer.status"
|
||||
:can-edit="planStatus.key === state.catalog.key && planStatus.editable"
|
||||
:notice="planStatus.key === state.catalog.key ? planStatus.notice : ''"
|
||||
@edit="editPlan"
|
||||
@@ -391,23 +362,19 @@ onUnmounted(() => {
|
||||
<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">
|
||||
<p v-if="state.scene.status === 'pending'" role="status">Подготовка показа…</p>
|
||||
<p
|
||||
v-if="
|
||||
state.timer.durationMs > 0 &&
|
||||
(state.scene.mode === 7 || state.timer.status === 'running')
|
||||
"
|
||||
role="status"
|
||||
class="timer-status"
|
||||
>
|
||||
{{ state.timer.label }} · {{ timerDisplay }} · {{ timerLabels[state.timer.status] }}
|
||||
</p>
|
||||
<p v-if="state.scene.mode !== 7 && state.timer.status === 'running'">
|
||||
Таймер продолжает отсчёт в фоне.
|
||||
</p>
|
||||
<n-alert v-if="state.scene.error" type="error">{{ state.scene.error }}</n-alert>
|
||||
<n-button
|
||||
v-if="state.scene.status === 'error'"
|
||||
@@ -464,18 +431,8 @@ onUnmounted(() => {
|
||||
<n-space>
|
||||
<n-button
|
||||
:disabled="!state.outputOpen || !state.channels[channel].source || isBusy"
|
||||
@click="send({ action: 'play', channel })"
|
||||
>Пуск с начала</n-button
|
||||
>
|
||||
<n-button
|
||||
:disabled="!state.outputOpen || !state.channels[channel].source || isBusy"
|
||||
@click="send({ action: 'pause', channel })"
|
||||
>Пауза</n-button
|
||||
>
|
||||
<n-button
|
||||
:disabled="!state.outputOpen || !state.channels[channel].source || isBusy"
|
||||
@click="send({ action: 'resume', channel })"
|
||||
>Продолжить</n-button
|
||||
@click="send({ action: musicControl.action, channel })"
|
||||
>{{ musicControl.label }}</n-button
|
||||
>
|
||||
<n-button
|
||||
:disabled="!state.outputOpen || !state.channels[channel].source || isBusy"
|
||||
@@ -485,7 +442,7 @@ onUnmounted(() => {
|
||||
<n-button
|
||||
:disabled="!state.outputOpen || !state.channels[channel].source || isBusy"
|
||||
@click="send({ action: 'replay', channel })"
|
||||
>Повторить с начала</n-button
|
||||
>Сначала</n-button
|
||||
>
|
||||
<n-button
|
||||
:disabled="!state.outputOpen || !state.channels.music.source || isBusy"
|
||||
@@ -521,14 +478,6 @@ onUnmounted(() => {
|
||||
<n-alert v-if="state.channels[channel].error" type="error">{{
|
||||
state.channels[channel].error
|
||||
}}</n-alert>
|
||||
<details>
|
||||
<summary>Программные замеры запуска</summary>
|
||||
<p>{{ metrics(channel) }}</p>
|
||||
<p>
|
||||
Приём команды → подтверждение проигрывателя. Физический звук и первый кадр проектора
|
||||
здесь не измеряются.
|
||||
</p>
|
||||
</details>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
@@ -94,7 +94,6 @@ function remove(trackId?: number) {
|
||||
<n-space vertical data-testid="music-plan-editor">
|
||||
<p v-if="!canEdit">Для редактирования библиотеки подключитесь к серверу.</p>
|
||||
<template v-else>
|
||||
<n-button :disabled="isDisabled" @click="refresh">Обновить плейлисты</n-button>
|
||||
<label for="new-playlist-name">Новый плейлист</label>
|
||||
<n-input
|
||||
v-model:value="newName"
|
||||
@@ -138,7 +137,6 @@ function remove(trackId?: number) {
|
||||
>Использовать в игре</n-button
|
||||
>
|
||||
</n-space>
|
||||
<p>Выбор в редакторе не меняет музыку игры. MP3 — до 64 МиБ.</p>
|
||||
<p v-if="!order.length">Треков пока нет.</p>
|
||||
<ol class="track-list">
|
||||
<li v-for="(id, index) in order" :key="id">
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NSpace } from 'naive-ui'
|
||||
import type { OnlineIntent, OnlineState } from '../types/online'
|
||||
defineProps<{ state: OnlineState; isBusy: boolean; outputOpen: boolean }>()
|
||||
defineProps<{ state: OnlineState; isBusy: boolean }>()
|
||||
const emit = defineEmits<{ action: [intent: OnlineIntent] }>()
|
||||
function run(intent: OnlineIntent) {
|
||||
emit('action', intent)
|
||||
}
|
||||
const connectionLabels = {
|
||||
disabled: 'Управление не подключено',
|
||||
waiting: 'Ожидание ответа сервера',
|
||||
connected: 'Связь с сервером есть',
|
||||
offline: 'Нет связи; локальный показ продолжается',
|
||||
expired: 'Сессия экрана недействительна; подключите управление заново',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<p v-if="state.screenId" role="status">
|
||||
{{ connectionLabels[state.connection]
|
||||
}}{{ state.screenId ? ` · Экран №${state.screenId}` : '' }}
|
||||
</p>
|
||||
<p v-if="state.screenId && !outputOpen">
|
||||
Связь и загрузка материалов продолжаются; показ откройте вручную.
|
||||
</p>
|
||||
<p v-if="state.screenId">Экран №{{ state.screenId }}</p>
|
||||
<template v-if="state.screenId">
|
||||
<p role="status" data-testid="sound-assignment">
|
||||
{{
|
||||
@@ -32,13 +19,13 @@ const connectionLabels = {
|
||||
: 'Экран не назначен для звука'
|
||||
}}
|
||||
<template v-if="state.soundEnabled !== null && state.connection !== 'connected'">
|
||||
· Последнее подтверждённое состояние; сейчас назначение не проверяется.
|
||||
· Последнее состояние
|
||||
</template>
|
||||
</p>
|
||||
<p>Переключатель «Выключить весь звук» управляет локальным звуком отдельно.</p>
|
||||
</template>
|
||||
<n-space v-if="state.screenId">
|
||||
<n-button
|
||||
v-if="state.soundEnabled !== true"
|
||||
:disabled="isBusy || state.busy || !state.authenticated"
|
||||
@click="run({ action: 'sound' })"
|
||||
>Назначить звук этому экрану</n-button
|
||||
@@ -47,8 +34,4 @@ const connectionLabels = {
|
||||
>Отключить управление</n-button
|
||||
>
|
||||
</n-space>
|
||||
<p v-if="state.screenId">
|
||||
«Отключить управление» прекращает управление приложением; привязка экрана на сервере
|
||||
сохраняется.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { NAlert, NButton, NInput, NInputNumber, NSelect, NSpace } from 'naive-ui'
|
||||
import type { Intent, ScreenMode, ScreenStep } from '../types/screen'
|
||||
import { computed } from 'vue'
|
||||
import { playbackControl } from '../utils/playbackControl'
|
||||
import type { ChannelState, Intent, ScreenMode, ScreenStep, TimerState } from '../types/screen'
|
||||
import { modeLabels } from '../utils/presentation'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
steps: ScreenStep[]
|
||||
currentIndex: number | null
|
||||
draft: ScreenStep[]
|
||||
isEditing: boolean
|
||||
isBusy: boolean
|
||||
canPlayMain: boolean
|
||||
mainDesired: ChannelState['desired']
|
||||
mainStatus: ChannelState['status']
|
||||
timerStatus: TimerState['status']
|
||||
canEdit: boolean
|
||||
notice: string
|
||||
}>()
|
||||
@@ -24,6 +29,9 @@ const emit = defineEmits<{
|
||||
updateStep: [index: number, patch: Partial<ScreenStep>]
|
||||
command: [intent: Intent]
|
||||
}>()
|
||||
const mainControl = computed(() =>
|
||||
playbackControl({ desired: props.mainDesired, status: props.mainStatus }),
|
||||
)
|
||||
const modeOptions = Object.entries(modeLabels).map(([mode, label]) => ({
|
||||
value: Number(mode) as ScreenMode,
|
||||
label,
|
||||
@@ -49,7 +57,7 @@ const modeOptions = Object.entries(modeLabels).map(([mode, label]) => ({
|
||||
</template>
|
||||
</n-space>
|
||||
<n-alert v-if="notice" type="warning" role="status" class="plan-notice">{{ notice }}</n-alert>
|
||||
<p v-if="!canEdit">Для редактирования войдите на сервер выбранной подготовленной игры.</p>
|
||||
<p v-if="!canEdit">Для редактирования войдите на сервер игры.</p>
|
||||
<template v-if="isEditing">
|
||||
<n-space v-for="(step, index) in draft" :key="index" vertical class="plan-step">
|
||||
<span>Шаг {{ index + 1 }}</span>
|
||||
@@ -111,7 +119,7 @@ const modeOptions = Object.entries(modeLabels).map(([mode, label]) => ({
|
||||
</n-space>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p v-if="!steps.length">План экранов ещё не создан. Создайте план, чтобы настроить экраны.</p>
|
||||
<p v-if="!steps.length">План экранов пуст.</p>
|
||||
<n-space
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
@@ -136,15 +144,27 @@ const modeOptions = Object.entries(modeLabels).map(([mode, label]) => ({
|
||||
>{{ currentIndex === index ? 'Показан' : 'Показать' }}</n-button
|
||||
>
|
||||
<template v-if="currentIndex === index && step.mode === 7">
|
||||
<n-button :disabled="isBusy" @click="emit('command', { action: 'timer-start' })"
|
||||
>Старт</n-button
|
||||
>
|
||||
<n-button :disabled="isBusy" @click="emit('command', { action: 'timer-pause' })"
|
||||
>Пауза</n-button
|
||||
>
|
||||
<n-button :disabled="isBusy" @click="emit('command', { action: 'timer-resume' })"
|
||||
>Продолжить</n-button
|
||||
<n-button
|
||||
:disabled="isBusy"
|
||||
@click="
|
||||
emit('command', {
|
||||
action:
|
||||
timerStatus === 'running'
|
||||
? 'timer-pause'
|
||||
: timerStatus === 'paused'
|
||||
? 'timer-resume'
|
||||
: 'timer-start',
|
||||
})
|
||||
"
|
||||
>
|
||||
{{
|
||||
timerStatus === 'running'
|
||||
? 'Пауза'
|
||||
: timerStatus === 'paused'
|
||||
? 'Продолжить'
|
||||
: 'Старт'
|
||||
}}
|
||||
</n-button>
|
||||
<n-button :disabled="isBusy" @click="emit('command', { action: 'timer-reset' })"
|
||||
>Сбросить</n-button
|
||||
>
|
||||
@@ -152,18 +172,8 @@ const modeOptions = Object.entries(modeLabels).map(([mode, label]) => ({
|
||||
<template v-if="currentIndex === index && (step.mode === 2 || step.mode === 4)">
|
||||
<n-button
|
||||
:disabled="isBusy || !canPlayMain"
|
||||
@click="emit('command', { action: 'play', channel: 'main' })"
|
||||
>Запуск</n-button
|
||||
>
|
||||
<n-button
|
||||
:disabled="isBusy || !canPlayMain"
|
||||
@click="emit('command', { action: 'pause', channel: 'main' })"
|
||||
>Пауза</n-button
|
||||
>
|
||||
<n-button
|
||||
:disabled="isBusy || !canPlayMain"
|
||||
@click="emit('command', { action: 'resume', channel: 'main' })"
|
||||
>Продолжить</n-button
|
||||
@click="emit('command', { action: mainControl.action, channel: 'main' })"
|
||||
>{{ mainControl.label }}</n-button
|
||||
>
|
||||
<n-button
|
||||
:disabled="isBusy || !canPlayMain"
|
||||
@@ -173,7 +183,7 @@ const modeOptions = Object.entries(modeLabels).map(([mode, label]) => ({
|
||||
<n-button
|
||||
:disabled="isBusy || !canPlayMain"
|
||||
@click="emit('command', { action: 'replay', channel: 'main' })"
|
||||
>Повтор</n-button
|
||||
>Сначала</n-button
|
||||
>
|
||||
</template>
|
||||
</n-space>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { ChannelState } from '../types/screen.js'
|
||||
|
||||
export function playbackControl(channel: Pick<ChannelState, 'desired' | 'status'>): {
|
||||
action: 'play' | 'pause' | 'resume'
|
||||
label: string
|
||||
} {
|
||||
if (channel.status === 'error' || channel.status === 'ended')
|
||||
return { action: 'play', label: 'Запуск' }
|
||||
if (channel.desired === 'playing') return { action: 'pause', label: 'Пауза' }
|
||||
if (channel.desired === 'paused') return { action: 'resume', label: 'Продолжить' }
|
||||
return { action: 'play', label: 'Запуск' }
|
||||
}
|
||||
Reference in New Issue
Block a user