Initial Commit
This commit is contained in:
commit
6d2e379a0c
41 changed files with 5064 additions and 0 deletions
319
public/scripts/jukebox.js
Normal file
319
public/scripts/jukebox.js
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
import {
|
||||
createApiClient,
|
||||
createStatusManager,
|
||||
escapeHtml,
|
||||
getStoredApiKey,
|
||||
getStoredDisplayName,
|
||||
formatBytes,
|
||||
formatDate,
|
||||
setStoredApiKey,
|
||||
setStoredDisplayName,
|
||||
} from "./common.js";
|
||||
|
||||
const state = {
|
||||
apiKey: getStoredApiKey(),
|
||||
displayName: getStoredDisplayName(),
|
||||
queue: [],
|
||||
current: null,
|
||||
library: [],
|
||||
status: "idle",
|
||||
};
|
||||
|
||||
const elements = {
|
||||
status: document.getElementById("status"),
|
||||
apiKeyInput: document.getElementById("apiKey"),
|
||||
displayNameInput: document.getElementById("displayName"),
|
||||
settingsForm: document.getElementById("settingsForm"),
|
||||
uploadForm: document.getElementById("uploadForm"),
|
||||
fileInput: document.getElementById("musicFile"),
|
||||
notesInput: document.getElementById("uploadNotes"),
|
||||
permanentCheckbox: document.getElementById("permanentFile"),
|
||||
enqueueCheckbox: document.getElementById("enqueueFile"),
|
||||
currentTrack: document.getElementById("currentTrack"),
|
||||
queueList: document.getElementById("queueList"),
|
||||
queueEmpty: document.getElementById("queueEmpty"),
|
||||
libraryList: document.getElementById("libraryList"),
|
||||
libraryEmpty: document.getElementById("libraryEmpty"),
|
||||
refreshBtn: document.getElementById("refreshBtn"),
|
||||
pauseResumeBtn: document.getElementById("pauseResumeBtn"),
|
||||
playerButtons: document.querySelectorAll("[data-player-action]"),
|
||||
heroUploadBtn: document.getElementById("heroUploadBtn"),
|
||||
heroQueueBtn: document.getElementById("heroQueueBtn"),
|
||||
uploadSection: document.getElementById("uploadSection"),
|
||||
queueSection: document.getElementById("queueSection"),
|
||||
};
|
||||
|
||||
let pollIntervalId = null;
|
||||
|
||||
const { setStatus, clearStatus } = createStatusManager(elements.status);
|
||||
const apiFetch = createApiClient({
|
||||
basePath: "/api",
|
||||
getApiKey: () => state.apiKey,
|
||||
});
|
||||
|
||||
async function refreshAll({ silent = false } = {}) {
|
||||
if (!silent) {
|
||||
clearStatus();
|
||||
}
|
||||
|
||||
try {
|
||||
const [queueData, libraryData] = await Promise.all([
|
||||
apiFetch("/queue"),
|
||||
apiFetch("/library"),
|
||||
]);
|
||||
|
||||
state.current = queueData.current;
|
||||
state.queue = queueData.upcoming || [];
|
||||
state.library = libraryData.tracks || [];
|
||||
state.status = queueData.status || "idle";
|
||||
|
||||
renderCurrentTrack();
|
||||
renderQueue();
|
||||
renderLibrary();
|
||||
updatePauseResumeButton();
|
||||
} catch (error) {
|
||||
setStatus(`Erreur: ${error.message}`, "error", false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderCurrentTrack() {
|
||||
if (!elements.currentTrack) return;
|
||||
if (!state.current) {
|
||||
elements.currentTrack.classList.add("player-current--empty");
|
||||
elements.currentTrack.innerHTML = "En attente de lecture...";
|
||||
return;
|
||||
}
|
||||
|
||||
elements.currentTrack.classList.remove("player-current--empty");
|
||||
const track = state.current;
|
||||
const permanence = track.permanent ? "Permanent" : "Temporaire";
|
||||
elements.currentTrack.innerHTML = `
|
||||
<div class="media-item__title">${escapeHtml(track.title || track.originalName)}</div>
|
||||
<div class="media-item__meta">
|
||||
${permanence} • ${formatBytes(track.size)} • Ajouté par ${escapeHtml(
|
||||
track.uploader || "inconnu"
|
||||
)}
|
||||
</div>
|
||||
<div class="media-item__meta">Commencé à ${formatDate(track.createdAt)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderQueue() {
|
||||
if (!elements.queueList) return;
|
||||
elements.queueList.innerHTML = "";
|
||||
|
||||
if (!state.queue || state.queue.length === 0) {
|
||||
elements.queueEmpty.style.display = "block";
|
||||
return;
|
||||
}
|
||||
|
||||
elements.queueEmpty.style.display = "none";
|
||||
|
||||
state.queue.forEach((track, index) => {
|
||||
const li = document.createElement("li");
|
||||
li.className = "media-item media-item--queue";
|
||||
const permanence = track.permanent ? "Permanent" : "Temporaire";
|
||||
li.innerHTML = `
|
||||
<div class="media-item__info">
|
||||
<div class="media-item__title">${escapeHtml(track.title || track.originalName)}</div>
|
||||
<div class="media-item__meta">
|
||||
${permanence} • ${formatBytes(track.size)} • Ajouté par ${escapeHtml(
|
||||
track.uploader || "inconnu"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-item__actions">
|
||||
<button class="btn btn--secondary" data-action="up" data-track="${track.id}">▲</button>
|
||||
<button class="btn btn--secondary" data-action="down" data-track="${track.id}">▼</button>
|
||||
<button class="btn btn--danger" data-action="remove" data-track="${track.id}">Supprimer</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
li.querySelectorAll("button").forEach((btn) => {
|
||||
btn.addEventListener("click", (event) => {
|
||||
const action = event.currentTarget.dataset.action;
|
||||
const trackId = event.currentTarget.dataset.track;
|
||||
handleQueueAction(action, trackId, index);
|
||||
});
|
||||
});
|
||||
|
||||
elements.queueList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function renderLibrary() {
|
||||
if (!elements.libraryList) return;
|
||||
elements.libraryList.innerHTML = "";
|
||||
|
||||
if (!state.library || state.library.length === 0) {
|
||||
elements.libraryEmpty.style.display = "block";
|
||||
return;
|
||||
}
|
||||
|
||||
elements.libraryEmpty.style.display = "none";
|
||||
|
||||
state.library.forEach((track) => {
|
||||
const li = document.createElement("li");
|
||||
li.className = "media-item media-item--library";
|
||||
li.innerHTML = `
|
||||
<div class="media-item__info">
|
||||
<div class="media-item__title">${escapeHtml(track.originalName)}</div>
|
||||
<div class="media-item__meta">
|
||||
${formatBytes(track.size)} • Ajouté le ${formatDate(track.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-item__actions">
|
||||
<button class="btn" data-library-action="enqueue" data-library-id="${track.id}">Ajouter</button>
|
||||
<button class="btn btn--danger" data-library-action="delete" data-library-id="${track.id}">
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
li.querySelectorAll("button").forEach((btn) => {
|
||||
btn.addEventListener("click", (event) => {
|
||||
const action = event.currentTarget.dataset.libraryAction;
|
||||
const libraryId = event.currentTarget.dataset.libraryId;
|
||||
handleLibraryAction(action, libraryId);
|
||||
});
|
||||
});
|
||||
|
||||
elements.libraryList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function updatePauseResumeButton() {
|
||||
if (!elements.pauseResumeBtn) return;
|
||||
const normalizedStatus =
|
||||
typeof state.status === "string" ? state.status.toLowerCase() : "idle";
|
||||
const isPaused = normalizedStatus === "paused";
|
||||
const isPlaying = normalizedStatus === "playing";
|
||||
|
||||
if (isPaused) {
|
||||
elements.pauseResumeBtn.textContent = "Reprendre";
|
||||
elements.pauseResumeBtn.dataset.playerAction = "resume";
|
||||
} else {
|
||||
elements.pauseResumeBtn.textContent = "Pause";
|
||||
elements.pauseResumeBtn.dataset.playerAction = "pause";
|
||||
}
|
||||
|
||||
const actionable = isPaused || isPlaying;
|
||||
elements.pauseResumeBtn.disabled = !actionable;
|
||||
}
|
||||
|
||||
async function handleQueueAction(action, trackId, index) {
|
||||
try {
|
||||
if (action === "remove") {
|
||||
await apiFetch(`/queue/${trackId}`, { method: "DELETE" });
|
||||
} else if (action === "up" || action === "down") {
|
||||
const delta = action === "up" ? -1 : 1;
|
||||
const newPosition = Math.max(0, Math.min(index + delta, state.queue.length - 1));
|
||||
if (newPosition === index) return;
|
||||
await apiFetch(`/queue/${trackId}/move`, {
|
||||
method: "PATCH",
|
||||
body: { position: newPosition },
|
||||
});
|
||||
}
|
||||
await refreshAll({ silent: true });
|
||||
} catch (error) {
|
||||
setStatus(`Impossible de mettre à jour la file: ${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLibraryAction(action, libraryId) {
|
||||
try {
|
||||
if (action === "enqueue") {
|
||||
await apiFetch(`/library/${libraryId}/enqueue`, {
|
||||
method: "POST",
|
||||
body: { requestedBy: state.displayName || "web-ui" },
|
||||
});
|
||||
} else if (action === "delete") {
|
||||
if (!confirm("Supprimer ce fichier permanent ?")) {
|
||||
return;
|
||||
}
|
||||
await apiFetch(`/library/${libraryId}`, { method: "DELETE" });
|
||||
}
|
||||
await refreshAll({ silent: true });
|
||||
} catch (error) {
|
||||
setStatus(`Action bibliothèque impossible: ${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
clearInterval(pollIntervalId);
|
||||
pollIntervalId = setInterval(() => refreshAll({ silent: true }), 5000);
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
elements.settingsForm?.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
state.apiKey = elements.apiKeyInput.value.trim();
|
||||
state.displayName = elements.displayNameInput.value.trim();
|
||||
setStoredApiKey(state.apiKey);
|
||||
setStoredDisplayName(state.displayName);
|
||||
setStatus("Paramètres sauvegardés", "success");
|
||||
refreshAll({ silent: true });
|
||||
});
|
||||
|
||||
elements.uploadForm?.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const file = elements.fileInput.files[0];
|
||||
if (!file) {
|
||||
setStatus("Merci de sélectionner un fichier audio", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = new FormData();
|
||||
data.append("file", file);
|
||||
data.append("permanent", elements.permanentCheckbox.checked ? "true" : "false");
|
||||
data.append("enqueue", elements.enqueueCheckbox.checked ? "true" : "false");
|
||||
if (state.displayName) {
|
||||
data.append("uploader", state.displayName);
|
||||
}
|
||||
if (elements.notesInput.value) {
|
||||
data.append("notes", elements.notesInput.value.trim());
|
||||
}
|
||||
|
||||
setStatus("Upload en cours...", "info", false);
|
||||
await apiFetch("/uploads", { method: "POST", body: data });
|
||||
elements.uploadForm.reset();
|
||||
elements.enqueueCheckbox.checked = true;
|
||||
setStatus("Fichier envoyé avec succès", "success");
|
||||
await refreshAll({ silent: true });
|
||||
} catch (error) {
|
||||
setStatus(`Échec de l'upload: ${error.message}`, "error");
|
||||
}
|
||||
});
|
||||
|
||||
elements.refreshBtn?.addEventListener("click", () => refreshAll());
|
||||
elements.heroUploadBtn?.addEventListener("click", () =>
|
||||
elements.uploadSection?.scrollIntoView({ behavior: "smooth" })
|
||||
);
|
||||
elements.heroQueueBtn?.addEventListener("click", () =>
|
||||
elements.queueSection?.scrollIntoView({ behavior: "smooth" })
|
||||
);
|
||||
|
||||
elements.playerButtons?.forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
const action = button.dataset.playerAction;
|
||||
try {
|
||||
await apiFetch(`/player/${action}`, { method: "POST" });
|
||||
await refreshAll({ silent: true });
|
||||
} catch (error) {
|
||||
setStatus(`Action player impossible: ${error.message}`, "error");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
elements.apiKeyInput.value = state.apiKey;
|
||||
elements.displayNameInput.value = state.displayName;
|
||||
bindEvents();
|
||||
updatePauseResumeButton();
|
||||
refreshAll();
|
||||
startPolling();
|
||||
}
|
||||
|
||||
initialize();
|
||||
Loading…
Add table
Add a link
Reference in a new issue