(function() { const M = window.MusicRoom; document.addEventListener("DOMContentLoaded", () => { const addBtn = M.$("#btn-add"); const addPanel = M.$("#add-panel"); const addCloseBtn = M.$("#btn-add-close"); const uploadFilesBtn = M.$("#btn-upload-files"); const fileInput = M.$("#file-input"); const dropzone = M.$("#upload-dropzone"); const libraryPanel = M.$("#library-panel"); const tasksList = M.$("#tasks-list"); const tasksEmpty = M.$("#tasks-empty"); if (!addBtn || !fileInput || !dropzone) return; function openPanel() { addPanel.classList.remove("hidden", "closing"); addBtn.classList.add("hidden"); } function closePanel() { addPanel.classList.add("closing"); addBtn.classList.remove("hidden"); addPanel.addEventListener("animationend", () => { if (addPanel.classList.contains("closing")) { addPanel.classList.add("hidden"); addPanel.classList.remove("closing"); } }, { once: true }); } // Open add panel addBtn.onclick = () => { if (!M.currentUser) { M.showToast("Sign in to add tracks"); return; } openPanel(); }; // Close add panel addCloseBtn.onclick = () => { closePanel(); }; // Upload files option uploadFilesBtn.onclick = () => { fileInput.click(); }; // Fetch from URL option const fetchUrlBtn = M.$("#btn-fetch-url"); const fetchDialog = M.$("#fetch-dialog"); const fetchCloseBtn = M.$("#btn-fetch-close"); const fetchUrlInput = M.$("#fetch-url-input"); const fetchSubmitBtn = M.$("#btn-fetch-submit"); function openFetchDialog() { closePanel(); fetchDialog.classList.remove("hidden", "closing"); fetchUrlInput.value = ""; fetchUrlInput.focus(); } function closeFetchDialog() { fetchDialog.classList.add("closing"); fetchDialog.addEventListener("animationend", () => { if (fetchDialog.classList.contains("closing")) { fetchDialog.classList.add("hidden"); fetchDialog.classList.remove("closing"); } }, { once: true }); } if (fetchUrlBtn) { fetchUrlBtn.onclick = openFetchDialog; } if (fetchCloseBtn) { fetchCloseBtn.onclick = closeFetchDialog; } if (fetchSubmitBtn) { fetchSubmitBtn.onclick = async () => { const url = fetchUrlInput.value.trim(); if (!url) { M.showToast("Please enter a URL"); return; } closeFetchDialog(); // Create a task for this fetch const task = createTask(`Fetching: ${url.substring(0, 50)}...`); try { const res = await fetch("/api/fetch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url }) }); if (res.ok) { const data = await res.json(); task.setComplete(); M.showToast(data.message || "Fetch started"); } else { const err = await res.json().catch(() => ({})); task.setError(err.error || "Failed"); M.showToast(err.error || "Fetch failed", "error"); } } catch (e) { task.setError("Error"); M.showToast("Fetch failed", "error"); } }; } if (fetchUrlInput) { fetchUrlInput.onkeydown = (e) => { if (e.key === "Enter") fetchSubmitBtn.click(); if (e.key === "Escape") closeFetchDialog(); }; } // File input change fileInput.onchange = () => { if (fileInput.files.length > 0) { closePanel(); uploadFiles(fileInput.files); fileInput.value = ""; } }; // Drag and drop on library panel let dragCounter = 0; libraryPanel.ondragenter = (e) => { if (!M.currentUser) return; if (!e.dataTransfer.types.includes("Files")) return; e.preventDefault(); dragCounter++; dropzone.classList.remove("hidden"); }; libraryPanel.ondragleave = (e) => { e.preventDefault(); dragCounter--; if (dragCounter === 0) { dropzone.classList.add("hidden"); } }; libraryPanel.ondragover = (e) => { if (!M.currentUser) return; if (!e.dataTransfer.types.includes("Files")) return; e.preventDefault(); e.dataTransfer.dropEffect = "copy"; }; libraryPanel.ondrop = (e) => { e.preventDefault(); dragCounter = 0; dropzone.classList.add("hidden"); if (!M.currentUser) { M.showToast("Sign in to upload"); return; } const files = e.dataTransfer.files; if (files.length > 0) { uploadFiles(files); } }; // Task management function updateTasksEmpty() { const hasTasks = tasksList.children.length > 0; tasksEmpty.classList.toggle("hidden", hasTasks); } function createTask(filename) { const task = document.createElement("div"); task.className = "task-item"; task.innerHTML = ` ${filename} 0%
`; tasksList.appendChild(task); updateTasksEmpty(); // Switch to tasks tab const tasksTab = document.querySelector('.panel-tab[data-tab="tasks"]'); if (tasksTab) tasksTab.click(); return { setProgress(percent) { task.querySelector(".task-progress").textContent = `${Math.round(percent)}%`; task.querySelector(".task-bar").style.width = `${percent}%`; }, setComplete() { task.classList.add("complete"); task.querySelector(".task-progress").textContent = "Done"; task.querySelector(".task-bar").style.width = "100%"; // Remove after delay setTimeout(() => { task.remove(); updateTasksEmpty(); }, 3000); }, setError(msg) { task.classList.add("error"); task.querySelector(".task-progress").textContent = msg || "Failed"; // Remove after delay setTimeout(() => { task.remove(); updateTasksEmpty(); }, 5000); } }; } function uploadFile(file) { return new Promise((resolve) => { const task = createTask(file.name); const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append("file", file); xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const percent = (e.loaded / e.total) * 100; task.setProgress(percent); } }; xhr.onload = () => { if (xhr.status === 200) { task.setComplete(); resolve({ success: true }); } else if (xhr.status === 409) { task.setError("Duplicate"); resolve({ success: false, duplicate: true }); } else { task.setError("Failed"); resolve({ success: false }); } }; xhr.onerror = () => { task.setError("Error"); resolve({ success: false }); }; xhr.open("POST", "/api/upload"); xhr.send(formData); }); } async function uploadFiles(files) { const validExts = [".mp3", ".ogg", ".flac", ".wav", ".m4a", ".aac", ".opus", ".wma", ".mp4"]; const audioFiles = [...files].filter(f => { const ext = f.name.toLowerCase().match(/\.[^.]+$/)?.[0]; return ext && validExts.includes(ext); }); if (audioFiles.length === 0) { M.showToast("No valid audio files"); return; } let uploaded = 0; let failed = 0; // Upload files in parallel (max 3 concurrent) const concurrency = 3; const queue = [...audioFiles]; const active = []; while (queue.length > 0 || active.length > 0) { while (active.length < concurrency && queue.length > 0) { const file = queue.shift(); const promise = uploadFile(file).then(result => { if (result.success) uploaded++; else failed++; active.splice(active.indexOf(promise), 1); }); active.push(promise); } if (active.length > 0) { await Promise.race(active); } } if (uploaded > 0) { M.showToast(`Uploaded ${uploaded} track${uploaded > 1 ? 's' : ''}${failed > 0 ? `, ${failed} failed` : ''}`); } else if (failed > 0) { M.showToast(`Upload failed`); } } }); })();