Files
hitstar/public/js/main.js
Elmar Kresse 8c5ca0044f
All checks were successful
Build and Push Docker Image / docker (push) Successful in 21s
Refactor code structure for improved readability and maintainability
2025-09-04 21:53:54 +02:00

389 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
$audio,
$copyRoomCode,
$leaveRoom,
$nameDisplay,
$nameLobby,
$npArtist,
$npTitle,
$npYear,
$pauseBtn,
$placeBtn,
$readyChk,
$roomId,
$roomCode,
$slotSelect,
$startGame,
$volumeSlider,
$playBtn,
$nextBtn,
$createRoom,
$joinRoom,
$lobby,
$room,
$setNameLobby,
$guessTitle,
$guessArtist,
$answerResult,
} from './dom.js';
import { state } from './state.js';
import { connectWS, sendMsg, cacheSessionId, cacheLastRoomId } from './ws.js';
import { renderRoom } from './render.js';
import { initAudioUI, applySync, stopAudioPlayback } from './audio.js';
function showToast(msg) {
const el = document.getElementById('toast');
if (!el) {
return;
}
el.textContent = msg;
el.style.opacity = '1';
setTimeout(() => {
el.style.opacity = '0';
}, 1200);
}
function handleConnected(msg) {
state.playerId = msg.playerId;
if (msg.sessionId) {
// Don't clobber an existing session on reconnect; only set if none exists yet.
const existing = localStorage.getItem('sessionId');
if (!existing) {
cacheSessionId(msg.sessionId);
}
}
const stored = localStorage.getItem('playerName');
if (stored) {
if ($nameLobby && $nameLobby.value !== stored) {
$nameLobby.value = stored;
}
if ($nameDisplay) {
$nameDisplay.textContent = stored;
}
sendMsg({ type: 'set_name', name: stored });
}
const last = state.room?.id || localStorage.getItem('lastRoomId');
if (last && !localStorage.getItem('sessionId')) {
sendMsg({ type: 'join_room', code: last });
}
}
function handleRoomUpdate(msg) {
if (msg?.room?.id) cacheLastRoomId(msg.room.id);
renderRoom(msg.room);
}
function handlePlayTrack(msg) {
const t = msg.track;
state.lastTrack = t;
state.revealed = false;
$npTitle.textContent = '???';
$npArtist.textContent = '';
$npYear.textContent = '';
// reset answer UI
if ($guessTitle) $guessTitle.value = '';
if ($guessArtist) $guessArtist.value = '';
if ($answerResult) {
$answerResult.textContent = '';
$answerResult.className = 'mt-1 text-sm';
}
try {
$audio.preload = 'auto';
} catch {}
$audio.src = t.url;
const pf = document.getElementById('progressFill');
if (pf) {
pf.style.width = '0%';
}
const rd = document.getElementById('recordDisc');
if (rd) {
rd.classList.remove('spin-record');
// Reset cover to default logo at the start of a new track
rd.src = '/hitstar.png';
}
const { startAt, serverNow } = msg;
const now = Date.now();
const offsetMs = startAt - serverNow;
const localStart = now + offsetMs;
const delay = Math.max(0, localStart - now);
setTimeout(() => {
$audio.currentTime = 0;
$audio.play().catch(() => {});
const disc = document.getElementById('recordDisc');
if (disc) {
disc.classList.add('spin-record');
}
}, delay);
if (state.room) {
renderRoom(state.room);
}
}
function handleSync(msg) {
applySync(msg.startAt, msg.serverNow);
}
function handleControl(msg) {
const { action, startAt, serverNow } = msg;
if (action === 'pause') {
$audio.pause();
const disc = document.getElementById('recordDisc');
if (disc) {
disc.classList.remove('spin-record');
}
$audio.playbackRate = 1.0;
} else if (action === 'play') {
if (startAt && serverNow) {
const now = Date.now();
const elapsed = (now - startAt) / 1000;
$audio.currentTime = Math.max(0, elapsed);
}
$audio.play().catch(() => {});
const disc = document.getElementById('recordDisc');
if (disc) {
disc.classList.add('spin-record');
}
}
}
function handleReveal(msg) {
const { result, track } = msg;
$npTitle.textContent = track.title || track.id || 'Track';
$npArtist.textContent = track.artist ? ` ${track.artist}` : '';
$npYear.textContent = track.year ? ` (${track.year})` : '';
state.revealed = true;
const $rb = document.getElementById('revealBanner');
if ($rb) {
if (result.correct) {
$rb.textContent = 'Richtig!';
$rb.className =
'inline-block rounded-md bg-emerald-600 text-white px-3 py-1 text-sm font-medium';
} else {
$rb.textContent = 'Falsch!';
$rb.className =
'inline-block rounded-md bg-rose-600 text-white px-3 py-1 text-sm font-medium';
}
}
const $placeArea = document.getElementById('placeArea');
if ($placeArea) {
$placeArea.classList.add('hidden');
}
// Try to load embedded cover art and replace the center image
const rd = document.getElementById('recordDisc');
if (rd && track?.id) {
const coverUrl = `/cover/${encodeURIComponent(track.id)}`;
const img = new Image();
img.onload = () => {
rd.src = coverUrl;
};
img.onerror = () => {
/* keep default logo */
};
img.src = coverUrl + `?t=${Date.now()}`; // bypass cache just in case
}
}
function handleGameEnded(msg) {
alert(`Gewinner: ${shortName(msg.winner)}`);
}
function onMessage(ev) {
const msg = JSON.parse(ev.data);
switch (msg.type) {
case 'resume_result':
if (msg.ok) {
if (msg.playerId) state.playerId = msg.playerId;
const code = msg.roomId || state.room?.id || localStorage.getItem('lastRoomId');
if (code) sendMsg({ type: 'join_room', code });
} else {
const code = state.room?.id || localStorage.getItem('lastRoomId');
if (code) sendMsg({ type: 'join_room', code });
}
break;
case 'connected':
handleConnected(msg);
break;
case 'room_update':
handleRoomUpdate(msg);
break;
case 'play_track':
handlePlayTrack(msg);
break;
case 'sync':
handleSync(msg);
break;
case 'control':
handleControl(msg);
break;
case 'reveal':
handleReveal(msg);
break;
case 'game_ended':
handleGameEnded(msg);
break;
case 'answer_result': {
if ($answerResult) {
if (!msg.ok) {
$answerResult.textContent = '⛔ Eingabe ungültig oder gerade nicht möglich';
$answerResult.className = 'mt-1 text-sm text-rose-600';
} else {
const okBoth = !!(msg.correctTitle && msg.correctArtist);
const parts = [];
parts.push(msg.correctTitle ? 'Titel ✓' : 'Titel ✗');
parts.push(msg.correctArtist ? 'Künstler ✓' : 'Künstler ✗');
let coin = '';
if (msg.awarded) coin = ' +1 Token';
else if (msg.alreadyAwarded) coin = ' (bereits erhalten)';
$answerResult.textContent = `${parts.join(' · ')}${coin}`;
$answerResult.className = okBoth
? 'mt-1 text-sm text-emerald-600'
: 'mt-1 text-sm text-amber-600';
}
}
break;
}
default:
break;
}
}
function shortName(id) {
if (!id) return '-';
const p = state.room?.players.find((x) => x.id === id);
return p ? p.name : id.slice(0, 4);
}
function wire(el, type, handler) {
if (el) {
el.addEventListener(type, handler);
}
}
function wireUi() {
initAudioUI();
wire($setNameLobby, 'click', () => {
const name = $nameLobby.value.trim();
if (!name) return;
localStorage.setItem('playerName', name);
if ($nameDisplay) {
$nameDisplay.textContent = name;
}
sendMsg({ type: 'set_name', name });
});
wire($createRoom, 'click', () => sendMsg({ type: 'create_room' }));
wire($joinRoom, 'click', () => {
const code = $roomCode.value.trim();
if (code) {
sendMsg({ type: 'join_room', code });
}
});
wire($leaveRoom, 'click', () => {
sendMsg({ type: 'leave_room' });
// Clear all local storage entries on leave
try {
localStorage.clear();
} catch {}
stopAudioPlayback();
state.room = null;
// Reset visible name inputs/labels
if ($nameLobby) {
try {
$nameLobby.value = '';
} catch {}
}
if ($nameDisplay) {
$nameDisplay.textContent = '';
}
if ($readyChk) {
try {
$readyChk.checked = false;
} catch {}
}
$lobby.classList.remove('hidden');
$room.classList.add('hidden');
});
wire($startGame, 'click', () => sendMsg({ type: 'start_game' }));
wire($readyChk, 'change', (e) => {
const val = !!e.target.checked;
state.pendingReady = val;
sendMsg({ type: 'set_ready', ready: val });
});
wire($placeBtn, 'click', () => {
const slot = parseInt($slotSelect.value, 10);
sendMsg({ type: 'place_guess', slot });
});
wire($playBtn, 'click', () => sendMsg({ type: 'player_control', action: 'play' }));
wire($pauseBtn, 'click', () => sendMsg({ type: 'player_control', action: 'pause' }));
wire($nextBtn, 'click', () => sendMsg({ type: 'next_track' }));
if ($volumeSlider && $audio) {
try {
$volumeSlider.value = String($audio.volume ?? 1);
} catch {}
$volumeSlider.addEventListener('input', () => {
$audio.volume = parseFloat($volumeSlider.value);
});
}
if ($copyRoomCode) {
$copyRoomCode.style.display = 'inline-block';
wire($copyRoomCode, 'click', () => {
if (state.room?.id) {
navigator.clipboard.writeText(state.room.id).then(() => {
$copyRoomCode.textContent = '✔️';
showToast('Code kopiert!');
setTimeout(() => {
$copyRoomCode.textContent = '📋';
}, 1200);
});
}
});
}
if ($roomId) {
wire($roomId, 'click', () => {
if (state.room?.id) {
navigator.clipboard.writeText(state.room.id).then(() => {
$roomId.title = 'Kopiert!';
showToast('Code kopiert!');
setTimeout(() => {
$roomId.title = 'Klicken zum Kopieren';
}, 1200);
});
}
});
$roomId.style.cursor = 'pointer';
}
// Answer submit
const form = document.getElementById('answerForm');
if (form) {
form.addEventListener('submit', (e) => {
e.preventDefault();
const title = ($guessTitle?.value || '').trim();
const artist = ($guessArtist?.value || '').trim();
if (!title || !artist) {
if ($answerResult) {
$answerResult.textContent = 'Bitte Titel und Künstler eingeben';
$answerResult.className = 'mt-1 text-sm text-amber-600';
}
return;
}
sendMsg({ type: 'submit_answer', guess: { title, artist } });
});
}
}
// boot
wireUi();
connectWS(onMessage);
// restore name immediately if present
(() => {
const saved = localStorage.getItem('playerName');
if (saved) {
if ($nameLobby && $nameLobby.value !== saved) {
$nameLobby.value = saved;
}
if ($nameDisplay) {
$nameDisplay.textContent = saved;
}
}
})();