mirror of
https://github.com/randyjc/Rahoot.git
synced 2026-03-13 20:15:35 +01:00
adding skip in cooldown and probing for media files
This commit is contained in:
@@ -73,6 +73,7 @@ export interface ClientToServerEvents {
|
|||||||
"manager:kickPlayer": (_message: { gameId: string; playerId: string }) => void
|
"manager:kickPlayer": (_message: { gameId: string; playerId: string }) => void
|
||||||
"manager:startGame": (_message: MessageGameId) => void
|
"manager:startGame": (_message: MessageGameId) => void
|
||||||
"manager:abortQuiz": (_message: MessageGameId) => void
|
"manager:abortQuiz": (_message: MessageGameId) => void
|
||||||
|
"manager:skipQuestionIntro": (_message: MessageGameId) => void
|
||||||
"manager:nextQuestion": (_message: MessageGameId) => void
|
"manager:nextQuestion": (_message: MessageGameId) => void
|
||||||
"manager:showLeaderboard": (_message: MessageGameId) => void
|
"manager:showLeaderboard": (_message: MessageGameId) => void
|
||||||
"manager:getQuizz": (_quizzId: string) => void
|
"manager:getQuizz": (_quizzId: string) => void
|
||||||
|
|||||||
@@ -171,6 +171,10 @@ io.on("connection", (socket) => {
|
|||||||
withGame(gameId, socket, (game) => game.nextRound(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 }) =>
|
socket.on("manager:showLeaderboard", ({ gameId }) =>
|
||||||
withGame(gameId, socket, (game) => game.showLeaderboard())
|
withGame(gameId, socket, (game) => game.showLeaderboard())
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -293,6 +293,18 @@ class Game {
|
|||||||
this.cooldown.active &&= false
|
this.cooldown.active &&= false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
skipQuestionIntro(socket: Socket) {
|
||||||
|
if (this.manager.id !== socket.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.started) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.abortCooldown()
|
||||||
|
}
|
||||||
|
|
||||||
async start(socket: Socket) {
|
async start(socket: Socket) {
|
||||||
if (this.manager.id !== socket.id) {
|
if (this.manager.id !== socket.id) {
|
||||||
return
|
return
|
||||||
@@ -346,11 +358,11 @@ class Game {
|
|||||||
this.broadcastStatus(STATUS.SHOW_QUESTION, {
|
this.broadcastStatus(STATUS.SHOW_QUESTION, {
|
||||||
question: question.question,
|
question: question.question,
|
||||||
image: question.image,
|
image: question.image,
|
||||||
media: question.media,
|
media: question.media,
|
||||||
cooldown: question.cooldown,
|
cooldown: question.cooldown,
|
||||||
})
|
})
|
||||||
|
|
||||||
await sleep(question.cooldown)
|
await this.startCooldown(question.cooldown)
|
||||||
|
|
||||||
if (!this.started) {
|
if (!this.started) {
|
||||||
return
|
return
|
||||||
@@ -362,7 +374,7 @@ class Game {
|
|||||||
question: question.question,
|
question: question.question,
|
||||||
answers: question.answers,
|
answers: question.answers,
|
||||||
image: question.image,
|
image: question.image,
|
||||||
media: question.media,
|
media: question.media,
|
||||||
time: question.time,
|
time: question.time,
|
||||||
totalPlayer: this.players.length,
|
totalPlayer: this.players.length,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ const ManagerGame = () => {
|
|||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case STATUS.SHOW_QUESTION:
|
||||||
|
socket?.emit("manager:skipQuestionIntro", { gameId })
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
case STATUS.SELECT_ANSWER:
|
case STATUS.SELECT_ANSWER:
|
||||||
socket?.emit("manager:abortQuiz", { gameId })
|
socket?.emit("manager:abortQuiz", { gameId })
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => {
|
|||||||
const [uploading, setUploading] = useState<Record<number, boolean>>({})
|
const [uploading, setUploading] = useState<Record<number, boolean>>({})
|
||||||
const [deleting, setDeleting] = useState<Record<number, boolean>>({})
|
const [deleting, setDeleting] = useState<Record<number, boolean>>({})
|
||||||
const [refreshingLibrary, setRefreshingLibrary] = useState(false)
|
const [refreshingLibrary, setRefreshingLibrary] = useState(false)
|
||||||
|
const [probing, setProbing] = useState<Record<number, boolean>>({})
|
||||||
|
|
||||||
useEvent("manager:quizzLoaded", (quizz) => {
|
useEvent("manager:quizzLoaded", (quizz) => {
|
||||||
setDraft(quizz)
|
setDraft(quizz)
|
||||||
@@ -257,12 +258,86 @@ const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setQuestionMedia(qIndex, nextMedia)
|
setQuestionMedia(qIndex, nextMedia)
|
||||||
|
adjustTimingWithMedia(qIndex, nextMedia)
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearQuestionMedia = (qIndex: number) => {
|
const clearQuestionMedia = (qIndex: number) => {
|
||||||
setQuestionMedia(qIndex, undefined)
|
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) => {
|
const handleMediaUpload = async (qIndex: number, file: File) => {
|
||||||
if (!draft) return
|
if (!draft) return
|
||||||
const question = draft.questions[qIndex]
|
const question = draft.questions[qIndex]
|
||||||
@@ -296,6 +371,11 @@ const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => {
|
|||||||
url: uploaded.url,
|
url: uploaded.url,
|
||||||
fileName: uploaded.fileName,
|
fileName: uploaded.fileName,
|
||||||
})
|
})
|
||||||
|
adjustTimingWithMedia(qIndex, {
|
||||||
|
type,
|
||||||
|
url: uploaded.url,
|
||||||
|
fileName: uploaded.fileName,
|
||||||
|
})
|
||||||
toast.success("Media uploaded")
|
toast.success("Media uploaded")
|
||||||
refreshMediaLibrary()
|
refreshMediaLibrary()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -509,18 +589,20 @@ const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 rounded-md border border-gray-200 p-3">
|
<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">
|
<div className="flex items-center justify-between text-sm font-semibold text-gray-600">
|
||||||
<span>Media upload</span>
|
<span>Media upload</span>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
{isUploading
|
{isUploading
|
||||||
? "Uploading..."
|
? "Uploading..."
|
||||||
: refreshingLibrary
|
: probing[qIndex]
|
||||||
? "Refreshing..."
|
? "Probing..."
|
||||||
: mediaFileName
|
: refreshingLibrary
|
||||||
? "Stored"
|
? "Refreshing..."
|
||||||
: "Not saved"}
|
: mediaFileName
|
||||||
</span>
|
? "Stored"
|
||||||
|
: "Not saved"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const MANAGER_SKIP_BTN = {
|
|||||||
[STATUS.SHOW_ROOM]: "Start Game",
|
[STATUS.SHOW_ROOM]: "Start Game",
|
||||||
[STATUS.SHOW_START]: null,
|
[STATUS.SHOW_START]: null,
|
||||||
[STATUS.SHOW_PREPARED]: null,
|
[STATUS.SHOW_PREPARED]: null,
|
||||||
[STATUS.SHOW_QUESTION]: null,
|
[STATUS.SHOW_QUESTION]: "Skip",
|
||||||
[STATUS.SELECT_ANSWER]: "Skip",
|
[STATUS.SELECT_ANSWER]: "Skip",
|
||||||
[STATUS.SHOW_RESULT]: null,
|
[STATUS.SHOW_RESULT]: null,
|
||||||
[STATUS.SHOW_RESPONSES]: "Next",
|
[STATUS.SHOW_RESPONSES]: "Next",
|
||||||
|
|||||||
Reference in New Issue
Block a user