From 97b3cc4d4766b698b1f4faf06dc13349da25af37 Mon Sep 17 00:00:00 2001 From: Peter Prince Date: Thu, 8 Aug 2024 16:18:54 +0100 Subject: [PATCH] Add modals and help --- index.html | 4 +- js/game.js | 6 ++- js/pkce.js | 35 ++++++++++---- js/player.js | 31 ++++++++++++- js/songClips.js | 91 +++++++++++++++++++++++++++++++----- js/timer.js | 121 +++++++++++++++++++++++++++++------------------- play.css | 2 +- play.html | 38 ++++++++++++++- play.js | 14 ++++-- 9 files changed, 261 insertions(+), 81 deletions(-) diff --git a/index.html b/index.html index 96f3b95..0e677be 100644 --- a/index.html +++ b/index.html @@ -6,8 +6,8 @@ Spotify Clip Quiz - - + + diff --git a/js/game.js b/js/game.js index 76623a0..0a685e3 100644 --- a/js/game.js +++ b/js/game.js @@ -4,8 +4,8 @@ * July 2024 *****************************************************************************/ -/* global populateClipList, connectToPlayer, endTimer, playAll */ -/* global playClipButtons, startTimerButton */ +/* global populateClipList, connectToPlayer, endTimer, playAll, stopClip */ +/* global playClipButtons, startTimerButton, helpButton */ /* global guessInput, giveUpButton, unguessedClips, revealSong */ /* global songClips */ @@ -44,6 +44,8 @@ function startGame () { guessInput.disabled = false; giveUpButton.disabled = false; + helpButton.disabled = false; + } function endGame () { diff --git a/js/pkce.js b/js/pkce.js index 4bc5373..9ea24a8 100644 --- a/js/pkce.js +++ b/js/pkce.js @@ -5,10 +5,11 @@ *****************************************************************************/ /* global localStorage, location */ +/* global enablePrepareUI */ const clientId = 'b91c4f9175aa4b9d8ed6f43c23a5620c'; -let redirectUri = (location.hostname === 'localhost' || location.hostname === '127.0.0.1') ? 'http://localhost:8000' : 'https://pcprince.co.uk/music-quiz'; -redirectUri += '/play.html'; +const homeURL = (location.hostname === 'localhost' || location.hostname === '127.0.0.1') ? 'http://localhost:8000' : 'https://pcprince.co.uk/music-quiz'; +const redirectURL = homeURL + '/play.html'; const scope = 'streaming user-read-email user-read-private'; const authUrl = new URL('https://accounts.spotify.com/authorize'); @@ -56,7 +57,7 @@ async function redirect () { scope, code_challenge_method: 'S256', code_challenge: codeChallenge, - redirect_uri: redirectUri + redirect_uri: redirectURL }; authUrl.search = new URLSearchParams(params).toString(); @@ -82,7 +83,7 @@ async function authorise () { client_id: clientId, grant_type: 'authorization_code', code, - redirect_uri: redirectUri, + redirect_uri: redirectURL, code_verifier: codeVerifier }) }; @@ -92,13 +93,31 @@ async function authorise () { const body = await fetch(url, payload); const response = await body.json(); - localStorage.setItem('access_token', response.access_token); + if (response.access_token) { - console.log('Obtained access token:', response.access_token); + localStorage.setItem('access_token', response.access_token); - token = response.access_token; + console.log('Obtained access token:', response.access_token); - spotifyReady = true; + token = response.access_token; + + spotifyReady = true; + + enablePrepareUI(); + + } else { + + console.error('Failed to verify code'); + console.error('Error:', response.error); + console.log('Redirecting to home page in 3 seconds...'); + + setTimeout(() => { + + window.location.href = homeURL; + + }, 3000); + + } } else { diff --git a/js/player.js b/js/player.js index dee8f30..00926df 100644 --- a/js/player.js +++ b/js/player.js @@ -6,12 +6,15 @@ /* global Spotify */ /* global token, songClips, updateGuessUI */ +/* global helpButton */ // Define browser elements at the top of the script const stopButton = document.getElementById('stop-button'); const resumeButton = document.getElementById('resume-button'); + const prevButton = document.getElementById('prev-button'); const nextButton = document.getElementById('next-button'); + const clipInfo = document.getElementById('clip-info'); let currentClipIndex = 0; @@ -20,6 +23,9 @@ let isStopped = false; let player; let deviceId; +let stopAttempts = 0; +const MAX_STOP_ATTEMPTS = 5; + function playSpecificClip (index) { currentClipIndex = index; @@ -173,6 +179,15 @@ function playClipsSequentially () { function stopClip () { + stopAttempts++; + + if (stopAttempts > MAX_STOP_ATTEMPTS) { + + console.error('Failed to stop playback after ' + MAX_STOP_ATTEMPTS + ' attempts. Giving up.'); + return; + + } + if (currentClipIndex !== -1) { console.log('Stopping:', currentClipIndex); @@ -190,9 +205,19 @@ function stopClip () { headers: { 'Authorization': `Bearer ${token}` } - }).then(() => { + }).then((response) => { - isStopped = true; + if (response.ok) { + + isStopped = true; + stopAttempts = 0; + + } else { + + console.log('Failed to stop clip. Trying again...'); + stopClip(); + + } }); @@ -268,4 +293,6 @@ function resetUI () { prevButton.disabled = true; nextButton.disabled = true; + helpButton.disabled = true; + } diff --git a/js/songClips.js b/js/songClips.js index 70d9178..65e8d0c 100644 --- a/js/songClips.js +++ b/js/songClips.js @@ -6,11 +6,22 @@ // https://open.spotify.com/playlist/5Rrf7mqN8uus2AaQQQNdc1 -/* global token, prepareUI */ -/* global stopButton, resumeButton, prevButton, nextButton, playSpecificClip */ +/* global bootstrap */ +/* global token, currentClipIndex */ +/* global stopButton, resumeButton, prevButton, nextButton */ +/* global prepareUI, resumeClip, stopClip, pauseTimer, resumeTimer, addSecondsToTimer, playSpecificClip */ const songUiContainer = document.getElementById('song-ui-container'); +const helpButton = document.getElementById('help-button'); +const helpModal = new bootstrap.Modal(document.getElementById('help-modal'), { + backdrop: 'static', + keyboard: false +}); +const reselectNumberSpan = document.getElementById('reselect-number-span'); +const cancelReselectButton = document.getElementById('cancel-reselect-button'); +const reselectButton = document.getElementById('reselect-button'); + // Song clip JSON structure const fixedSongs = [ { @@ -38,6 +49,21 @@ const DISTANCE_FROM_END_MS = 10000; let songClips = []; +function selectRandomClip (durationMs) { + + // Ensure that the start time is within valid range + const maxStartTime = Math.max(0, durationMs - DISTANCE_FROM_END_MS - 5000); // Last 10 seconds + 5 seconds clip length + const minStartTime = DISTANCE_FROM_START_MS; // Avoid the first 10 seconds + let startTime = Math.floor(Math.random() * (maxStartTime - minStartTime + 1)) + minStartTime; + startTime = Math.floor(startTime / 1000); + + let clipLength = 5000; // Always 5 seconds + clipLength = Math.floor(clipLength / 1000); + + return {startTime, clipLength}; + +} + async function getRandomSongsFromSpotifyRadio (playlistId, numberOfSongs, token) { const allTracks = []; @@ -75,21 +101,19 @@ async function getRandomSongsFromSpotifyRadio (playlistId, numberOfSongs, token) const track = item.track; const durationMs = track.duration_ms; // Track duration in milliseconds - // Ensure that the start time is within valid range - const maxStartTime = Math.max(0, durationMs - DISTANCE_FROM_END_MS - 5000); // Last 10 seconds + 5 seconds clip length - const minStartTime = DISTANCE_FROM_START_MS; // Avoid the first 10 seconds - const startTime = Math.floor(Math.random() * (maxStartTime - minStartTime + 1)) + minStartTime; + const randomClip = selectRandomClip(durationMs); // Specific remasters seem to break the search const trackName = track.name.split(' - ')[0]; return { songName: trackName, - // TODO: This may cause issues when guessing the name of an artist for a song with multiple artists + // FIXME: This may cause issues when guessing the name of an artist for a song with multiple artists. Replace with array and check array artist: track.artists.map(artist => artist.name).join(', '), + durationMs: track.duration_ms, uri: track.uri, - startTime: Math.floor(startTime / 1000), // Convert to seconds - clipLength: 5 // Always 5 seconds + startTime: randomClip.startTime, // Convert to seconds + clipLength: randomClip.clipLength }; }); @@ -133,7 +157,7 @@ async function searchTrack (songName, artist) { const track = data.tracks.items[0]; - return track.uri; + return track; } else { @@ -234,9 +258,14 @@ function processSongList (songs) { if (!clip.uri) { - searchTrack(clip.songName, clip.artist).then(uri => { + searchTrack(clip.songName, clip.artist).then(track => { - songClips[i].uri = uri; + if (track) { + + songClips[i].uri = track.uri; + songClips[i].durationMs = track.durationMs; + + } }); @@ -258,3 +287,41 @@ async function populateClipList (playlistId) { processSongList(songs); } + +helpButton.addEventListener('click', () => { + + reselectNumberSpan.innerText = currentClipIndex + 1; + + stopClip(); + pauseTimer(); + + helpModal.show(); + +}); + +cancelReselectButton.addEventListener('click', () => { + + resumeClip(); + resumeTimer(); + + helpModal.hide(); + +}); + +reselectButton.addEventListener('click', () => { + + const newRandomClip = selectRandomClip(songClips[currentClipIndex].durationMs); + + console.log('Updated clip ' + currentClipIndex + '. ' + songClips[currentClipIndex].startTime + ' -> ' + newRandomClip.startTime); + + songClips[currentClipIndex].startTime = newRandomClip.startTime; + songClips[currentClipIndex].clipLength = newRandomClip.clipLength; + + addSecondsToTimer(5); + + resumeClip(); + resumeTimer(); + + helpModal.hide(); + +}); diff --git a/js/timer.js b/js/timer.js index f17acc1..493307c 100644 --- a/js/timer.js +++ b/js/timer.js @@ -4,104 +4,131 @@ * July 2024 *****************************************************************************/ -/* global startGame, endGame */ +/* global bootstrap */ +/* global startGame, endGame, stopClip */ const timerSpan = document.getElementById('timer-span'); const startTimerButton = document.getElementById('start-timer-button'); const pauseTimerButton = document.getElementById('pause-timer-button'); const resumeTimerButton = document.getElementById('resume-timer-button'); +const pauseModal = new bootstrap.Modal(document.getElementById('pause-modal'), { + backdrop: 'static', + keyboard: false +}); + +const GAME_LENGTH = 15 * 60 * 1000; + let timeRemaining = 0; -let tickTimeout; -let timerPaused = false; +let timerInterval; +let isPaused = true; +let lastUpdateTime = Date.now(); -function updateTimerSpan () { +function formatTime (ms) { - const minsRemaining = Math.floor(timeRemaining / 60); - const secsRemaining = timeRemaining - (minsRemaining * 60); + const totalSeconds = Math.floor(ms / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; - timerSpan.innerText = String(minsRemaining).padStart(2, '0') + ':' + String(secsRemaining).padStart(2, '0'); + return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; } -function tickTimer () { +function updateTimer () { - updateTimerSpan(); + const now = Date.now(); + const elapsed = now - lastUpdateTime; - if (timeRemaining <= 0) { + if (!isPaused) { - endTimer(); - endGame(); + timeRemaining -= elapsed; - return; + if (timeRemaining <= 0) { - } + clearInterval(timerInterval); + timeRemaining = 0; - timeRemaining--; + endGame(); - tickTimeout = setTimeout(tickTimer, 1000); + } -} + } -function startTimer () { + lastUpdateTime = now; + timerSpan.textContent = formatTime(timeRemaining); - timeRemaining = 15 * 60; +} - updateTimerSpan(); +function resumeTimer () { - pauseTimerButton.disabled = false; - resumeTimerButton.disabled = false; - startTimerButton.disabled = true; + if (isPaused) { - startGame(); + isPaused = false; + lastUpdateTime = Date.now(); + timerInterval = setInterval(updateTimer, 100); - tickTimer(); + } } function pauseTimer () { - if (timerPaused) { + if (!isPaused) { - return; + isPaused = true; + clearInterval(timerInterval); } - clearTimeout(tickTimeout); +} - timerPaused = true; +function endTimer () { - pauseTimerButton.style.display = 'none'; - resumeTimerButton.style.display = ''; + clearInterval(timerInterval); -} + pauseTimerButton.disabled = true; + resumeTimerButton.disabled = true; -function resumeTimer () { +} - if (!timerPaused) { +function startTimer () { - return; + timeRemaining = GAME_LENGTH; - } + lastUpdateTime = Date.now(); + timerInterval = setInterval(updateTimer, 100); - timerPaused = false; + isPaused = false; - resumeTimerButton.style.display = 'none'; - pauseTimerButton.style.display = ''; + pauseTimerButton.disabled = false; + resumeTimerButton.disabled = false; + startTimerButton.disabled = true; - tickTimer(); + startGame(); } -function endTimer () { +function addSecondsToTimer (t) { - clearTimeout(tickTimeout); - - pauseTimerButton.disabled = true; - resumeTimerButton.disabled = true; + timeRemaining += t * 1000; } startTimerButton.addEventListener('click', startTimer); -pauseTimerButton.addEventListener('click', pauseTimer); -resumeTimerButton.addEventListener('click', resumeTimer); + +pauseTimerButton.addEventListener('click', () => { + + pauseModal.show(); + + pauseTimer(); + stopClip(); + +}); + +resumeTimerButton.addEventListener('click', () => { + + pauseModal.hide(); + + resumeTimer(); + +}); diff --git a/play.css b/play.css index 0ee9a69..8dc59c4 100644 --- a/play.css +++ b/play.css @@ -1,6 +1,6 @@ .btn-timer { width: 80px; - height: 40px; + height: 38px; } .play-button { diff --git a/play.html b/play.html index ba38455..661fa3d 100644 --- a/play.html +++ b/play.html @@ -10,6 +10,7 @@ + + + + + + + 00:00 -

@@ -46,6 +78,8 @@

Spotify Quiz


Score: -/- +
+

diff --git a/play.js b/play.js index 5bcecab..577331e 100644 --- a/play.js +++ b/play.js @@ -35,6 +35,13 @@ prepareButton.addEventListener('click', () => { }); +function enablePrepareUI() { + + // TODO: Change this to include all UI in modal + prepareButton.disabled = false; + +} + window.onload = () => { startModal.show(); @@ -46,11 +53,6 @@ window.onSpotifyWebPlaybackSDKReady = authorise; // https://developer.spotify.com/dashboard // https://developer.spotify.com/documentation/web-playback-sdk/tutorials/getting-started -// TODO: Table - -// TODO: Don't generate quiz until function is called to do so -// TODO: Artist score/custom scoring - // TODO: Select from an array of playlists preselected playlists (All On 90s, etc.) // TODO: Paste in playlist URL // TODO: Combine multiple playlists @@ -58,6 +60,8 @@ window.onSpotifyWebPlaybackSDKReady = authorise; // TODO: Help button which reselects clip and adds 5 seconds to timer +// TODO: Table // TODO: Interface +// TODO: Artist score/custom scoring // TODO: Album mode