diff --git a/custom_components/beatify/game/state.py b/custom_components/beatify/game/state.py index 0d99c866..a27e38dd 100644 --- a/custom_components/beatify/game/state.py +++ b/custom_components/beatify/game/state.py @@ -1833,6 +1833,16 @@ async def stop_media(self) -> None: if self._media_player_service: await self._media_player_service.stop() + async def seek_forward_on_player(self, seconds: float = 10.0) -> bool: + """Seek forward on the media player. + + Returns: + True if successful, False if failed or no media player. + """ + if self._media_player_service: + return await self._media_player_service.seek_forward(seconds) + return False + async def set_volume_on_player(self, level: float) -> bool: """Apply volume level to the media player (#321). diff --git a/custom_components/beatify/server/websocket.py b/custom_components/beatify/server/websocket.py index 9462d529..3b58a201 100644 --- a/custom_components/beatify/server/websocket.py +++ b/custom_components/beatify/server/websocket.py @@ -363,6 +363,7 @@ async def _handle_admin( "confirm_intro_splash": self._admin_confirm_intro_splash, "set_party_lights": self._admin_set_party_lights, "toggle_party_lights": self._admin_toggle_party_lights, + "seek_forward": self._admin_seek_forward, } handler = admin_handlers.get(action) if handler: @@ -581,6 +582,40 @@ async def _admin_set_volume( } ) + async def _admin_seek_forward( + self, ws: web.WebSocketResponse, data: dict, game_state: GameState + ) -> None: + """Handle admin seek_forward action.""" + if game_state.phase not in (GamePhase.PLAYING, GamePhase.REVEAL): + await ws.send_json( + { + "type": "error", + "code": ERR_INVALID_ACTION, + "message": "Can only seek during playback", + } + ) + return + + try: + seconds = float(data.get("seconds", 10.0)) + except (ValueError, TypeError): + await ws.send_json( + { + "type": "error", + "code": ERR_INVALID_ACTION, + "message": "Invalid value for 'seconds'", + } + ) + return + + success = await game_state.seek_forward_on_player(seconds) + if not success: + _LOGGER.warning("Failed to seek forward") + + _LOGGER.info("Admin seeked forward %.0fs", seconds) + + await ws.send_json({"type": "seek_performed", "seconds": seconds}) + async def _admin_end_game( self, ws: web.WebSocketResponse, data: dict, game_state: GameState ) -> None: diff --git a/custom_components/beatify/services/media_player.py b/custom_components/beatify/services/media_player.py index 4effc942..df38afa9 100644 --- a/custom_components/beatify/services/media_player.py +++ b/custom_components/beatify/services/media_player.py @@ -551,6 +551,65 @@ async def set_volume(self, level: float) -> bool: self._record_error("MEDIA_PLAYER_ERROR", f"Failed to set volume: {err}") return False + async def seek_forward(self, seconds: float = 10.0) -> bool: + """ + Seek forward by a number of seconds from the current position. + + Args: + seconds: Number of seconds to skip forward (default 10) + + Returns: + True if successful, False otherwise + + """ + try: + state = self._hass.states.get(self._entity_id) + if not state: + _LOGGER.warning("Cannot seek: media player not found") + return False + + snapshot_position = state.attributes.get("media_position") + if snapshot_position is None: + _LOGGER.warning("Cannot seek: no media_position available") + return False + + # HA's media_position is a snapshot from media_position_updated_at. + # The actual position = snapshot + elapsed time since that update. + elapsed = 0.0 + if state.state == "playing": + updated_at = state.attributes.get("media_position_updated_at") + if updated_at: + from datetime import datetime, timezone # noqa: PLC0415 + + try: + if isinstance(updated_at, str): + updated_dt = datetime.fromisoformat(updated_at) + else: + updated_dt = updated_at + now = datetime.now(timezone.utc) + elapsed = max(0.0, (now - updated_dt).total_seconds()) + except (ValueError, TypeError): + elapsed = 0.0 + + current_position = float(snapshot_position) + elapsed + new_position = current_position + seconds + await self._hass.services.async_call( + "media_player", + "media_seek", + { + "entity_id": self._entity_id, + "seek_position": new_position, + }, + ) + _LOGGER.info( + "Seeked forward %.0fs to position %.1f", seconds, new_position + ) + return True # noqa: TRY300 + except Exception as err: # noqa: BLE001 + _LOGGER.error("Failed to seek forward: %s", err) # noqa: TRY400 + self._record_error("MEDIA_PLAYER_ERROR", f"Failed to seek: {err}") + return False + def is_available(self) -> bool: """ Check if media player is available. diff --git a/custom_components/beatify/www/i18n/de.json b/custom_components/beatify/www/i18n/de.json index 5724c784..64619f36 100644 --- a/custom_components/beatify/www/i18n/de.json +++ b/custom_components/beatify/www/i18n/de.json @@ -277,9 +277,18 @@ "startNewGame": "Neues Spiel starten", "stop": "Stop", "stopSong": "Stop", + "stopTooltip": "Aktuellen Song stoppen", "next": "Weiter", "nextRound": "Nächste Runde", + "nextTooltip": "Nächste Runde / Überspringen", "end": "Ende", + "endTooltip": "Spiel beenden", + "volumeDown": "Leiser", + "volumeDownTooltip": "Leiser", + "volumeUp": "Lauter", + "volumeUpTooltip": "Lauter", + "seekForward": "+10s", + "seekForwardTooltip": "10 Sekunden vorspulen", "volume": "Lautstärke", "controls": "Spielsteuerung", "status": "Status", diff --git a/custom_components/beatify/www/i18n/en.json b/custom_components/beatify/www/i18n/en.json index a4d7ec6e..e6336fad 100644 --- a/custom_components/beatify/www/i18n/en.json +++ b/custom_components/beatify/www/i18n/en.json @@ -277,9 +277,18 @@ "startNewGame": "Start New Game", "stop": "Stop", "stopSong": "Stop", + "stopTooltip": "Stop current song", "next": "Next", "nextRound": "Next Round", + "nextTooltip": "Next round / Skip", "end": "End", + "endTooltip": "End game", + "volumeDown": "Quieter", + "volumeDownTooltip": "Volume down", + "volumeUp": "Louder", + "volumeUpTooltip": "Volume up", + "seekForward": "+10s", + "seekForwardTooltip": "Skip 10 seconds forward", "volume": "Volume", "controls": "Game Controls", "status": "Status", diff --git a/custom_components/beatify/www/i18n/es.json b/custom_components/beatify/www/i18n/es.json index 740df148..1e70b0dd 100644 --- a/custom_components/beatify/www/i18n/es.json +++ b/custom_components/beatify/www/i18n/es.json @@ -277,9 +277,18 @@ "startNewGame": "Iniciar nuevo juego", "stop": "Detener", "stopSong": "Detener", + "stopTooltip": "Detener canción actual", "next": "Siguiente", "nextRound": "Siguiente ronda", + "nextTooltip": "Siguiente ronda / Saltar", "end": "Fin", + "endTooltip": "Terminar juego", + "volumeDown": "Menos", + "volumeDownTooltip": "Bajar volumen", + "volumeUp": "Más", + "volumeUpTooltip": "Subir volumen", + "seekForward": "+10s", + "seekForwardTooltip": "Saltar 10 segundos adelante", "volume": "Volumen", "controls": "Controles del juego", "status": "Estado", diff --git a/custom_components/beatify/www/i18n/fr.json b/custom_components/beatify/www/i18n/fr.json index b779cc7a..eb7f663d 100644 --- a/custom_components/beatify/www/i18n/fr.json +++ b/custom_components/beatify/www/i18n/fr.json @@ -277,9 +277,18 @@ "startNewGame": "Lancer une nouvelle partie", "stop": "Arrêter", "stopSong": "Arrêter", + "stopTooltip": "Arrêter la chanson en cours", "next": "Suivant", "nextRound": "Manche suivante", + "nextTooltip": "Manche suivante / Passer", "end": "Fin", + "endTooltip": "Terminer la partie", + "volumeDown": "Moins", + "volumeDownTooltip": "Baisser le volume", + "volumeUp": "Plus", + "volumeUpTooltip": "Augmenter le volume", + "seekForward": "+10s", + "seekForwardTooltip": "Avancer de 10 secondes", "volume": "Volume", "controls": "Contrôles du jeu", "status": "Statut", diff --git a/custom_components/beatify/www/i18n/nl.json b/custom_components/beatify/www/i18n/nl.json index ae939f47..bced6f87 100644 --- a/custom_components/beatify/www/i18n/nl.json +++ b/custom_components/beatify/www/i18n/nl.json @@ -277,9 +277,18 @@ "startNewGame": "Nieuw spel starten", "stop": "Stop", "stopSong": "Stop", + "stopTooltip": "Huidig nummer stoppen", "next": "Volgende", "nextRound": "Volgende ronde", + "nextTooltip": "Volgende ronde / Overslaan", "end": "Beëindigen", + "endTooltip": "Spel beëindigen", + "volumeDown": "Zachter", + "volumeDownTooltip": "Zachter", + "volumeUp": "Harder", + "volumeUpTooltip": "Harder", + "seekForward": "+10s", + "seekForwardTooltip": "10 seconden vooruitspoelen", "volume": "Volume", "controls": "Spelbediening", "status": "Status", diff --git a/custom_components/beatify/www/js/player-game.js b/custom_components/beatify/www/js/player-game.js index b103eea8..20c0709e 100644 --- a/custom_components/beatify/www/js/player-game.js +++ b/custom_components/beatify/www/js/player-game.js @@ -1518,6 +1518,7 @@ export function showFloatingReaction(senderName, emoji) { */ export function updateControlBarState(phase) { var stopBtn = document.getElementById('stop-song-btn'); + var seekBtn = document.getElementById('seek-forward-btn'); var nextBtn = document.getElementById('next-round-admin-btn'); if (phase === 'PLAYING') { @@ -1526,6 +1527,10 @@ export function updateControlBarState(phase) { stopBtn.classList.remove('is-disabled'); stopBtn.disabled = false; } + if (seekBtn) { + seekBtn.classList.remove('is-disabled', 'hidden'); + seekBtn.disabled = false; + } if (nextBtn) { nextBtn.classList.remove('is-disabled'); nextBtn.disabled = false; @@ -1537,6 +1542,10 @@ export function updateControlBarState(phase) { stopBtn.classList.remove('is-disabled'); stopBtn.disabled = false; } + if (seekBtn) { + seekBtn.classList.remove('hidden', 'is-disabled'); + seekBtn.disabled = false; + } if (nextBtn) { nextBtn.classList.remove('is-disabled'); nextBtn.disabled = false; @@ -1544,6 +1553,10 @@ export function updateControlBarState(phase) { if (labelEl) labelEl.textContent = utils.t('game.next'); } } else { + if (seekBtn) { + seekBtn.classList.add('hidden'); + seekBtn.disabled = true; + } if (nextBtn) { nextBtn.classList.add('is-disabled'); nextBtn.disabled = true; @@ -1598,6 +1611,20 @@ function handleVolumeUp() { })); } +/** + * Handle Seek Forward button (+10s) + */ +function handleSeekForward() { + if (!debounceAdminAction()) return; + if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return; + + state.ws.send(JSON.stringify({ + type: 'admin', + action: 'seek_forward', + seconds: 10 + })); +} + /** * Handle Volume Down button */ @@ -1721,12 +1748,14 @@ export function setupAdminControlBar() { var stopBtn = document.getElementById('stop-song-btn'); var volUpBtn = document.getElementById('volume-up-btn'); var volDownBtn = document.getElementById('volume-down-btn'); + var seekBtn = document.getElementById('seek-forward-btn'); var nextBtn = document.getElementById('next-round-admin-btn'); var endBtn = document.getElementById('end-game-btn'); if (stopBtn) stopBtn.addEventListener('click', handleStopSong); if (volUpBtn) volUpBtn.addEventListener('click', handleVolumeUp); if (volDownBtn) volDownBtn.addEventListener('click', handleVolumeDown); + if (seekBtn) seekBtn.addEventListener('click', handleSeekForward); if (nextBtn) nextBtn.addEventListener('click', handleNextRoundFromBar); if (endBtn) endBtn.addEventListener('click', handleEndGame); } diff --git a/custom_components/beatify/www/js/player.bundle.min.js b/custom_components/beatify/www/js/player.bundle.min.js index a3d1e84f..ce1f6aa7 100644 --- a/custom_components/beatify/www/js/player.bundle.min.js +++ b/custom_components/beatify/www/js/player.bundle.min.js @@ -1,3 +1,3 @@ -var Ae=window.BeatifyUtils||{},s={ws:null,playerName:null,isAdmin:!1,reconnectAttempts:0,isReconnecting:!1,intentionalLeave:!1,hasReactedThisPhase:!1,currentRoundNumber:0,gameId:new URLSearchParams(window.location.search).get("game"),connectWithSession:null,connectWebSocket:null},En=document.getElementById("loading-view"),Ln=document.getElementById("not-found-view"),wn=document.getElementById("ended-view"),Sn=document.getElementById("in-progress-view"),In=document.getElementById("join-view"),Bn=document.getElementById("lobby-view"),xn=document.getElementById("game-view"),Cn=document.getElementById("reveal-view"),_n=document.getElementById("paused-view"),kn=document.getElementById("end-view"),An=document.getElementById("connection-lost-view"),Tn=[En,Ln,wn,Sn,In,Bn,xn,Cn,_n,kn,An];function I(e){Ae.showView(Tn,e),(e==="join-view"||e==="loading-view"||e==="not-found-view"||e==="ended-view"||e==="in-progress-view"||e==="connection-lost-view")&&q("calm"),e==="join-view"&&setTimeout(function(){var t=document.getElementById("name-input");t&&t.focus()},100)}function Z(e,t,n,a){return new Promise(function(i){var r=document.getElementById("confirm-modal"),o=document.getElementById("confirm-modal-title"),l=document.getElementById("confirm-modal-message"),c=document.getElementById("confirm-modal-yes"),u=document.getElementById("confirm-modal-no");if(!r||!o||!l||!c||!u){i(confirm(t||e));return}o.textContent=e,l.textContent=t,c.textContent=n||Ae.t("common.confirm")||"Confirm",u.textContent=a||Ae.t("common.cancel")||"Cancel",r.classList.remove("hidden");function m(){r.classList.add("hidden"),c.removeEventListener("click",d),u.removeEventListener("click",f),v.removeEventListener("click",f)}function d(){m(),i(!0)}function f(){m(),i(!1)}var v=r.querySelector(".modal-backdrop");c.addEventListener("click",d),u.addEventListener("click",f),v&&v.addEventListener("click",f)})}function x(e){var t=document.createElement("div");return t.textContent=e,t.innerHTML}function be(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}function Nn(e){return 1-Math.pow(1-e,4)}function se(e,t,n,a,i){if(be()||t===n)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var r=K.getQualitySettings();if(r.scoreDuration===0)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var o=Math.min(a,r.scoreDuration||a);i=i||Nn;var l=null,c=null,u=!1,m=n;function d(f){if(!u){l||(l=f);var v=f-l,E=Math.min(v/o,1),w=i(E),b=Math.round(t+(m-t)*w);e.textContent=b,E<1&&(c=requestAnimationFrame(d))}}return c=requestAnimationFrame(d),{cancel:function(){u=!0,c&&cancelAnimationFrame(c)},skipToEnd:function(){u=!0,c&&cancelAnimationFrame(c),e.textContent=m}}}function ot(e,t,n,a){a=a||{};var i=500;a.betWon?i=800:a.isBigScore?i=700:a.betLost&&(i=400),e.classList.add("score-animating");var r=null;a.betWon?r="score-glow-gold":a.betLost?(r="score-shake",e.classList.add("score-flash-red")):a.streakMilestone?r="score-burst":a.isBigScore&&(r="score-pop"),r&&!be()&&e.classList.add(r),se(e,t,n,i);function o(){e.classList.remove("score-animating"),r&&e.classList.remove(r),e.classList.remove("score-flash-red")}r&&!be()?e.addEventListener("animationend",function l(){e.removeEventListener("animationend",l),o()}):setTimeout(o,i+50)}function Ne(e,t,n){if(n=n||{},!be()){var a=document.createElement("div");a.className="points-popup",a.textContent=n.text||"+"+t,n.isStreak?a.classList.add("points-popup--streak"):n.isBetWin&&a.classList.add("points-popup--gold");var i=e.getBoundingClientRect();a.style.left=i.left+i.width/2+"px",a.style.top=i.top+"px",document.body.appendChild(a),a.addEventListener("animationend",function(){a.parentNode&&a.parentNode.removeChild(a)}),setTimeout(function(){a.parentNode&&a.parentNode.removeChild(a)},1200)}}var M={players:{},leaderboard:[],initialized:!1};function lt(){return M.initialized}var rt=[3,5,10,15,20,25];function dt(e,t){for(var n=0;n=a)return a}return null}function ct(e){var t=e.map(function(a){return a.name}),n={};return t.forEach(function(a,i){var r=M.leaderboard.indexOf(a);r===-1?n[a]="new":ir&&(n[a]="down")}),n}function ut(e,t){M.players={},e.forEach(function(n){M.players[n.name]={score:n.score,rank:n.rank||0,streak:n.streak||0}}),t&&(M.leaderboard=t.map(function(n){return n.name})),M.initialized=!0}var K=(function(){var e=window.matchMedia("(prefers-reduced-motion: reduce)"),t=e.matches;e.addEventListener("change",function(i){t=i.matches});var n=null;function a(){if(n!==null)return n;var i=navigator.hardwareConcurrency||2,r=navigator.deviceMemory||4,o=/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream;return i<=2||r<=2?n="low":i<=4||r<=4||o?n="medium":n="high",n}return a(),{prefersReducedMotion:function(){return t},getDeviceTier:a,getQualitySettings:function(){var i=a();if(t)return{confettiParticles:0,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!1};switch(i){case"low":return{confettiParticles:5,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!0};case"medium":return{confettiParticles:10,scoreDuration:300,leaderboardAnimation:"simplified",neonGlow:!1,enableAnimations:!0};default:return{confettiParticles:15,scoreDuration:500,leaderboardAnimation:"full",neonGlow:!0,enableAnimations:!0}}},ifMotionAllowed:function(i,r){t?r&&r():i()},withWillChange:function(i,r,o){i&&(i.style.willChange=r,setTimeout(function(){i&&i.style&&(i.style.willChange="auto")},(o||500)+100))}}})(),$=(function(){var e=[],t=!1,n=null,a=null,i=2e3;function r(){if(a&&(clearTimeout(a),a=null),e.length===0){t=!1,n=null;return}n=e.shift(),a=setTimeout(function(){n&&n.skipToEnd&&n.skipToEnd(),r()},i),n.run(function(){a&&(clearTimeout(a),a=null),r()})}return{add:function(o){e.push(o),t||(t=!0,r())},skipAll:function(){a&&(clearTimeout(a),a=null),n&&n.skipToEnd&&n.skipToEnd(),e.forEach(function(o){o.skipToEnd&&o.skipToEnd()}),e=[],t=!1,n=null},clear:function(){a&&(clearTimeout(a),a=null),e=[],t=!1,n=null},isRunning:function(){return t},getMaxDuration:function(){return i}}})(),X={VISIBLE_BUFFER:2,ENTRY_HEIGHT:48,MIN_PLAYERS_FOR_LAZY:10,ROOT_MARGIN:"96px 0px",DEFAULT_VIEWPORT_HEIGHT:280},h={observer:null,fullData:[],visibleRange:{start:0,end:10},listEl:null,isLazyEnabled:!1};function mt(e){e&&(h.observer&&h.listEl!==e&&(h.observer.disconnect(),h.observer=null),!h.observer&&(h.listEl=e,h.observer=new IntersectionObserver(function(t){t.forEach(function(n){if(!(!n.isIntersecting||!h.isLazyEnabled)){var a=h.fullData,i=h.visibleRange,r=X.VISIBLE_BUFFER;if(n.target.classList.contains("leaderboard-sentinel--top")){if(i.start>0){var o=Math.max(0,i.start-r);h.visibleRange.start=o,re()}}else if(n.target.classList.contains("leaderboard-sentinel--bottom")&&i.end0&&(l+='
'),l+='
';for(var c=n.start;c',r>0&&(l+='
'),e.innerHTML=l,e.scrollTop=o,h.observer){var u=e.querySelectorAll(".leaderboard-sentinel");u.forEach(function(m){h.observer.observe(m)})}}}function Me(e){if(!e)return"";if(e.separator)return'
...
';var t=e.name||"Unknown",n=e.rank||0,a=e.score||0,i=n<=3?"is-top-"+n:"",r=e.is_current?"is-current":"",o="";e.rank_change>0||e._rankChange==="up"?o="leaderboard-entry--climbing leaderboard-entry--slide-up":(e.rank_change<0||e._rankChange==="down")&&(o="leaderboard-entry--falling leaderboard-entry--slide-down");var l="";e.rank_change>0?l='\u25B2'+e.rank_change+"":e.rank_change<0&&(l='\u25BC'+Math.abs(e.rank_change)+"");var c="";if(e.streak>=2){var u=e.streak>=5?"streak-indicator--hot":"";c='\u{1F525}'+e.streak+""}var m=e.connected===!1?"leaderboard-entry--disconnected":"",d=e.connected===!1?'(away)':"",f=e._displayScore!==void 0?e._displayScore:a;return'
#'+n+''+x(t)+d+''+f+"
"}function Re(e,t){for(var n=X,a=h.listEl&&h.listEl.clientHeight||n.DEFAULT_VIEWPORT_HEIGHT,i=Math.ceil(a/n.ENTRY_HEIGHT),r=n.VISIBLE_BUFFER,o=-1,l=0;l=e.length-i?(c=Math.max(0,e.length-i-r),u=e.length):(c=Math.max(0,o-Math.floor(i/2)-r),u=Math.min(e.length,o+Math.ceil(i/2)+r)),{start:c,end:u}}function ft(){h.observer&&(h.observer.disconnect(),h.observer=null),h.isLazyEnabled=!1,h.fullData=[]}function vt(){var e;function t(){clearTimeout(e),e=setTimeout(function(){h.isLazyEnabled&&h.fullData.length>0&&(h.visibleRange=Re(h.fullData,s.playerName),re())},150)}window.addEventListener("resize",t),window.addEventListener("orientationchange",t)}function gt(){var e=document.getElementById("qr-share-area");if(!(!e||e.tagName!=="DETAILS")){var t="beatify_qr_expanded",n=768,a=sessionStorage.getItem(t);a!==null?e.open=a==="true":e.open=window.innerWidth>=n,e.addEventListener("toggle",function(){sessionStorage.setItem(t,e.open.toString())})}}function pt(){var e=document.querySelectorAll(".lobby-container--compact .section-header-collapsible");e.forEach(function(t){t.addEventListener("click",function(){var n=t.closest(".section-collapsible");if(n){var a=n.classList.contains("collapsed");n.classList.toggle("collapsed"),t.setAttribute("aria-expanded",a?"true":"false")}})})}var yt={ITEM_HEIGHT:60,OVERSCAN:3,THRESHOLD:15,CONTAINER_HEIGHT:320},g={container:null,items:[],scrollTop:0,isVirtual:!1,topSpacer:null,bottomSpacer:null,contentWrapper:null,scrollHandler:null,resizeHandler:null};function bt(e){if(e){g.container=e;var t=!1;g.scrollHandler=function(){g.scrollTop=e.scrollTop,t||(requestAnimationFrame(function(){Te(),t=!1}),t=!0)};var n;g.resizeHandler=function(){clearTimeout(n),n=setTimeout(function(){g.isVirtual&&Te()},100)},e.addEventListener("scroll",g.scrollHandler,{passive:!0}),window.addEventListener("resize",g.resizeHandler)}}function ht(e,t){g.items=e,g.renderItem=t;var n=g.container;if(n){var a=n.scrollTop,i=g.isVirtual;e.length0&&(n.scrollTop=a,g.scrollTop=a)}}function Mn(){var e=g.container;if(e){e.innerHTML="";var t=document.createElement("div");t.className="virtual-spacer-top",g.topSpacer=t;var n=document.createElement("div");n.className="virtual-content-wrapper",g.contentWrapper=n;var a=document.createElement("div");a.className="virtual-spacer-bottom",g.bottomSpacer=a,e.appendChild(t),e.appendChild(n),e.appendChild(a)}}function Te(){var e=yt,t=g.items,n=g.container,a=g.contentWrapper;if(!(!n||!a||!t.length)){var i=n.clientHeight||e.CONTAINER_HEIGHT,r=g.scrollTop,o=e.ITEM_HEIGHT,l=e.OVERSCAN,c=Math.max(0,Math.floor(r/o)-l),u=Math.min(t.length,Math.ceil((r+i)/o)+l);g.topSpacer&&(g.topSpacer.style.height=c*o+"px"),g.bottomSpacer&&(g.bottomSpacer.style.height=(t.length-u)*o+"px");for(var m="",d=c;d"u"){console.warn("[Confetti] Library not loaded");return}O();var t=K.getQualitySettings(),n=t.confettiParticles;if(n===0){st();return}var a=K.getDeviceTier(),i=a==="low"?.5:a==="medium"?.75:1;switch(e=e||"exact",e){case"exact":var r=Math.round(2e3*i),o=Date.now()+r;(function v(){confetti({particleCount:n,spread:70,origin:{y:.6},colors:["#FFD700","#FFA500","#FFEC8B"]}),Date.now()0);var l=e.slice().sort(function(d,f){return d.connected!==f.connected?d.connected?-1:1:0}),c=Lt.map(function(d){return d.name}),u=l.filter(function(d){return c.indexOf(d.name)===-1}).map(function(d){return d.name});g.container||bt(t);var m=function(d){var f=u.indexOf(d.name)!==-1,v=d.name===s.playerName,E=d.connected===!1,w=["player-card",f?"is-new":"",v?"player-card--you":"",E?"player-card--disconnected":""].filter(Boolean).join(" "),b=E?'(away)':"";return'
'+x(d.name)+(v?''+U.t("leaderboard.you")+"":"")+b+"
"};ht(l,m),setTimeout(function(){var d=g.isVirtual?g.contentWrapper:t;if(d)for(var f=d.querySelectorAll(".is-new"),v=0;v=2?(n[0][0]+n[1][0]).toUpperCase():t.slice(0,Math.min(2,t.length)).toUpperCase()}function Gn(e){var t=document.getElementById("submission-tracker"),n=document.getElementById("submitted-players");if(!(!t||!n)){var a=e||[],i=a.filter(function(l){return l.submitted}).length,r=a.length,o=i===r&&r>0;t.classList.toggle("all-submitted",o),n.innerHTML=a.map(function(l){var c=Hn(l.name),u=l.name===s.playerName,m=l.connected===!1,d=["player-indicator",l.submitted?"is-submitted":"",u?"is-current-player":"",m?"player-indicator--disconnected":""].filter(Boolean).join(" "),f="";return l.steal_used&&(f+='\u{1F977}'),l.bet&&(f+='\u{1F3B2}'),'
'+f+'
'+x(c)+'
'+x(l.name)+"
"}).join("")}}function Ue(e,t,n){var a=e.leaderboard||[],i=document.getElementById(t||"leaderboard-list");if(i){var r=n&<(),o=r?ct(a):{};a.forEach(function(d){d.is_current=d.name===s.playerName;var f=o[d.name];f&&(d._rankChange=f);var v=M.players[d.name],E=v?v.score:d.score;d._prevScore=E,d._displayScore=n?E:d.score});var l=Dn(a,s.playerName),c=a.length>=X.MIN_PLAYERS_FOR_LAZY;if(c)h.observer||mt(i),h.fullData=l,h.isLazyEnabled=!0,h.listEl=i,h.visibleRange=Re(l,s.playerName),re();else{h.isLazyEnabled=!1;var u="";l.forEach(function(d){u+=Me(d)}),i.innerHTML=u}var m=[];r&&l.forEach(function(d){!d.separator&&d._prevScore!==d.score&&m.push({name:d.name,prevScore:d._prevScore,newScore:d.score})}),r&&m.length>0&&requestAnimationFrame(function(){for(var d={},f=i.querySelectorAll(".leaderboard-entry[data-name]"),v=0;v8&&Vn(i),Fn(a),jn(a),ut(e.players||[],a)}}function Dn(e,t){if(e.length<=10)return e;for(var n=e.slice(0,5),a=e.slice(-3),i=-1,r=0;r=e.length-3?[].concat(n,[{separator:!0}],a):[].concat(n,[{separator:!0}],[e[i]],[{separator:!0}],a)}function Vn(e){var t=e.querySelector(".leaderboard-entry.is-current");t&&t.scrollIntoView({behavior:"smooth",block:"center"})}function Fn(e){var t=document.getElementById("leaderboard-you"),n=e.find(function(a){return a.is_current});t&&n&&(t.textContent=y.t("leaderboard.you")+" #"+n.rank,t.classList.remove("hidden"))}function Ht(){var e=document.getElementById("leaderboard-toggle"),t=document.getElementById("game-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function jn(e,t){var n=t?[t]:["leaderboard-summary","reveal-leaderboard-summary"];n.forEach(function(a){var i=document.getElementById(a);if(!(!i||!e||e.length===0)){var r=e[0];r&&(i.textContent=r.name+": "+r.score)}})}function Gt(){var e=document.getElementById("reveal-leaderboard-toggle"),t=document.getElementById("reveal-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function Dt(){var e=document.getElementById("round-analytics-toggle"),t=document.getElementById("round-analytics");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}var G=!1,le=!1,de=!1,z=!1,H=null,Le=null,Wn=300,Nt=0,te=!1,ne=null,qn=500,Mt=0;function Vt(){var e=document.getElementById("year-slider"),t=document.getElementById("selected-year");if(!(!e||!t)){e.addEventListener("input",function(){t.textContent=this.value});var n=document.getElementById("bet-toggle");n&&n.addEventListener("click",function(){G||(le=!le,n.classList.toggle("is-active",le))});var a=document.getElementById("submit-btn");a&&a.addEventListener("click",Un);var i=document.getElementById("steal-btn");i&&i.addEventListener("click",$n);var r=document.getElementById("steal-modal-close");r&&r.addEventListener("click",qe);var o=document.getElementById("steal-modal");if(o){var l=o.querySelector(".steal-modal-backdrop");l&&l.addEventListener("click",qe)}}}function Un(){if(!G){var e=document.getElementById("year-slider"),t=document.getElementById("submit-btn");if(!(!e||!t)){var n=parseInt(e.value,10);t.disabled=!0,t.classList.add("is-loading"),s.ws&&s.ws.readyState===WebSocket.OPEN?s.ws.send(JSON.stringify({type:"submit",year:n,bet:le})):(We(y.t("errors.connectionLost")),t.disabled=!1,t.classList.remove("is-loading"))}}}function ze(){G=!0;var e=document.getElementById("year-selector"),t=document.getElementById("submit-btn"),n=document.getElementById("submitted-confirmation"),a=document.getElementById("bet-toggle");e&&e.classList.add("is-submitted"),t&&t.classList.add("hidden"),a&&a.classList.add("hidden"),n&&n.classList.remove("hidden")}function Ft(e){var t=document.getElementById("submit-btn");t&&(t.disabled=!1,t.classList.remove("is-loading")),e.code==="ROUND_EXPIRED"?(We(y.t("errors.timesUp")),G=!0,t&&(t.disabled=!0)):e.code==="ALREADY_SUBMITTED"?ze():We(e.message||"Submission failed")}function We(e){var t=document.getElementById("submit-btn");t&&(t.textContent=e,t.classList.add("is-error"),setTimeout(function(){t.textContent=y.t("game.submitGuess"),t.classList.remove("is-error")},2e3))}function jt(){G=!1,le=!1;var e=document.getElementById("year-selector"),t=document.getElementById("submit-btn"),n=document.getElementById("submitted-confirmation"),a=document.getElementById("year-slider"),i=document.getElementById("bet-toggle");if(e&&e.classList.remove("is-submitted"),t&&(t.disabled=!1,t.classList.remove("hidden","is-loading","is-error"),t.textContent=y.t("game.submitGuess")),i&&i.classList.remove("hidden","is-active"),n&&n.classList.add("hidden"),a){a.value=1990;var r=document.getElementById("selected-year");r&&(r.textContent="1990")}de=!1,Ye(),Qn(),Xn()}function zn(e,t){var n=document.getElementById("artist-challenge-container");if(n){if(!e||!e.options){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("artist-options"),i=document.getElementById("artist-result"),r=Array.from(a.querySelectorAll(".artist-option-btn")).map(function(m){return m.dataset.artist}),o=e.options;if(JSON.stringify(r)!==JSON.stringify(o)&&(a.innerHTML="",o.forEach(function(m,d){var f=document.createElement("button");f.className="artist-option-btn",f.dataset.artist=m,f.dataset.index=d,f.textContent=m,f.addEventListener("click",function(){Yn(m)}),a.appendChild(f)})),e.winner){var l=a.querySelectorAll(".artist-option-btn");if(l.forEach(function(m){m.classList.add("is-disabled"),m.classList.remove("is-loading","is-wrong");var d=e.correct_artist||Le;d&&m.dataset.artist===d&&m.classList.add("is-winner")}),e.winner===s.playerName){var c=e.bonus_points||5;i.textContent=(y.t("artistChallenge.youGotIt")||"You got it! +{points} points").replace("{points}",c),i.className="artist-result is-winner"}else{var u=(y.t("artistChallenge.someoneBeatYou")||"{winner} got it first!").replace("{winner}",e.winner);i.textContent=u,i.className="artist-result is-late"}i.classList.remove("hidden"),z=!0}else z||i.classList.add("hidden")}}function Yn(e){var t=Date.now();if(!(t-Nt0)){var n=document.createElement("span");n.className="movie-rank-badge",n.textContent="+"+e.bonus,t.appendChild(n)}De();var a=(y.t("movieChallenge.youGotIt")||"Correct! #{rank} \u2014 +{bonus} points").replace("{rank}",e.rank||1).replace("{bonus}",e.bonus||0);Ve(a,!0),te=!0}else t&&(t.classList.remove("is-loading"),t.classList.add("is-wrong","is-selected")),De(),Ve(y.t("movieChallenge.wrongGuess")||"Not quite...",!1),te=!0;ne=null}function De(){document.querySelectorAll(".movie-option-btn").forEach(function(e){e.classList.add("is-disabled"),e.classList.remove("is-loading")})}function Ve(e,t){var n=document.getElementById("movie-result");n&&(n.textContent=e,n.className="movie-result "+(t?"is-winner":"is-late"),n.classList.remove("hidden"))}function Xn(){te=!1,ne=null;var e=document.getElementById("movie-challenge-container");e&&e.classList.add("hidden");var t=document.getElementById("movie-options");t&&(t.innerHTML="");var n=document.getElementById("movie-result");n&&(n.classList.add("hidden"),n.className="movie-result hidden")}function zt(e,t){var n=document.getElementById("movie-reveal-section");if(n){if(!e||!e.correct_movie){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("movie-reveal-name");a&&(a.textContent=e.correct_movie);var i=document.getElementById("movie-reveal-winners");if(i&&e.results){var r=e.results.winners||[];if(r.length>0){i.innerHTML="",i.classList.remove("hidden");var o=document.createElement("div");o.className="movie-reveal-winners-title",o.textContent=y.t("movieChallenge.winnersTitle")||"Movie Quiz Winners",i.appendChild(o),r.forEach(function(c){var u=document.createElement("div");u.className="movie-reveal-winner-entry",c.name===t?u.classList.add("is-you"):u.classList.add("is-other"),u.textContent=c.name+" \u2014 +"+c.bonus+" ("+c.time+"s)",i.appendChild(u)})}else{i.innerHTML="",i.classList.remove("hidden");var l=document.createElement("div");l.className="movie-reveal-no-winner",l.textContent=y.t("movieChallenge.noWinner")||"No one guessed the movie",i.appendChild(l)}}}}function Zn(e){if(!(!s.playerName||!e)){var t=e.find(function(i){return i.name===s.playerName});if(t){de=t.steal_available&&!G;var n=document.getElementById("steal-indicator"),a=document.getElementById("steal-btn");de?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden")):Ye()}}}function Ye(){var e=document.getElementById("steal-indicator"),t=document.getElementById("steal-btn");e&&e.classList.add("hidden"),t&&t.classList.add("hidden")}function $n(){!de||G||s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"get_steal_targets"}))}function ea(e){var t=document.getElementById("steal-modal"),n=document.getElementById("steal-target-list");if(!(!t||!n)){if(n.innerHTML="",!e||e.length===0){var a=document.createElement("p");a.className="steal-no-targets",a.textContent=y.t("steal.waitForSubmit"),n.appendChild(a)}else e.forEach(function(i){var r=document.createElement("button");r.className="steal-target-btn",r.textContent=i,r.addEventListener("click",function(){ta(i)}),n.appendChild(r)});t.classList.remove("hidden")}}function qe(){var e=document.getElementById("steal-modal");e&&e.classList.add("hidden")}async function ta(e){var t=y.t("steal.confirm").replace("{name}",e),n=await Z(y.t("steal.confirmTitle")||"Steal Answer?",t,y.t("steal.confirmButton")||"Steal",y.t("common.cancel"));n&&(s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"steal",target:e})),qe())}function Yt(e){if(e.success){de=!1,G=!0,Ye();var t=document.getElementById("year-selector"),n=document.getElementById("submit-btn"),a=document.getElementById("submitted-confirmation");t&&t.classList.add("is-submitted"),n&&n.classList.add("hidden"),a&&a.classList.remove("hidden"),na(e.target,e.year);var i=document.getElementById("selected-year"),r=document.getElementById("year-slider");i&&(i.textContent=e.year),r&&(r.value=e.year)}}function Qt(e){ea(e.targets||[])}function na(e,t){var n=document.getElementById("steal-confirmation"),a=document.getElementById("steal-confirmation-text");if(!(!n||!a)){var i=y.t("steal.success").replace("{name}",e).replace("{year}",t);a.textContent=i,n.classList.remove("hidden"),setTimeout(function(){n.classList.add("hidden")},3e3)}}var Fe=!1,aa=500,ce=!1,Qe=.5;function we(){return Fe?!1:(Fe=!0,setTimeout(function(){Fe=!1},aa),!0)}function Je(){if(s.isAdmin){var e=document.getElementById("admin-control-bar");e&&(e.classList.remove("hidden"),document.body.classList.add("has-control-bar"))}}function ue(){var e=document.getElementById("admin-control-bar");e&&(e.classList.add("hidden"),document.body.classList.remove("has-control-bar"))}function Jt(){var e=document.getElementById("reaction-bar");e&&e.classList.remove("hidden")}function me(){var e=document.getElementById("reaction-bar");e&&e.classList.add("hidden")}function ia(e){s.hasReactedThisPhase||(s.hasReactedThisPhase=!0,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"reaction",emoji:e})))}function Kt(){var e=document.getElementById("reaction-bar");if(e){var t=e.querySelectorAll(".reaction-btn");t.forEach(function(n){n.addEventListener("click",function(){var a=n.getAttribute("data-emoji");a&&ia(a)})})}}function Xt(e,t){var n=document.getElementById("reaction-container");if(n){var a=document.createElement("div");a.className="reaction-bubble",a.textContent=e+" "+t,a.style.left=20+Math.random()*60+"%",n.appendChild(a),setTimeout(function(){a.remove()},3e3)}}function Se(e){var t=document.getElementById("stop-song-btn"),n=document.getElementById("next-round-admin-btn");if(e==="PLAYING"){if(Ke(),t&&!ce&&(t.classList.remove("is-disabled"),t.disabled=!1),n){n.classList.remove("is-disabled"),n.disabled=!1;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.skip"))}}else if(e==="REVEAL"){if(t&&!ce&&(t.classList.remove("is-disabled"),t.disabled=!1),n){n.classList.remove("is-disabled"),n.disabled=!1;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.next"))}}else if(n){n.classList.add("is-disabled"),n.disabled=!0;var a=n.querySelector(".control-label");a&&(a.textContent=y.t("game.next"))}}function ra(){if(!ce&&we()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){console.warn("[Beatify] Cannot stop song: WebSocket not connected");return}var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-label");t&&(t.textContent=y.t("game.stopping"))}s.ws.send(JSON.stringify({type:"admin",action:"stop_song"}))}}function sa(){if(Qe>=1){Zt("max");return}we()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"up"})))}function oa(){if(Qe<=0){Zt("min");return}we()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"down"})))}function Zt(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=e==="max"?"\u{1F50A} Max":"\u{1F507} Min",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1e3))}async function la(){var e=await Z(y.t("admin.endGameConfirm")||"End Game?",y.t("admin.endGameWarning")||"All players will be disconnected.",y.t("admin.endGame")||"End Game",y.t("common.cancel"));if(e&&we()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){alert(y.t("errors.CONNECTION_LOST"));return}var t=document.getElementById("end-game-btn");if(t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=y.t("game.ending"))}s.ws.send(JSON.stringify({type:"admin",action:"end_game"}))}}var je=!1,da=2e3;function $t(){if(!je&&s.ws&&s.ws.readyState===WebSocket.OPEN){je=!0;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!0,e.textContent=y.t("game.loading")),t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=y.t("game.wait"))}s.ws.send(JSON.stringify({type:"admin",action:"next_round"})),setTimeout(function(){je=!1,e&&(e.disabled=!1),t&&(t.disabled=!1)},da)}}function ca(){$t()}function en(){var e=document.getElementById("stop-song-btn"),t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn"),a=document.getElementById("next-round-admin-btn"),i=document.getElementById("end-game-btn");e&&e.addEventListener("click",ra),t&&t.addEventListener("click",sa),n&&n.addEventListener("click",oa),a&&a.addEventListener("click",ca),i&&i.addEventListener("click",la)}function tn(){ce=!0;var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-stopped"),e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u2713"),n&&(n.textContent=y.t("game.stopped"))}}function Ke(){ce=!1;var e=document.getElementById("stop-song-btn");if(e){e.classList.remove("is-stopped"),e.classList.remove("is-disabled"),e.disabled=!1;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u23F9\uFE0F"),n&&(n.textContent=y.t("game.stop"))}}function nn(e){Qe=e,ua(e),ma(e)}function ua(e){var t=document.getElementById("volume-indicator");if(t){var n=Math.round(e*100);t.textContent="\u{1F50A} "+n+"%",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1500)}}function ma(e){var t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn");t&&t.classList.toggle("is-at-limit",e>=1),n&&n.classList.toggle("is-at-limit",e<=0)}function an(){var e=document.getElementById("next-round-btn");e&&e.addEventListener("click",$t);var t=document.getElementById("reveal-view");t&&t.addEventListener("click",function(n){n.target.tagName==="BUTTON"||n.target.closest("button")||($.isRunning()&&$.skipAll(),O())})}function rn(e){var t=document.getElementById("intro-splash-modal");if(t){t.classList.remove("hidden");var n=document.getElementById("intro-splash-confirm-btn"),a=t.querySelector(".intro-splash-modal-waiting");n&&(e?(n.classList.remove("hidden"),a&&a.classList.add("hidden"),n.onclick=function(){s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"admin",action:"confirm_intro_splash"}))}):(n.classList.add("hidden"),a&&a.classList.remove("hidden")))}}function sn(){var e=document.getElementById("intro-splash-modal");e&&e.classList.add("hidden")}var p=window.BeatifyUtils||{};function Xe(e){var t=e.song||{},n=e.players||[],a=document.getElementById("reveal-round"),i=document.getElementById("reveal-total");a&&(a.textContent=e.round||1),i&&(i.textContent=e.total_rounds||10);var r=document.getElementById("intro-badge");if(r)if(e.is_intro_round){r.classList.remove("hidden"),r.classList.add("intro-badge--stopped");var o=r.querySelector("[data-i18n]");o&&(o.setAttribute("data-i18n","game.introStopped"),o.textContent=p.t("game.introStopped")||"Intro complete!")}else r.classList.add("hidden");var l=document.getElementById("reveal-album-cover");l&&(l.src=t.album_art||"/beatify/static/img/no-artwork.svg");var c=document.getElementById("correct-year");c&&(c.textContent=t.year||"????");var u=document.getElementById("song-title"),m=document.getElementById("song-artist");u&&(u.textContent=t.title||"Unknown Song"),m&&(m.textContent=t.artist||"Unknown Artist");var d=document.getElementById("fun-fact-container"),f=document.getElementById("fun-fact"),v=d?d.querySelector(".fun-fact-header"):null,E=p.getLocalizedSongField(t,"fun_fact");if(f&&(f.textContent=E||""),v&&(v.style.display=E?"flex":"none"),pa(t),ga(e.song_difficulty),d){var w=document.getElementById("song-rich-info"),b=w&&w.innerHTML.trim()!=="",L=E&&E.trim()!=="";d.classList.toggle("hidden",!L&&!b)}for(var S=null,C=0;C'+p.t("analytics.noSubmissions")+"",n.classList.remove("hidden");return}var i="";if(e.average_guess!==null&&t){var r=Math.round(e.average_guess-t);r===0?i=p.t("analytics.onTarget"):r>0?i=p.t("analytics.yearsLate",{years:r}):i=p.t("analytics.yearsEarly",{years:Math.abs(r)})}var o=va(e.all_guesses,t),l="";if(e.exact_match_players&&e.exact_match_players.length>0&&(l+='
🎯'+p.t("analytics.exactMatches")+':'+e.exact_match_players.map(x).join(", ")+"
"),e.speed_champion&&e.speed_champion.names){var c=e.speed_champion.names.map(x).join(", ");l+='
'+p.t("analytics.speedChampion")+':'+c+'('+e.speed_champion.time+"s)
"}if(e.furthest_players&&e.furthest_players.length>0&&e.all_guesses&&e.all_guesses.length>0){var u=e.all_guesses[e.all_guesses.length-1].years_off;u>0&&(l+='
😅'+p.t("analytics.furthestGuess")+':'+e.furthest_players.map(x).join(", ")+'('+u+" years)
")}var m=e.average_guess!==null?Math.round(e.average_guess):"?";a.innerHTML='
'+p.t("analytics.averageGuess")+''+m+'
'+e.accuracy_percentage+'%'+p.t("analytics.accuracy",{percent:""}).replace("%","")+'
'+i+'

'+p.t("analytics.histogram")+"

"+o+"
"+(l?'
'+l+"
":""),n.classList.remove("hidden")}function va(e,t){var n=7;if(!e||e.length===0)return'
'+p.t("analytics.noGuesses")+"
";for(var a=e.map(function(ae){return ae.guess}),i=Math.min.apply(null,a),r=Math.max.apply(null,a),o=r-i,l=Math.max(1,Math.ceil(o/n)),c=l*n,u=c-o-1,m=i-Math.floor(u/2),d=[],f=0;f=v&&t<=E})}for(var w=0;w=d[L].start&&b<=d[L].end){d[L].count++;break}for(var S=1,C=0;CS&&(S=d[C].count);for(var k="",B=0;B0?Math.max(R,10):0,F=A.count>0?''+A.count+"":"",ke=l===1?String(A.start):A.start+"-"+String(A.end).slice(-2);k+='
'+F+'
'+ke+"
"}return'
'+k+"
"}function ga(e){var t=document.getElementById("song-difficulty");if(t){if(!e){t.classList.add("hidden");return}for(var n="",a=0;a★';t.innerHTML='
'+n+'
'+p.t("difficulty."+e.label)+''+e.accuracy+"% "+p.t("difficulty.accuracy")+"",t.classList.remove("hidden")}}function pa(e){var t=document.getElementById("song-rich-info");if(t){var n=[],a=ya(e.chart_info||{});a.length>0&&(n=n.concat(a));var i=ba(e.certifications||[]);i.length>0&&(n=n.concat(i));var r=p.getLocalizedSongField(e,"awards")||[],o=La(r);o.length>0&&(n=n.concat(o)),n.length>0?t.innerHTML='
'+n.join("")+"
":t.innerHTML=""}}function ya(e){if(!e)return[];var t=[];if(e.billboard_peak&&e.billboard_peak>0){var n=e.weeks_on_chart?' \xB7 '+e.weeks_on_chart+" "+p.t("reveal.weeksShort")+"":"";t.push('\u{1F4CA}#'+e.billboard_peak+" "+p.t("reveal.chartBillboard")+n+"")}return e.german_peak&&e.german_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.german_peak+" "+p.t("reveal.chartGerman")+""),e.uk_peak&&e.uk_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.uk_peak+" "+p.t("reveal.chartUK")+""),t}function ba(e){if(!e||e.length===0)return[];for(var t=[],n=0;n'+r+""+x(a)+"")}return t}function ha(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"song-badge--diamond":t.indexOf("platinum")!==-1?"song-badge--platinum":t.indexOf("gold")!==-1?"song-badge--gold":"song-badge--platinum"}function Ea(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"\u{1F48E}":t.indexOf("platinum")!==-1?"\u{1F4BF}":t.indexOf("gold")!==-1?"\u{1F947}":"\u{1F4BF}"}function La(e){if(!e||e.length===0)return[];for(var t=[],n=e.slice(0,3),a=0;a'+o+""+x(i)+"")}return e.length>3&&t.push('+'+(e.length-3)+" more"),t}function wa(e){var t=e.toLowerCase();return t.indexOf("grammy")!==-1?"song-badge--grammy":t.indexOf("eurovision")!==-1?"song-badge--eurovision":t.indexOf("oscar")!==-1||t.indexOf("academy award")!==-1?"song-badge--oscar":t.indexOf("hall of fame")!==-1?"song-badge--halloffame":"song-badge--award"}function Sa(e){var t=e.toLowerCase();return t.indexOf("eurovision")!==-1?"\u{1F3A4}":t.indexOf("grammy")!==-1?"\u{1F3C6}":t.indexOf("hall of fame")!==-1?"\u2B50":"\u{1F3C6}"}function Ia(e,t){var n=document.getElementById("reveal-emotion"),a=document.getElementById("personal-result");if(!n)return;var i=n.classList.contains("reveal-emotion-inline")||document.querySelector(".reveal-container--compact");n.className=i?"reveal-emotion-inline":"reveal-emotion",n.innerHTML="",n.classList.add("hidden"),a&&a.classList.remove("is-delayed"),O();var r=p.t("reveal.emotions");function o(v){return v[Math.floor(Math.random()*v.length)]}function l(v){return v===1?p.t("reveal.offByYear"):p.t("reveal.offByYears",{years:v})}var c="missed",u=o(r.missed),m=o(r.missedSub);if(e&&!e.missed_round){var d=e.years_off||0;d===0?(c="exact",u=o(r.exact),m=o(r.exactSub)):d<=2?(c="close",u=o(r.close),m=o(r.closeSub)+" "+l(d)):d<=5?(c="close",u=o(r.close),m=l(d)):(c="wrong",u=o(r.wrong),m=o(r.wrongSub)+" "+l(d))}else e&&e.missed_round&&(c="missed",u=o(r.missed),m=o(r.missedSub));var f=''+u+"";m&&(f+='
'+m+"
"),n.innerHTML=f,n.classList.add("reveal-emotion--"+c),n.classList.remove("hidden"),c==="exact"&&ee(),a&&c!=="missed"&&a.classList.add("is-delayed")}function Ba(e,t){var n=document.getElementById("result-content");if(n){if(!e){n.innerHTML='
'+p.t("reveal.playerNotFound")+"
";return}if(e.missed_round){var a='
\u23F0
'+p.t("reveal.noSubmission")+"
",i=e.previous_streak||0;i>=2&&(a+='
\u{1F494}Lost '+i+"-streak!
"),a+='
0 pts
',n.innerHTML=a;return}var r=e.years_off||0,o=r===0?p.t("reveal.exact"):r===1?p.t("reveal.yearOff",{years:1}):p.t("reveal.yearsOff",{years:r}),l=r===0?"is-exact":r<=3?"is-close":"is-far",c=e.speed_multiplier||1,u=e.base_score||0,m=c>1,d=e.streak_bonus||0,f=e.artist_bonus||0,v="";m&&u>0&&(v='
'+p.t("reveal.baseScore")+''+u+' pts
'+p.t("reveal.speedBonus")+''+c.toFixed(2)+"x
");var E="";e.bet_outcome==="won"?E='
\u{1F3B2} '+p.t("reveal.betWon").replace("! 2x points","")+'2x
':e.bet_outcome==="lost"&&(E='
\u{1F3B2} '+p.t("reveal.betLost")+'-
');var w="";d>0&&(w='
'+e.streak+'-streak bonus!+'+d+" pts
");var b="";f>0&&(b='
\u{1F3A4} '+(p.t("artistChallenge.artistBonus")||"Artist Bonus")+'+'+f+" pts
");var L=e.round_score+d+f,S=d>0||f>0,C=e.round_score>=20,k=M.players[e.name],B=k?k.score:e.score-L,A=k?k.streak:0,R=dt(A,e.streak||0);n.innerHTML='
'+p.t("reveal.yourGuess")+''+(e.guess||"n/a")+'
'+p.t("reveal.correctYear")+''+t+'
'+p.t("reveal.accuracy")+''+o+"
"+v+E+'
+0 pts
'+w+b+(S?'
'+p.t("reveal.total")+': +0 pts
':"");var D=n.querySelector(".score-value");D&&(ot(D,0,e.round_score,{betWon:e.bet_outcome==="won",betLost:e.bet_outcome==="lost",streakMilestone:R,isBigScore:C}),e.bet_outcome==="won"&&e.round_score>0&&setTimeout(function(){var T=document.getElementById("personal-result-score");T&&Ne(T,e.round_score,{isBetWin:!0})},200));var V=n.querySelector(".total-value");V&&S&&(setTimeout(function(){se(V,0,L,600)},300),R&&setTimeout(function(){var T=n.querySelector(".result-total");if(T){var F={3:20,5:50,10:100}[R]||0;Ne(T,F,{isStreak:!0,text:"+"+F+" "+R+"-Streak!"})}},500))}}function xa(e){var t=document.getElementById("reveal-results-cards");if(t){if(!e||e.length===0){t.innerHTML="";return}var n=e.slice().sort(function(i,r){return(r.round_score||0)-(i.round_score||0)}),a='
';n.forEach(function(i){var r=i.name===s.playerName,o=i.missed_round===!0,l=i.years_off||0,c=i.round_score||0,u=o?"is-score-zero":c>=10?"is-score-high":c>=1?"is-score-medium":"is-score-zero",m=o?"\u2014":i.guess||"n/a",d=o?p.t("reveal.noGuessShort"):l===0?p.t("reveal.exact"):p.t("reveal.shortOff",{years:l}),f=i.bet?'\u{1F3B2}':"",v="";i.artist_bonus&&i.artist_bonus>0&&(v='\u{1F3A4} +'+i.artist_bonus+"");var E="";if(i.stole_from)E='
\u{1F977}'+p.t("steal.stolenFrom",{name:x(i.stole_from)})+"
";else if(i.was_stolen_by&&i.was_stolen_by.length>0){var w=i.was_stolen_by.map(x).join(", ");E='
\u{1F3AF}'+p.t("steal.stolenBy",{name:w})+"
"}a+='
'+x(i.name)+f+'
'+m+'
'+d+"
"+E+'
+'+c+v+"
"}),a+="
",t.innerHTML=a}}var _=window.BeatifyUtils||{};function ln(e){window.scrollTo(0,0);var t=e.leaderboard||[];t.forEach(function(b){b.is_current=b.name===s.playerName}),[1,2,3].forEach(function(b){var L=t.find(function(k){return k.rank===b}),S=document.getElementById("podium-"+b+"-name"),C=document.getElementById("podium-"+b+"-score");S&&(S.textContent=L?x(L.name):"---"),C&&(C.textContent=L?L.score:"0")});var n=t.find(function(b){return b.is_current}),a=document.getElementById("your-final-rank"),i=document.getElementById("your-final-score"),r=document.getElementById("stat-best-streak"),o=document.getElementById("stat-rounds"),l=document.getElementById("stat-bets");n&&(a&&(a.textContent="#"+n.rank),i&&(i.textContent=n.score+" "+_.t("leaderboard.points")),r&&(r.textContent=n.best_streak||0),o&&(o.textContent=n.rounds_played||0),l&&(l.textContent=n.bets_won||0));var c=document.getElementById("final-leaderboard-list");c&&(c.innerHTML=t.map(function(b){var L=b.is_current?"is-current":"",S=b.connected===!1?"final-entry--disconnected":"",C=b.connected===!1?'(away)':"";return'
#'+b.rank+''+x(b.name)+C+''+b.score+"
"}).join("")),Ca(e.superlatives),_a(e.highlights),ka(e.share_data);var u=document.getElementById("end-admin-controls"),m=document.getElementById("end-player-message");if(n&&n.is_admin){u&&u.classList.remove("hidden"),m&&m.classList.add("hidden");var d=document.getElementById("new-game-btn");d&&(d.onclick=Ta);var f=document.getElementById("player-rematch-btn");f&&(f.onclick=function(){f.disabled=!0;var b=f.textContent;f.textContent="\u23F3",fetch("/beatify/api/rematch-game",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json"}}).then(function(L){if(!L.ok)return L.json().then(function(S){throw new Error(S.message||"Rematch failed")});f.textContent="\u23F3"}).catch(function(L){console.error("[Player] Rematch failed:",L),alert(L.message||"Failed to start rematch"),f.disabled=!1,f.textContent=b})})}else u&&u.classList.add("hidden"),m&&m.classList.remove("hidden");if(n){var v=e.total_rounds||10,E=n.best_streak||0,w=E===v&&v>0;w?ee("perfect"):n.rank===1&&ee("winner")}}function Ca(e){var t=document.getElementById("superlatives-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n="";e.forEach(function(a,i){var r="";switch(a.value_label){case"avg_time":r=a.value+"s "+_.t("superlatives.avgTime");break;case"streak":r=a.value+" "+_.t("superlatives.streak");break;case"bets":r=a.value+" "+_.t("superlatives.bets");break;case"points":r=a.value+" "+_.t("superlatives.points");break;case"close_guesses":r=a.value+" "+_.t("superlatives.closeGuesses");break;default:r=a.value}n+='
'+a.emoji+'
'+_.t("superlatives."+a.title)+'
'+x(a.player_name)+'
'+r+"
"}),t.innerHTML=n,t.classList.remove("hidden")}}function _a(e){var t=document.getElementById("highlights-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n=document.getElementById("highlights-list");if(n){var a="";e.forEach(function(i,r){var o=_.t("highlights."+i.description,i.description_params)||i.description;o===i.description&&i.description_params&&(o=_.t("highlights."+i.description)||i.description,Object.keys(i.description_params).forEach(function(l){o=o.replace("{"+l+"}",x(i.description_params[l]))})),a+='
'+(i.emoji||"\u2728")+'
'+o+'
'+_.t("highlights.roundLabel",{round:i.round})+"
"}),n.innerHTML=a,t.classList.remove("hidden")}}}function ka(e){var t=document.getElementById("share-container");if(t){if(!e||!e.emoji_grids){t.classList.add("hidden");return}var n=e.emoji_grids[s.playerName];if(!n){var a=Object.keys(e.emoji_grids);a.length===1&&(n=e.emoji_grids[a[0]])}if(!n){t.classList.add("hidden");return}var i=document.getElementById("share-emoji-grid");if(i){var r=n.split(` -`).map(function(c){return'
'+_.escapeHtml(c)+"
"}).join("");i.innerHTML=r,i.dataset.rawText=n}var o=document.getElementById("share-copy-btn");o&&(o.onclick=function(){navigator.clipboard.writeText(n).then(function(){var c=document.getElementById("share-toast");c&&(c.classList.remove("hidden"),setTimeout(function(){c.classList.add("hidden")},2e3))})});var l=document.getElementById("share-save-btn");l&&(l.onclick=function(){Aa(n,e.playlist_name,e)}),t.classList.remove("hidden")}}function Aa(e,t,n){var a=document.createElement("canvas");a.width=800,a.height=800;var i=a.getContext("2d"),r=i.createLinearGradient(0,0,0,800);r.addColorStop(0,"#0f0c29"),r.addColorStop(.5,"#302b63"),r.addColorStop(1,"#24243e"),i.fillStyle=r,i.fillRect(0,0,800,800);var o=i.createLinearGradient(0,0,800,0);o.addColorStop(0,"#e94560"),o.addColorStop(1,"#0f3460"),i.fillStyle=o,i.fillRect(0,0,800,4);var l=new Image;l.src="/beatify/static/img/icon-256.png",l.onerror=function(){c(null)},l.onload=function(){c(l)};function c(u){u?(i.drawImage(u,28,20,64,64),i.fillStyle="#ffffff",i.font="bold 28px system-ui, sans-serif",i.textAlign="left",i.fillText("Beatify",104,60)):(i.fillStyle="#ffffff",i.font="bold 28px system-ui, sans-serif",i.textAlign="center",i.fillText("\u{1F3B5} Beatify",400,55)),i.textAlign="center";var m=(t||"").toUpperCase();i.font="bold 13px system-ui, sans-serif";var d=i.measureText(m).width+32,f=400-d/2;i.fillStyle="rgba(233,69,96,0.18)",i.beginPath(),i.roundRect(f,96,d,30,15),i.fill(),i.strokeStyle="#e94560",i.lineWidth=1,i.stroke(),i.fillStyle="#e94560",i.fillText(m,400,116);for(var v=e.split(` -`).filter(function(N){return N.trim()!==""}),E="",w=[],b="",L="",S="",C="",k=0;k=ge&&(s.isReconnecting=!1,pe(),et())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}}function Q(e){s.playerName=e,fn(e);var t=window.location.protocol==="https:"?"wss:":"ws:",n=t+"//"+window.location.host+"/beatify/ws";s.ws=new WebSocket(n),s.ws.onopen=function(){s.reconnectAttempts=0,s.isReconnecting=!1,pe(),gn();var a={type:"join",name:e};s.isAdmin&&(a.is_admin=!0),s.ws.send(JSON.stringify(a))},s.ws.onmessage=function(a){try{var i=JSON.parse(a.data);bn(i)}catch(r){console.error("Failed to parse WebSocket message:",r)}},s.ws.onclose=function(){if(s.intentionalLeave){s.intentionalLeave=!1;return}if(s.playerName&&s.reconnectAttempts=ge&&(s.isReconnecting=!1,pe(),et())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}s.connectWithSession=_e;s.connectWebSocket=Q;function bn(e){var t=document.getElementById("join-btn"),n=document.getElementById("name-input");if(e.type==="state"){var a=e.players||[],i=a.find(function(u){return u.name===s.playerName});if(i&&(s.isAdmin=i.is_admin===!0),e.language&&(Ha(e.language),typeof BeatifyI18n<"u"&&e.language!==BeatifyI18n.getLanguage()&&BeatifyI18n.setLanguage(e.language).then(function(){BeatifyI18n.initPageTranslations(),Pe(a),e.difficulty&&he(e.difficulty),e.phase==="REVEAL"&&Xe(e),(e.phase==="PLAYING"||e.phase==="REVEAL")&&Se(e.phase)})),e.phase==="LOBBY"){Y(),ue(),me(),s.currentRoundNumber=0,q("warmup");var r=document.getElementById("start-game-btn");r&&(r.disabled=!1,r.innerHTML=''+ve.t("lobby.startGame")+""),I("lobby-view"),Pe(a),e.difficulty&&he(e.difficulty),e.join_url&&It(e.join_url),_t(a)}else if(e.phase==="PLAYING"){var o=e.round||1;o!==s.currentRoundNumber&&(s.currentRoundNumber=o,jt()),q("party"),I("game-view"),oe(),Ot(e),e.intro_splash_pending?rn(s.isAdmin):sn(),e.difficulty&&he(e.difficulty),e.deadline&&Rt(e.deadline),Vt(),Ht(),Je(),Se("PLAYING"),me()}else e.phase==="REVEAL"?(Y(),e.early_reveal&&Tt(),q("party"),I("reveal-view"),Xe(e),Gt(),Dt(),Je(),Se("REVEAL"),s.hasReactedThisPhase=!1,Jt()):e.phase==="PAUSED"?(Y(),ue(),me(),q("warmup"),I("paused-view"),dn(e)):e.phase==="END"&&(Y(),ue(),me(),s.currentRoundNumber=0,q("warmup"),I("end-view"),ln(e),fe())}else if(e.type==="join_ack"){e.session_id&&Oa(e.session_id);try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}}else if(e.type==="reconnect_ack")e.success&&e.name?(s.playerName=e.name,fn(e.name),At(e.name)):(Ze(),fe(),s.playerName=null,I("join-view"));else if(e.type==="submit_ack")ze();else if(e.type==="metadata_update")Pt(e.song);else if(e.type==="error"){if(e.code==="ROUND_EXPIRED"||e.code==="ALREADY_SUBMITTED"){Ft(e);return}if(e.code==="GAME_ENDED"){I("end-view");return}if(e.code==="NOT_ADMIN"){s.isAdmin=!1,ue(),console.warn("Admin action rejected: not admin");return}if(e.code==="SESSION_TAKEOVER"){s.isReconnecting=!1,pe(),s.playerName=null,et(),console.warn("Session taken over by another tab");return}if(e.code==="SESSION_NOT_FOUND"){Ze(),s.intentionalLeave=!0,s.ws&&s.ws.close(),I("join-view");return}if(e.code==="ADMIN_CANNOT_LEAVE"){s.intentionalLeave=!1,alert(e.message||"Host cannot leave. End the game instead.");return}if(e.code==="INVALID_ACTION"&&e.message==="No song playing"){Ke(),console.warn("[Beatify] Stop song failed: No song playing");return}I("join-view"),Wa(e.message),t&&(t.disabled=!1,t.textContent=ve.t("join.joinButton")),n&&n.focus(),s.playerName=null,fe()}else if(e.type==="song_stopped")tn();else if(e.type==="volume_changed")nn(e.level);else if(e.type==="game_ended")ja();else if(e.type==="rematch_started"){console.log("[Player] Rematch started - transitioning to lobby"),$.clear(),O(),I("lobby-view");var l=document.getElementById("player-rematch-btn");l&&(l.disabled=!1,l.textContent="\u{1F501}");var c=$e();c&&(s.ws&&s.ws.readyState===WebSocket.OPEN?(s.reconnectAttempts=0,s.ws.send(JSON.stringify({type:"reconnect",session_id:c}))):(s.reconnectAttempts=0,_e()))}else e.type==="left"?Fa():e.type==="steal_targets"?Qt(e):e.type==="steal_ack"?Yt(e):e.type==="artist_guess_ack"?Wt(e):e.type==="movie_guess_ack"?Ut(e):e.type==="player_reaction"&&Xt(e.player_name,e.emoji)}function Fa(){fe(),Ze(),s.playerName=null,s.isAdmin=!1,I("join-view")}function ja(){var e=s.isAdmin;fe();try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}ft(),Et(),$.clear(),O(),s.playerName=null,s.isAdmin=!1,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.close(),s.ws=null;var t=document.getElementById("end-view");if(!(!t||!t.classList.contains("hidden"))){var n=document.getElementById("end-player-message");n&&(n.innerHTML='

Thanks for playing!

Scan the QR code again to join the next game.

',n.classList.remove("hidden")),I("end-view")}}function Wa(e){var t=document.getElementById("name-validation-msg");t&&(t.textContent=e,t.classList.remove("hidden"))}function tt(e){var t=(e||"").trim();return t?t.length>Ma?{valid:!1,error:"Name too long (max 20 characters)"}:{valid:!0,name:t}:{valid:!1,error:"Please enter a name"}}function cn(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");if(!(!e||!t)){var a=tt(e.value);a.valid&&(t.disabled=!0,t.textContent=ve.t("game.joining"),n&&n.classList.add("hidden"),Q(a.name))}}function qa(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");!e||!t||(e.addEventListener("input",function(){var a=tt(this.value);t.disabled=!a.valid,n&&(n.textContent=!a.valid&&this.value?a.error:"",n.classList.toggle("hidden",a.valid||!this.value))}),t.addEventListener("click",cn),e.addEventListener("keypress",function(a){a.key==="Enter"&&!t.disabled&&cn()}))}function Ua(){var e=document.getElementById("retry-connection-btn");e&&e.addEventListener("click",function(){s.playerName?(s.reconnectAttempts=0,I("loading-view"),Q(s.playerName)):Ce()})}async function un(){var e=K.getDeviceTier();document.body.classList.add("device-tier-"+e);var t=await ve.waitForI18n();if(!t)console.error("[Player] BeatifyI18n module failed to load - UI will use fallback text");else{var n=Ga();await BeatifyI18n.init(n),BeatifyI18n.initPageTranslations()}var a=document.getElementById("dashboard-hint-url");a&&(a.textContent=window.location.origin+"/beatify/dashboard");var i=document.getElementById("player-dashboard-url");if(i&&(i.href=window.location.origin+"/beatify/dashboard"),qa(),Bt(),Ct(),kt(),an(),en(),Ua(),vt(),gt(),pt(),Kt(),Va()&&s.playerName){Q(s.playerName);return}var r=Pa();if(r&&s.gameId){console.log("[Beatify] Auto-reconnecting as:",r),Q(r);return}if(r){var o=document.getElementById("name-input"),l=document.getElementById("join-btn");if(o&&(o.value=r,l)){var c=tt(r);l.disabled=!c.valid}}}Ce();document.getElementById("refresh-btn")?.addEventListener("click",function(){I("loading-view"),Ce()});document.getElementById("retry-btn")?.addEventListener("click",function(){I("loading-view"),Ce()});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",un):un();"serviceWorker"in navigator&&window.addEventListener("load",function(){navigator.serviceWorker.register("/beatify/static/sw.js",{scope:"/beatify/"}).then(function(e){console.log("[Beatify] SW registered:",e.scope)}).catch(function(e){console.warn("[Beatify] SW registration failed:",e)})});document.addEventListener("visibilitychange",function(){if(document.visibilityState==="visible"){var e=s.ws;(!e||e.readyState===WebSocket.CLOSING||e.readyState===WebSocket.CLOSED)&&s.playerName&&(console.log("[Beatify] Page visible, WebSocket dead \u2014 reconnecting immediately."),s.reconnectAttempts=0,_e())}}); +var Ae=window.BeatifyUtils||{},s={ws:null,playerName:null,isAdmin:!1,reconnectAttempts:0,isReconnecting:!1,intentionalLeave:!1,hasReactedThisPhase:!1,currentRoundNumber:0,gameId:new URLSearchParams(window.location.search).get("game"),connectWithSession:null,connectWebSocket:null},En=document.getElementById("loading-view"),wn=document.getElementById("not-found-view"),Ln=document.getElementById("ended-view"),Sn=document.getElementById("in-progress-view"),In=document.getElementById("join-view"),Bn=document.getElementById("lobby-view"),xn=document.getElementById("game-view"),_n=document.getElementById("reveal-view"),Cn=document.getElementById("paused-view"),kn=document.getElementById("end-view"),An=document.getElementById("connection-lost-view"),Tn=[En,wn,Ln,Sn,In,Bn,xn,_n,Cn,kn,An];function B(e){Ae.showView(Tn,e),(e==="join-view"||e==="loading-view"||e==="not-found-view"||e==="ended-view"||e==="in-progress-view"||e==="connection-lost-view")&&q("calm"),e==="join-view"&&setTimeout(function(){var t=document.getElementById("name-input");t&&t.focus()},100)}function Z(e,t,n,a){return new Promise(function(i){var r=document.getElementById("confirm-modal"),o=document.getElementById("confirm-modal-title"),l=document.getElementById("confirm-modal-message"),d=document.getElementById("confirm-modal-yes"),u=document.getElementById("confirm-modal-no");if(!r||!o||!l||!d||!u){i(confirm(t||e));return}o.textContent=e,l.textContent=t,d.textContent=n||Ae.t("common.confirm")||"Confirm",u.textContent=a||Ae.t("common.cancel")||"Cancel",r.classList.remove("hidden");function m(){r.classList.add("hidden"),d.removeEventListener("click",c),u.removeEventListener("click",f),v.removeEventListener("click",f)}function c(){m(),i(!0)}function f(){m(),i(!1)}var v=r.querySelector(".modal-backdrop");d.addEventListener("click",c),u.addEventListener("click",f),v&&v.addEventListener("click",f)})}function x(e){var t=document.createElement("div");return t.textContent=e,t.innerHTML}function he(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}function Nn(e){return 1-Math.pow(1-e,4)}function se(e,t,n,a,i){if(he()||t===n)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var r=K.getQualitySettings();if(r.scoreDuration===0)return e.textContent=n,{cancel:function(){},skipToEnd:function(){e.textContent=n}};var o=Math.min(a,r.scoreDuration||a);i=i||Nn;var l=null,d=null,u=!1,m=n;function c(f){if(!u){l||(l=f);var v=f-l,w=Math.min(v/o,1),L=i(w),b=Math.round(t+(m-t)*L);e.textContent=b,w<1&&(d=requestAnimationFrame(c))}}return d=requestAnimationFrame(c),{cancel:function(){u=!0,d&&cancelAnimationFrame(d)},skipToEnd:function(){u=!0,d&&cancelAnimationFrame(d),e.textContent=m}}}function ot(e,t,n,a){a=a||{};var i=500;a.betWon?i=800:a.isBigScore?i=700:a.betLost&&(i=400),e.classList.add("score-animating");var r=null;a.betWon?r="score-glow-gold":a.betLost?(r="score-shake",e.classList.add("score-flash-red")):a.streakMilestone?r="score-burst":a.isBigScore&&(r="score-pop"),r&&!he()&&e.classList.add(r),se(e,t,n,i);function o(){e.classList.remove("score-animating"),r&&e.classList.remove(r),e.classList.remove("score-flash-red")}r&&!he()?e.addEventListener("animationend",function l(){e.removeEventListener("animationend",l),o()}):setTimeout(o,i+50)}function Ne(e,t,n){if(n=n||{},!he()){var a=document.createElement("div");a.className="points-popup",a.textContent=n.text||"+"+t,n.isStreak?a.classList.add("points-popup--streak"):n.isBetWin&&a.classList.add("points-popup--gold");var i=e.getBoundingClientRect();a.style.left=i.left+i.width/2+"px",a.style.top=i.top+"px",document.body.appendChild(a),a.addEventListener("animationend",function(){a.parentNode&&a.parentNode.removeChild(a)}),setTimeout(function(){a.parentNode&&a.parentNode.removeChild(a)},1200)}}var M={players:{},leaderboard:[],initialized:!1};function lt(){return M.initialized}var rt=[3,5,10,15,20,25];function dt(e,t){for(var n=0;n=a)return a}return null}function ct(e){var t=e.map(function(a){return a.name}),n={};return t.forEach(function(a,i){var r=M.leaderboard.indexOf(a);r===-1?n[a]="new":ir&&(n[a]="down")}),n}function ut(e,t){M.players={},e.forEach(function(n){M.players[n.name]={score:n.score,rank:n.rank||0,streak:n.streak||0}}),t&&(M.leaderboard=t.map(function(n){return n.name})),M.initialized=!0}var K=(function(){var e=window.matchMedia("(prefers-reduced-motion: reduce)"),t=e.matches;e.addEventListener("change",function(i){t=i.matches});var n=null;function a(){if(n!==null)return n;var i=navigator.hardwareConcurrency||2,r=navigator.deviceMemory||4,o=/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream;return i<=2||r<=2?n="low":i<=4||r<=4||o?n="medium":n="high",n}return a(),{prefersReducedMotion:function(){return t},getDeviceTier:a,getQualitySettings:function(){var i=a();if(t)return{confettiParticles:0,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!1};switch(i){case"low":return{confettiParticles:5,scoreDuration:0,leaderboardAnimation:"none",neonGlow:!1,enableAnimations:!0};case"medium":return{confettiParticles:10,scoreDuration:300,leaderboardAnimation:"simplified",neonGlow:!1,enableAnimations:!0};default:return{confettiParticles:15,scoreDuration:500,leaderboardAnimation:"full",neonGlow:!0,enableAnimations:!0}}},ifMotionAllowed:function(i,r){t?r&&r():i()},withWillChange:function(i,r,o){i&&(i.style.willChange=r,setTimeout(function(){i&&i.style&&(i.style.willChange="auto")},(o||500)+100))}}})(),$=(function(){var e=[],t=!1,n=null,a=null,i=2e3;function r(){if(a&&(clearTimeout(a),a=null),e.length===0){t=!1,n=null;return}n=e.shift(),a=setTimeout(function(){n&&n.skipToEnd&&n.skipToEnd(),r()},i),n.run(function(){a&&(clearTimeout(a),a=null),r()})}return{add:function(o){e.push(o),t||(t=!0,r())},skipAll:function(){a&&(clearTimeout(a),a=null),n&&n.skipToEnd&&n.skipToEnd(),e.forEach(function(o){o.skipToEnd&&o.skipToEnd()}),e=[],t=!1,n=null},clear:function(){a&&(clearTimeout(a),a=null),e=[],t=!1,n=null},isRunning:function(){return t},getMaxDuration:function(){return i}}})(),X={VISIBLE_BUFFER:2,ENTRY_HEIGHT:48,MIN_PLAYERS_FOR_LAZY:10,ROOT_MARGIN:"96px 0px",DEFAULT_VIEWPORT_HEIGHT:280},h={observer:null,fullData:[],visibleRange:{start:0,end:10},listEl:null,isLazyEnabled:!1};function mt(e){e&&(h.observer&&h.listEl!==e&&(h.observer.disconnect(),h.observer=null),!h.observer&&(h.listEl=e,h.observer=new IntersectionObserver(function(t){t.forEach(function(n){if(!(!n.isIntersecting||!h.isLazyEnabled)){var a=h.fullData,i=h.visibleRange,r=X.VISIBLE_BUFFER;if(n.target.classList.contains("leaderboard-sentinel--top")){if(i.start>0){var o=Math.max(0,i.start-r);h.visibleRange.start=o,re()}}else if(n.target.classList.contains("leaderboard-sentinel--bottom")&&i.end0&&(l+='
'),l+='
';for(var d=n.start;d',r>0&&(l+='
'),e.innerHTML=l,e.scrollTop=o,h.observer){var u=e.querySelectorAll(".leaderboard-sentinel");u.forEach(function(m){h.observer.observe(m)})}}}function Me(e){if(!e)return"";if(e.separator)return'
...
';var t=e.name||"Unknown",n=e.rank||0,a=e.score||0,i=n<=3?"is-top-"+n:"",r=e.is_current?"is-current":"",o="";e.rank_change>0||e._rankChange==="up"?o="leaderboard-entry--climbing leaderboard-entry--slide-up":(e.rank_change<0||e._rankChange==="down")&&(o="leaderboard-entry--falling leaderboard-entry--slide-down");var l="";e.rank_change>0?l='\u25B2'+e.rank_change+"":e.rank_change<0&&(l='\u25BC'+Math.abs(e.rank_change)+"");var d="";if(e.streak>=2){var u=e.streak>=5?"streak-indicator--hot":"";d='\u{1F525}'+e.streak+""}var m=e.connected===!1?"leaderboard-entry--disconnected":"",c=e.connected===!1?'(away)':"",f=e._displayScore!==void 0?e._displayScore:a;return'
#'+n+''+x(t)+c+''+f+"
"}function Re(e,t){for(var n=X,a=h.listEl&&h.listEl.clientHeight||n.DEFAULT_VIEWPORT_HEIGHT,i=Math.ceil(a/n.ENTRY_HEIGHT),r=n.VISIBLE_BUFFER,o=-1,l=0;l=e.length-i?(d=Math.max(0,e.length-i-r),u=e.length):(d=Math.max(0,o-Math.floor(i/2)-r),u=Math.min(e.length,o+Math.ceil(i/2)+r)),{start:d,end:u}}function ft(){h.observer&&(h.observer.disconnect(),h.observer=null),h.isLazyEnabled=!1,h.fullData=[]}function vt(){var e;function t(){clearTimeout(e),e=setTimeout(function(){h.isLazyEnabled&&h.fullData.length>0&&(h.visibleRange=Re(h.fullData,s.playerName),re())},150)}window.addEventListener("resize",t),window.addEventListener("orientationchange",t)}function gt(){var e=document.getElementById("qr-share-area");if(!(!e||e.tagName!=="DETAILS")){var t="beatify_qr_expanded",n=768,a=sessionStorage.getItem(t);a!==null?e.open=a==="true":e.open=window.innerWidth>=n,e.addEventListener("toggle",function(){sessionStorage.setItem(t,e.open.toString())})}}function pt(){var e=document.querySelectorAll(".lobby-container--compact .section-header-collapsible");e.forEach(function(t){t.addEventListener("click",function(){var n=t.closest(".section-collapsible");if(n){var a=n.classList.contains("collapsed");n.classList.toggle("collapsed"),t.setAttribute("aria-expanded",a?"true":"false")}})})}var yt={ITEM_HEIGHT:60,OVERSCAN:3,THRESHOLD:15,CONTAINER_HEIGHT:320},g={container:null,items:[],scrollTop:0,isVirtual:!1,topSpacer:null,bottomSpacer:null,contentWrapper:null,scrollHandler:null,resizeHandler:null};function bt(e){if(e){g.container=e;var t=!1;g.scrollHandler=function(){g.scrollTop=e.scrollTop,t||(requestAnimationFrame(function(){Te(),t=!1}),t=!0)};var n;g.resizeHandler=function(){clearTimeout(n),n=setTimeout(function(){g.isVirtual&&Te()},100)},e.addEventListener("scroll",g.scrollHandler,{passive:!0}),window.addEventListener("resize",g.resizeHandler)}}function ht(e,t){g.items=e,g.renderItem=t;var n=g.container;if(n){var a=n.scrollTop,i=g.isVirtual;e.length0&&(n.scrollTop=a,g.scrollTop=a)}}function Mn(){var e=g.container;if(e){e.innerHTML="";var t=document.createElement("div");t.className="virtual-spacer-top",g.topSpacer=t;var n=document.createElement("div");n.className="virtual-content-wrapper",g.contentWrapper=n;var a=document.createElement("div");a.className="virtual-spacer-bottom",g.bottomSpacer=a,e.appendChild(t),e.appendChild(n),e.appendChild(a)}}function Te(){var e=yt,t=g.items,n=g.container,a=g.contentWrapper;if(!(!n||!a||!t.length)){var i=n.clientHeight||e.CONTAINER_HEIGHT,r=g.scrollTop,o=e.ITEM_HEIGHT,l=e.OVERSCAN,d=Math.max(0,Math.floor(r/o)-l),u=Math.min(t.length,Math.ceil((r+i)/o)+l);g.topSpacer&&(g.topSpacer.style.height=d*o+"px"),g.bottomSpacer&&(g.bottomSpacer.style.height=(t.length-u)*o+"px");for(var m="",c=d;c"u"){console.warn("[Confetti] Library not loaded");return}O();var t=K.getQualitySettings(),n=t.confettiParticles;if(n===0){st();return}var a=K.getDeviceTier(),i=a==="low"?.5:a==="medium"?.75:1;switch(e=e||"exact",e){case"exact":var r=Math.round(2e3*i),o=Date.now()+r;(function v(){confetti({particleCount:n,spread:70,origin:{y:.6},colors:["#FFD700","#FFA500","#FFEC8B"]}),Date.now()0);var l=e.slice().sort(function(c,f){return c.connected!==f.connected?c.connected?-1:1:0}),d=wt.map(function(c){return c.name}),u=l.filter(function(c){return d.indexOf(c.name)===-1}).map(function(c){return c.name});g.container||bt(t);var m=function(c){var f=u.indexOf(c.name)!==-1,v=c.name===s.playerName,w=c.connected===!1,L=["player-card",f?"is-new":"",v?"player-card--you":"",w?"player-card--disconnected":""].filter(Boolean).join(" "),b=w?'(away)':"";return'
'+x(c.name)+(v?''+U.t("leaderboard.you")+"":"")+b+"
"};ht(l,m),setTimeout(function(){var c=g.isVirtual?g.contentWrapper:t;if(c)for(var f=c.querySelectorAll(".is-new"),v=0;v=2?(n[0][0]+n[1][0]).toUpperCase():t.slice(0,Math.min(2,t.length)).toUpperCase()}function Gn(e){var t=document.getElementById("submission-tracker"),n=document.getElementById("submitted-players");if(!(!t||!n)){var a=e||[],i=a.filter(function(l){return l.submitted}).length,r=a.length,o=i===r&&r>0;t.classList.toggle("all-submitted",o),n.innerHTML=a.map(function(l){var d=Hn(l.name),u=l.name===s.playerName,m=l.connected===!1,c=["player-indicator",l.submitted?"is-submitted":"",u?"is-current-player":"",m?"player-indicator--disconnected":""].filter(Boolean).join(" "),f="";return l.steal_used&&(f+='\u{1F977}'),l.bet&&(f+='\u{1F3B2}'),'
'+f+'
'+x(d)+'
'+x(l.name)+"
"}).join("")}}function Ue(e,t,n){var a=e.leaderboard||[],i=document.getElementById(t||"leaderboard-list");if(i){var r=n&<(),o=r?ct(a):{};a.forEach(function(c){c.is_current=c.name===s.playerName;var f=o[c.name];f&&(c._rankChange=f);var v=M.players[c.name],w=v?v.score:c.score;c._prevScore=w,c._displayScore=n?w:c.score});var l=Dn(a,s.playerName),d=a.length>=X.MIN_PLAYERS_FOR_LAZY;if(d)h.observer||mt(i),h.fullData=l,h.isLazyEnabled=!0,h.listEl=i,h.visibleRange=Re(l,s.playerName),re();else{h.isLazyEnabled=!1;var u="";l.forEach(function(c){u+=Me(c)}),i.innerHTML=u}var m=[];r&&l.forEach(function(c){!c.separator&&c._prevScore!==c.score&&m.push({name:c.name,prevScore:c._prevScore,newScore:c.score})}),r&&m.length>0&&requestAnimationFrame(function(){for(var c={},f=i.querySelectorAll(".leaderboard-entry[data-name]"),v=0;v8&&Vn(i),Fn(a),jn(a),ut(e.players||[],a)}}function Dn(e,t){if(e.length<=10)return e;for(var n=e.slice(0,5),a=e.slice(-3),i=-1,r=0;r=e.length-3?[].concat(n,[{separator:!0}],a):[].concat(n,[{separator:!0}],[e[i]],[{separator:!0}],a)}function Vn(e){var t=e.querySelector(".leaderboard-entry.is-current");t&&t.scrollIntoView({behavior:"smooth",block:"center"})}function Fn(e){var t=document.getElementById("leaderboard-you"),n=e.find(function(a){return a.is_current});t&&n&&(t.textContent=y.t("leaderboard.you")+" #"+n.rank,t.classList.remove("hidden"))}function Ht(){var e=document.getElementById("leaderboard-toggle"),t=document.getElementById("game-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function jn(e,t){var n=t?[t]:["leaderboard-summary","reveal-leaderboard-summary"];n.forEach(function(a){var i=document.getElementById(a);if(!(!i||!e||e.length===0)){var r=e[0];r&&(i.textContent=r.name+": "+r.score)}})}function Gt(){var e=document.getElementById("reveal-leaderboard-toggle"),t=document.getElementById("reveal-leaderboard");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}function Dt(){var e=document.getElementById("round-analytics-toggle"),t=document.getElementById("round-analytics");e&&t&&!e.hasAttribute("data-initialized")&&(e.setAttribute("data-initialized","true"),e.addEventListener("click",function(){var n=t.classList.toggle("collapsed");e.setAttribute("aria-expanded",!n)}))}var G=!1,le=!1,de=!1,z=!1,H=null,Le=null,Wn=300,Nt=0,te=!1,ne=null,qn=500,Mt=0;function Vt(){var e=document.getElementById("year-slider"),t=document.getElementById("selected-year");if(!(!e||!t)){e.addEventListener("input",function(){t.textContent=this.value});var n=document.getElementById("bet-toggle");n&&n.addEventListener("click",function(){G||(le=!le,n.classList.toggle("is-active",le))});var a=document.getElementById("submit-btn");a&&a.addEventListener("click",Un);var i=document.getElementById("steal-btn");i&&i.addEventListener("click",$n);var r=document.getElementById("steal-modal-close");r&&r.addEventListener("click",qe);var o=document.getElementById("steal-modal");if(o){var l=o.querySelector(".steal-modal-backdrop");l&&l.addEventListener("click",qe)}}}function Un(){if(!G){var e=document.getElementById("year-slider"),t=document.getElementById("submit-btn");if(!(!e||!t)){var n=parseInt(e.value,10);t.disabled=!0,t.classList.add("is-loading"),s.ws&&s.ws.readyState===WebSocket.OPEN?s.ws.send(JSON.stringify({type:"submit",year:n,bet:le})):(We(y.t("errors.connectionLost")),t.disabled=!1,t.classList.remove("is-loading"))}}}function ze(){G=!0;var e=document.getElementById("year-selector"),t=document.getElementById("submit-btn"),n=document.getElementById("submitted-confirmation"),a=document.getElementById("bet-toggle");e&&e.classList.add("is-submitted"),t&&t.classList.add("hidden"),a&&a.classList.add("hidden"),n&&n.classList.remove("hidden")}function Ft(e){var t=document.getElementById("submit-btn");t&&(t.disabled=!1,t.classList.remove("is-loading")),e.code==="ROUND_EXPIRED"?(We(y.t("errors.timesUp")),G=!0,t&&(t.disabled=!0)):e.code==="ALREADY_SUBMITTED"?ze():We(e.message||"Submission failed")}function We(e){var t=document.getElementById("submit-btn");t&&(t.textContent=e,t.classList.add("is-error"),setTimeout(function(){t.textContent=y.t("game.submitGuess"),t.classList.remove("is-error")},2e3))}function jt(){G=!1,le=!1;var e=document.getElementById("year-selector"),t=document.getElementById("submit-btn"),n=document.getElementById("submitted-confirmation"),a=document.getElementById("year-slider"),i=document.getElementById("bet-toggle");if(e&&e.classList.remove("is-submitted"),t&&(t.disabled=!1,t.classList.remove("hidden","is-loading","is-error"),t.textContent=y.t("game.submitGuess")),i&&i.classList.remove("hidden","is-active"),n&&n.classList.add("hidden"),a){a.value=1990;var r=document.getElementById("selected-year");r&&(r.textContent="1990")}de=!1,Ye(),Jn(),Xn()}function zn(e,t){var n=document.getElementById("artist-challenge-container");if(n){if(!e||!e.options){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("artist-options"),i=document.getElementById("artist-result"),r=Array.from(a.querySelectorAll(".artist-option-btn")).map(function(m){return m.dataset.artist}),o=e.options;if(JSON.stringify(r)!==JSON.stringify(o)&&(a.innerHTML="",o.forEach(function(m,c){var f=document.createElement("button");f.className="artist-option-btn",f.dataset.artist=m,f.dataset.index=c,f.textContent=m,f.addEventListener("click",function(){Yn(m)}),a.appendChild(f)})),e.winner){var l=a.querySelectorAll(".artist-option-btn");if(l.forEach(function(m){m.classList.add("is-disabled"),m.classList.remove("is-loading","is-wrong");var c=e.correct_artist||Le;c&&m.dataset.artist===c&&m.classList.add("is-winner")}),e.winner===s.playerName){var d=e.bonus_points||5;i.textContent=(y.t("artistChallenge.youGotIt")||"You got it! +{points} points").replace("{points}",d),i.className="artist-result is-winner"}else{var u=(y.t("artistChallenge.someoneBeatYou")||"{winner} got it first!").replace("{winner}",e.winner);i.textContent=u,i.className="artist-result is-late"}i.classList.remove("hidden"),z=!0}else z||i.classList.add("hidden")}}function Yn(e){var t=Date.now();if(!(t-Nt0)){var n=document.createElement("span");n.className="movie-rank-badge",n.textContent="+"+e.bonus,t.appendChild(n)}De();var a=(y.t("movieChallenge.youGotIt")||"Correct! #{rank} \u2014 +{bonus} points").replace("{rank}",e.rank||1).replace("{bonus}",e.bonus||0);Ve(a,!0),te=!0}else t&&(t.classList.remove("is-loading"),t.classList.add("is-wrong","is-selected")),De(),Ve(y.t("movieChallenge.wrongGuess")||"Not quite...",!1),te=!0;ne=null}function De(){document.querySelectorAll(".movie-option-btn").forEach(function(e){e.classList.add("is-disabled"),e.classList.remove("is-loading")})}function Ve(e,t){var n=document.getElementById("movie-result");n&&(n.textContent=e,n.className="movie-result "+(t?"is-winner":"is-late"),n.classList.remove("hidden"))}function Xn(){te=!1,ne=null;var e=document.getElementById("movie-challenge-container");e&&e.classList.add("hidden");var t=document.getElementById("movie-options");t&&(t.innerHTML="");var n=document.getElementById("movie-result");n&&(n.classList.add("hidden"),n.className="movie-result hidden")}function zt(e,t){var n=document.getElementById("movie-reveal-section");if(n){if(!e||!e.correct_movie){n.classList.add("hidden");return}n.classList.remove("hidden");var a=document.getElementById("movie-reveal-name");a&&(a.textContent=e.correct_movie);var i=document.getElementById("movie-reveal-winners");if(i&&e.results){var r=e.results.winners||[];if(r.length>0){i.innerHTML="",i.classList.remove("hidden");var o=document.createElement("div");o.className="movie-reveal-winners-title",o.textContent=y.t("movieChallenge.winnersTitle")||"Movie Quiz Winners",i.appendChild(o),r.forEach(function(d){var u=document.createElement("div");u.className="movie-reveal-winner-entry",d.name===t?u.classList.add("is-you"):u.classList.add("is-other"),u.textContent=d.name+" \u2014 +"+d.bonus+" ("+d.time+"s)",i.appendChild(u)})}else{i.innerHTML="",i.classList.remove("hidden");var l=document.createElement("div");l.className="movie-reveal-no-winner",l.textContent=y.t("movieChallenge.noWinner")||"No one guessed the movie",i.appendChild(l)}}}}function Zn(e){if(!(!s.playerName||!e)){var t=e.find(function(i){return i.name===s.playerName});if(t){de=t.steal_available&&!G;var n=document.getElementById("steal-indicator"),a=document.getElementById("steal-btn");de?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden")):Ye()}}}function Ye(){var e=document.getElementById("steal-indicator"),t=document.getElementById("steal-btn");e&&e.classList.add("hidden"),t&&t.classList.add("hidden")}function $n(){!de||G||s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"get_steal_targets"}))}function ea(e){var t=document.getElementById("steal-modal"),n=document.getElementById("steal-target-list");if(!(!t||!n)){if(n.innerHTML="",!e||e.length===0){var a=document.createElement("p");a.className="steal-no-targets",a.textContent=y.t("steal.waitForSubmit"),n.appendChild(a)}else e.forEach(function(i){var r=document.createElement("button");r.className="steal-target-btn",r.textContent=i,r.addEventListener("click",function(){ta(i)}),n.appendChild(r)});t.classList.remove("hidden")}}function qe(){var e=document.getElementById("steal-modal");e&&e.classList.add("hidden")}async function ta(e){var t=y.t("steal.confirm").replace("{name}",e),n=await Z(y.t("steal.confirmTitle")||"Steal Answer?",t,y.t("steal.confirmButton")||"Steal",y.t("common.cancel"));n&&(s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"steal",target:e})),qe())}function Yt(e){if(e.success){de=!1,G=!0,Ye();var t=document.getElementById("year-selector"),n=document.getElementById("submit-btn"),a=document.getElementById("submitted-confirmation");t&&t.classList.add("is-submitted"),n&&n.classList.add("hidden"),a&&a.classList.remove("hidden"),na(e.target,e.year);var i=document.getElementById("selected-year"),r=document.getElementById("year-slider");i&&(i.textContent=e.year),r&&(r.value=e.year)}}function Jt(e){ea(e.targets||[])}function na(e,t){var n=document.getElementById("steal-confirmation"),a=document.getElementById("steal-confirmation-text");if(!(!n||!a)){var i=y.t("steal.success").replace("{name}",e).replace("{year}",t);a.textContent=i,n.classList.remove("hidden"),setTimeout(function(){n.classList.add("hidden")},3e3)}}var Fe=!1,aa=500,ce=!1,Je=.5;function ue(){return Fe?!1:(Fe=!0,setTimeout(function(){Fe=!1},aa),!0)}function Qe(){if(s.isAdmin){var e=document.getElementById("admin-control-bar");e&&(e.classList.remove("hidden"),document.body.classList.add("has-control-bar"))}}function me(){var e=document.getElementById("admin-control-bar");e&&(e.classList.add("hidden"),document.body.classList.remove("has-control-bar"))}function Qt(){var e=document.getElementById("reaction-bar");e&&e.classList.remove("hidden")}function fe(){var e=document.getElementById("reaction-bar");e&&e.classList.add("hidden")}function ia(e){s.hasReactedThisPhase||(s.hasReactedThisPhase=!0,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"reaction",emoji:e})))}function Kt(){var e=document.getElementById("reaction-bar");if(e){var t=e.querySelectorAll(".reaction-btn");t.forEach(function(n){n.addEventListener("click",function(){var a=n.getAttribute("data-emoji");a&&ia(a)})})}}function Xt(e,t){var n=document.getElementById("reaction-container");if(n){var a=document.createElement("div");a.className="reaction-bubble",a.textContent=e+" "+t,a.style.left=20+Math.random()*60+"%",n.appendChild(a),setTimeout(function(){a.remove()},3e3)}}function Se(e){var t=document.getElementById("stop-song-btn"),n=document.getElementById("seek-forward-btn"),a=document.getElementById("next-round-admin-btn");if(e==="PLAYING"){if(Ke(),t&&!ce&&(t.classList.remove("is-disabled"),t.disabled=!1),n&&(n.classList.remove("is-disabled","hidden"),n.disabled=!1),a){a.classList.remove("is-disabled"),a.disabled=!1;var i=a.querySelector(".control-label");i&&(i.textContent=y.t("game.skip"))}}else if(e==="REVEAL"){if(t&&!ce&&(t.classList.remove("is-disabled"),t.disabled=!1),n&&(n.classList.remove("hidden","is-disabled"),n.disabled=!1),a){a.classList.remove("is-disabled"),a.disabled=!1;var i=a.querySelector(".control-label");i&&(i.textContent=y.t("game.next"))}}else if(n&&(n.classList.add("hidden"),n.disabled=!0),a){a.classList.add("is-disabled"),a.disabled=!0;var i=a.querySelector(".control-label");i&&(i.textContent=y.t("game.next"))}}function ra(){if(!ce&&ue()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){console.warn("[Beatify] Cannot stop song: WebSocket not connected");return}var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-label");t&&(t.textContent=y.t("game.stopping"))}s.ws.send(JSON.stringify({type:"admin",action:"stop_song"}))}}function sa(){if(Je>=1){Zt("max");return}ue()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"up"})))}function oa(){ue()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"seek_forward",seconds:10})))}function la(){if(Je<=0){Zt("min");return}ue()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"down"})))}function Zt(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=e==="max"?"\u{1F50A} Max":"\u{1F507} Min",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1e3))}async function da(){var e=await Z(y.t("admin.endGameConfirm")||"End Game?",y.t("admin.endGameWarning")||"All players will be disconnected.",y.t("admin.endGame")||"End Game",y.t("common.cancel"));if(e&&ue()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){alert(y.t("errors.CONNECTION_LOST"));return}var t=document.getElementById("end-game-btn");if(t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=y.t("game.ending"))}s.ws.send(JSON.stringify({type:"admin",action:"end_game"}))}}var je=!1,ca=2e3;function $t(){if(!je&&s.ws&&s.ws.readyState===WebSocket.OPEN){je=!0;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!0,e.textContent=y.t("game.loading")),t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=y.t("game.wait"))}s.ws.send(JSON.stringify({type:"admin",action:"next_round"})),setTimeout(function(){je=!1,e&&(e.disabled=!1),t&&(t.disabled=!1)},ca)}}function ua(){$t()}function en(){var e=document.getElementById("stop-song-btn"),t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn"),a=document.getElementById("seek-forward-btn"),i=document.getElementById("next-round-admin-btn"),r=document.getElementById("end-game-btn");e&&e.addEventListener("click",ra),t&&t.addEventListener("click",sa),n&&n.addEventListener("click",la),a&&a.addEventListener("click",oa),i&&i.addEventListener("click",ua),r&&r.addEventListener("click",da)}function tn(){ce=!0;var e=document.getElementById("stop-song-btn");if(e){e.classList.add("is-stopped"),e.classList.add("is-disabled"),e.disabled=!0;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u2713"),n&&(n.textContent=y.t("game.stopped"))}}function Ke(){ce=!1;var e=document.getElementById("stop-song-btn");if(e){e.classList.remove("is-stopped"),e.classList.remove("is-disabled"),e.disabled=!1;var t=e.querySelector(".control-icon"),n=e.querySelector(".control-label");t&&(t.textContent="\u23F9\uFE0F"),n&&(n.textContent=y.t("game.stop"))}}function nn(e){Je=e,ma(e),fa(e)}function ma(e){var t=document.getElementById("volume-indicator");if(t){var n=Math.round(e*100);t.textContent="\u{1F50A} "+n+"%",t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},1500)}}function fa(e){var t=document.getElementById("volume-up-btn"),n=document.getElementById("volume-down-btn");t&&t.classList.toggle("is-at-limit",e>=1),n&&n.classList.toggle("is-at-limit",e<=0)}function an(){var e=document.getElementById("next-round-btn");e&&e.addEventListener("click",$t);var t=document.getElementById("reveal-view");t&&t.addEventListener("click",function(n){n.target.tagName==="BUTTON"||n.target.closest("button")||($.isRunning()&&$.skipAll(),O())})}function rn(e){var t=document.getElementById("intro-splash-modal");if(t){t.classList.remove("hidden");var n=document.getElementById("intro-splash-confirm-btn"),a=t.querySelector(".intro-splash-modal-waiting");n&&(e?(n.classList.remove("hidden"),a&&a.classList.add("hidden"),n.onclick=function(){s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"admin",action:"confirm_intro_splash"}))}):(n.classList.add("hidden"),a&&a.classList.remove("hidden")))}}function sn(){var e=document.getElementById("intro-splash-modal");e&&e.classList.add("hidden")}var p=window.BeatifyUtils||{};function Xe(e){var t=e.song||{},n=e.players||[],a=document.getElementById("reveal-round"),i=document.getElementById("reveal-total");a&&(a.textContent=e.round||1),i&&(i.textContent=e.total_rounds||10);var r=document.getElementById("closest-wins-badge");r&&(e.closest_wins_mode?r.classList.remove("hidden"):r.classList.add("hidden"));var o=document.getElementById("intro-badge");if(o)if(e.is_intro_round){o.classList.remove("hidden"),o.classList.add("intro-badge--stopped");var l=o.querySelector("[data-i18n]");l&&(l.setAttribute("data-i18n","game.introStopped"),l.textContent=p.t("game.introStopped")||"Intro complete!")}else o.classList.add("hidden");var d=document.getElementById("reveal-album-cover");d&&(d.src=t.album_art||"/beatify/static/img/no-artwork.svg");var u=document.getElementById("correct-year");u&&(u.textContent=t.year||"????");var m=document.getElementById("song-title"),c=document.getElementById("song-artist");m&&(m.textContent=t.title||"Unknown Song"),c&&(c.textContent=t.artist||"Unknown Artist");var f=document.getElementById("fun-fact-container"),v=document.getElementById("fun-fact"),w=f?f.querySelector(".fun-fact-header"):null,L=p.getLocalizedSongField(t,"fun_fact");if(v&&(v.textContent=L||""),w&&(w.style.display=L?"flex":"none"),ya(t),pa(e.song_difficulty),f){var b=document.getElementById("song-rich-info"),E=b&&b.innerHTML.trim()!=="",S=L&&L.trim()!=="";f.classList.toggle("hidden",!S&&!E)}for(var I=null,C=0;C'+p.t("analytics.noSubmissions")+"",n.classList.remove("hidden");return}var i="";if(e.average_guess!==null&&t){var r=Math.round(e.average_guess-t);r===0?i=p.t("analytics.onTarget"):r>0?i=p.t("analytics.yearsLate",{years:r}):i=p.t("analytics.yearsEarly",{years:Math.abs(r)})}var o=ga(e.all_guesses,t),l="";if(e.exact_match_players&&e.exact_match_players.length>0&&(l+='
🎯'+p.t("analytics.exactMatches")+':'+e.exact_match_players.map(x).join(", ")+"
"),e.speed_champion&&e.speed_champion.names){var d=e.speed_champion.names.map(x).join(", ");l+='
'+p.t("analytics.speedChampion")+':'+d+'('+e.speed_champion.time+"s)
"}if(e.furthest_players&&e.furthest_players.length>0&&e.all_guesses&&e.all_guesses.length>0){var u=e.all_guesses[e.all_guesses.length-1].years_off;u>0&&(l+='
😅'+p.t("analytics.furthestGuess")+':'+e.furthest_players.map(x).join(", ")+'('+u+" years)
")}var m=e.average_guess!==null?Math.round(e.average_guess):"?";a.innerHTML='
'+p.t("analytics.averageGuess")+''+m+'
'+e.accuracy_percentage+'%'+p.t("analytics.accuracy",{percent:""}).replace("%","")+'
'+i+'

'+p.t("analytics.histogram")+"

"+o+"
"+(l?'
'+l+"
":""),n.classList.remove("hidden")}function ga(e,t){var n=7;if(!e||e.length===0)return'
'+p.t("analytics.noGuesses")+"
";for(var a=e.map(function(ae){return ae.guess}),i=Math.min.apply(null,a),r=Math.max.apply(null,a),o=r-i,l=Math.max(1,Math.ceil(o/n)),d=l*n,u=d-o-1,m=i-Math.floor(u/2),c=[],f=0;f=v&&t<=w})}for(var L=0;L=c[E].start&&b<=c[E].end){c[E].count++;break}for(var S=1,I=0;IS&&(S=c[I].count);for(var C="",_=0;_0?Math.max(R,10):0,F=k.count>0?''+k.count+"":"",ke=l===1?String(k.start):k.start+"-"+String(k.end).slice(-2);C+='
'+F+'
'+ke+"
"}return'
'+C+"
"}function pa(e){var t=document.getElementById("song-difficulty");if(t){if(!e){t.classList.add("hidden");return}for(var n="",a=0;a★';t.innerHTML='
'+n+'
'+p.t("difficulty."+e.label)+''+e.accuracy+"% "+p.t("difficulty.accuracy")+"",t.classList.remove("hidden")}}function ya(e){var t=document.getElementById("song-rich-info");if(t){var n=[],a=ba(e.chart_info||{});a.length>0&&(n=n.concat(a));var i=ha(e.certifications||[]);i.length>0&&(n=n.concat(i));var r=p.getLocalizedSongField(e,"awards")||[],o=La(r);o.length>0&&(n=n.concat(o)),n.length>0?t.innerHTML='
'+n.join("")+"
":t.innerHTML=""}}function ba(e){if(!e)return[];var t=[];if(e.billboard_peak&&e.billboard_peak>0){var n=e.weeks_on_chart?' \xB7 '+e.weeks_on_chart+" "+p.t("reveal.weeksShort")+"":"";t.push('\u{1F4CA}#'+e.billboard_peak+" "+p.t("reveal.chartBillboard")+n+"")}return e.german_peak&&e.german_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.german_peak+" "+p.t("reveal.chartGerman")+""),e.uk_peak&&e.uk_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA}#'+e.uk_peak+" "+p.t("reveal.chartUK")+""),t}function ha(e){if(!e||e.length===0)return[];for(var t=[],n=0;n'+r+""+x(a)+"")}return t}function Ea(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"song-badge--diamond":t.indexOf("platinum")!==-1?"song-badge--platinum":t.indexOf("gold")!==-1?"song-badge--gold":"song-badge--platinum"}function wa(e){var t=e.toLowerCase();return t.indexOf("diamond")!==-1?"\u{1F48E}":t.indexOf("platinum")!==-1?"\u{1F4BF}":t.indexOf("gold")!==-1?"\u{1F947}":"\u{1F4BF}"}function La(e){if(!e||e.length===0)return[];for(var t=[],n=e.slice(0,3),a=0;a'+o+""+x(i)+"")}return e.length>3&&t.push('+'+(e.length-3)+" more"),t}function Sa(e){var t=e.toLowerCase();return t.indexOf("grammy")!==-1?"song-badge--grammy":t.indexOf("eurovision")!==-1?"song-badge--eurovision":t.indexOf("oscar")!==-1||t.indexOf("academy award")!==-1?"song-badge--oscar":t.indexOf("hall of fame")!==-1?"song-badge--halloffame":"song-badge--award"}function Ia(e){var t=e.toLowerCase();return t.indexOf("eurovision")!==-1?"\u{1F3A4}":t.indexOf("grammy")!==-1?"\u{1F3C6}":t.indexOf("hall of fame")!==-1?"\u2B50":"\u{1F3C6}"}function Ba(e,t){var n=document.getElementById("reveal-emotion"),a=document.getElementById("personal-result");if(!n)return;var i=n.classList.contains("reveal-emotion-inline")||document.querySelector(".reveal-container--compact");n.className=i?"reveal-emotion-inline":"reveal-emotion",n.innerHTML="",n.classList.add("hidden"),a&&a.classList.remove("is-delayed"),O();var r=p.t("reveal.emotions");function o(v){return v[Math.floor(Math.random()*v.length)]}function l(v){return v===1?p.t("reveal.offByYear"):p.t("reveal.offByYears",{years:v})}var d="missed",u=o(r.missed),m=o(r.missedSub);if(e&&!e.missed_round){var c=e.years_off||0;c===0?(d="exact",u=o(r.exact),m=o(r.exactSub)):c<=2?(d="close",u=o(r.close),m=o(r.closeSub)+" "+l(c)):c<=5?(d="close",u=o(r.close),m=l(c)):(d="wrong",u=o(r.wrong),m=o(r.wrongSub)+" "+l(c))}else e&&e.missed_round&&(d="missed",u=o(r.missed),m=o(r.missedSub));var f=''+u+"";m&&(f+='
'+m+"
"),n.innerHTML=f,n.classList.add("reveal-emotion--"+d),n.classList.remove("hidden"),d==="exact"&&ee(),a&&d!=="missed"&&a.classList.add("is-delayed")}function xa(e,t){var n=document.getElementById("result-content");if(n){if(!e){n.innerHTML='
'+p.t("reveal.playerNotFound")+"
";return}if(e.missed_round){var a='
\u23F0
'+p.t("reveal.noSubmission")+"
",i=e.previous_streak||0;i>=2&&(a+='
\u{1F494}Lost '+i+"-streak!
"),a+='
0 pts
',n.innerHTML=a;return}var r=e.years_off||0,o=r===0?p.t("reveal.exact"):r===1?p.t("reveal.yearOff",{years:1}):p.t("reveal.yearsOff",{years:r}),l=r===0?"is-exact":r<=3?"is-close":"is-far",d=e.speed_multiplier||1,u=e.base_score||0,m=d>1,c=e.streak_bonus||0,f=e.artist_bonus||0,v="";m&&u>0&&(v='
'+p.t("reveal.baseScore")+''+u+' pts
'+p.t("reveal.speedBonus")+''+d.toFixed(2)+"x
");var w="";e.bet_outcome==="won"?w='
\u{1F3B2} '+p.t("reveal.betWon").replace("! 2x points","")+'2x
':e.bet_outcome==="lost"&&(w='
\u{1F3B2} '+p.t("reveal.betLost")+'-
');var L="";c>0&&(L='
'+e.streak+'-streak bonus!+'+c+" pts
");var b="";f>0&&(b='
\u{1F3A4} '+(p.t("artistChallenge.artistBonus")||"Artist Bonus")+'+'+f+" pts
");var E=e.round_score+c+f,S=c>0||f>0,I=e.round_score>=20,C=M.players[e.name],_=C?C.score:e.score-E,k=C?C.streak:0,R=dt(k,e.streak||0);n.innerHTML='
'+p.t("reveal.yourGuess")+''+(e.guess||"n/a")+'
'+p.t("reveal.correctYear")+''+t+'
'+p.t("reveal.accuracy")+''+o+"
"+v+w+'
+0 pts
'+L+b+(S?'
'+p.t("reveal.total")+': +0 pts
':"");var D=n.querySelector(".score-value");D&&(ot(D,0,e.round_score,{betWon:e.bet_outcome==="won",betLost:e.bet_outcome==="lost",streakMilestone:R,isBigScore:I}),e.bet_outcome==="won"&&e.round_score>0&&setTimeout(function(){var T=document.getElementById("personal-result-score");T&&Ne(T,e.round_score,{isBetWin:!0})},200));var V=n.querySelector(".total-value");V&&S&&(setTimeout(function(){se(V,0,E,600)},300),R&&setTimeout(function(){var T=n.querySelector(".result-total");if(T){var F={3:20,5:50,10:100}[R]||0;Ne(T,F,{isStreak:!0,text:"+"+F+" "+R+"-Streak!"})}},500))}}function _a(e,t){var n=document.getElementById("reveal-results-cards");if(n){if(!e||e.length===0){n.innerHTML="";return}var a=null;t&&e.forEach(function(o){!o.missed_round&&o.years_off!=null&&(a===null||o.years_off';i.forEach(function(o){var l=o.name===s.playerName,d=o.missed_round===!0,u=o.years_off||0,m=o.round_score||0,c=d?"is-score-zero":m>=10?"is-score-high":m>=1?"is-score-medium":"is-score-zero",f=t&&!d&&a!==null&&(o.years_off||0)===a,v=f?" is-closest-winner":"",w=d?"\u2014":o.guess||"n/a",L=d?p.t("reveal.noGuessShort"):u===0?p.t("reveal.exact"):p.t("reveal.shortOff",{years:u}),b=o.bet?'\u{1F3B2}':"",E=f?'\u{1F3AF}':"",S="";o.artist_bonus&&o.artist_bonus>0&&(S='\u{1F3A4} +'+o.artist_bonus+"");var I="";if(o.stole_from)I='
\u{1F977}'+p.t("steal.stolenFrom",{name:x(o.stole_from)})+"
";else if(o.was_stolen_by&&o.was_stolen_by.length>0){var C=o.was_stolen_by.map(x).join(", ");I='
\u{1F3AF}'+p.t("steal.stolenBy",{name:C})+"
"}r+='
'+x(o.name)+b+E+'
'+w+'
'+L+"
"+I+'
+'+m+S+"
"}),r+="",n.innerHTML=r}}var A=window.BeatifyUtils||{};function ln(e){window.scrollTo(0,0);var t=e.leaderboard||[];t.forEach(function(b){b.is_current=b.name===s.playerName}),[1,2,3].forEach(function(b){var E=t.find(function(C){return C.rank===b}),S=document.getElementById("podium-"+b+"-name"),I=document.getElementById("podium-"+b+"-score");S&&(S.textContent=E?x(E.name):"---"),I&&(I.textContent=E?E.score:"0")});var n=t.find(function(b){return b.is_current}),a=document.getElementById("your-final-rank"),i=document.getElementById("your-final-score"),r=document.getElementById("stat-best-streak"),o=document.getElementById("stat-rounds"),l=document.getElementById("stat-bets");n&&(a&&(a.textContent="#"+n.rank),i&&(i.textContent=n.score+" "+A.t("leaderboard.points")),r&&(r.textContent=n.best_streak||0),o&&(o.textContent=n.rounds_played||0),l&&(l.textContent=n.bets_won||0));var d=document.getElementById("final-leaderboard-list");d&&(d.innerHTML=t.map(function(b){var E=b.is_current?"is-current":"",S=b.connected===!1?"final-entry--disconnected":"",I=b.connected===!1?'(away)':"";return'
#'+b.rank+''+x(b.name)+I+''+b.score+"
"}).join("")),Ca(e.superlatives),ka(e.highlights),Aa(e.share_data);var u=document.getElementById("end-admin-controls"),m=document.getElementById("end-player-message");if(n&&n.is_admin){u&&u.classList.remove("hidden"),m&&m.classList.add("hidden");var c=document.getElementById("new-game-btn");c&&(c.onclick=Na);var f=document.getElementById("player-rematch-btn");f&&(f.onclick=function(){f.disabled=!0;var b=f.textContent;f.textContent="\u23F3",fetch("/beatify/api/rematch-game",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json"}}).then(function(E){if(!E.ok)return E.json().then(function(S){throw new Error(S.message||"Rematch failed")});f.textContent="\u23F3"}).catch(function(E){console.error("[Player] Rematch failed:",E),alert(E.message||"Failed to start rematch"),f.disabled=!1,f.textContent=b})})}else u&&u.classList.add("hidden"),m&&m.classList.remove("hidden");if(n){var v=e.total_rounds||10,w=n.best_streak||0,L=w===v&&v>0;L?ee("perfect"):n.rank===1&&ee("winner")}}function Ca(e){var t=document.getElementById("superlatives-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n="";e.forEach(function(a,i){var r="";switch(a.value_label){case"avg_time":r=a.value+"s "+A.t("superlatives.avgTime");break;case"streak":r=a.value+" "+A.t("superlatives.streak");break;case"bets":r=a.value+" "+A.t("superlatives.bets");break;case"points":r=a.value+" "+A.t("superlatives.points");break;case"close_guesses":r=a.value+" "+A.t("superlatives.closeGuesses");break;default:r=a.value}n+='
'+a.emoji+'
'+A.t("superlatives."+a.title)+'
'+x(a.player_name)+'
'+r+"
"}),t.innerHTML=n,t.classList.remove("hidden")}}function ka(e){var t=document.getElementById("highlights-container");if(t){if(!e||e.length===0){t.classList.add("hidden");return}var n=document.getElementById("highlights-list");if(n){var a="";e.forEach(function(i,r){var o=A.t("highlights."+i.description,i.description_params)||i.description;o===i.description&&i.description_params&&(o=A.t("highlights."+i.description)||i.description,Object.keys(i.description_params).forEach(function(l){o=o.replace("{"+l+"}",x(i.description_params[l]))})),a+='
'+(i.emoji||"\u2728")+'
'+o+'
'+A.t("highlights.roundLabel",{round:i.round})+"
"}),n.innerHTML=a,t.classList.remove("hidden")}}}function Aa(e){var t=document.getElementById("share-container");if(t){if(!e||!e.emoji_grids){t.classList.add("hidden");return}var n=e.emoji_grids[s.playerName];if(!n){var a=Object.keys(e.emoji_grids);a.length===1&&(n=e.emoji_grids[a[0]])}if(!n){t.classList.add("hidden");return}var i=document.getElementById("share-emoji-grid");if(i){var r=n.split(` +`).map(function(d){return'
'+A.escapeHtml(d)+"
"}).join("");i.innerHTML=r,i.dataset.rawText=n}var o=document.getElementById("share-copy-btn");o&&(o.onclick=function(){navigator.clipboard.writeText(n).then(function(){var d=document.getElementById("share-toast");d&&(d.classList.remove("hidden"),setTimeout(function(){d.classList.add("hidden")},2e3))})});var l=document.getElementById("share-save-btn");l&&(l.onclick=function(){Ta(n,e.playlist_name,e)}),t.classList.remove("hidden")}}function Ta(e,t,n){var a=document.createElement("canvas");a.width=800,a.height=800;var i=a.getContext("2d"),r=i.createLinearGradient(0,0,0,800);r.addColorStop(0,"#0f0c29"),r.addColorStop(.5,"#302b63"),r.addColorStop(1,"#24243e"),i.fillStyle=r,i.fillRect(0,0,800,800);var o=i.createLinearGradient(0,0,800,0);o.addColorStop(0,"#e94560"),o.addColorStop(1,"#0f3460"),i.fillStyle=o,i.fillRect(0,0,800,4);var l=new Image;l.src="/beatify/static/img/icon-256.png",l.onerror=function(){d(null)},l.onload=function(){d(l)};function d(u){u?(i.drawImage(u,28,20,64,64),i.fillStyle="#ffffff",i.font="bold 28px system-ui, sans-serif",i.textAlign="left",i.fillText("Beatify",104,60)):(i.fillStyle="#ffffff",i.font="bold 28px system-ui, sans-serif",i.textAlign="center",i.fillText("\u{1F3B5} Beatify",400,55)),i.textAlign="center";var m=(t||"").toUpperCase();i.font="bold 13px system-ui, sans-serif";var c=i.measureText(m).width+32,f=400-c/2;i.fillStyle="rgba(233,69,96,0.18)",i.beginPath(),i.roundRect(f,96,c,30,15),i.fill(),i.strokeStyle="#e94560",i.lineWidth=1,i.stroke(),i.fillStyle="#e94560",i.fillText(m,400,116);for(var v=e.split(` +`).filter(function(N){return N.trim()!==""}),w="",L=[],b="",E="",S="",I="",C=0;C=pe&&(s.isReconnecting=!1,ye(),et())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}}function J(e){s.playerName=e,fn(e);var t=window.location.protocol==="https:"?"wss:":"ws:",n=t+"//"+window.location.host+"/beatify/ws";s.ws=new WebSocket(n),s.ws.onopen=function(){s.reconnectAttempts=0,s.isReconnecting=!1,ye(),gn();var a={type:"join",name:e};s.isAdmin&&(a.is_admin=!0),s.ws.send(JSON.stringify(a))},s.ws.onmessage=function(a){try{var i=JSON.parse(a.data);bn(i)}catch(r){console.error("Failed to parse WebSocket message:",r)}},s.ws.onclose=function(){if(s.intentionalLeave){s.intentionalLeave=!1;return}if(s.playerName&&s.reconnectAttempts=pe&&(s.isReconnecting=!1,ye(),et())},s.ws.onerror=function(a){console.error("WebSocket error:",a)}}s.connectWithSession=Ce;s.connectWebSocket=J;function bn(e){var t=document.getElementById("join-btn"),n=document.getElementById("name-input");if(e.type==="state"){var a=e.players||[],i=a.find(function(u){return u.name===s.playerName});if(i&&(s.isAdmin=i.is_admin===!0),e.language&&(Ga(e.language),typeof BeatifyI18n<"u"&&e.language!==BeatifyI18n.getLanguage()&&BeatifyI18n.setLanguage(e.language).then(function(){BeatifyI18n.initPageTranslations(),Pe(a),e.difficulty&&Ee(e.difficulty),e.phase==="REVEAL"&&Xe(e),(e.phase==="PLAYING"||e.phase==="REVEAL")&&Se(e.phase)})),e.phase==="LOBBY"){Y(),me(),fe(),s.currentRoundNumber=0,q("warmup");var r=document.getElementById("start-game-btn");r&&(r.disabled=!1,r.innerHTML=''+ge.t("lobby.startGame")+""),B("lobby-view"),Pe(a),e.difficulty&&Ee(e.difficulty),e.join_url&&It(e.join_url),Ct(a)}else if(e.phase==="PLAYING"){var o=e.round||1;o!==s.currentRoundNumber&&(s.currentRoundNumber=o,jt()),q("party"),B("game-view"),oe(),Ot(e),e.intro_splash_pending?rn(s.isAdmin):sn(),e.difficulty&&Ee(e.difficulty),e.deadline&&Rt(e.deadline),Vt(),Ht(),Qe(),Se("PLAYING"),fe()}else e.phase==="REVEAL"?(Y(),e.early_reveal&&Tt(),q("party"),B("reveal-view"),Xe(e),Gt(),Dt(),Qe(),Se("REVEAL"),s.hasReactedThisPhase=!1,Qt()):e.phase==="PAUSED"?(Y(),me(),fe(),q("warmup"),B("paused-view"),dn(e)):e.phase==="END"&&(Y(),me(),fe(),s.currentRoundNumber=0,q("warmup"),B("end-view"),ln(e),ve())}else if(e.type==="join_ack"){e.session_id&&Pa(e.session_id);try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}}else if(e.type==="reconnect_ack")e.success&&e.name?(s.playerName=e.name,fn(e.name),At(e.name)):(Ze(),ve(),s.playerName=null,B("join-view"));else if(e.type==="submit_ack")ze();else if(e.type==="metadata_update")Pt(e.song);else if(e.type==="error"){if(e.code==="ROUND_EXPIRED"||e.code==="ALREADY_SUBMITTED"){Ft(e);return}if(e.code==="GAME_ENDED"){B("end-view");return}if(e.code==="NOT_ADMIN"){s.isAdmin=!1,me(),console.warn("Admin action rejected: not admin");return}if(e.code==="SESSION_TAKEOVER"){s.isReconnecting=!1,ye(),s.playerName=null,et(),console.warn("Session taken over by another tab");return}if(e.code==="SESSION_NOT_FOUND"){Ze(),s.intentionalLeave=!0,s.ws&&s.ws.close(),B("join-view");return}if(e.code==="ADMIN_CANNOT_LEAVE"){s.intentionalLeave=!1,alert(e.message||"Host cannot leave. End the game instead.");return}if(e.code==="INVALID_ACTION"&&e.message==="No song playing"){Ke(),console.warn("[Beatify] Stop song failed: No song playing");return}B("join-view"),qa(e.message),t&&(t.disabled=!1,t.textContent=ge.t("join.joinButton")),n&&n.focus(),s.playerName=null,ve()}else if(e.type==="song_stopped")tn();else if(e.type==="volume_changed")nn(e.level);else if(e.type==="game_ended")Wa();else if(e.type==="rematch_started"){console.log("[Player] Rematch started - transitioning to lobby"),$.clear(),O(),B("lobby-view");var l=document.getElementById("player-rematch-btn");l&&(l.disabled=!1,l.textContent="\u{1F501}");var d=$e();d&&(s.ws&&s.ws.readyState===WebSocket.OPEN?(s.reconnectAttempts=0,s.ws.send(JSON.stringify({type:"reconnect",session_id:d}))):(s.reconnectAttempts=0,Ce()))}else e.type==="left"?ja():e.type==="steal_targets"?Jt(e):e.type==="steal_ack"?Yt(e):e.type==="artist_guess_ack"?Wt(e):e.type==="movie_guess_ack"?Ut(e):e.type==="player_reaction"&&Xt(e.player_name,e.emoji)}function ja(){ve(),Ze(),s.playerName=null,s.isAdmin=!1,B("join-view")}function Wa(){var e=s.isAdmin;ve();try{sessionStorage.removeItem("beatify_admin_name"),sessionStorage.removeItem("beatify_is_admin")}catch{}ft(),Et(),$.clear(),O(),s.playerName=null,s.isAdmin=!1,s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.close(),s.ws=null;var t=document.getElementById("end-view");if(!(!t||!t.classList.contains("hidden"))){var n=document.getElementById("end-player-message");n&&(n.innerHTML='

Thanks for playing!

Scan the QR code again to join the next game.

',n.classList.remove("hidden")),B("end-view")}}function qa(e){var t=document.getElementById("name-validation-msg");t&&(t.textContent=e,t.classList.remove("hidden"))}function tt(e){var t=(e||"").trim();return t?t.length>Ra?{valid:!1,error:"Name too long (max 20 characters)"}:{valid:!0,name:t}:{valid:!1,error:"Please enter a name"}}function cn(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");if(!(!e||!t)){var a=tt(e.value);a.valid&&(t.disabled=!0,t.textContent=ge.t("game.joining"),n&&n.classList.add("hidden"),J(a.name))}}function Ua(){var e=document.getElementById("name-input"),t=document.getElementById("join-btn"),n=document.getElementById("name-validation-msg");!e||!t||(e.addEventListener("input",function(){var a=tt(this.value);t.disabled=!a.valid,n&&(n.textContent=!a.valid&&this.value?a.error:"",n.classList.toggle("hidden",a.valid||!this.value))}),t.addEventListener("click",cn),e.addEventListener("keypress",function(a){a.key==="Enter"&&!t.disabled&&cn()}))}function za(){var e=document.getElementById("retry-connection-btn");e&&e.addEventListener("click",function(){s.playerName?(s.reconnectAttempts=0,B("loading-view"),J(s.playerName)):_e()})}async function un(){var e=K.getDeviceTier();document.body.classList.add("device-tier-"+e);var t=await ge.waitForI18n();if(!t)console.error("[Player] BeatifyI18n module failed to load - UI will use fallback text");else{var n=Da();await BeatifyI18n.init(n),BeatifyI18n.initPageTranslations()}var a=document.getElementById("dashboard-hint-url");a&&(a.textContent=window.location.origin+"/beatify/dashboard");var i=document.getElementById("player-dashboard-url");if(i&&(i.href=window.location.origin+"/beatify/dashboard"),Ua(),Bt(),_t(),kt(),an(),en(),za(),vt(),gt(),pt(),Kt(),Fa()&&s.playerName){J(s.playerName);return}var r=Ha();if(r&&s.gameId){console.log("[Beatify] Auto-reconnecting as:",r),J(r);return}if(r){var o=document.getElementById("name-input"),l=document.getElementById("join-btn");if(o&&(o.value=r,l)){var d=tt(r);l.disabled=!d.valid}}}_e();document.getElementById("refresh-btn")?.addEventListener("click",function(){B("loading-view"),_e()});document.getElementById("retry-btn")?.addEventListener("click",function(){B("loading-view"),_e()});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",un):un();"serviceWorker"in navigator&&window.addEventListener("load",function(){navigator.serviceWorker.register("/beatify/static/sw.js",{scope:"/beatify/"}).then(function(e){console.log("[Beatify] SW registered:",e.scope)}).catch(function(e){console.warn("[Beatify] SW registration failed:",e)})});document.addEventListener("visibilitychange",function(){if(document.visibilityState==="visible"){var e=s.ws;(!e||e.readyState===WebSocket.CLOSING||e.readyState===WebSocket.CLOSED)&&s.playerName&&(console.log("[Beatify] Page visible, WebSocket dead \u2014 reconnecting immediately."),s.reconnectAttempts=0,Ce())}}); diff --git a/custom_components/beatify/www/player.html b/custom_components/beatify/www/player.html index 37664fbd..c2372f3f 100644 --- a/custom_components/beatify/www/player.html +++ b/custom_components/beatify/www/player.html @@ -591,21 +591,27 @@

Game Over!