This commit is contained in:
2026-10-18 03:11:10 +07:00
parent 8610f45a29
commit 3e2608eae3
23 changed files with 1460 additions and 302 deletions
+29 -5
View File
@@ -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">
+1 -1
View File
@@ -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
View File
@@ -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
}
+1
View File
@@ -23,6 +23,7 @@ export interface MediaSource {
name: string
kind: MediaKind
url: string
ready?: boolean
}
export interface Playlist {
id: number
+4 -4
View File
@@ -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(