up
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -142,6 +142,8 @@ export interface PackageSummary {
|
||||
key: string
|
||||
version: string
|
||||
name: string
|
||||
downloadedFiles: number
|
||||
totalFiles: number
|
||||
}
|
||||
export interface ScreenPlan {
|
||||
steps: ScreenStep[]
|
||||
|
||||
Reference in New Issue
Block a user