(() => { const PAGE_SIZE = 10; const ICONS = { listen: ``, }; const el = (id) => document.getElementById(id); const t = (key, vars) => window.I18n?.t(key, vars) ?? key; const listEl = el("soundList"); const emptyEl = el("soundListEmpty"); const createBtnEl = el("soundCreateBtn"); const filterInputEl = el("soundsFilterInput"); const filterCountEl = el("soundsFilterCount"); const pageLabelEl = el("soundsPageLabel"); const dialogEl = el("soundEditDialog"); const formEl = el("soundEditForm"); const titleEl = el("soundEditTitle"); const systemBadgeEl = el("soundEditSystemBadge"); const nameEl = el("soundEditName"); const descEl = el("soundEditDescription"); const volumeEl = el("soundEditVolume"); const volumeOutEl = el("soundEditVolumeOut"); const enabledEl = el("soundEditEnabled"); const fileMetaEl = el("soundEditFileMeta"); const fileSectionEl = el("soundEditFileSection"); const uploadInputEl = el("soundEditUploadInput"); const uploadBtnEl = el("soundEditUploadBtn"); const listenBtnEl = el("soundEditListenBtn"); const playRobotBtnEl = el("soundEditPlayRobotBtn"); const deleteBtnEl = el("soundEditDeleteBtn"); const deleteConfirmDialogEl = el("soundDeleteConfirmDialog"); const deleteConfirmTextEl = el("soundDeleteConfirmText"); const store = { sounds: [], editingId: null, previewAudio: null, filter: "", page: 1, }; function canWrite() { if (!window.AuthApp?.canWrite) return true; return window.AuthApp.canWrite("sounds") || window.AuthApp.canWrite("integrations"); } function escapeHtml(str) { return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } async function apiJson(url, opts = {}) { if (window.AuthApp && !window.AuthApp.isReady()) { throw new Error("not authenticated"); } const res = await fetch(url, { credentials: "include", ...opts }); const text = await res.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = null; } if (!res.ok) { const msg = (data && data.error) || text || res.statusText; throw new Error(msg); } return data; } async function refreshSounds() { const data = await apiJson("/api/sounds"); store.sounds = Array.isArray(data.sounds) ? data.sounds : []; } function formatDuration(ms) { if (!Number.isFinite(Number(ms))) return "—"; const sec = Math.round(Number(ms) / 100) / 10; return `${sec}s`; } function currentEditingSound() { return store.editingId ? store.sounds.find((s) => s.id === store.editingId) : null; } function canListen(sound) { return !!(sound?.file_name); } function sortedSounds() { return [...store.sounds].sort((a, b) => { if (!!a.is_system !== !!b.is_system) return a.is_system ? -1 : 1; return (a.name || "").localeCompare(b.name || ""); }); } function filteredSounds() { const q = store.filter.trim().toLowerCase(); let items = sortedSounds(); if (q) { items = items.filter((sound) => { const name = (sound.name || "").toLowerCase(); const file = (sound.file_name || "").toLowerCase(); const desc = (sound.description || "").toLowerCase(); return name.includes(q) || file.includes(q) || desc.includes(q); }); } return items; } function pageCount(total) { return Math.max(1, Math.ceil(total / PAGE_SIZE)); } function pagedItems(items) { const totalPages = pageCount(items.length); if (store.page > totalPages) store.page = totalPages; if (store.page < 1) store.page = 1; const start = (store.page - 1) * PAGE_SIZE; return items.slice(start, start + PAGE_SIZE); } function updatePagerUi(totalItems) { const totalPages = pageCount(totalItems); if (filterCountEl) filterCountEl.textContent = t("sounds.itemsFound", { n: totalItems }); if (pageLabelEl) pageLabelEl.textContent = t("sounds.pageOf", { page: store.page, total: totalPages }); const atStart = store.page <= 1; const atEnd = store.page >= totalPages; el("soundsPageFirst")?.toggleAttribute("disabled", atStart); el("soundsPagePrev")?.toggleAttribute("disabled", atStart); el("soundsPageNext")?.toggleAttribute("disabled", atEnd); el("soundsPageLast")?.toggleAttribute("disabled", atEnd); } function soundMetaLine(sound) { const hasFile = !!sound.file_name; const parts = [ sound.enabled === false ? t("common.disabled") : t("common.enabled"), t("sounds.volumeShort", { volume: sound.volume != null ? sound.volume : 100 }), hasFile ? sound.file_name : t("sounds.noFile"), ]; if (sound.duration_ms != null) parts.push(formatDuration(sound.duration_ms)); if (sound.is_system) parts.push(t("sounds.systemShort")); return parts.map((p) => escapeHtml(p)).join(" · "); } function renderList() { if (!listEl) return; const items = filteredSounds(); const pageItems = pagedItems(items); updatePagerUi(items.length); listEl.innerHTML = ""; const showEmpty = items.length === 0; if (listEl) listEl.hidden = showEmpty; if (emptyEl) { emptyEl.hidden = !showEmpty; emptyEl.textContent = store.filter.trim() ? t("sounds.emptyFilter") : t("sounds.empty"); } pageItems.forEach((sound) => { const row = document.createElement("article"); row.className = "soundsMirRow"; row.setAttribute("role", "listitem"); row.dataset.id = sound.id; const listenDisabled = !canListen(sound); const editDisabled = !canWrite(); row.innerHTML = `
${soundMetaLine(sound)}
`; row.querySelectorAll(".soundEditBtn").forEach((btn) => { btn.addEventListener("click", () => { if (!canWrite()) return; openDialog(btn.dataset.id); }); }); row.querySelector(".soundListenBtn")?.addEventListener("click", () => listenSound(sound.id)); row.addEventListener("dblclick", () => { if (canWrite()) openDialog(sound.id); }); listEl.appendChild(row); }); if (createBtnEl) createBtnEl.hidden = !canWrite(); } function stopPreview() { if (store.previewAudio) { store.previewAudio.pause(); store.previewAudio = null; } } function listenSound(id) { const sound = store.sounds.find((s) => s.id === id); if (!canListen(sound)) return; stopPreview(); const audio = new Audio(`/api/sounds/${encodeURIComponent(id)}/file`); store.previewAudio = audio; audio.play().catch(() => alert(t("sounds.playFailed"))); } function readVolume() { return Number(volumeEl?.value) || 0; } function syncVolumeOut() { if (volumeOutEl) volumeOutEl.textContent = String(readVolume()); } function updateFileMeta(sound) { if (!fileMetaEl) return; if (sound?.file_name) { fileMetaEl.textContent = t("sounds.fileMeta", { name: sound.file_name, duration: formatDuration(sound.duration_ms), }); } else if (sound?.is_system) { fileMetaEl.textContent = t("sounds.systemNoFile"); } else { fileMetaEl.textContent = t("sounds.noFile"); } const listenOk = canListen(sound); if (listenBtnEl) listenBtnEl.disabled = !listenOk; if (playRobotBtnEl) playRobotBtnEl.disabled = !sound?.id || sound.enabled === false; } function applyDialogPermissions(sound) { const ro = !canWrite(); const isSystem = !!sound?.is_system; if (nameEl) nameEl.readOnly = ro || isSystem; if (descEl) descEl.readOnly = ro; if (volumeEl) volumeEl.disabled = ro; if (enabledEl) enabledEl.disabled = ro; if (uploadBtnEl) uploadBtnEl.hidden = isSystem || ro; if (fileSectionEl) fileSectionEl.hidden = false; if (deleteBtnEl) deleteBtnEl.hidden = !sound || isSystem || ro; if (systemBadgeEl) systemBadgeEl.hidden = !isSystem; } function openDialog(id = null) { store.editingId = id; const existing = id ? store.sounds.find((s) => s.id === id) : null; if (titleEl) titleEl.textContent = existing ? t("sounds.editTitle") : t("sounds.createTitle"); if (nameEl) nameEl.value = existing?.name || ""; if (descEl) descEl.value = existing?.description || ""; if (enabledEl) enabledEl.checked = existing?.enabled !== false; if (volumeEl) volumeEl.value = existing?.volume != null ? existing.volume : 100; syncVolumeOut(); updateFileMeta(existing); applyDialogPermissions(existing); dialogEl?.showModal(); } function readPayload() { return { name: nameEl?.value.trim() || "", description: descEl?.value.trim() || "", enabled: enabledEl?.checked !== false, volume: readVolume(), }; } function isReservedName(name) { return ["beep", "horn", "chime"].includes(String(name || "").trim().toLowerCase()); } function nameExists(name, exceptId = null) { const lower = String(name || "").trim().toLowerCase(); return store.sounds.some( (s) => s.id !== exceptId && String(s.name || "").trim().toLowerCase() === lower, ); } async function saveDialog() { if (!canWrite()) return; const soundId = store.editingId; const payload = readPayload(); if (!payload.name) { alert(t("sounds.nameRequired")); return; } if (!soundId) { if (isReservedName(payload.name)) { alert(t("sounds.reservedName")); return; } if (nameExists(payload.name)) { alert(t("sounds.nameDuplicate")); return; } } try { if (soundId) { await apiJson(`/api/sounds/${encodeURIComponent(soundId)}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } else { const created = await apiJson("/api/sounds", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); store.editingId = created.id; } await refreshSounds(); renderList(); const updated = currentEditingSound(); updateFileMeta(updated); applyDialogPermissions(updated); if (!uploadInputEl?.files?.length) { dialogEl?.close(); } } catch (e) { alert(e.message); } } async function uploadFile() { if (!canWrite() || !store.editingId) return; const sound = currentEditingSound(); if (sound?.is_system) return; const file = uploadInputEl?.files?.[0]; if (!file) return; const form = new FormData(); form.append("file", file); try { await apiJson(`/api/sounds/${encodeURIComponent(store.editingId)}/file`, { method: "POST", body: form, }); uploadInputEl.value = ""; await refreshSounds(); renderList(); updateFileMeta(currentEditingSound()); } catch (e) { alert(e.message); } } function openDeleteConfirm() { const sound = currentEditingSound(); if (!sound || sound.is_system || !canWrite()) return; if (deleteConfirmTextEl) { deleteConfirmTextEl.textContent = t("sounds.deleteConfirmText", { name: sound.name || sound.id }); } deleteConfirmDialogEl?.showModal(); } async function confirmDelete() { if (!canWrite() || !store.editingId) return; try { await apiJson(`/api/sounds/${encodeURIComponent(store.editingId)}`, { method: "DELETE" }); deleteConfirmDialogEl?.close(); dialogEl?.close(); store.editingId = null; await refreshSounds(); renderList(); } catch (e) { alert(e.message); } } async function playOnRobot() { if (!store.editingId) return; try { await apiJson(`/api/sounds/${encodeURIComponent(store.editingId)}/play`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ volume: readVolume() }), }); } catch (e) { alert(e.message); } } function clearFilters() { store.filter = ""; store.page = 1; if (filterInputEl) filterInputEl.value = ""; renderList(); } function bindEvents() { createBtnEl?.addEventListener("click", () => { if (!canWrite()) return; openDialog(null); }); formEl?.addEventListener("submit", (evt) => { evt.preventDefault(); saveDialog(); }); el("soundEditCancelBtn")?.addEventListener("click", () => { stopPreview(); store.editingId = null; dialogEl?.close(); }); dialogEl?.addEventListener("cancel", (evt) => { evt.preventDefault(); stopPreview(); store.editingId = null; dialogEl?.close(); }); volumeEl?.addEventListener("input", syncVolumeOut); uploadBtnEl?.addEventListener("click", () => uploadInputEl?.click()); uploadInputEl?.addEventListener("change", () => { saveDialog().then(() => uploadFile()); }); listenBtnEl?.addEventListener("click", () => { if (store.editingId) listenSound(store.editingId); }); playRobotBtnEl?.addEventListener("click", () => playOnRobot()); deleteBtnEl?.addEventListener("click", () => openDeleteConfirm()); el("soundDeleteCancelBtn")?.addEventListener("click", () => deleteConfirmDialogEl?.close()); el("soundDeleteYesBtn")?.addEventListener("click", () => confirmDelete()); deleteConfirmDialogEl?.addEventListener("cancel", (evt) => { evt.preventDefault(); deleteConfirmDialogEl?.close(); }); filterInputEl?.addEventListener("input", () => { store.filter = filterInputEl.value; store.page = 1; renderList(); }); el("soundsClearFiltersBtn")?.addEventListener("click", clearFilters); el("soundsPageFirst")?.addEventListener("click", () => { store.page = 1; renderList(); }); el("soundsPagePrev")?.addEventListener("click", () => { store.page = Math.max(1, store.page - 1); renderList(); }); el("soundsPageNext")?.addEventListener("click", () => { store.page += 1; renderList(); }); el("soundsPageLast")?.addEventListener("click", () => { store.page = pageCount(filteredSounds().length); renderList(); }); el("soundsHelpBtn")?.addEventListener("click", () => { alert(t("sounds.helpBody")); }); window.addEventListener("lm:locale-change", () => renderList()); } async function onPageShow() { stopPreview(); if (createBtnEl) createBtnEl.hidden = !canWrite(); document.body.classList.toggle("auth-readonly-sounds", !canWrite()); try { await refreshSounds(); renderList(); } catch (e) { if (emptyEl) { emptyEl.hidden = false; emptyEl.textContent = e.message; } if (listEl) listEl.hidden = true; } } function onPageHide() { stopPreview(); dialogEl?.close(); deleteConfirmDialogEl?.close(); } function getSounds() { return JSON.parse(JSON.stringify(store.sounds)); } bindEvents(); window.SoundsApp = { onPageShow, onPageHide, getSounds, refreshSounds, }; })();