blastoise-archive/public/upload.js

233 lines
6.6 KiB
JavaScript

(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();
};
// 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 = `
<span class="task-spinner"></span>
<span class="task-icon"></span>
<span class="task-name">${filename}</span>
<span class="task-progress">0%</span>
<div class="task-bar" style="width: 0%"></div>
`;
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`);
}
}
});
})();