generated from VLADIMIR/template_frontend
up
This commit is contained in:
@@ -921,6 +921,10 @@ service EveningDetectiveServer {
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetScreenTeams(GetScreenTeamsReq) returns (GetScreenTeamsRsp) {
|
||||
option (google.api.http) = { post: "/api/screens/teams" body: "*" };
|
||||
}
|
||||
|
||||
rpc GetScreenResults(GetScreenResultsReq) returns (GetScreenResultsRsp) {
|
||||
option (google.api.http) = { post: "/api/screens/results" body: "*" };
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
@@ -1783,6 +1787,14 @@ enum ScreenMode {
|
||||
SCREEN_MODE_RESULTS = 6;
|
||||
SCREEN_MODE_TIMER = 7;
|
||||
SCREEN_MODE_TEXT = 8;
|
||||
SCREEN_MODE_QR = 9;
|
||||
SCREEN_MODE_TEAMS = 10;
|
||||
}
|
||||
|
||||
enum ScreenTimerType {
|
||||
SCREEN_TIMER_TYPE_BREAK = 0;
|
||||
SCREEN_TIMER_TYPE_BEFORE_GAME = 1;
|
||||
SCREEN_TIMER_TYPE_UNTIL_END = 2;
|
||||
}
|
||||
|
||||
enum ScreenTimerState {
|
||||
@@ -1830,6 +1842,8 @@ message ScreenState {
|
||||
ScreenTimerState timer_state = 19;
|
||||
string text = 20;
|
||||
MusicState music = 21;
|
||||
string qr_url = 22;
|
||||
ScreenTimerType timer_type = 23;
|
||||
}
|
||||
|
||||
message CreateScreenSessionReq {}
|
||||
@@ -1966,6 +1980,8 @@ message UpdateGameMapItemRsp {}
|
||||
message SetScreenModeReq {
|
||||
int32 gameID = 1;
|
||||
ScreenMode mode = 2;
|
||||
string qr_url = 3;
|
||||
string text = 4;
|
||||
}
|
||||
|
||||
message SetScreenModeRsp {
|
||||
@@ -1975,6 +1991,7 @@ message SetScreenTimerReq {
|
||||
int32 gameID = 1;
|
||||
int64 duration_seconds = 2;
|
||||
string label = 3;
|
||||
ScreenTimerType timer_type = 4;
|
||||
}
|
||||
|
||||
message SetScreenTimerRsp {
|
||||
@@ -1995,6 +2012,8 @@ message ScreenPlanStep {
|
||||
string text = 2;
|
||||
string timer_label = 3;
|
||||
int64 timer_duration_seconds = 4;
|
||||
string qr_url = 5;
|
||||
ScreenTimerType timer_type = 6;
|
||||
}
|
||||
|
||||
message ScreenPlan {
|
||||
@@ -2029,6 +2048,18 @@ message SetScreenPlanStepRsp {
|
||||
ScreenPlan plan = 1;
|
||||
}
|
||||
|
||||
message ScreenTeam {
|
||||
string ID = 1;
|
||||
string name = 2;
|
||||
}
|
||||
message GetScreenTeamsReq {
|
||||
string screenID = 1;
|
||||
string access_token = 2;
|
||||
}
|
||||
message GetScreenTeamsRsp {
|
||||
repeated ScreenTeam teams = 1;
|
||||
}
|
||||
|
||||
message GetScreenResultsReq {
|
||||
string screenID = 1;
|
||||
string accessToken = 2;
|
||||
|
||||
@@ -9,7 +9,13 @@ export type ScreenMode =
|
||||
| "SCREEN_MODE_CONCLUSION"
|
||||
| "SCREEN_MODE_RESULTS"
|
||||
| "SCREEN_MODE_TIMER"
|
||||
| "SCREEN_MODE_TEXT";
|
||||
| "SCREEN_MODE_TEXT"
|
||||
| "SCREEN_MODE_QR"
|
||||
| "SCREEN_MODE_TEAMS";
|
||||
export type ScreenTimerType =
|
||||
| "SCREEN_TIMER_TYPE_BREAK"
|
||||
| "SCREEN_TIMER_TYPE_BEFORE_GAME"
|
||||
| "SCREEN_TIMER_TYPE_UNTIL_END";
|
||||
export type ScreenTimerState =
|
||||
| "SCREEN_TIMER_STATE_UNSPECIFIED"
|
||||
| "SCREEN_TIMER_STATE_STOPPED"
|
||||
@@ -631,6 +637,8 @@ export type ScreenState = {
|
||||
timerState: ScreenTimerState | undefined;
|
||||
text: string | undefined;
|
||||
music: MusicState | undefined;
|
||||
qrUrl: string | undefined;
|
||||
timerType: ScreenTimerType | undefined;
|
||||
};
|
||||
|
||||
export type MusicState = {
|
||||
@@ -762,6 +770,8 @@ export type UpdateGameMapItemRsp = {
|
||||
export type SetScreenModeReq = {
|
||||
gameID: number | undefined;
|
||||
mode: ScreenMode | undefined;
|
||||
qrUrl: string | undefined;
|
||||
text: string | undefined;
|
||||
};
|
||||
|
||||
export type SetScreenModeRsp = {
|
||||
@@ -771,6 +781,7 @@ export type SetScreenTimerReq = {
|
||||
gameID: number | undefined;
|
||||
durationSeconds: number | undefined;
|
||||
label: string | undefined;
|
||||
timerType: ScreenTimerType | undefined;
|
||||
};
|
||||
|
||||
export type SetScreenTimerRsp = {
|
||||
@@ -791,6 +802,8 @@ export type ScreenPlanStep = {
|
||||
text: string | undefined;
|
||||
timerLabel: string | undefined;
|
||||
timerDurationSeconds: number | undefined;
|
||||
qrUrl: string | undefined;
|
||||
timerType: ScreenTimerType | undefined;
|
||||
};
|
||||
|
||||
export type ScreenPlan = {
|
||||
@@ -825,6 +838,20 @@ export type SetScreenPlanStepRsp = {
|
||||
plan: ScreenPlan | undefined;
|
||||
};
|
||||
|
||||
export type ScreenTeam = {
|
||||
ID: string | undefined;
|
||||
name: string | undefined;
|
||||
};
|
||||
|
||||
export type GetScreenTeamsReq = {
|
||||
screenID: string | undefined;
|
||||
accessToken: string | undefined;
|
||||
};
|
||||
|
||||
export type GetScreenTeamsRsp = {
|
||||
teams: ScreenTeam[] | undefined;
|
||||
};
|
||||
|
||||
export type GetScreenResultsReq = {
|
||||
screenID: string | undefined;
|
||||
accessToken: string | undefined;
|
||||
@@ -1471,6 +1498,7 @@ export interface EveningDetectiveServer {
|
||||
GetScreenPlan(request: GetScreenPlanReq): Promise<GetScreenPlanRsp>;
|
||||
SaveScreenPlan(request: SaveScreenPlanReq): Promise<SaveScreenPlanRsp>;
|
||||
SetScreenPlanStep(request: SetScreenPlanStepReq): Promise<SetScreenPlanStepRsp>;
|
||||
GetScreenTeams(request: GetScreenTeamsReq): Promise<GetScreenTeamsRsp>;
|
||||
GetScreenResults(request: GetScreenResultsReq): Promise<GetScreenResultsRsp>;
|
||||
DownloadGamePrintMaterials(request: GetGameReq): Promise<googleapi_HttpBody>;
|
||||
PreviewTeamReceipt(request: TeamReceiptReq): Promise<googleapi_HttpBody>;
|
||||
@@ -3163,6 +3191,23 @@ export function createEveningDetectiveServerClient(
|
||||
method: "SetScreenPlanStep",
|
||||
}) as Promise<SetScreenPlanStepRsp>;
|
||||
},
|
||||
GetScreenTeams(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
const path = `api/screens/teams`; // eslint-disable-line quotes
|
||||
const body = JSON.stringify(request);
|
||||
const queryParams: string[] = [];
|
||||
let uri = path;
|
||||
if (queryParams.length > 0) {
|
||||
uri += `?${queryParams.join("&")}`
|
||||
}
|
||||
return handler({
|
||||
path: uri,
|
||||
method: "POST",
|
||||
body,
|
||||
}, {
|
||||
service: "EveningDetectiveServer",
|
||||
method: "GetScreenTeams",
|
||||
}) as Promise<GetScreenTeamsRsp>;
|
||||
},
|
||||
GetScreenResults(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
const path = `api/screens/results`; // eslint-disable-line quotes
|
||||
const body = JSON.stringify(request);
|
||||
|
||||
@@ -64,6 +64,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { useUIStore } from '@/stores/ui'
|
||||
import { applicationFileType, applicationFileURL } from '@/utils/application'
|
||||
import { formatGameDateTime, parseDateWithoutTimezone } from '@/utils/datetime'
|
||||
import { validScreenQR } from '@/utils/screenPresentation'
|
||||
import { hasNewTeamApplications } from '@/utils/teamApplications'
|
||||
|
||||
const emit = defineEmits<{ resultsApplied: [] }>()
|
||||
@@ -262,6 +263,7 @@ const screenPlanMode = ref<'use' | 'edit'>('use')
|
||||
let screenPlanRequestVersion = 0
|
||||
const previewQuestions = ref<ResultsQuestion[]>([])
|
||||
const previewTeams = ref<TeamResult[]>([])
|
||||
const screenTeamNames = ref<string[]>()
|
||||
const mapItems = ref<MapItem[]>([])
|
||||
const mapSaving = ref(false)
|
||||
const mapDragging = ref(false)
|
||||
@@ -402,6 +404,18 @@ async function getGameScreens() {
|
||||
const res = await client.GetGameScreens({ gameID: Number(gameId) })
|
||||
screens.value = res.screens || []
|
||||
screensError.value = ''
|
||||
if (
|
||||
previewScreen.value?.mode === 'SCREEN_MODE_TEAMS' ||
|
||||
(screenPlanMode.value === 'edit' ? screenPlanDraft.value : screenPlan.value?.steps)?.some(
|
||||
(step) => step.mode === 'SCREEN_MODE_TEAMS',
|
||||
)
|
||||
) {
|
||||
const gameResponse = await client.GetGame({ ID: Number(gameId) })
|
||||
screenTeamNames.value = (gameResponse.game?.teams ?? [])
|
||||
.filter((team) => team.participationStatus !== 'cancelled')
|
||||
.map((team) => team.name ?? '')
|
||||
.sort((a, b) => a.localeCompare(b, 'ru'))
|
||||
}
|
||||
if (
|
||||
(screenPlanMode.value === 'edit' ? screenPlanDraft.value : screenPlan.value?.steps)?.some(
|
||||
(step) => step.mode === 'SCREEN_MODE_RESULTS',
|
||||
@@ -467,6 +481,8 @@ function addScreenPlanStep() {
|
||||
text: 'Новый экран',
|
||||
timerLabel: 'Перерыв',
|
||||
timerDurationSeconds: 600,
|
||||
qrUrl: '',
|
||||
timerType: 'SCREEN_TIMER_TYPE_BREAK',
|
||||
})
|
||||
screenPlanDirty.value = true
|
||||
screenPlanMode.value = 'edit'
|
||||
@@ -512,13 +528,14 @@ async function saveScreenPlan() {
|
||||
const invalidStep = screenPlanDraft.value.findIndex(
|
||||
(step) =>
|
||||
(step.mode === 'SCREEN_MODE_TEXT' && !step.text?.trim()) ||
|
||||
(step.mode === 'SCREEN_MODE_QR' && !validScreenQR(step.qrUrl, step.text)) ||
|
||||
(step.mode === 'SCREEN_MODE_TIMER' &&
|
||||
(!Number.isInteger(step.timerDurationSeconds) ||
|
||||
(step.timerDurationSeconds ?? 0) < 1 ||
|
||||
(step.timerDurationSeconds ?? 0) > 86400)),
|
||||
)
|
||||
if (invalidStep !== -1) {
|
||||
message.error(`Проверьте текст или длительность таймера в шаге ${invalidStep + 1}`)
|
||||
message.error(`Проверьте ссылку QR, текст или длительность таймера в шаге ${invalidStep + 1}`)
|
||||
return
|
||||
}
|
||||
screenPlanRequestVersion += 1
|
||||
@@ -1579,6 +1596,7 @@ useSimplePolling(() => {
|
||||
:state="previewScreen"
|
||||
:questions="previewQuestions"
|
||||
:teams="previewTeams"
|
||||
:team-names="screenTeamNames"
|
||||
/>
|
||||
<div v-else class="screen-preview-placeholder">Экран не подключён</div>
|
||||
</UiBlock>
|
||||
@@ -1596,6 +1614,7 @@ useSimplePolling(() => {
|
||||
:screens="screens"
|
||||
:preview-questions="previewQuestions"
|
||||
:preview-teams="previewTeams"
|
||||
:team-names="screenTeamNames"
|
||||
:screen-plan="screenPlan"
|
||||
:screen-plan-draft="screenPlanDraft"
|
||||
:screen-plan-saving="screenPlanSaving"
|
||||
|
||||
@@ -11,15 +11,18 @@ import type {
|
||||
ScreenPlan,
|
||||
ScreenPlanStep,
|
||||
ScreenState,
|
||||
ScreenTimerType,
|
||||
TeamResult,
|
||||
} from '@/api/generated/crabs/evening_detective_server'
|
||||
import ScreenPreview from '@/components/ScreenPreview.vue'
|
||||
import UiBlock from '@/components/UiBlock.vue'
|
||||
import { changeTimerLabel, timerTypes } from '@/utils/screenPresentation'
|
||||
|
||||
const props = defineProps<{
|
||||
screens: ScreenState[]
|
||||
previewQuestions: ResultsQuestion[]
|
||||
previewTeams: TeamResult[]
|
||||
teamNames?: string[]
|
||||
screenPlan?: ScreenPlan
|
||||
screenPlanDraft: ScreenPlanStep[]
|
||||
screenPlanSaving: boolean
|
||||
@@ -76,7 +79,14 @@ const modeOptions = [
|
||||
{ label: 'Результаты', value: 'SCREEN_MODE_RESULTS' },
|
||||
{ label: 'Таймер', value: 'SCREEN_MODE_TIMER' },
|
||||
{ label: 'Текст', value: 'SCREEN_MODE_TEXT' },
|
||||
{ label: 'QR-код', value: 'SCREEN_MODE_QR' },
|
||||
{ label: 'Список команд', value: 'SCREEN_MODE_TEAMS' },
|
||||
]
|
||||
function setTimerType(step: ScreenPlanStep, type: ScreenTimerType) {
|
||||
step.timerLabel = changeTimerLabel(step.timerLabel, step.timerType, type)
|
||||
step.timerType = type
|
||||
emit('dirty')
|
||||
}
|
||||
function thumbnail(step: ScreenPlanStep): Partial<ScreenState> {
|
||||
return {
|
||||
...previewScreen.value,
|
||||
@@ -146,6 +156,7 @@ function thumbnail(step: ScreenPlanStep): Partial<ScreenState> {
|
||||
:state="thumbnail(step)"
|
||||
:questions="previewQuestions"
|
||||
:teams="previewTeams"
|
||||
:team-names="teamNames"
|
||||
is-thumbnail
|
||||
/>
|
||||
</div>
|
||||
@@ -184,7 +195,7 @@ function thumbnail(step: ScreenPlanStep): Partial<ScreenState> {
|
||||
@update:value="emit('dirty')"
|
||||
/>
|
||||
<n-input
|
||||
v-if="step.mode === 'SCREEN_MODE_TEXT'"
|
||||
v-if="step.mode === 'SCREEN_MODE_TEXT' || step.mode === 'SCREEN_MODE_QR'"
|
||||
v-model:value="step.text"
|
||||
type="textarea"
|
||||
placeholder="Текст экрана"
|
||||
@@ -192,7 +203,25 @@ function thumbnail(step: ScreenPlanStep): Partial<ScreenState> {
|
||||
:input-props="{ 'aria-label': `Текст шага ${index + 1}` }"
|
||||
@update:value="emit('dirty')"
|
||||
/>
|
||||
<template v-if="step.mode === 'SCREEN_MODE_QR'">
|
||||
<label :for="`qr-link-${index}`">Ссылка для QR-кода (обязательно)</label>
|
||||
<n-input
|
||||
v-model:value="step.qrUrl"
|
||||
:disabled="isDisabled"
|
||||
:input-props="{ id: `qr-link-${index}` }"
|
||||
placeholder="https://example.com"
|
||||
@update:value="emit('dirty')"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="step.mode === 'SCREEN_MODE_TIMER'">
|
||||
<label :for="`timer-type-${index}`">Тип таймера</label>
|
||||
<n-select
|
||||
:value="step.timerType ?? 'SCREEN_TIMER_TYPE_BREAK'"
|
||||
:options="timerTypes"
|
||||
:disabled="isDisabled"
|
||||
:input-props="{ id: `timer-type-${index}` }"
|
||||
@update:value="setTimerType(step, $event)"
|
||||
/>
|
||||
<label :for="`timer-label-${index}`">Надпись таймера</label>
|
||||
<n-input
|
||||
v-model:value="step.timerLabel"
|
||||
@@ -216,7 +245,7 @@ function thumbnail(step: ScreenPlanStep): Partial<ScreenState> {
|
||||
<span
|
||||
>{{ index + 1 }}.
|
||||
{{ modeOptions.find((option) => option.value === step.mode)?.label
|
||||
}}<template v-if="step.mode === 'SCREEN_MODE_TEXT'">: {{ step.text }}</template
|
||||
}}<template v-if="step.mode === 'SCREEN_MODE_TEXT' || step.mode === 'SCREEN_MODE_QR'">: {{ step.text }}</template
|
||||
><template v-if="step.mode === 'SCREEN_MODE_TIMER'"
|
||||
>: {{ step.timerLabel }} ({{ step.timerDurationSeconds }} с)</template
|
||||
></span
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getAuthClient } from '@/api/auth_client'
|
||||
import type { Game, ScreenState, TeamResult } from '@/api/generated/crabs/evening_detective_server'
|
||||
import { buildClient } from '@/api/generated/crabs/evening_detective_server/client'
|
||||
import HeaderMenu from '@/components/HeaderMenu.vue'
|
||||
import ScreenPreview from '@/components/ScreenPreview.vue'
|
||||
import { useDeviceContext } from '@/composables/useDeviceContext'
|
||||
import { useSimplePolling } from '@/composables/useSimplePolling'
|
||||
import { applicationFileURL } from '@/utils/application'
|
||||
@@ -21,6 +22,8 @@ const route = useRoute()
|
||||
const screenId = ref('')
|
||||
const accessToken = ref('')
|
||||
const state = ref<ScreenState>()
|
||||
const screenTeamNames = ref<string[]>()
|
||||
let lastTeamsLoaded = -Infinity
|
||||
const resultTeams = ref<TeamResult[]>([])
|
||||
const resultQuestions = ref<{ code: string | undefined; text: string | undefined }[]>([])
|
||||
const resultsContainer = ref<HTMLElement>()
|
||||
@@ -130,9 +133,14 @@ async function loadState(recreateOnUnauthorized = true) {
|
||||
if (!screenId.value || !accessToken.value) return
|
||||
try {
|
||||
const res = await screenClient.GetScreenState({ screenID: screenId.value, accessToken: accessToken.value })
|
||||
if (state.value?.gameID !== res.state?.gameID) {
|
||||
screenTeamNames.value = undefined
|
||||
lastTeamsLoaded = -Infinity
|
||||
}
|
||||
state.value = res.state
|
||||
syncTimer(state.value)
|
||||
if (state.value?.gameID && state.value.mode === 'SCREEN_MODE_RESULTS') await loadResults()
|
||||
if (state.value?.gameID && state.value.mode === 'SCREEN_MODE_TEAMS' && Date.now() - lastTeamsLoaded >= 5000) await loadScreenTeams()
|
||||
} catch (err) {
|
||||
if (recreateOnUnauthorized && err instanceof Error && err.message === 'unauthorized') {
|
||||
sessionStorage.removeItem(sessionKey)
|
||||
@@ -146,6 +154,20 @@ async function loadState(recreateOnUnauthorized = true) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadScreenTeams() {
|
||||
const gameID = state.value?.gameID
|
||||
const sessionID = screenId.value
|
||||
lastTeamsLoaded = Date.now()
|
||||
const res = await screenClient.GetScreenTeams({
|
||||
screenID: sessionID,
|
||||
accessToken: accessToken.value,
|
||||
})
|
||||
if (state.value?.gameID !== gameID || screenId.value !== sessionID) return
|
||||
screenTeamNames.value = (res.teams ?? [])
|
||||
.map((team) => team.name ?? '')
|
||||
.sort((a, b) => a.localeCompare(b, 'ru'))
|
||||
}
|
||||
|
||||
async function loadResults() {
|
||||
if (!screenId.value || !accessToken.value) return
|
||||
const res = await screenClient.GetScreenResults({ screenID: screenId.value, accessToken: accessToken.value })
|
||||
@@ -380,6 +402,14 @@ onUnmounted(() => {
|
||||
<video ref="conclusionVideoElement" :src="conclusionVideo" :muted="!state.soundEnabled" playsinline />
|
||||
</div>
|
||||
<div v-if="screenMode === 'SCREEN_MODE_CONCLUSION' && !state.conclusionVideo" class="screen-message">Видео закрывашки не задано</div>
|
||||
<ScreenPreview
|
||||
v-if="screenMode === 'SCREEN_MODE_QR' || screenMode === 'SCREEN_MODE_TEAMS'"
|
||||
class="static-screen"
|
||||
:state="state"
|
||||
:questions="[]"
|
||||
:teams="[]"
|
||||
:team-names="screenTeamNames"
|
||||
/>
|
||||
<div v-if="screenMode === 'SCREEN_MODE_TEXT'" class="screen-message">{{ state.text }}</div>
|
||||
<div v-if="screenMode === 'SCREEN_MODE_TIMER'" class="timer-screen">
|
||||
<h1 class="timer-label">{{ state.timerLabel || 'Перерыв' }}</h1>
|
||||
@@ -597,4 +627,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
|
||||
.static-screen {
|
||||
height: 100dvh;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { NQrCode } from 'naive-ui'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import type {
|
||||
@@ -7,14 +8,18 @@ import type {
|
||||
TeamResult,
|
||||
} from '@/api/generated/crabs/evening_detective_server'
|
||||
import { applicationFileURL } from '@/utils/application'
|
||||
import { validScreenQR } from '@/utils/screenPresentation'
|
||||
|
||||
const props = defineProps<{
|
||||
state: Partial<ScreenState>
|
||||
questions: ResultsQuestion[]
|
||||
teams: TeamResult[]
|
||||
teamNames?: string[]
|
||||
isThumbnail?: boolean
|
||||
}>()
|
||||
|
||||
const teamColumns = computed(() => Math.max(1, Math.ceil(Math.sqrt((props.teamNames?.length ?? 0) / 4))))
|
||||
const teamRows = computed(() => Math.max(1, Math.ceil((props.teamNames?.length ?? 0) / teamColumns.value)))
|
||||
const timerNow = ref(Date.now())
|
||||
const timerRemainingAtSync = ref(0)
|
||||
const timerSyncedAt = ref(Date.now())
|
||||
@@ -101,6 +106,18 @@ onUnmounted(() => {
|
||||
<div v-else-if="state.mode === 'SCREEN_MODE_TEXT'" class="preview-message">
|
||||
{{ state.text }}
|
||||
</div>
|
||||
<div v-else-if="state.mode === 'SCREEN_MODE_QR'" class="preview-qr">
|
||||
<NQrCode v-if="validScreenQR(state.qrUrl)" :value="state.qrUrl" :size="512" :padding="24" />
|
||||
<span v-else>Укажите ссылку для QR-кода</span>
|
||||
<p v-if="state.text">{{ state.text }}</p>
|
||||
</div>
|
||||
<div v-else-if="state.mode === 'SCREEN_MODE_TEAMS'" class="preview-team-list">
|
||||
<p v-if="!teamNames" class="preview-message">Список команд ещё не загружен</p>
|
||||
<div v-else-if="teamNames.length" class="team-names" :style="{ gridTemplateColumns: `repeat(${teamColumns}, minmax(0, 1fr))`, gridTemplateRows: `repeat(${teamRows}, minmax(0, 1fr))`, fontSize: `min(${22.5 / teamRows}cqw, ${5 / teamColumns}cqw)` }">
|
||||
<p v-for="(name, index) in teamNames" :key="index">{{ name }}</p>
|
||||
</div>
|
||||
<p v-else class="preview-message">Команды пока не зарегистрированы</p>
|
||||
</div>
|
||||
<div v-else-if="state.mode === 'SCREEN_MODE_RESULTS'" class="preview-results">
|
||||
<h2>Результаты</h2>
|
||||
<table>
|
||||
@@ -222,4 +239,54 @@ onUnmounted(() => {
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.preview-qr {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 4cqw;
|
||||
justify-content: center;
|
||||
max-width: 92%;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line selector-pseudo-class-no-unknown -- Vue scoped: размер QR в дочернем компоненте. */
|
||||
.preview-qr :deep(.n-qr-code) {
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
height: auto !important;
|
||||
padding: 1.8cqw !important;
|
||||
width: 38cqw !important;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line selector-pseudo-class-no-unknown -- Vue scoped: размер QR в дочернем компоненте. */
|
||||
.preview-qr :deep(canvas) {
|
||||
height: auto !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.preview-qr p {
|
||||
font-size: 3.5cqw;
|
||||
max-height: 48cqw;
|
||||
max-width: 40cqw;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.preview-team-list {
|
||||
height: 90%;
|
||||
width: 92%;
|
||||
}
|
||||
|
||||
.team-names {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: 1.5cqw;
|
||||
grid-auto-flow: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.team-names p {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ScreenTimerType } from '@/api/generated/crabs/evening_detective_server'
|
||||
|
||||
export const timerTypes = [
|
||||
{ value: 'SCREEN_TIMER_TYPE_BREAK', label: 'Перерыв' },
|
||||
{ value: 'SCREEN_TIMER_TYPE_BEFORE_GAME', label: 'До начала игры' },
|
||||
{ value: 'SCREEN_TIMER_TYPE_UNTIL_END', label: 'До конца игры' },
|
||||
] satisfies { value: ScreenTimerType; label: string }[]
|
||||
|
||||
export function changeTimerLabel(
|
||||
label: string | undefined,
|
||||
previous: ScreenTimerType | undefined,
|
||||
next: ScreenTimerType,
|
||||
): string {
|
||||
const oldLabel = timerTypes.find(
|
||||
(type) => type.value === (previous ?? 'SCREEN_TIMER_TYPE_BREAK'),
|
||||
)!.label
|
||||
return !label?.trim() || label === oldLabel
|
||||
? timerTypes.find((type) => type.value === next)!.label
|
||||
: label
|
||||
}
|
||||
|
||||
export function validScreenQR(link = '', caption = ''): boolean {
|
||||
try {
|
||||
const url = new URL(link)
|
||||
return (
|
||||
/^https?:\/\/[^/\\\s?#]/.test(link) &&
|
||||
['http:', 'https:'].includes(url.protocol) &&
|
||||
!!url.hostname &&
|
||||
!url.username &&
|
||||
!url.password &&
|
||||
link === link.trim() &&
|
||||
new TextEncoder().encode(link).length <= 2048 &&
|
||||
new TextEncoder().encode(caption).length <= 10000
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,15 @@ import ts from 'typescript'
|
||||
import * as vue from 'vue'
|
||||
import { compileScript, parse } from 'vue/compiler-sfc'
|
||||
|
||||
const presentationModule = { exports: {} }
|
||||
runInNewContext(ts.transpile(readFileSync(new URL('../src/utils/screenPresentation.ts', import.meta.url), 'utf8'), { module: ts.ModuleKind.CommonJS }), { exports: presentationModule.exports, URL, TextEncoder })
|
||||
function source(name) {
|
||||
return parse(readFileSync(new URL(`../src/components/${name}.vue`, import.meta.url), 'utf8'))
|
||||
.descriptor
|
||||
}
|
||||
function setup(name, props) {
|
||||
const script = compileScript(source(name), { id: name })
|
||||
const context = { exports: {}, require: (name) => (name === 'vue' ? vue : {}) }
|
||||
const context = { exports: {}, require: (name) => (name === 'vue' ? vue : name === '@/utils/screenPresentation' ? presentationModule.exports : {}) }
|
||||
runInNewContext(ts.transpile(script.content, { module: ts.ModuleKind.CommonJS }), context)
|
||||
const events = []
|
||||
const scope = vue.effectScope()
|
||||
@@ -110,6 +112,14 @@ assert.equal(
|
||||
screens.state.thumbnail({ mode: 'SCREEN_MODE_TEXT', text: 'Offline draft' }).text,
|
||||
'Offline draft',
|
||||
)
|
||||
const timerStep = { timerLabel: 'Перерыв', timerType: 'SCREEN_TIMER_TYPE_BREAK' }
|
||||
screens.state.setTimerType(timerStep, 'SCREEN_TIMER_TYPE_BEFORE_GAME')
|
||||
assert.equal(timerStep.timerLabel, 'До начала игры')
|
||||
timerStep.timerLabel = 'Обсуждение'
|
||||
screens.state.setTimerType(timerStep, 'SCREEN_TIMER_TYPE_UNTIL_END')
|
||||
assert.equal(timerStep.timerLabel, 'Обсуждение')
|
||||
assert.equal(presentationModule.exports.validScreenQR('https://example.org'), true)
|
||||
assert.equal(presentationModule.exports.validScreenQR('javascript:alert(1)'), false)
|
||||
screens.stop()
|
||||
|
||||
function functions(name, names) {
|
||||
@@ -320,3 +330,26 @@ assert.equal(media.currentTime, 23, 'Polling must not seek the player')
|
||||
console.log(
|
||||
'Screen controls: drafts, conflicts, explicit playlist IDs, timer/music controls, errors, stale polls and playback positions passed',
|
||||
)
|
||||
|
||||
const browserTeams = {
|
||||
state: vue.ref({ gameID: 7 }),
|
||||
screenId: vue.ref('12'),
|
||||
accessToken: vue.ref('test-token'),
|
||||
screenTeamNames: vue.ref(),
|
||||
lastTeamsLoaded: -Infinity,
|
||||
screenClient: { GetScreenTeams: async () => ({ teams: [{ name: 'Ястребы' }, { name: 'Агенты' }] }) },
|
||||
}
|
||||
runInNewContext(ts.transpile(functions('ScreenPage', ['loadScreenTeams']), { target: ts.ScriptTarget.ES2022 }), browserTeams)
|
||||
await browserTeams.loadScreenTeams()
|
||||
assert.deepEqual(Array.from(browserTeams.screenTeamNames.value), ['Агенты', 'Ястребы'])
|
||||
browserTeams.screenClient.GetScreenTeams = async () => { throw new Error('offline') }
|
||||
await assert.rejects(browserTeams.loadScreenTeams(), /offline/)
|
||||
assert.equal(browserTeams.screenTeamNames.value.length, 2)
|
||||
let resolveTeams
|
||||
browserTeams.screenClient.GetScreenTeams = () => new Promise(resolve => { resolveTeams = resolve })
|
||||
const pendingTeams = browserTeams.loadScreenTeams()
|
||||
browserTeams.state.value.gameID = 8
|
||||
browserTeams.screenTeamNames.value = undefined
|
||||
resolveTeams({ teams: [{ name: 'Из прошлой игры' }] })
|
||||
await pendingTeams
|
||||
assert.equal(browserTeams.screenTeamNames.value, undefined)
|
||||
|
||||
Reference in New Issue
Block a user