up
This commit is contained in:
+111
-3
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user