generated from VLADIMIR/template_frontend
up
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
|||||||
type UploadFileInfo,
|
type UploadFileInfo,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { getAuthClient } from '@/api/auth_client'
|
import { getAuthClient } from '@/api/auth_client'
|
||||||
@@ -58,6 +58,7 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
import { useUIStore } from '@/stores/ui'
|
import { useUIStore } from '@/stores/ui'
|
||||||
import { applicationFileType, applicationFileURL } from '@/utils/application'
|
import { applicationFileType, applicationFileURL } from '@/utils/application'
|
||||||
import { formatGameDateTime, parseDateWithoutTimezone } from '@/utils/datetime'
|
import { formatGameDateTime, parseDateWithoutTimezone } from '@/utils/datetime'
|
||||||
|
import { hasNewTeamApplications } from '@/utils/teamApplications'
|
||||||
|
|
||||||
const emit = defineEmits<{ resultsApplied: [] }>()
|
const emit = defineEmits<{ resultsApplied: [] }>()
|
||||||
|
|
||||||
@@ -128,6 +129,12 @@ const game = ref<Game>({
|
|||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const uiStore = useUIStore()
|
const uiStore = useUIStore()
|
||||||
const canManageGame = computed(() => authStore.hasAnyRole(['admin', 'organizer', 'author']))
|
const canManageGame = computed(() => authStore.hasAnyRole(['admin', 'organizer', 'author']))
|
||||||
|
let gameRequestVersion = 0
|
||||||
|
let appliedGameRequestVersion = 0
|
||||||
|
let hasLoadedGame = false
|
||||||
|
let isDisposed = false
|
||||||
|
let notificationAudio: AudioContext | undefined
|
||||||
|
let hasReportedSoundError = false
|
||||||
const canViewState = computed(
|
const canViewState = computed(
|
||||||
() =>
|
() =>
|
||||||
uiStore.environment === 'venue' &&
|
uiStore.environment === 'venue' &&
|
||||||
@@ -220,10 +227,80 @@ const mapDragging = ref(false)
|
|||||||
const screenVolume = ref(100)
|
const screenVolume = ref(100)
|
||||||
const screenPosition = ref(0)
|
const screenPosition = ref(0)
|
||||||
|
|
||||||
|
function reportSoundError(error: unknown) {
|
||||||
|
if (isDisposed || hasReportedSoundError) return
|
||||||
|
hasReportedSoundError = true
|
||||||
|
message.warning('Не удалось включить звуковые уведомления о выдаче улик')
|
||||||
|
console.error('Звуковое уведомление о выдаче улик', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enableNotificationSound() {
|
||||||
|
if (!canManageGame.value || isDisposed) return
|
||||||
|
try {
|
||||||
|
notificationAudio ??= new AudioContext()
|
||||||
|
if (notificationAudio.state === 'suspended') await notificationAudio.resume()
|
||||||
|
} catch (error) {
|
||||||
|
reportSoundError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playApplicationNotification() {
|
||||||
|
// До первого взаимодействия браузер блокирует звук; пропущенные сигналы не накапливаем.
|
||||||
|
if (!notificationAudio || notificationAudio.state !== 'running') return
|
||||||
|
try {
|
||||||
|
const oscillator = notificationAudio.createOscillator()
|
||||||
|
const gain = notificationAudio.createGain()
|
||||||
|
const now = notificationAudio.currentTime
|
||||||
|
oscillator.frequency.value = 880
|
||||||
|
gain.gain.setValueAtTime(0, now)
|
||||||
|
gain.gain.linearRampToValueAtTime(0.1, now + 0.02)
|
||||||
|
gain.gain.linearRampToValueAtTime(0, now + 0.3)
|
||||||
|
oscillator.connect(gain)
|
||||||
|
gain.connect(notificationAudio.destination)
|
||||||
|
oscillator.onended = () => {
|
||||||
|
oscillator.disconnect()
|
||||||
|
gain.disconnect()
|
||||||
|
}
|
||||||
|
oscillator.start(now)
|
||||||
|
oscillator.stop(now + 0.3)
|
||||||
|
} catch (error) {
|
||||||
|
reportSoundError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('click', enableNotificationSound)
|
||||||
|
window.addEventListener('keydown', enableNotificationSound)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
isDisposed = true
|
||||||
|
window.removeEventListener('click', enableNotificationSound)
|
||||||
|
window.removeEventListener('keydown', enableNotificationSound)
|
||||||
|
void notificationAudio?.close().catch((error: unknown) => {
|
||||||
|
console.error('Закрытие звуковых уведомлений', error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
async function getGame(id: number) {
|
async function getGame(id: number) {
|
||||||
|
const requestVersion = ++gameRequestVersion
|
||||||
const wasManaging = canManageGame.value
|
const wasManaging = canManageGame.value
|
||||||
const res = await client.GetGame({ ID: id })
|
const res = await client.GetGame({ ID: id })
|
||||||
if (wasManaging !== canManageGame.value) return
|
if (
|
||||||
|
isDisposed ||
|
||||||
|
wasManaging !== canManageGame.value ||
|
||||||
|
requestVersion < appliedGameRequestVersion
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
appliedGameRequestVersion = requestVersion
|
||||||
|
if (
|
||||||
|
canManageGame.value &&
|
||||||
|
hasLoadedGame &&
|
||||||
|
hasNewTeamApplications(game.value.teams || [], res.game?.teams || [])
|
||||||
|
) {
|
||||||
|
playApplicationNotification()
|
||||||
|
}
|
||||||
|
hasLoadedGame = true
|
||||||
game.value = res.game!
|
game.value = res.game!
|
||||||
if (!settingsDraftInitialized.value) {
|
if (!settingsDraftInitialized.value) {
|
||||||
settingsDraft.value = {
|
settingsDraft.value = {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { test } from 'node:test'
|
||||||
|
|
||||||
|
import type { Team } from '../../api/generated/crabs/evening_detective_server/index.ts'
|
||||||
|
import { hasNewTeamApplications } from '../teamApplications.ts'
|
||||||
|
|
||||||
|
function team(ID: string, names: string[]): Team {
|
||||||
|
return {
|
||||||
|
ID,
|
||||||
|
name: undefined,
|
||||||
|
status: undefined,
|
||||||
|
password: undefined,
|
||||||
|
actionsCount: undefined,
|
||||||
|
applications: names.map((name) => ({ name, image: undefined, file_type: undefined })),
|
||||||
|
captainEmail: undefined,
|
||||||
|
participantsCount: undefined,
|
||||||
|
participationStatus: undefined,
|
||||||
|
participationSource: undefined,
|
||||||
|
participationEmailError: undefined,
|
||||||
|
participationEmailSent: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const scenario of [
|
||||||
|
{ name: 'empty lists', previous: [], current: [], expected: false },
|
||||||
|
{
|
||||||
|
name: 'new application',
|
||||||
|
previous: [team('a', [])],
|
||||||
|
current: [team('a', ['clue'])],
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'unchanged applications and reordered teams',
|
||||||
|
previous: [team('a', ['one', 'two']), team('b', ['clue'])],
|
||||||
|
current: [team('b', ['clue']), team('a', ['two', 'one'])],
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'issued application',
|
||||||
|
previous: [team('a', ['clue'])],
|
||||||
|
current: [team('a', [])],
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'replacement with unchanged count',
|
||||||
|
previous: [team('a', ['one'])],
|
||||||
|
current: [team('a', ['two'])],
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'same application for another team',
|
||||||
|
previous: [team('a', ['clue']), team('b', [])],
|
||||||
|
current: [team('a', ['clue']), team('b', ['clue'])],
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'multiple new applications',
|
||||||
|
previous: [],
|
||||||
|
current: [team('a', ['one', 'two']), team('b', ['clue'])],
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'team and application names cannot collide',
|
||||||
|
previous: [team('a:b', ['c'])],
|
||||||
|
current: [team('a', ['b:c'])],
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
test(scenario.name, () => {
|
||||||
|
assert.equal(hasNewTeamApplications(scenario.previous, scenario.current), scenario.expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Team } from '@/api/generated/crabs/evening_detective_server'
|
||||||
|
|
||||||
|
export function hasNewTeamApplications(previous: Team[], current: Team[]): boolean {
|
||||||
|
const previousKeys = new Set(
|
||||||
|
previous.flatMap((team) =>
|
||||||
|
(team.applications || []).map((application) => JSON.stringify([team.ID, application.name])),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return current.some((team) =>
|
||||||
|
team.applications?.some(
|
||||||
|
(application) => !previousKeys.has(JSON.stringify([team.ID, application.name])),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user