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; } this.currentData = response.data Alpine.store("audioPlayback").isDisabledByServer = !this.currentData.soundEnabled this.header = this.currentData.header }) setInterval(() => { if(!this.currentData) return; const passedSeconds = parseInt(((Date.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; const requestId = Date.now(); fetch(`/api/${username}#${requestId}`) .then((response) => { requestUrl = response.url return response.json() }) .then((data) => { if(data && data.status === 200) { const performanceEntry = performance.getEntriesByName(`${requestUrl}#${requestId}`)[0]; console.log(performanceEntry) const requestDuration = performanceEntry.responseEnd - performanceEntry.requestStart; this.timeOffsetToServer = performanceEntry.responseEnd - data.data.currentServerTime + (requestDuration / 2); console.log(requestDuration, "offset: ", this.timeOffsetToServer) } this.lastResponse = data this.scheduleNextRequest() }); }, 1000) } }) Alpine.store("audioPlayback", { hasBeenTested: false, isAvailable: null, isDisabledByUser: false, isDisabledByServer: true, audioPlayer: null, 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) { return; } this.audioPlayer.src = soundFile; this.audioPlayer.play(); } }) 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 " " + remaningMinutes + ":" + remaningSeconds + " " } return remaningHours + ":" + remaningMinutes + ":" + remaningSeconds } function playTimerSound(remaningSeconds) { if(parseInt(remaningSeconds) === 5) { Alpine.store("audioPlayback").playSound('/static/sound/countdown.mp3') } else if(parseInt(remaningSeconds) === 60){ Alpine.store("audioPlayback").playSound('/static/sound/beep.mp3') } }