adding skip in cooldown and probing for media files

This commit is contained in:
RandyJC
2025-12-08 21:32:27 +01:00
parent ea49971609
commit 03b00b2499
6 changed files with 120 additions and 16 deletions

View File

@@ -73,6 +73,7 @@ export interface ClientToServerEvents {
"manager:kickPlayer": (_message: { gameId: string; playerId: string }) => void
"manager:startGame": (_message: MessageGameId) => void
"manager:abortQuiz": (_message: MessageGameId) => void
"manager:skipQuestionIntro": (_message: MessageGameId) => void
"manager:nextQuestion": (_message: MessageGameId) => void
"manager:showLeaderboard": (_message: MessageGameId) => void
"manager:getQuizz": (_quizzId: string) => void

View File

@@ -171,6 +171,10 @@ io.on("connection", (socket) => {
withGame(gameId, socket, (game) => game.nextRound(socket))
)
socket.on("manager:skipQuestionIntro", ({ gameId }) =>
withGame(gameId, socket, (game) => game.skipQuestionIntro(socket))
)
socket.on("manager:showLeaderboard", ({ gameId }) =>
withGame(gameId, socket, (game) => game.showLeaderboard())
)

View File

@@ -293,6 +293,18 @@ class Game {
this.cooldown.active &&= false
}
skipQuestionIntro(socket: Socket) {
if (this.manager.id !== socket.id) {
return
}
if (!this.started) {
return
}
this.abortCooldown()
}
async start(socket: Socket) {
if (this.manager.id !== socket.id) {
return
@@ -346,11 +358,11 @@ class Game {
this.broadcastStatus(STATUS.SHOW_QUESTION, {
question: question.question,
image: question.image,
media: question.media,
media: question.media,
cooldown: question.cooldown,
})
await sleep(question.cooldown)
await this.startCooldown(question.cooldown)
if (!this.started) {
return
@@ -362,7 +374,7 @@ class Game {
question: question.question,
answers: question.answers,
image: question.image,
media: question.media,
media: question.media,
time: question.time,
totalPlayer: this.players.length,
})

View File

@@ -65,6 +65,11 @@ const ManagerGame = () => {
break
case STATUS.SHOW_QUESTION:
socket?.emit("manager:skipQuestionIntro", { gameId })
break
case STATUS.SELECT_ANSWER:
socket?.emit("manager:abortQuiz", { gameId })

View File

@@ -65,6 +65,7 @@ const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => {
const [uploading, setUploading] = useState<Record<number, boolean>>({})
const [deleting, setDeleting] = useState<Record<number, boolean>>({})
const [refreshingLibrary, setRefreshingLibrary] = useState(false)
const [probing, setProbing] = useState<Record<number, boolean>>({})
useEvent("manager:quizzLoaded", (quizz) => {
setDraft(quizz)
@@ -257,12 +258,86 @@ const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => {
}
setQuestionMedia(qIndex, nextMedia)
adjustTimingWithMedia(qIndex, nextMedia)
}
const clearQuestionMedia = (qIndex: number) => {
setQuestionMedia(qIndex, undefined)
}
const probeMediaDuration = async (url: string, type: QuestionMedia["type"]) => {
if (!url || (type !== "audio" && type !== "video")) {
return null
}
try {
const el = document.createElement(type)
el.preload = "metadata"
el.src = url
await new Promise<void>((resolve, reject) => {
const cleanup = () => {
el.onloadedmetadata = null
el.onerror = null
}
el.onloadedmetadata = () => {
cleanup()
resolve()
}
el.onerror = () => {
cleanup()
reject(new Error("Failed to load media metadata"))
}
})
const duration = el.duration
return Number.isFinite(duration) && duration > 0 ? duration : null
} catch (error) {
console.warn("Failed to probe media duration", error)
return null
}
}
const adjustTimingWithMedia = async (
qIndex: number,
media: QuestionMedia | undefined,
) => {
if (!draft || !media?.url || !media.type || media.type === "image") {
return
}
setProbing((prev) => ({ ...prev, [qIndex]: true }))
try {
const duration = await probeMediaDuration(media.url, media.type)
if (!duration || !draft) {
return
}
const rounded = Math.ceil(duration)
const buffer = 3
const minCooldown = rounded
const minAnswer = rounded + buffer
const question = draft.questions[qIndex]
const nextCooldown = Math.max(question.cooldown, minCooldown)
const nextTime = Math.max(question.time, minAnswer)
if (nextCooldown !== question.cooldown || nextTime !== question.time) {
updateQuestion(qIndex, {
cooldown: nextCooldown,
time: nextTime,
})
toast.success(
`Adjusted timing to media length (~${rounded}s, answers ${nextTime}s)`,
{ id: `timing-${qIndex}` },
)
}
} finally {
setProbing((prev) => ({ ...prev, [qIndex]: false }))
}
}
const handleMediaUpload = async (qIndex: number, file: File) => {
if (!draft) return
const question = draft.questions[qIndex]
@@ -296,6 +371,11 @@ const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => {
url: uploaded.url,
fileName: uploaded.fileName,
})
adjustTimingWithMedia(qIndex, {
type,
url: uploaded.url,
fileName: uploaded.fileName,
})
toast.success("Media uploaded")
refreshMediaLibrary()
} catch (error) {
@@ -509,18 +589,20 @@ const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => {
</select>
</label>
<div className="flex flex-col gap-2 rounded-md border border-gray-200 p-3">
<div className="flex items-center justify-between text-sm font-semibold text-gray-600">
<span>Media upload</span>
<span className="text-xs text-gray-500">
{isUploading
? "Uploading..."
: refreshingLibrary
? "Refreshing..."
: mediaFileName
? "Stored"
: "Not saved"}
</span>
<div className="flex flex-col gap-2 rounded-md border border-gray-200 p-3">
<div className="flex items-center justify-between text-sm font-semibold text-gray-600">
<span>Media upload</span>
<span className="text-xs text-gray-500">
{isUploading
? "Uploading..."
: probing[qIndex]
? "Probing..."
: refreshingLibrary
? "Refreshing..."
: mediaFileName
? "Stored"
: "Not saved"}
</span>
</div>
<input
type="file"

View File

@@ -66,7 +66,7 @@ export const MANAGER_SKIP_BTN = {
[STATUS.SHOW_ROOM]: "Start Game",
[STATUS.SHOW_START]: null,
[STATUS.SHOW_PREPARED]: null,
[STATUS.SHOW_QUESTION]: null,
[STATUS.SHOW_QUESTION]: "Skip",
[STATUS.SELECT_ANSWER]: "Skip",
[STATUS.SHOW_RESULT]: null,
[STATUS.SHOW_RESPONSES]: "Next",