Files
quizify/templates/quiz.html
Simon 97d8444441 modified: app.py
modified:   templates/quiz.html
2025-11-14 23:56:46 +01:00

540 lines
20 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'] }}</title>
<!-- Spotify Web Playback SDK -->
<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);
}
.header-section {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.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;
}
.controls {
margin: 25px 0;
text-align: center;
}
.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);
text-decoration: none;
display: inline-block;
}
.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-secondary:hover {
box-shadow: 0 6px 20px rgba(83, 83, 83, 0.5);
}
.btn-success {
background: linear-gradient(135deg, #4CAF50 0%, #66bb6a 100%);
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
}
.btn-danger {
background: linear-gradient(135deg, #f44336 0%, #e57373 100%);
box-shadow: 0 4px 15px rgba(244, 67, 54, 0.3);
}
.game-modes {
margin: 25px 0;
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
.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;
}
.result-container {
margin: 25px 0;
padding: 20px;
border-radius: 15px;
text-align: center;
display: none;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.correct {
background: rgba(76, 175, 80, 0.15);
border: 2px solid #4CAF50;
}
.incorrect {
background: rgba(244, 67, 54, 0.15);
border: 2px solid #f44336;
}
input[type="text"], input[type="number"], select {
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, input[type="number"]:focus, select:focus {
border-color: #1DB954;
box-shadow: 0 0 15px rgba(29, 185, 84, 0.4);
}
h2 {
color: #ffffff;
text-align: center;
font-size: 2em;
margin-bottom: 15px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
}
.hint-container {
margin: 20px 0;
font-style: italic;
color: #999;
text-align: center;
padding: 10px;
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
}
.game-options {
text-align: center;
margin-bottom: 25px;
padding: 20px;
background: rgba(15, 20, 35, 0.5);
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.game-options label {
margin: 0 15px;
color: #b0b0b0;
font-weight: 500;
}
.game-options select {
max-width: 150px;
margin: 5px;
}
@media (max-width: 600px) {
.quiz-container {
padding: 20px;
}
input[type="text"], input[type="number"] {
width: 100%;
}
.progress-info {
flex-direction: column;
}
}
</style>
<script>
// Speichern aller Tracks für die Suche
let allTracks = {{ all_tracks|tojson }};
let currentGameMode = "{{ game_mode }}";
let correctAnswer = "";
// translations für JS verfügbar machen
const i18n = {{ translations|tojson }};
// Wird aufgerufen, wenn Spotify Web Playback SDK geladen ist
window.onSpotifyWebPlaybackSDKReady = () => {
const token = '{{ access_token }}';
const player = new Spotify.Player({
name: 'Musik Quiz Player',
getOAuthToken: cb => { cb(token); },
volume: 0.5
});
// Error handling
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); });
// Playback status updates
player.addListener('player_state_changed', state => {
console.log(state);
updatePlayButton(state);
});
// Ready
player.addListener('ready', ({ device_id }) => {
console.log('Ready with Device ID', device_id);
document.getElementById('device_id').value = device_id;
// Hole Optionen
const playDuration = parseInt(getOption('playDuration', '0'), 10);
const startPosition = getOption('startPosition', 'start');
let position_ms = 0;
if (startPosition === 'random') {
// Schätze Songlänge (du kannst {{ track.duration_ms }} als Variable mitgeben!)
const duration = {{ track.duration_ms if track.duration_ms else 180000 }};
position_ms = Math.floor(Math.random() * (duration - 30000)); // max 30s vor Ende
}
// Starte Wiedergabe an gewünschter Stelle
fetch(`/play_track?device_id=${device_id}&track_uri={{ track.uri }}&position_ms=${position_ms}`, { method: 'POST' })
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.catch(error => {
console.error('Error starting playback:', error);
});
// Stoppe nach playDuration Sekunden (wenn nicht unendlich)
if (playDuration > 0) {
setTimeout(() => {
player.pause();
}, playDuration * 1000);
}
setCorrectAnswer();
});
// Not Ready
player.addListener('not_ready', ({ device_id }) => {
console.log('Device ID has gone offline', device_id);
});
// Connect to the player!
player.connect();
// Globale Referenz zum Player für andere Funktionen
window.spotifyPlayer = player;
};
function updatePlayButton(state) {
let playButton = document.getElementById('playPauseBtn');
if (state && !state.paused) {
playButton.innerHTML = i18n.pause;
} else {
playButton.innerHTML = '▶️ Play';
}
}
function setCorrectAnswer() {
if (currentGameMode === 'artist') {
correctAnswer = "{{ track.artists[0].name }}";
document.getElementById('question-text').innerText = i18n.question_artist;
document.getElementById('answerInput').placeholder = i18n.input_artist;
} else if (currentGameMode === 'title') {
correctAnswer = "{{ track.name }}";
document.getElementById('question-text').innerText = i18n.question_title;
document.getElementById('answerInput').placeholder = i18n.input_title;
} else if (currentGameMode === 'year') {
correctAnswer = "{{ track.album.release_date[:4] }}";
document.getElementById('question-text').innerText = i18n.question_year;
document.getElementById('answerInput').placeholder = i18n.input_year;
document.getElementById('answerInput').type = "number";
}
}
// Hilfsfunktion: Entfernt Sonderzeichen wie in Python clean_title
function cleanTitle(title) {
// Entferne Klammerzusätze
title = title.replace(/(\s*[\(\[][^)\]]*[\)\]])/g, '');
// Entferne Apostrophe und Anführungszeichen
title = title.replace(/['''`´ʼ""„""']/g, '');
// Entferne weitere Sonderzeichen (außer Buchstaben, Zahlen, Leerzeichen, - und &)
title = title.replace(/[^\w\s\-&]/g, '');
return title.trim();
}
function togglePlay() {
const deviceId = document.getElementById('device_id').value;
fetch('/toggle_playback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_id: deviceId })
})
.then(response => response.json())
.catch(error => {
console.error('Error toggling playback:', error);
});
}
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() {
// Setze den bereinigten Wert (ohne Sonderzeichen) ins Input
const valueToSet = currentGameMode === 'artist' ? result.artist : result.name;
document.getElementById('answerInput').value = cleanTitle(valueToSet);
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', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
guess: guess,
correct_answer: correctAnswer,
game_mode: currentGameMode,
playlist_id: "{{ playlist_id }}"
})
})
.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}</h3>`;
} else {
resultContainer.className = 'result-container incorrect';
resultContainer.innerHTML = `<h3>${i18n.wrong}</h3>
<p>${i18n.right_answer} <strong>${data.correct_answer}</strong></p>`;
}
// Song-Infos ergänzen
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 }}</span></p>
<p style="margin:8px 0;"><strong style="color:#1DB954;">${i18n.artist || 'Artist'}:</strong> <span style="color:#e0e0e0;">{{ track.artists[0].name }}</span></p>
<p style="margin:8px 0;"><strong style="color:#1DB954;">${i18n.album || 'Album'}:</strong> <span style="color:#e0e0e0;">{{ track.album.name }}</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>
`;
// Zeige den "Nächste Frage" Button
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}`;
}
function setOption(key, value) {
localStorage.setItem(key, value);
}
function getOption(key, defaultValue) {
return localStorage.getItem(key) || defaultValue;
}
window.onload = function() {
document.getElementById('playDuration').value = getOption('playDuration', '0');
document.getElementById('startPosition').value = getOption('startPosition', 'start');
};
</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="quiz-container">
<!-- Header Section -->
<div class="header-section">
<h2 id="question-text">{{ translations['question_artist'] }}</h2>
<div class="progress-info">
<div class="progress-item">
<div class="progress-label">{{ translations['songs_in_playlist'] if translations.get('songs_in_playlist') else 'Songs in Playlist' }}</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 }} / {{ answered if answered > 0 else 1 }}</div>
</div>
<div class="progress-item">
<div class="progress-label">{{ translations['accuracy'] if translations.get('accuracy') else 'Accuracy' }}</div>
<div class="progress-value">{{ ((score / (answered if answered > 0 else 1)) * 100) | round(0) if answered > 0 else 0 }}%</div>
</div>
</div>
</div>
<!-- Verstecktes Feld für device_id -->
<input type="hidden" id="device_id" value="">
<!-- Spielmodi -->
<div class="game-modes">
<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>
<!-- Optionen für das Spiel -->
<div class="game-options">
<label>{{ translations['play_duration'] if translations.get('play_duration') else 'Play Duration' }}:
<select id="playDuration" onchange="setOption('playDuration', this.value)">
<option value="10">10s</option>
<option value="15">15s</option>
<option value="30">30s</option>
<option value="0" selected>{{ translations['unlimited'] if translations.get('unlimited') else 'Unlimited' }}</option>
</select>
</label>
<label style="margin-left:20px;">{{ translations['start_position'] if translations.get('start_position') else 'Start Position' }}:
<select id="startPosition" onchange="setOption('startPosition', this.value)">
<option value="start" selected>{{ translations['start'] if translations.get('start') else 'Start' }}</option>
<option value="random">{{ translations['random'] if translations.get('random') else 'Random' }}</option>
</select>
</label>
</div>
<!-- Player Controls -->
<div class="controls">
<button id="playPauseBtn" class="btn" onclick="togglePlay()">{{ translations['pause'] if translations.get('pause') else '⏸ Pause' }}</button>
</div>
<!-- Antwort-Eingabe -->
<div style="text-align: center; margin-top: 30px;">
<input type="text" id="answerInput" placeholder="{{ translations['input_artist'] }}" oninput="searchTracks()">
<button class="btn" onclick="checkAnswer()">{{ translations['answer_button'] }}</button>
<!-- Suchergebnisse -->
<div id="searchResults" class="search-results"></div>
<!-- Ergebnis-Anzeige -->
<div id="resultContainer" class="result-container"></div>
<!-- Nächste Frage Button, wird nach Antwort angezeigt -->
<a id="nextQuestionBtn" href="/quiz/{{ playlist_id }}?mode={{ game_mode }}" class="btn" style="display: none;">{{ translations['next_question'] }}</a>
</div>
<!-- Hilfe-Text je nach Modus -->
<div class="hint-container">
{% if game_mode == 'artist' %}
<p>{{ translations['tip_artist'] if translations.get('tip_artist') else 'Tip: Start typing to search for artists' }}</p>
{% elif game_mode == 'title' %}
<p>{{ translations['tip_title'] if translations.get('tip_title') else 'Tip: Start typing to search for song titles' }}</p>
{% elif game_mode == 'year' %}
<p>{{ translations['tip_year'] if translations.get('tip_year') else 'Tip: Enter the release year' }}</p>
{% endif %}
</div>
<!-- Navigation -->
<div class="controls">
<a href="/reset_quiz/{{ playlist_id }}" 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 to Playlists' }}</a>
</div>
</div>
</body>
</html>