|
|
document.addEventListener("DOMContentLoaded", () => { |
|
|
|
|
|
const joinScreen = document.getElementById("join-screen"); |
|
|
const roomScreen = document.getElementById("room-screen"); |
|
|
const roomNameInput = document.getElementById("room-name-input"); |
|
|
const userNameInput = document.getElementById("user-name-input"); |
|
|
const joinBtn = document.getElementById("join-btn"); |
|
|
const errorMessage = document.getElementById("error-message"); |
|
|
|
|
|
const roomHeader = document.getElementById("room-header"); |
|
|
const userList = document.getElementById("user-list"); |
|
|
const nowPlaying = document.getElementById("now-playing"); |
|
|
const audioPlayer = document.getElementById("audio-player"); |
|
|
const playPauseBtn = document.getElementById("play-pause-btn"); |
|
|
const seekBar = document.getElementById("seek-bar"); |
|
|
const timeDisplay = document.getElementById("time-display"); |
|
|
|
|
|
const playlistElement = document.getElementById("playlist"); |
|
|
const adminControls = document.getElementById("admin-controls"); |
|
|
const fileInput = document.getElementById("file-input"); |
|
|
const uploadBtn = document.getElementById("upload-btn"); |
|
|
const uploadStatus = document.getElementById("upload-status"); |
|
|
|
|
|
|
|
|
let ws = null; |
|
|
let roomName = ""; |
|
|
let userName = ""; |
|
|
let userId = ""; |
|
|
let isAdmin = false; |
|
|
let isSeeking = false; |
|
|
|
|
|
|
|
|
joinBtn.addEventListener("click", joinRoom); |
|
|
playPauseBtn.addEventListener("click", togglePlayPause); |
|
|
uploadBtn.addEventListener("click", uploadFile); |
|
|
seekBar.addEventListener("input", () => { isSeeking = true; }); |
|
|
seekBar.addEventListener("change", () => { |
|
|
isSeeking = false; |
|
|
if (isAdmin) { |
|
|
const newPosition = parseFloat(seekBar.value); |
|
|
audioPlayer.currentTime = newPosition; |
|
|
sendMessage({ action: "seek", position: newPosition }); |
|
|
} |
|
|
}); |
|
|
audioPlayer.addEventListener("timeupdate", updateSeekBar); |
|
|
audioPlayer.addEventListener("loadedmetadata", updateSeekBar); |
|
|
|
|
|
|
|
|
|
|
|
function joinRoom() { |
|
|
roomName = roomNameInput.value.trim(); |
|
|
userName = userNameInput.value.trim(); |
|
|
|
|
|
if (!roomName || !userName) { |
|
|
errorMessage.textContent = "Room and User name are required."; |
|
|
return; |
|
|
} |
|
|
errorMessage.textContent = ""; |
|
|
|
|
|
const wsProtocol = window.location.protocol === "https:" ? "wss://" : "ws://"; |
|
|
const wsURL = `${wsProtocol}${window.location.host}/ws/${encodeURIComponent(roomName)}/${encodeURIComponent(userName)}`; |
|
|
|
|
|
ws = new WebSocket(wsURL); |
|
|
|
|
|
ws.onopen = () => { |
|
|
console.log("WebSocket connection established."); |
|
|
joinScreen.classList.remove("active"); |
|
|
roomScreen.classList.add("active"); |
|
|
roomHeader.textContent = `Room: ${roomName}`; |
|
|
}; |
|
|
|
|
|
ws.onmessage = (event) => { |
|
|
const message = JSON.parse(event.data); |
|
|
handleWebSocketMessage(message); |
|
|
}; |
|
|
|
|
|
ws.onerror = (error) => { |
|
|
console.error("WebSocket error:", error); |
|
|
errorMessage.textContent = "Failed to connect to the room."; |
|
|
}; |
|
|
|
|
|
ws.onclose = () => { |
|
|
console.log("WebSocket connection closed."); |
|
|
alert("Connection lost. Please refresh the page to reconnect."); |
|
|
roomScreen.classList.remove("active"); |
|
|
joinScreen.classList.add("active"); |
|
|
}; |
|
|
} |
|
|
|
|
|
function handleWebSocketMessage(message) { |
|
|
console.log("Received message:", message); |
|
|
switch (message.type) { |
|
|
case "initial_state": |
|
|
userId = message.user_id; |
|
|
updateState(message.payload); |
|
|
break; |
|
|
case "state_update": |
|
|
updateState(message.payload); |
|
|
break; |
|
|
case "user_list_update": |
|
|
updateUI(message.payload); |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
function updateState(state) { |
|
|
|
|
|
isAdmin = state.admin_user_id === userId; |
|
|
|
|
|
updateUI(state); |
|
|
|
|
|
|
|
|
const currentTrackUrl = state.current_track ? `/tmp/${encodeURIComponent(state.current_track)}` : ""; |
|
|
if (state.current_track && audioPlayer.src.endsWith(currentTrackUrl) === false) { |
|
|
audioPlayer.src = currentTrackUrl; |
|
|
} |
|
|
|
|
|
if (!state.current_track) { |
|
|
audioPlayer.src = ""; |
|
|
nowPlaying.textContent = "Now Playing: Nothing"; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (!isSeeking) { |
|
|
|
|
|
const timeDifference = Math.abs(audioPlayer.currentTime - state.position); |
|
|
if (timeDifference > 1.5) { |
|
|
audioPlayer.currentTime = state.position; |
|
|
} |
|
|
} |
|
|
|
|
|
if (state.is_playing && audioPlayer.paused) { |
|
|
audioPlayer.play().catch(e => console.error("Autoplay failed:", e)); |
|
|
} else if (!state.is_playing && !audioPlayer.paused) { |
|
|
audioPlayer.pause(); |
|
|
} |
|
|
} |
|
|
|
|
|
function updateUI(state) { |
|
|
|
|
|
userList.innerHTML = "Users: " + Object.values(state.users) |
|
|
.map(u => `<span class="${state.admin_user_id === Object.keys(state.users).find(key => state.users[key] === u) ? 'admin-badge' : ''}">${u.name}${state.admin_user_id === Object.keys(state.users).find(key => state.users[key] === u) ? ' (Admin)' : ''}</span>`) |
|
|
.join(", "); |
|
|
|
|
|
|
|
|
nowPlaying.textContent = state.current_track ? `Now Playing: ${state.current_track}` : "Now Playing: Nothing"; |
|
|
|
|
|
|
|
|
playlistElement.innerHTML = ""; |
|
|
state.playlist.forEach(track => { |
|
|
const li = document.createElement("li"); |
|
|
li.textContent = track; |
|
|
if (track === state.current_track) { |
|
|
li.classList.add("active"); |
|
|
} |
|
|
if (isAdmin) { |
|
|
const playTrackBtn = document.createElement("button"); |
|
|
playTrackBtn.textContent = "Play"; |
|
|
playTrackBtn.className = "play-track-btn"; |
|
|
playTrackBtn.onclick = () => { |
|
|
sendMessage({ action: "change_track", track: track }); |
|
|
}; |
|
|
li.appendChild(playTrackBtn); |
|
|
} |
|
|
playlistElement.appendChild(li); |
|
|
}); |
|
|
|
|
|
|
|
|
const isCurrentlyAdmin = state.admin_user_id === userId; |
|
|
if (isCurrentlyAdmin) { |
|
|
adminControls.style.display = "block"; |
|
|
playPauseBtn.disabled = false; |
|
|
seekBar.disabled = false; |
|
|
} else { |
|
|
adminControls.style.display = "none"; |
|
|
playPauseBtn.disabled = true; |
|
|
seekBar.disabled = true; |
|
|
} |
|
|
|
|
|
|
|
|
playPauseBtn.textContent = state.is_playing ? "Pause" : "Play"; |
|
|
} |
|
|
|
|
|
function togglePlayPause() { |
|
|
if (!isAdmin) return; |
|
|
const isPlaying = !audioPlayer.paused; |
|
|
const action = isPlaying ? "pause" : "play"; |
|
|
sendMessage({ action: action, position: audioPlayer.currentTime }); |
|
|
} |
|
|
|
|
|
function updateSeekBar() { |
|
|
if (!isSeeking) { |
|
|
seekBar.max = audioPlayer.duration || 0; |
|
|
seekBar.value = audioPlayer.currentTime; |
|
|
} |
|
|
timeDisplay.textContent = `${formatTime(audioPlayer.currentTime)} / ${formatTime(audioPlayer.duration || 0)}`; |
|
|
} |
|
|
|
|
|
function formatTime(seconds) { |
|
|
const minutes = Math.floor(seconds / 60); |
|
|
const secs = Math.floor(seconds % 60); |
|
|
return `${minutes}:${secs.toString().padStart(2, '0')}`; |
|
|
} |
|
|
|
|
|
async function uploadFile() { |
|
|
if (!fileInput.files.length) { |
|
|
uploadStatus.textContent = "Please select a file."; |
|
|
return; |
|
|
} |
|
|
const file = fileInput.files[0]; |
|
|
const formData = new FormData(); |
|
|
formData.append("file", file); |
|
|
|
|
|
uploadStatus.textContent = "Uploading..."; |
|
|
try { |
|
|
const response = await fetch(`/upload/${encodeURIComponent(roomName)}`, { |
|
|
method: "POST", |
|
|
body: formData, |
|
|
}); |
|
|
if (!response.ok) { |
|
|
const errorData = await response.json(); |
|
|
throw new Error(errorData.detail || "Upload failed"); |
|
|
} |
|
|
const result = await response.json(); |
|
|
uploadStatus.textContent = `Upload successful: ${result.filename}`; |
|
|
fileInput.value = ""; |
|
|
} catch (error) { |
|
|
uploadStatus.textContent = `Error: ${error.message}`; |
|
|
console.error("Upload error:", error); |
|
|
} |
|
|
} |
|
|
|
|
|
function sendMessage(message) { |
|
|
if (ws && ws.readyState === WebSocket.OPEN) { |
|
|
ws.send(JSON.stringify(message)); |
|
|
} |
|
|
} |
|
|
}); |