up
This commit is contained in:
@@ -21,6 +21,7 @@ const state = ref<OnlineState>({
|
||||
error: '',
|
||||
notice: '',
|
||||
progress: null,
|
||||
preparation: { active: false, completed: 0, total: 0, downloadedBytes: 0, error: '' },
|
||||
packagesRevision: 0,
|
||||
})
|
||||
const server = ref('')
|
||||
@@ -36,7 +37,7 @@ const gameStatusLabels = new Map([
|
||||
])
|
||||
const connectionLabels = {
|
||||
disabled: 'Управление не подключено',
|
||||
waiting: 'Ожидание открытия показа',
|
||||
waiting: 'Ожидание ответа сервера',
|
||||
connected: 'Связь с сервером есть',
|
||||
offline: 'Нет связи; локальный показ продолжается',
|
||||
expired: 'Сессия экрана недействительна; подключите управление заново',
|
||||
@@ -152,6 +153,11 @@ onUnmounted(() => clearInterval(timer))
|
||||
:input-props="{ 'aria-label': 'Игра на сервере' }"
|
||||
/>
|
||||
<p v-if="!state.games.length">Список игр пока пуст. Обновите его при доступном сервере.</p>
|
||||
<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"
|
||||
@@ -165,10 +171,13 @@ onUnmounted(() => clearInterval(timer))
|
||||
>
|
||||
</n-space>
|
||||
<p>
|
||||
Выбранную игру можно обновить после закрытия показа. Для обновления файлов по прежним
|
||||
адресам загрузите материалы повторно.
|
||||
Новые материалы загружаются автоматически. Для принудительного обновления файлов по
|
||||
прежним адресам закройте показ и загрузите материалы повторно.
|
||||
</p>
|
||||
<p>
|
||||
Игру можно подключить без предварительной загрузки: недостающие файлы сохранятся
|
||||
автоматически.
|
||||
</p>
|
||||
<p>Выбор игры выше используется для загрузки материалов.</p>
|
||||
<p v-if="selectedKey" data-testid="control-game">Игра для управления: {{ selectedName }}</p>
|
||||
<n-button
|
||||
v-if="selectedKey"
|
||||
@@ -181,7 +190,9 @@ onUnmounted(() => clearInterval(timer))
|
||||
{{ connectionLabels[state.connection]
|
||||
}}{{ state.screenId ? ` · Экран №${state.screenId}` : '' }}
|
||||
</p>
|
||||
<p v-if="state.screenId && !outputOpen">Опрос сервера начнётся после открытия показа.</p>
|
||||
<p v-if="state.screenId && !outputOpen">
|
||||
Связь и загрузка материалов продолжаются; показ откройте вручную.
|
||||
</p>
|
||||
<template v-if="state.screenId">
|
||||
<p role="status" data-testid="sound-assignment">
|
||||
{{
|
||||
@@ -216,6 +227,19 @@ onUnmounted(() => clearInterval(timer))
|
||||
«Отключить управление» прекращает управление приложением; привязка экрана на сервере
|
||||
сохраняется.
|
||||
</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) }} МиБ.
|
||||
</p>
|
||||
<n-alert v-if="state.preparation.error" type="warning">
|
||||
{{ state.preparation.error }}. Прежний показ сохраняется до готовности замены.
|
||||
<n-button
|
||||
:disabled="isBusy || state.busy || state.preparation.active"
|
||||
@click="run({ action: 'retry-materials' })"
|
||||
>Повторить подготовку</n-button
|
||||
>
|
||||
</n-alert>
|
||||
<n-alert v-if="state.error" type="error">{{ state.error }}</n-alert>
|
||||
<n-alert v-if="state.notice" type="info">{{ state.notice }}</n-alert>
|
||||
<template v-if="state.busy">
|
||||
|
||||
@@ -308,7 +308,7 @@ onUnmounted(() => {
|
||||
:value="state?.catalog.key ?? 'fixtures'"
|
||||
:options="packageOptions"
|
||||
:disabled="!bridge || isBusy || isPlanSaving"
|
||||
:input-props="{ 'aria-label': 'Подготовленный пакет' }"
|
||||
:input-props="{ 'aria-label': 'Игра показа' }"
|
||||
@update:value="selectPackage"
|
||||
/>
|
||||
<n-alert v-if="state && !state.catalog.key" type="info"
|
||||
|
||||
+10
-1
@@ -4,7 +4,9 @@ export type OnlineIntent =
|
||||
| { action: 'plan-save'; key: string; steps: ScreenStep[] }
|
||||
| { action: 'plan-refresh' }
|
||||
| { action: 'login'; server: string; email: string; password: string }
|
||||
| { action: 'logout' | 'games' | 'connect' | 'disconnect' | 'sound' | 'results' | 'cancel' }
|
||||
| { action: 'retry-materials' }
|
||||
| { action: 'connect'; gameId?: number }
|
||||
| { action: 'logout' | 'games' | 'disconnect' | 'sound' | 'results' | 'cancel' }
|
||||
| { action: 'download' | 'check'; gameId: number }
|
||||
| { action: 'remove'; key: string }
|
||||
export interface OnlineState {
|
||||
@@ -20,5 +22,12 @@ export interface OnlineState {
|
||||
error: string
|
||||
notice: string
|
||||
progress: { downloadedBytes: number; completedFiles: number; totalFiles: number } | null
|
||||
preparation: {
|
||||
active: boolean
|
||||
completed: number
|
||||
total: number
|
||||
downloadedBytes: number
|
||||
error: string
|
||||
}
|
||||
packagesRevision: number
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface MediaSource {
|
||||
name: string
|
||||
kind: MediaKind
|
||||
url: string
|
||||
ready?: boolean
|
||||
}
|
||||
export interface Playlist {
|
||||
id: number
|
||||
|
||||
@@ -112,7 +112,7 @@ export class MediaPlayer {
|
||||
const spare = this.elements.music.find((media) => media !== active)!
|
||||
spare.pause()
|
||||
spare.muted = true
|
||||
if (next) this.source(spare, next.source.url)
|
||||
if (next && next.source.ready !== false) this.source(spare, next.source.url)
|
||||
else this.clear(spare)
|
||||
}
|
||||
private async applyChannel(channel: Channel, snapshot: State) {
|
||||
@@ -124,7 +124,7 @@ export class MediaPlayer {
|
||||
r.operation = c.operation
|
||||
const session = snapshot.session
|
||||
const previousId = r.sourceId
|
||||
r.sourceId = c.source?.id ?? null
|
||||
r.sourceId = c.source ? `${c.source.id}:${c.source.url}` : null
|
||||
if (!c.source) {
|
||||
if (channel === 'main') {
|
||||
this.clear(this.elements.audio)
|
||||
@@ -139,7 +139,7 @@ export class MediaPlayer {
|
||||
this.clear(c.source.kind === 'audio' ? this.elements.video : this.elements.audio)
|
||||
} else {
|
||||
media =
|
||||
r.media && previousId === c.source.id
|
||||
r.media && previousId === r.sourceId
|
||||
? r.media
|
||||
: this.elements.music.find((element) => element !== r.media)!
|
||||
if (r.media && r.media !== media) r.media.pause()
|
||||
@@ -191,7 +191,7 @@ export class MediaPlayer {
|
||||
throw new Error('Не удалось определить длительность файла')
|
||||
const position = Math.min(c.position, media.duration)
|
||||
if (
|
||||
(c.seekOperation > r.seekOperation || previousId !== c.source.id) &&
|
||||
(c.seekOperation > r.seekOperation || previousId !== r.sourceId) &&
|
||||
Math.abs(media.currentTime - position) > 0.01
|
||||
) {
|
||||
await waitUntil(
|
||||
|
||||
Reference in New Issue
Block a user