Files
quizify/templates/quiz_buzzer.html

566 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<title>{{ translations['quiz_title'] }} Buzzer Mode</title>
<script src="https://sdk.scdn.co/spotify-player.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #0a0e27 0%, #1a1f3a 50%, #0f1419 100%);
color: #e0e0e0;
min-height: 100vh;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.quiz-container {
max-width: 900px;
width: 100%;
background: rgba(25, 30, 45, 0.95);
border-radius: 20px;
padding: 40px;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.buzzer-timer {
text-align: center;
font-size: 4em;
font-weight: bold;
color: #1DB954;
margin: 30px 0;
text-shadow: 0 0 20px rgba(29, 185, 84, 0.5);
}
.buzzer-timer.warning {
color: #FFA500;
animation: pulse 1s infinite;
}
.buzzer-timer.critical {
color: #f44336;
animation: pulse 0.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.05); }
}
.points-display {
text-align: center;
font-size: 2.5em;
font-weight: bold;
margin: 20px 0;
}
.points-display.high {
color: #4CAF50;
}
.points-display.medium {
color: #FFA500;
}
.points-display.low {
color: #f44336;
}
.buzzer-button {
display: block;
width: 250px;
height: 250px;
margin: 30px auto;
border-radius: 50%;
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
border: 10px solid rgba(244, 67, 54, 0.3);
font-size: 2em;
font-weight: bold;
color: white;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 10px 40px rgba(244, 67, 54, 0.5);
}
.buzzer-button:hover {
transform: scale(1.05);
box-shadow: 0 15px 50px rgba(244, 67, 54, 0.7);
}
.buzzer-button:active {
transform: scale(0.95);
}
.buzzer-button.buzzed {
background: linear-gradient(135deg, #666 0%, #444 100%);
border-color: rgba(100, 100, 100, 0.3);
cursor: not-allowed;
}
.answer-section {
display: none;
text-align: center;
margin-top: 30px;
}
.answer-section.active {
display: block;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.btn {
padding: 12px 24px;
margin: 5px;
background: linear-gradient(135deg, #1DB954 0%, #1ed760 100%);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 1em;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(29, 185, 84, 0.3);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(29, 185, 84, 0.5);
}
.btn-secondary {
background: linear-gradient(135deg, #535353 0%, #6b6b6b 100%);
box-shadow: 0 4px 15px rgba(83, 83, 83, 0.3);
}
.btn-danger {
background: linear-gradient(135deg, #f44336 0%, #e57373 100%);
}
input[type="text"] {
padding: 14px 20px;
width: 100%;
max-width: 400px;
border: 2px solid rgba(29, 185, 84, 0.3);
background: rgba(15, 20, 35, 0.6);
color: #e0e0e0;
border-radius: 25px;
font-size: 16px;
transition: all 0.3s ease;
outline: none;
}
input[type="text"]:focus {
border-color: #1DB954;
box-shadow: 0 0 15px rgba(29, 185, 84, 0.4);
}
.result-container {
margin: 25px 0;
padding: 20px;
border-radius: 15px;
text-align: center;
display: none;
}
.correct {
background: rgba(76, 175, 80, 0.15);
border: 2px solid #4CAF50;
}
.incorrect {
background: rgba(244, 67, 54, 0.15);
border: 2px solid #f44336;
}
.search-results {
margin-top: 15px;
max-height: 250px;
overflow-y: auto;
background: rgba(15, 20, 35, 0.9);
border: 1px solid rgba(29, 185, 84, 0.3);
border-radius: 12px;
display: none;
}
.search-item {
padding: 12px 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
cursor: pointer;
transition: all 0.2s ease;
color: #e0e0e0;
}
.search-item:hover {
background: rgba(29, 185, 84, 0.2);
padding-left: 20px;
}
.progress-info {
display: flex;
justify-content: space-around;
align-items: center;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 20px;
padding: 15px;
background: rgba(29, 185, 84, 0.1);
border-radius: 15px;
border: 1px solid rgba(29, 185, 84, 0.3);
}
.progress-item {
text-align: center;
}
.progress-label {
font-size: 0.85em;
color: #999;
text-transform: uppercase;
letter-spacing: 1px;
}
.progress-value {
font-size: 1.5em;
font-weight: bold;
color: #1DB954;
margin-top: 5px;
}
</style>
<script>
let allTracks = {{ all_tracks|tojson }};
let currentGameMode = "{{ game_mode }}";
let correctAnswer = "";
const i18n = {{ translations|tojson }};
let buzzTimer = null;
let startTime = null;
let buzzTime = null;
let maxPoints = {{ max_points | default(1000) }};
let gracePeriod = {{ grace_period | default(5) }}; // Sekunden
let decayRate = {{ decay_rate | default(50) }}; // Punkte pro Sekunde nach Grace Period
let hasBuzzed = false;
let gameStarted = false;
window.onSpotifyWebPlaybackSDKReady = () => {
const token = '{{ access_token }}';
const player = new Spotify.Player({
name: 'Musik Quiz Buzzer Player',
getOAuthToken: cb => { cb(token); },
volume: 0.5
});
player.addListener('initialization_error', ({ message }) => { console.error(message); });
player.addListener('authentication_error', ({ message }) => { console.error(message); });
player.addListener('account_error', ({ message }) => { console.error(message); });
player.addListener('playback_error', ({ message }) => { console.error(message); });
player.addListener('ready', ({ device_id }) => {
console.log('Ready with Device ID', device_id);
document.getElementById('device_id').value = device_id;
window.deviceId = device_id;
// Musik wird NICHT automatisch gestartet
setCorrectAnswer();
});
player.addListener('player_state_changed', state => {
if (!state) return;
// Wenn Musik anfängt zu spielen und Timer noch nicht gestartet wurde
if (!state.paused && gameStarted && !startTime) {
console.log('Music started playing, starting timer now');
startBuzzerTimer();
}
});
player.addListener('not_ready', ({ device_id }) => {
console.log('Device ID has gone offline', device_id);
});
player.connect();
window.spotifyPlayer = player;
};
function startBuzzerTimer() {
startTime = Date.now();
updateTimer();
}
function updateTimer() {
if (hasBuzzed) return;
const elapsed = (Date.now() - startTime) / 1000;
const timerDisplay = document.getElementById('buzzerTimer');
const pointsDisplay = document.getElementById('pointsDisplay');
timerDisplay.textContent = elapsed.toFixed(2) + 's';
// Berechne Punkte
const currentPoints = calculatePoints(elapsed);
pointsDisplay.textContent = currentPoints + ' Punkte';
// Farben anpassen
timerDisplay.className = 'buzzer-timer';
pointsDisplay.className = 'points-display';
if (elapsed > gracePeriod + 10) {
timerDisplay.classList.add('critical');
pointsDisplay.classList.add('low');
} else if (elapsed > gracePeriod) {
timerDisplay.classList.add('warning');
pointsDisplay.classList.add('medium');
} else {
pointsDisplay.classList.add('high');
}
buzzTimer = requestAnimationFrame(updateTimer);
}
function calculatePoints(elapsed) {
if (elapsed <= gracePeriod) {
return maxPoints;
}
const overtime = elapsed - gracePeriod;
const points = Math.max(0, maxPoints - Math.floor(overtime * decayRate));
return points;
}
function buzz() {
if (!gameStarted) {
// Erster Klick: Starte Spiel
gameStarted = true;
document.getElementById('buzzerButton').innerHTML = '🔴<br>BUZZ!';
document.getElementById('buzzerButton').style.background = 'linear-gradient(135deg, #666 0%, #444 100%)';
document.getElementById('buzzerButton').style.cursor = 'wait';
// Starte Musik - Timer startet automatisch wenn Musik wirklich abgespielt wird
const device_id = window.deviceId;
const startPosition = getOption('startPosition', 'start');
let position_ms = 0;
if (startPosition === 'random') {
const duration = {{ track.duration_ms if track.duration_ms else 180000 }};
position_ms = Math.floor(Math.random() * (duration - 30000));
}
fetch(`/play_track?device_id=${device_id}&track_uri={{ track.uri }}&position_ms=${position_ms}`, { method: 'POST' })
.then(response => response.json())
.then(() => {
console.log('Play command sent');
document.getElementById('buzzerButton').style.background = 'linear-gradient(135deg, #f44336 0%, #d32f2f 100%)';
document.getElementById('buzzerButton').style.cursor = 'pointer';
})
.catch(error => console.error('Error starting playback:', error));
return;
}
// Ab hier: normaler Buzzer-Klick
if (hasBuzzed) return;
hasBuzzed = true;
buzzTime = Date.now();
const elapsed = (buzzTime - startTime) / 1000;
cancelAnimationFrame(buzzTimer);
// Pausiere Musik
if (window.spotifyPlayer) {
window.spotifyPlayer.pause();
}
// Zeige Antwortsektion
document.getElementById('answerSection').classList.add('active');
document.getElementById('buzzerButton').classList.add('buzzed');
document.getElementById('buzzerButton').disabled = true;
// Speichere erreichte Punkte
window.earnedPoints = calculatePoints(elapsed);
}
function setCorrectAnswer() {
if (currentGameMode === 'artist') {
correctAnswer = "{{ track.artists[0].name | clean }}";
document.getElementById('question-text').innerText = i18n.question_artist || 'Guess the artist!';
document.getElementById('answerInput').placeholder = i18n.input_artist || 'Artist name';
} else if (currentGameMode === 'title') {
correctAnswer = "{{ track.name | clean }}";
document.getElementById('question-text').innerText = i18n.question_title || 'Guess the title!';
document.getElementById('answerInput').placeholder = i18n.input_title || 'Song title';
} else if (currentGameMode === 'year') {
correctAnswer = "{{ track.album.release_date[:4] }}";
document.getElementById('question-text').innerText = i18n.question_year || 'Guess the year!';
document.getElementById('answerInput').placeholder = i18n.input_year || 'Year';
document.getElementById('answerInput').type = "number";
}
}
function searchTracks() {
const query = document.getElementById('answerInput').value;
if (query.length < 2) {
document.getElementById('searchResults').style.display = 'none';
return;
}
fetch('/search_track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: query,
all_tracks: allTracks
})
})
.then(response => response.json())
.then(data => {
const resultsContainer = document.getElementById('searchResults');
resultsContainer.innerHTML = '';
if (data.results.length === 0) {
resultsContainer.style.display = 'none';
return;
}
data.results.forEach(result => {
const item = document.createElement('div');
item.className = 'search-item';
item.innerHTML = `<strong>${result.name}</strong> - ${result.artist}`;
item.onclick = function() {
document.getElementById('answerInput').value =
currentGameMode === 'artist' ? result.artist : result.name;
resultsContainer.style.display = 'none';
};
resultsContainer.appendChild(item);
});
resultsContainer.style.display = 'block';
})
.catch(error => {
console.error('Error searching tracks:', error);
});
}
function checkAnswer() {
const guess = document.getElementById('answerInput').value;
if (!guess) return;
fetch('/check_answer_buzzer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
guess: guess,
correct_answer: correctAnswer,
game_mode: currentGameMode,
playlist_id: "{{ playlist_id }}",
earned_points: window.earnedPoints,
all_tracks: allTracks
})
})
.then(response => response.json())
.then(data => {
const resultContainer = document.getElementById('resultContainer');
resultContainer.style.display = 'block';
if (data.correct) {
resultContainer.className = 'result-container correct';
resultContainer.innerHTML = `<h3>✓ ${i18n.correct || 'Correct'}!</h3>
<p style="font-size:1.5em; color:#4CAF50;">+${window.earnedPoints} Punkte</p>`;
} else {
resultContainer.className = 'result-container incorrect';
if (data.guessed_track) {
resultContainer.innerHTML = `
<h3>✗ ${i18n.wrong || 'Wrong'}!</h3>
<p style="color:#f44336;">0 Punkte</p>
<div style="display:flex; gap:20px; margin-top:20px; justify-content:center; flex-wrap:wrap;">
<div style="flex:1; min-width:250px; max-width:350px; padding:15px; background:rgba(244,67,54,0.15); border:2px solid #f44336; border-radius:12px;">
<h4 style="color:#f44336; margin-bottom:10px;">❌ ${i18n.your_answer || 'Your Answer'}</h4>
<p style="margin:5px 0;"><strong>${i18n.song || 'Song'}:</strong> ${data.guessed_track.name}</p>
<p style="margin:5px 0;"><strong>${i18n.artist || 'Artist'}:</strong> ${data.guessed_track.artist}</p>
</div>
<div style="flex:1; min-width:250px; max-width:350px; padding:15px; background:rgba(76,175,80,0.15); border:2px solid #4CAF50; border-radius:12px;">
<h4 style="color:#4CAF50; margin-bottom:10px;">✓ ${i18n.correct_answer || 'Correct Answer'}</h4>
<p style="margin:5px 0;"><strong>${i18n.song || 'Song'}:</strong> {{ track.name | clean }}</p>
<p style="margin:5px 0;"><strong>${i18n.artist || 'Artist'}:</strong> {{ track.artists[0].name | clean }}</p>
</div>
</div>
`;
} else {
resultContainer.innerHTML = `<h3>✗ ${i18n.wrong || 'Wrong'}!</h3>
<p>0 Punkte</p>
<p>${i18n.right_answer || 'Correct answer'}: <strong>${data.correct_answer}</strong></p>`;
}
}
resultContainer.innerHTML += `
<div style="margin-top:20px; padding:20px; background:rgba(15,20,35,0.8); border-radius:15px; border:1px solid rgba(29,185,84,0.3);">
<img src="{{ track.album.images[0].url }}" alt="Cover" style="width:120px; height:120px; border-radius:12px; margin-bottom:15px; box-shadow:0 4px 15px rgba(0,0,0,0.5);"><br>
<div style="text-align:left; display:inline-block; margin-top:10px;">
<p style="margin:8px 0;"><strong style="color:#1DB954;">${i18n.song || 'Song'}:</strong> <span style="color:#e0e0e0;">{{ track.name | clean }}</span></p>
<p style="margin:8px 0;"><strong style="color:#1DB954;">${i18n.artist || 'Artist'}:</strong> <span style="color:#e0e0e0;">{{ track.artists[0].name | clean }}</span></p>
<p style="margin:8px 0;"><strong style="color:#1DB954;">${i18n.album || 'Album'}:</strong> <span style="color:#e0e0e0;">{{ track.album.name | clean }}</span></p>
<p style="margin:8px 0;"><strong style="color:#1DB954;">${i18n.year || 'Year'}:</strong> <span style="color:#e0e0e0;">{{ track.album.release_date[:4] }}</span></p>
<a href="{{ track.external_urls.spotify }}" target="_blank" style="display:inline-block; margin-top:10px; padding:8px 16px; background:linear-gradient(135deg, #1DB954 0%, #1ed760 100%); color:#fff; text-decoration:none; border-radius:20px; font-weight:600; transition:all 0.3s ease;">${i18n.open_on_spotify || 'Open on Spotify'}</a>
</div>
</div>
`;
document.getElementById('nextQuestionBtn').style.display = 'inline-block';
})
.catch(error => {
console.error('Error checking answer:', error);
});
}
function switchGameMode(mode) {
window.location.href = `/reset_quiz/{{ playlist_id }}?next_mode=${mode}&buzzer=1`;
}
function getOption(key, defaultValue) {
return localStorage.getItem(key) || defaultValue;
}
window.onload = function() {
document.getElementById('startPosition').value = getOption('startPosition', 'start');
};
</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="quiz-container">
<!-- Progress Info -->
<div class="progress-info">
<div class="progress-item">
<div class="progress-label">{{ translations['songs_in_playlist'] if translations.get('songs_in_playlist') else 'Songs' }}</div>
<div class="progress-value">{{ total_questions }}</div>
</div>
<div class="progress-item">
<div class="progress-label">{{ translations['score'] if translations.get('score') else 'Score' }}</div>
<div class="progress-value">{{ score }}</div>
</div>
<div class="progress-item">
<div class="progress-label">{{ translations['answered'] if translations.get('answered') else 'Answered' }}</div>
<div class="progress-value">{{ answered }}</div>
</div>
</div>
<input type="hidden" id="device_id" value="">
<h2 id="question-text" style="text-align:center; margin:20px 0;">{{ translations['question_artist'] }}</h2>
<!-- Timer & Points Display -->
<div class="buzzer-timer" id="buzzerTimer">0.00s</div>
<div class="points-display high" id="pointsDisplay">{{ max_points | default(1000) }} Punkte</div>
<!-- Buzzer Button -->
<button class="buzzer-button" id="buzzerButton" onclick="buzz()">
▶️<br>START
</button>
<!-- Answer Section (hidden until buzzed) -->
<div class="answer-section" id="answerSection">
<h3 style="margin-bottom:15px;">{{ translations['enter_answer'] if translations.get('enter_answer') else 'Enter your answer' }}:</h3>
<input type="text" id="answerInput" placeholder="{{ translations['input_artist'] }}" oninput="searchTracks()">
<button class="btn" onclick="checkAnswer()">{{ translations['answer_button'] if translations.get('answer_button') else 'Submit' }}</button>
<div id="searchResults" class="search-results"></div>
<div id="resultContainer" class="result-container"></div>
<a id="nextQuestionBtn" href="/quiz/{{ playlist_id }}?mode={{ game_mode }}&buzzer=1" class="btn" style="display: none; margin-top:15px;">{{ translations['next_question'] if translations.get('next_question') else 'Next Question' }}</a>
</div>
<!-- Game Mode Switcher -->
<div style="text-align:center; margin-top:30px;">
<button class="btn {{ 'btn-success' if game_mode == 'artist' else 'btn-secondary' }}" onclick="switchGameMode('artist')">{{ translations['guess_artist'] }}</button>
<button class="btn {{ 'btn-success' if game_mode == 'title' else 'btn-secondary' }}" onclick="switchGameMode('title')">{{ translations['guess_title'] }}</button>
<button class="btn {{ 'btn-success' if game_mode == 'year' else 'btn-secondary' }}" onclick="switchGameMode('year')">{{ translations['guess_year'] }}</button>
</div>
<!-- Navigation -->
<div style="text-align:center; margin-top:30px;">
<a href="/reset_quiz/{{ playlist_id }}?buzzer=1" class="btn btn-danger">{{ translations['end_quiz'] if translations.get('end_quiz') else '🏁 End Quiz' }}</a>
<a href="/playlists" class="btn btn-secondary">{{ translations['back_to_playlists'] if translations.get('back_to_playlists') else '⬅️ Back' }}</a>
</div>
</div>
</body>
</html>