This commit is contained in:
2026-10-18 23:18:25 +07:00
parent 8da494c424
commit fc326f46c8
10 changed files with 336 additions and 29 deletions
+88 -13
View File
@@ -11,8 +11,10 @@ import {
NTag,
NTable,
useMessage,
useDialog,
} from 'naive-ui'
import { computed, ref, watch } from 'vue'
import type { PackageSummary } from '../types/screen'
import ScreenConnectionPanel from './ScreenConnectionPanel.vue'
import GameControlPanel from './GameControlPanel.vue'
import {
@@ -29,10 +31,50 @@ const props = defineProps<{
selectedKey: string | null
selectedName: string | null
canConnectCurrent: boolean
outputOpen: boolean
}>()
const emit = defineEmits<{ action: [intent: OnlineIntent]; cancel: []; resetGame: [] }>()
const emit = defineEmits<{ action: [intent: OnlineIntent]; cancel: []; resetGame: []; selectLocal: [key: string] }>()
const bridge = window.screenBridge
const message = useMessage()
const dialog = useDialog()
const cannotClearGames = computed(() => props.isBusy || props.state.busy || props.outputOpen || props.state.preparation.active)
function clearGames() {
if (cannotClearGames.value) return
dialog.warning({
title: 'Удалить все скачанные игры?',
content: 'Удалить все сохранённые игры, материалы, локальные планы экранов и снимки результатов? Для повторной загрузки потребуется сеть. Данные на сервере сохранятся.',
positiveText: 'Удалить всё',
negativeText: 'Отмена',
onPositiveClick: () => {
if (!cannotClearGames.value) run({ action: 'clear-games' })
},
})
}
const localPackages = ref<PackageSummary[]>([])
const localKey = ref<string | null>(null)
const localPackagesLoading = ref(false)
const localPackagesError = ref('')
const localOptions = computed(() => localPackages.value.map((pkg) => ({
label: `${pkg.name} · скачано ${pkg.downloadedFiles} из ${pkg.totalFiles}`,
value: pkg.key,
})))
watch(
[() => props.isReady, () => props.state.packagesRevision, () => props.state.busy, () => props.selectedKey],
async () => {
if (!bridge || !props.isReady || props.state.busy) return
localPackagesLoading.value = true
localPackagesError.value = ''
try {
localPackages.value = await bridge.packages()
if (!localPackages.value.some((pkg) => pkg.key === localKey.value)) localKey.value = null
} catch (error) {
localPackagesError.value = error instanceof Error ? error.message : 'Не удалось прочитать скачанные игры'
} finally {
localPackagesLoading.value = false
}
},
{ immediate: true },
)
async function openMaterialsFolder() {
if (!bridge || !props.selectedKey) return
try {
@@ -223,8 +265,34 @@ const gameOptions = computed(() => {
<div class="connection-content">
<div v-if="selectedKey" class="current-game">
<span data-testid="control-game">Текущая игра: {{ selectedName }}</span>
<n-button :disabled="isBusy || state.busy" @click="emit('resetGame')">Сменить игру</n-button>
<n-button :disabled="isBusy || state.busy || outputOpen" @click="emit('resetGame')">Сменить игру</n-button>
</div>
<template v-if="!selectedKey">
<span>Скачанные игры</span>
<n-alert v-if="localPackagesError" type="warning">{{ localPackagesError }}</n-alert>
<div class="game-selection">
<n-select
v-model:value="localKey"
data-testid="local-game-select"
class="game-select"
clearable
:options="localOptions"
:loading="localPackagesLoading"
:disabled="isBusy || state.busy || outputOpen || localPackagesLoading"
placeholder="Выберите скачанную игру"
:input-props="{ 'aria-label': 'Скачанные игры' }"
/>
<n-button
data-testid="local-game-confirm"
:disabled="!localKey || isBusy || state.busy || outputOpen || localPackagesLoading"
@click="emit('selectLocal', localKey!)"
>Выбрать скачанную игру</n-button>
</div>
<span v-if="!localPackagesLoading && !localPackagesError && !localPackages.length">Скачанных игр пока нет</span>
</template>
<p v-if="(selectedKey && loadedMaterials < state.materials.length) || (!selectedKey && localPackages.some((pkg) => pkg.key === localKey && pkg.downloadedFiles < pkg.totalFiles))">
Без сети доступны только скачанные материалы.
</p>
<template v-if="state.authenticated">
<div v-if="!selectedKey" class="game-selection">
<n-select
@@ -256,14 +324,12 @@ const gameOptions = computed(() => {
<path d="M6.1 6.1A8 8 0 0 1 20 12M4 12a8 8 0 0 0 13.9 5.9" />
</svg>
</n-button>
<n-button
:disabled="gameId === null || isBusy || state.busy"
@click="run({ action: 'select-game', gameId: gameId! })"
>Выбрать игру</n-button
>
</div>
<n-button
v-if="!selectedKey"
class="game-confirm"
:disabled="gameId === null || isBusy || state.busy"
@click="run({ action: 'select-game', gameId: gameId! })"
>Выбрать игру</n-button
>
</template>
<p v-if="state.preparation.active" role="status" data-testid="materials-progress">
Материалы: {{ state.preparation.completed }}/{{ state.preparation.total }} ·
@@ -389,6 +455,15 @@ const gameOptions = computed(() => {
</n-collapse-item>
</n-collapse>
<p v-if="!state.authenticated">Войдите на сервер, чтобы выбрать игру.</p>
<n-button
class="clear-games"
data-testid="clear-games"
text
type="error"
size="small"
:disabled="cannotClearGames"
@click="clearGames"
>Удалить все скачанные игры</n-button>
</div>
</n-card>
<GameControlPanel v-if="bridge && isReady" :state="state" :is-busy="isBusy" :selected-key="selectedKey" :selected-name="selectedName" @action="run">
@@ -405,6 +480,10 @@ const gameOptions = computed(() => {
</template>
<style scoped>
.clear-games {
align-self: flex-start;
}
.materials-list {
display: flex;
flex-direction: column;
@@ -525,10 +604,6 @@ const gameOptions = computed(() => {
}
.connection-content > .auth-action,
.connection-content > .game-confirm {
align-self: flex-end;
}
.connection-error {
overflow-wrap: anywhere;
white-space: pre-wrap;
+10
View File
@@ -198,6 +198,14 @@ async function resetGame() {
onlineState.value = await bridge.onlineState()
})
}
async function selectLocalGame(key: string) {
if (!bridge || isBusy.value || isOnlineBusy.value || onlineState.value.busy) return
await run(async () => {
acceptState(await bridge.selectPackage(key))
onlineState.value = await bridge.onlineState()
await refreshHost()
})
}
async function refreshHost() {
if (!bridge) return
host.value = await bridge.host()
@@ -249,6 +257,8 @@ onUnmounted(() => {
@action="runOnline"
@cancel="cancelOnline"
@reset-game="resetGame"
@select-local="selectLocalGame"
:output-open="state?.outputOpen ?? false"
:selected-key="state?.catalog.key ?? null"
:selected-name="state?.catalog.name ?? null"
:can-connect-current="
+1 -1
View File
@@ -32,7 +32,7 @@ export type OnlineIntent =
| { action: 'login'; server: string; email: string; password: string }
| { action: 'retry-materials' }
| { action: 'connect'; gameId?: number }
| { action: 'logout' | 'games' | 'disconnect' | 'sound' | 'results' | 'cancel' }
| { action: 'logout' | 'games' | 'disconnect' | 'sound' | 'results' | 'cancel' | 'clear-games' }
| { action: 'select-game' | 'download' | 'check'; gameId: number }
| { action: 'remove'; key: string }
export interface OnlineState {
+2
View File
@@ -142,6 +142,8 @@ export interface PackageSummary {
key: string
version: string
name: string
downloadedFiles: number
totalFiles: number
}
export interface ScreenPlan {
steps: ScreenStep[]