synced-timer/src/static/js/synced-timer.js

242 lines
7.5 KiB
JavaScript

document.addEventListener('alpine:init', () => {
console.log("Synced-timer is starting")
Alpine.store("siteData", {
lastResponse: null,
currentData: null,
time: '00:00:00',
header: '...',
requestStartedAt: null,
requestEndedAt: null,
timeOffsetToServer: null,
init() {
Alpine.effect(() => {
const response = this.lastResponse
if (!response) return;
if (response.status !== 200) {
this.header = `Error: ${response.status}`
return;
}
if(response.version != activeSyncedTimerVersion) {
location.reload()
}
this.currentData = response.data
Alpine.store("audioPlayback").isDisabledByServer = !this.currentData.soundEnabled
this.header = this.currentData.header
})
setInterval(() => {
if (!this.currentData) return;
const passedSeconds = parseInt(((performance.now() - this.timeOffsetToServer) / 1000) - this.currentData.startedAt);
let remaningSeconds;
if (this.currentData.repeatEnabled) {
remaningSeconds = parseInt(this.currentData.time * 60 - (passedSeconds % (this.currentData.time * 60)));
}
else {
remaningSeconds = parseInt(this.currentData.time * 60 - passedSeconds);
}
this.time = getTimerText(remaningSeconds);
playTimerSound(remaningSeconds);
}, 100)
this.scheduleNextRequest()
},
scheduleNextRequest() {
setTimeout(() => {
let requestUrl;
// Clear timings to avoid overflow
performance.clearResourceTimings();
// Add a request Id to be able to identify this request later on
fetch(`/api/${username}?id=${parseInt(performance.now()) % 10000}`)
.then((response) => {
requestUrl = response.url
return response.json()
})
.then((data) => {
this.handleRequestFinished(data, requestUrl)
this.lastResponse = data
this.scheduleNextRequest()
});
}, 1000)
},
handleRequestFinished(data, requestName) {
if (!data || data.status !== 200) return;
// identify request using its id
const performanceEntry = performance.getEntriesByName(requestName)[0];
if (!performanceEntry) {
// retry
console.warn("Could not find performance entry, discarding...")
return
}
const requestDuration = performanceEntry.responseEnd - performanceEntry.requestStart;
this.timeOffsetToServer = performanceEntry.responseEnd - data.data.currentServerTime + (requestDuration / 2);
},
})
Alpine.store("audioPlayback", {
hasBeenTested: false,
isAvailable: null,
isDisabledByUser: false,
isDisabledByServer: true,
audioPlayer: null,
replayLocked: false,
sounds: {
silence: '/static/sound/silence.mp3',
countdown: '/static/sound/countdown.mp3',
beep: '/static/sound/beep.mp3'
},
init() {
this.audioPlayer = new Audio('/static/sound/silence.mp3');
this.testPermission()
const audioPlaybackStore = this
Alpine.bind('audioPlaybackEnableButton', () => ({
'@click'() {
audioPlaybackStore.testPermission()
}
}))
Alpine.bind('audioPlaybackDisableButton', () => ({
'@click'() {
audioPlaybackStore.isDisabledByUser = true
}
}))
Alpine.effect(() => {
if (Alpine.store("audioPlayback").isDisabledByUser) this.isAvailable = false;
})
},
testPermission() {
this.audioPlayer.src = '/static/sound/silence.mp3';
this.audioPlayer.play().then(() => {
this.isAvailable = true
this.hasBeenTested = true
}).catch((error) => {
console.warn("Audio permission not granted!")
this.isAvailable = false
this.hasBeenTested = true
})
},
playSound(soundFile) {
if (!this.audioPlayer.paused || !this.isAvailable || this.isDisabledByUser || this.isDisabledByServer || this.replayLocked) {
return;
}
if (!this.audioPlayer.src.endsWith(soundFile)) {
this.audioPlayer.src = soundFile;
}
this.replayLocked = true;
this.audioPlayer.play();
},
preloadSound(soundFile) {
if (this.audioPlayer.src.endsWith(soundFile)) {
return;
}
console.log(`Preloading sound ${soundFile}`)
this.audioPlayer.src = soundFile;
this.audioPlayer.load();
},
playCountdown() {
this.playSound(this.sounds.countdown)
},
preloadCountdown() {
this.preloadSound(this.sounds.countdown)
},
playBeep() {
this.playSound(this.sounds.beep);
},
preloadBeep() {
this.preloadSound(this.sounds.beep);
},
unlockReplay() {
this.replayLocked = false;
}
})
Alpine.store("fullscreen", {
enabled: document.fullscreenElement,
toggle() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
this.enabled = true
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}
})
Alpine.store("banners", {
hide: false,
init() {
setTimeout(() => {
this.hide = true
}, 10000)
}
})
})
document.addEventListener('alpine:initialized', () => {
})
const zeroPad = (num, places) => String(num).padStart(places, '0');
function getTimerText(remaningSeconds) {
if (remaningSeconds < 0) {
remaningSeconds = 0
}
var remaningHours = zeroPad(parseInt(remaningSeconds / 60 / 60) % (60 * 60), 2)
var remaningMinutes = zeroPad(parseInt(remaningSeconds / 60) % 60, 2)
var remaningSeconds = zeroPad(remaningSeconds % 60, 2)
if (parseInt(remaningHours) === 0) {
return "&nbsp;" + remaningMinutes + ":" + remaningSeconds + "&nbsp;"
}
return remaningHours + ":" + remaningMinutes + ":" + remaningSeconds
}
function playTimerSound(remaningSeconds) {
switch (parseInt(remaningSeconds)) {
case 65:
Alpine.store("audioPlayback").preloadBeep()
break;
case 60:
Alpine.store("audioPlayback").playBeep()
break;
case 59:
Alpine.store("audioPlayback").unlockReplay()
break;
case 10:
Alpine.store("audioPlayback").preloadCountdown()
break;
case 5:
Alpine.store("audioPlayback").playCountdown()
break;
case 4:
Alpine.store("audioPlayback").unlockReplay()
break;
}
}