Added Audio Questions
This commit is contained in:
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
"editor.formatOnSave": true,
|
||||
"[svg]": {
|
||||
"editor.defaultFormatter": "jock.svg"
|
||||
},
|
||||
"svg.preview.background": "dark-transparent"
|
||||
}
|
||||
|
||||
78
src/lib/AudioMultipleChoiceQuestionComponent.svelte
Normal file
78
src/lib/AudioMultipleChoiceQuestionComponent.svelte
Normal file
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import AudioPlayerComponent from "./AudioPlayerComponent.svelte";
|
||||
import type { AudioMultipleChoiceQuestion } from "./games/games";
|
||||
|
||||
const path = "/sounds/";
|
||||
|
||||
interface Props {
|
||||
question: AudioMultipleChoiceQuestion;
|
||||
showAnswer: boolean;
|
||||
showQuestion: boolean;
|
||||
showPlayer: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
let { question, showAnswer, showQuestion, showPlayer }: Props = $props();
|
||||
|
||||
function shuffle<T>(array: T[]) {
|
||||
let currentIndex = array.length;
|
||||
|
||||
// While there remain elements to shuffle...
|
||||
while (currentIndex != 0) {
|
||||
// Pick a remaining element...
|
||||
let randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
|
||||
// And swap it with the current element.
|
||||
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
|
||||
}
|
||||
}
|
||||
|
||||
const answer = question.data.choices[0];
|
||||
|
||||
let _choices = [...question.data.choices];
|
||||
shuffle(_choices);
|
||||
</script>
|
||||
|
||||
<div class="mb-4 flex grow flex-col items-center text-6xl">
|
||||
{#if showQuestion}
|
||||
<div class="flex grow-1 items-center">
|
||||
<div class="text-center">{question.data.question}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if showPlayer}
|
||||
<div class="flex w-full flex-col justify-center">
|
||||
<AudioPlayerComponent src={path + question.data.audio} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if showQuestion}
|
||||
<div class="flex w-full grow-1 flex-wrap items-center justify-around gap-2">
|
||||
{#each _choices as choice}
|
||||
<div class="choiceCard {showAnswer && choice === answer ? 'answer' : ''}">
|
||||
{choice}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.choiceCard {
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: fit-content;
|
||||
padding: 32px;
|
||||
/* font-size: larger;
|
||||
font-weight: bold;
|
||||
cursor: pointer; */
|
||||
}
|
||||
|
||||
.answer {
|
||||
background-color: rgb(87, 255, 87);
|
||||
box-shadow: 0 0 20px 5px rgb(87, 255, 87);
|
||||
border: 1px solid rgb(50, 141, 50);
|
||||
}
|
||||
</style>
|
||||
217
src/lib/AudioPlayerComponent.svelte
Normal file
217
src/lib/AudioPlayerComponent.svelte
Normal file
@@ -0,0 +1,217 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
interface Props {
|
||||
src: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const saveKey = "jeopardyAudioVolume";
|
||||
|
||||
let { src }: Props = $props();
|
||||
|
||||
let time = $state(0);
|
||||
let duration = $state(0);
|
||||
let paused = $state(true);
|
||||
let volume = $state(100);
|
||||
|
||||
onMount(() => {
|
||||
let savedVolume = localStorage.getItem(saveKey);
|
||||
if (savedVolume != null) {
|
||||
let parsedVolume = parseFloat(savedVolume);
|
||||
if (!isNaN(parsedVolume)) {
|
||||
setVolume(parsedVolume);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function setVolume(vol: number): void {
|
||||
const audioPlayer = document.getElementById("AudioQuestionAudioPlayer") as HTMLMediaElement;
|
||||
volume = Math.round(vol * 100);
|
||||
if (audioPlayer) {
|
||||
audioPlayer.volume = vol;
|
||||
}
|
||||
}
|
||||
|
||||
function format(time: number) {
|
||||
if (isNaN(time)) return "...";
|
||||
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
|
||||
return `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={["player", { paused }]}>
|
||||
<audio
|
||||
id="AudioQuestionAudioPlayer"
|
||||
{src}
|
||||
bind:currentTime={time}
|
||||
bind:duration
|
||||
bind:paused
|
||||
onended={() => {
|
||||
time = 0;
|
||||
}}
|
||||
></audio>
|
||||
|
||||
<button class="play" aria-label={paused ? "play" : "pause"} onclick={() => (paused = !paused)}
|
||||
></button>
|
||||
|
||||
<div class="info">
|
||||
<div class="time">
|
||||
<span>{format(time)}</span>
|
||||
<div
|
||||
class="slider"
|
||||
onpointerdown={(e) => {
|
||||
const div = e.currentTarget;
|
||||
|
||||
function seek(e: PointerEvent) {
|
||||
const { left, width } = div.getBoundingClientRect();
|
||||
|
||||
let p = (e.clientX - left) / width;
|
||||
if (p < 0) p = 0;
|
||||
if (p > 1) p = 1;
|
||||
|
||||
time = p * duration;
|
||||
}
|
||||
|
||||
seek(e);
|
||||
|
||||
window.addEventListener("pointermove", seek);
|
||||
|
||||
window.addEventListener(
|
||||
"pointerup",
|
||||
() => {
|
||||
window.removeEventListener("pointermove", seek);
|
||||
},
|
||||
{
|
||||
once: true
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div class="progress" style="--progress: {time / duration}%"></div>
|
||||
</div>
|
||||
<span>{duration ? format(duration) : "--:--"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
<div class="info">
|
||||
<div class="time">
|
||||
<div
|
||||
class="slider"
|
||||
onpointerdown={(e) => {
|
||||
const div = e.currentTarget;
|
||||
|
||||
function seek(e: PointerEvent) {
|
||||
const { left, width } = div.getBoundingClientRect();
|
||||
|
||||
let p = (e.clientX - left) / width;
|
||||
if (p < 0) p = 0;
|
||||
if (p > 1) p = 1;
|
||||
|
||||
setVolume(p);
|
||||
localStorage.setItem(saveKey, p.toString());
|
||||
}
|
||||
|
||||
seek(e);
|
||||
|
||||
window.addEventListener("pointermove", seek);
|
||||
|
||||
window.addEventListener(
|
||||
"pointerup",
|
||||
() => {
|
||||
window.removeEventListener("pointermove", seek);
|
||||
},
|
||||
{
|
||||
once: true
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div class="volume" style="--volume: {volume / 100}%"></div>
|
||||
</div>
|
||||
<span>{volume}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.player {
|
||||
display: grid;
|
||||
grid-template-columns: 2.5em 1fr;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
padding: 0.5em 1em 0.5em 0.5em;
|
||||
border-radius: 2em;
|
||||
background: gray;
|
||||
transition: filter 0.2s;
|
||||
color: white;
|
||||
user-select: none;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.player:not(.paused) {
|
||||
color: white;
|
||||
filter: drop-shadow(0.5em 0.5em 1em rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.play {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
[aria-label="pause"] {
|
||||
background-image: url(/images/pause.svg);
|
||||
}
|
||||
|
||||
[aria-label="play"] {
|
||||
background-image: url(/images/play.svg);
|
||||
}
|
||||
|
||||
.info {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.time span {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex: 1;
|
||||
height: 0.5em;
|
||||
background: lightgray;
|
||||
border-radius: 0.5em;
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
}
|
||||
.slider:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: calc(100 * var(--progress));
|
||||
height: 100%;
|
||||
background: lightgreen;
|
||||
}
|
||||
.volume {
|
||||
width: calc(100 * var(--volume));
|
||||
height: 100%;
|
||||
background: lightgreen;
|
||||
}
|
||||
</style>
|
||||
34
src/lib/AudioQuestionComponent.svelte
Normal file
34
src/lib/AudioQuestionComponent.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import AudioPlayerComponent from "./AudioPlayerComponent.svelte";
|
||||
import type { AudioQuestion } from "./games/games";
|
||||
|
||||
const path = "/sounds/";
|
||||
|
||||
interface Props {
|
||||
question: AudioQuestion;
|
||||
showAnswer: boolean;
|
||||
showQuestion: boolean;
|
||||
showPlayer: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
let { question, showAnswer, showQuestion, showPlayer }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="mb-4 flex grow flex-col items-center text-6xl">
|
||||
{#if showQuestion || showAnswer}
|
||||
<div class="flex grow-1 items-center">
|
||||
<div class="text-center">{question.data.question}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if showPlayer}
|
||||
<div class="flex w-full flex-col justify-center">
|
||||
<AudioPlayerComponent src={path + question.data.audio} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if showAnswer}
|
||||
<div class="flex grow-1 items-center text-center">
|
||||
{question.data.answer}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -46,18 +46,27 @@ const games: Games = [
|
||||
},
|
||||
{
|
||||
points: 400,
|
||||
type: "SIMPLE",
|
||||
type: "AUDIO",
|
||||
data: {
|
||||
question: "Question 4?",
|
||||
audio: "music.mp3",
|
||||
answer: "Answer 4"
|
||||
}
|
||||
},
|
||||
{
|
||||
points: 500,
|
||||
type: "SIMPLE",
|
||||
type: "AUDIO_MULTIPLE_CHOICE",
|
||||
data: {
|
||||
question: "Question 5?",
|
||||
answer: "Answer 5"
|
||||
audio: "music.mp3",
|
||||
choices: [
|
||||
"Choice 1",
|
||||
"Choice 2",
|
||||
"Choice 3",
|
||||
"Choice 4",
|
||||
"Choice 5",
|
||||
"Choice 6"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1224,7 +1233,12 @@ const games: Games = [
|
||||
}
|
||||
];
|
||||
|
||||
export type QuestionType = "SIMPLE" | "MULTIPLE_CHOICE" | "IMAGE";
|
||||
export type QuestionType =
|
||||
| "SIMPLE"
|
||||
| "MULTIPLE_CHOICE"
|
||||
| "IMAGE"
|
||||
| "AUDIO"
|
||||
| "AUDIO_MULTIPLE_CHOICE";
|
||||
|
||||
export type Question = {
|
||||
points: number;
|
||||
@@ -1259,6 +1273,24 @@ export type ImageQuestion = Question & {
|
||||
};
|
||||
};
|
||||
|
||||
export type AudioQuestion = Question & {
|
||||
type: "AUDIO";
|
||||
data: {
|
||||
question: string;
|
||||
audio: string;
|
||||
answer: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type AudioMultipleChoiceQuestion = Question & {
|
||||
type: "AUDIO_MULTIPLE_CHOICE";
|
||||
data: {
|
||||
question: string;
|
||||
audio: string;
|
||||
choices: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export function isSimpleQuestion(question: Question): question is SimpleQuestion {
|
||||
return (question as SimpleQuestion).type === "SIMPLE";
|
||||
}
|
||||
@@ -1268,10 +1300,24 @@ export function isMultipleChoiceQuestion(question: Question): question is Multip
|
||||
export function isImageQuestion(question: Question): question is ImageQuestion {
|
||||
return (question as ImageQuestion).type === "IMAGE";
|
||||
}
|
||||
export function isAudioQuestion(question: Question): question is AudioQuestion {
|
||||
return (question as AudioQuestion).type === "AUDIO";
|
||||
}
|
||||
export function isAudioMultipleChoiceQuestion(
|
||||
question: Question
|
||||
): question is AudioMultipleChoiceQuestion {
|
||||
return (question as AudioMultipleChoiceQuestion).type === "AUDIO_MULTIPLE_CHOICE";
|
||||
}
|
||||
|
||||
export type Category = {
|
||||
name: string;
|
||||
questions: (SimpleQuestion | MultipleChoiceQuestion | ImageQuestion)[];
|
||||
questions: (
|
||||
| SimpleQuestion
|
||||
| MultipleChoiceQuestion
|
||||
| ImageQuestion
|
||||
| AudioQuestion
|
||||
| AudioMultipleChoiceQuestion
|
||||
)[];
|
||||
};
|
||||
|
||||
export type Wall = {
|
||||
|
||||
@@ -3,12 +3,20 @@
|
||||
import { page } from "$app/state";
|
||||
import { error } from "@sveltejs/kit";
|
||||
import SimpleQuestionComponent from "$lib/SimpleQuestionComponent.svelte";
|
||||
import { isImageQuestion, isMultipleChoiceQuestion, isSimpleQuestion } from "$lib/games/games";
|
||||
import {
|
||||
isAudioMultipleChoiceQuestion,
|
||||
isAudioQuestion,
|
||||
isImageQuestion,
|
||||
isMultipleChoiceQuestion,
|
||||
isSimpleQuestion
|
||||
} from "$lib/games/games";
|
||||
import ws from "$lib/websocket.svelte";
|
||||
import { MessageType } from "$lib/MessageType";
|
||||
import { untrack } from "svelte";
|
||||
import MultipleChoiceQuestionComponent from "$lib/MultipleChoiceQuestionComponent.svelte";
|
||||
import ImageQuestionComponent from "$lib/ImageQuestionComponent.svelte";
|
||||
import AudioQuestionComponent from "$lib/AudioQuestionComponent.svelte";
|
||||
import AudioMultipleChoiceQuestionComponent from "$lib/AudioMultipleChoiceQuestionComponent.svelte";
|
||||
|
||||
console.log("wall:", page.params.wall);
|
||||
|
||||
@@ -126,6 +134,15 @@
|
||||
<MultipleChoiceQuestionComponent {question} {showAnswer} {showQuestion} />
|
||||
{:else if isImageQuestion(question)}
|
||||
<ImageQuestionComponent {question} {showAnswer} {showQuestion} {isBuzzed} />
|
||||
{:else if isAudioQuestion(question)}
|
||||
<AudioQuestionComponent {question} {showAnswer} {showQuestion} showPlayer={false} />
|
||||
{:else if isAudioMultipleChoiceQuestion(question)}
|
||||
<AudioMultipleChoiceQuestionComponent
|
||||
{question}
|
||||
{showAnswer}
|
||||
{showQuestion}
|
||||
showPlayer={false}
|
||||
/>
|
||||
{:else}
|
||||
<p>Type of question unknown</p>
|
||||
{/if}
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
isMultipleChoiceQuestion,
|
||||
isSimpleQuestion,
|
||||
isImageQuestion,
|
||||
type Game
|
||||
type Game,
|
||||
isAudioQuestion,
|
||||
isAudioMultipleChoiceQuestion
|
||||
} from "$lib/games/games";
|
||||
import ws from "$lib/websocket.svelte";
|
||||
import { page } from "$app/state";
|
||||
@@ -18,6 +20,8 @@
|
||||
import type { VisitedQuestions } from "$lib/Types.js";
|
||||
import MultipleChoiceQuestionComponent from "$lib/MultipleChoiceQuestionComponent.svelte";
|
||||
import ImageQuestionComponent from "$lib/ImageQuestionComponent.svelte";
|
||||
import AudioQuestionComponent from "$lib/AudioQuestionComponent.svelte";
|
||||
import AudioMultipleChoiceQuestionComponent from "$lib/AudioMultipleChoiceQuestionComponent.svelte";
|
||||
|
||||
let startDisabled = $state(true);
|
||||
|
||||
@@ -366,6 +370,20 @@
|
||||
showQuestion={true}
|
||||
isBuzzed={false}
|
||||
/>
|
||||
{:else if isAudioQuestion(gameManager.question)}
|
||||
<AudioQuestionComponent
|
||||
question={gameManager.question}
|
||||
showAnswer={true}
|
||||
showPlayer={true}
|
||||
showQuestion={true}
|
||||
/>
|
||||
{:else if isAudioMultipleChoiceQuestion(gameManager.question)}
|
||||
<AudioMultipleChoiceQuestionComponent
|
||||
question={gameManager.question}
|
||||
showAnswer={true}
|
||||
showPlayer={true}
|
||||
showQuestion={true}
|
||||
/>
|
||||
{:else}
|
||||
<p>Type of question unknown</p>
|
||||
{/if}
|
||||
|
||||
10
static/images/pause.svg
Normal file
10
static/images/pause.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<style>
|
||||
rect {
|
||||
fill: white;
|
||||
stroke: white;
|
||||
}
|
||||
</style>
|
||||
<rect x='7' y='6' width='2' height='12' stroke-width='1.5' stroke-linejoin='round' />
|
||||
<rect x='15' y='6' width='2' height='12' stroke-width='1.5' stroke-linejoin='round' />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 396 B |
10
static/images/play.svg
Normal file
10
static/images/play.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<style>
|
||||
path {
|
||||
fill: white;
|
||||
stroke: white;
|
||||
}
|
||||
</style>
|
||||
<path d='M7.5 11.9999V5.93774L12.75 8.96884L18 11.9999L12.75 15.031L7.5 18.0621V11.9999Z' stroke-width='1.5'
|
||||
stroke-linejoin='round' />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
BIN
static/sounds/music.mp3
Normal file
BIN
static/sounds/music.mp3
Normal file
Binary file not shown.
Reference in New Issue
Block a user