This commit is contained in:
2026-10-18 14:15:43 +07:00
parent 0cd736f682
commit 913b237cb8
14 changed files with 1042 additions and 100 deletions
+111 -3
View File
@@ -4,7 +4,12 @@ import { apiScreenMode, apiScreenCommand, apiPlan } from './api-values.js'
import { ApiError, NetworkError, SessionChangedError, networkFailure } from './request-error.js'
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { DEFAULT_SERVER, type OnlineIntent, type OnlineState } from '../src/types/online.js'
import {
DEFAULT_SERVER,
type GameMaterial,
type OnlineIntent,
type OnlineState,
} from '../src/types/online.js'
import type {
Intent,
PlanStatus,
@@ -67,9 +72,12 @@ function token(value: unknown): string {
export class Online {
readonly state: OnlineState = {
server: DEFAULT_SERVER,
currentGameId: null,
materials: [],
authenticated: false,
remembered: false,
busy: false,
busyMessage: '',
canCancel: false,
games: [],
playlists: [],
@@ -141,6 +149,7 @@ export class Online {
const local = await this.packages.remember(description)
if (generation !== this.generation) return
this.controller.selectPackage(local, true)
await this.checkMaterials(local.package)
this.state.packagesRevision++
}
this.materialSignature = signature
@@ -831,7 +840,55 @@ export class Online {
return this.state.busy
}
snapshot(): OnlineState {
return structuredClone(this.state)
const pkg = this.selectedGame()?.package
const names = new Map<string, string[]>()
const usages = new Map<string, GameMaterial['usages']>()
const add = (
id: string | null,
name: string,
order: number,
groupId = 'game',
groupName = 'Игра',
) => {
if (!id) return
names.set(id, [
...(names.get(id) ?? []),
groupId === 'game' ? name : `${groupName} · ${name}`,
])
usages.set(id, [...(usages.get(id) ?? []), { groupId, groupName, name, order }])
}
if (pkg) {
add(pkg.media.cover, 'Обложка', 0)
add(pkg.media.background, 'Фон', 1)
add(pkg.media.intro, 'Вступление', 2)
add(pkg.media.conclusion, 'Заключение', 3)
for (const playlist of pkg.playlists)
for (const track of playlist.tracks)
add(track.assetId, track.name, track.position, `playlist-${playlist.id}`, playlist.name)
}
const key = this.controller.state.catalog.key
return structuredClone({
...this.state,
currentGameId: pkg?.server === this.state.server ? pkg.game.id : null,
materials:
pkg && key
? pkg.assets.map((asset) => ({
id: asset.id,
name: names.get(asset.id)?.join(', ') ?? 'Материал игры',
usages: usages.get(asset.id) ?? [
{ groupId: 'game', groupName: 'Игра', name: 'Материал игры', order: 4 },
],
...this.packages.assetStatus(key, asset.id),
...(this.downloadKey === key && this.state.progress?.assetId === asset.id
? { status: 'loading' as const }
: {}),
}))
: [],
})
}
private async checkMaterials(pkg: ScreenPackage) {
const key = packageKey(pkg.server, pkg.game.id)
for (const asset of pkg.assets) await this.packages.checkAsset(key, asset.id)
}
private async atomic(name: string, data: string | Buffer) {
await mkdir(this.directory, { recursive: true, mode: 0o700 })
@@ -929,6 +986,7 @@ export class Online {
}
this.detach()
this.controller.selectPackage(pkg)
if (pkg) await this.checkMaterials(pkg.package)
this.plan = { steps: structuredClone(this.controller.state.catalog.steps), currentStep: 0 }
this.planNotice = ''
this.lastPlan = -Infinity
@@ -1098,6 +1156,33 @@ export class Online {
if (this.busy) throw new Error('Подождите завершения текущего действия')
const isBackgroundRefresh = data.action === 'playlists'
this.state.busy = true
const messages: Record<OnlineIntent['action'], string> = {
playlists: 'Загрузка списка плейлистов…',
'download-material': 'Загрузка файла игры…',
'playlist-create': 'Создание плейлиста…',
'playlist-rename': 'Сохранение названия плейлиста…',
'playlist-delete': 'Удаление плейлиста…',
'playlist-upload': 'Загрузка трека на сервер…',
'playlist-use': 'Подключение плейлиста к игре…',
'playlist-track-delete': 'Удаление трека…',
'playlist-reorder': 'Сохранение порядка треков…',
'plan-save': 'Сохранение плана экранов…',
'plan-refresh': 'Загрузка плана экранов…',
login: 'Вход на сервер…',
logout: 'Выход из аккаунта…',
games: 'Загрузка списка игр…',
connect: 'Подключение игры…',
disconnect: 'Отключение игры…',
download: 'Загрузка материалов игры…',
check: 'Проверка материалов игры…',
'retry-materials': 'Подготовка материалов игры…',
remove: 'Удаление локальных материалов…',
sound: 'Изменение назначения звука…',
results: 'Загрузка результатов игры…',
cancel: 'Отмена загрузки…',
}
this.state.busyMessage =
messages[data.action as OnlineIntent['action']] ?? 'Выполнение запроса…'
if (!isBackgroundRefresh) {
this.state.error = ''
this.state.notice = ''
@@ -1110,6 +1195,7 @@ export class Online {
throw new Error(message)
} finally {
this.state.busy = false
this.state.busyMessage = ''
this.state.progress = null
}
return this.snapshot()
@@ -1117,6 +1203,17 @@ export class Online {
private async perform(data: Record<string, unknown>) {
const action = data.action as OnlineIntent['action']
switch (action) {
case 'download-material': {
const pkg = this.selectedGame()
const assetId = text(data.assetId)
if (!pkg || data.key !== pkg.key || !pkg.package.assets.some((a) => a.id === assetId))
throw new Error('Материал не принадлежит текущей игре')
const generation = this.generation
await this.packages.ensureAsset(pkg.package, assetId, this.materialAbort.signal)
if (generation === this.generation && this.controller.state.catalog.key === pkg.key)
this.controller.markAssetReady(assetId)
return
}
case 'playlists':
return this.loadPlaylists()
case 'playlist-create':
@@ -1267,11 +1364,22 @@ export class Online {
let pkg = this.selectedGame()
const gameId = data.gameId == null ? pkg?.package.game.id : integer(data.gameId, 1)
if (!gameId) throw new Error('Выберите игру на сервере')
this.detach()
if (this.screen) {
this.state.busyMessage = 'Отключение предыдущего подключения…'
try {
await this.client().UnbindScreen({ screenID: this.screen.id })
} catch (error) {
if (!(error instanceof ApiError && error.status === 404)) throw error
}
this.detach()
await this.persistSecrets()
} else this.detach()
this.state.busyMessage = 'Подключение игры…'
if (!pkg || pkg.package.game.id !== gameId || pkg.package.server !== this.state.server) {
pkg = await this.packages.remember(await this.description(gameId))
this.controller.selectPackage(pkg)
this.plan = { steps: this.controller.state.catalog.steps, currentStep: 0 }
await this.checkMaterials(pkg.package)
this.state.packagesRevision++
await this.saveSelection()
}