This commit is contained in:
280
www/sounds.js
Normal file
280
www/sounds.js
Normal file
@@ -0,0 +1,280 @@
|
||||
(() => {
|
||||
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 dialogEl = el("soundEditDialog");
|
||||
const formEl = el("soundEditForm");
|
||||
const titleEl = el("soundEditTitle");
|
||||
const nameEl = el("soundEditName");
|
||||
const descEl = el("soundEditDescription");
|
||||
const enabledEl = el("soundEditEnabled");
|
||||
const fileMetaEl = el("soundEditFileMeta");
|
||||
const uploadInputEl = el("soundEditUploadInput");
|
||||
const uploadBtnEl = el("soundEditUploadBtn");
|
||||
const playBtnEl = el("soundEditPlayBtn");
|
||||
const deleteBtnEl = el("soundEditDeleteBtn");
|
||||
|
||||
const store = {
|
||||
sounds: [],
|
||||
editingId: null,
|
||||
previewAudio: null,
|
||||
};
|
||||
|
||||
function canWrite() {
|
||||
if (!window.AuthApp?.canWrite) return true;
|
||||
return window.AuthApp.canWrite("integrations");
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.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 renderList() {
|
||||
if (!listEl) return;
|
||||
listEl.innerHTML = "";
|
||||
if (emptyEl) emptyEl.hidden = store.sounds.length > 0;
|
||||
|
||||
store.sounds.forEach((sound) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "missionListItem soundListItem";
|
||||
const hasFile = !!sound.file_name;
|
||||
row.innerHTML = `
|
||||
<div>
|
||||
<div class="missionListItemTitle">${escapeHtml(sound.name || sound.id)}</div>
|
||||
<div class="missionListItemMeta">
|
||||
${sound.enabled === false ? t("common.disabled") : t("common.enabled")}
|
||||
· ${hasFile ? escapeHtml(sound.file_name) : t("sounds.noFile")}
|
||||
${sound.duration_ms != null ? ` · ${formatDuration(sound.duration_ms)}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="missionListItemActions">
|
||||
<button type="button" class="btn subtle soundPlayBtn" data-id="${escapeHtml(sound.id)}" ${hasFile ? "" : "disabled"}>${t("sounds.play")}</button>
|
||||
<button type="button" class="btn subtle soundEditBtn" data-id="${escapeHtml(sound.id)}">${t("common.edit")}</button>
|
||||
</div>
|
||||
`;
|
||||
listEl.appendChild(row);
|
||||
});
|
||||
|
||||
listEl.querySelectorAll(".soundEditBtn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => openDialog(btn.dataset.id));
|
||||
});
|
||||
listEl.querySelectorAll(".soundPlayBtn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => playSound(btn.dataset.id));
|
||||
});
|
||||
}
|
||||
|
||||
function stopPreview() {
|
||||
if (store.previewAudio) {
|
||||
store.previewAudio.pause();
|
||||
store.previewAudio = null;
|
||||
}
|
||||
}
|
||||
|
||||
function playSound(id) {
|
||||
stopPreview();
|
||||
const audio = new Audio(`/api/sounds/${encodeURIComponent(id)}/file`);
|
||||
store.previewAudio = audio;
|
||||
audio.play().catch(() => alert(t("sounds.playFailed")));
|
||||
}
|
||||
|
||||
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 {
|
||||
fileMetaEl.textContent = t("sounds.noFile");
|
||||
}
|
||||
if (playBtnEl) playBtnEl.disabled = !sound?.file_name;
|
||||
}
|
||||
|
||||
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;
|
||||
updateFileMeta(existing);
|
||||
if (deleteBtnEl) deleteBtnEl.hidden = !existing || !canWrite();
|
||||
if (uploadBtnEl) uploadBtnEl.disabled = !canWrite();
|
||||
if (nameEl) nameEl.readOnly = !canWrite();
|
||||
if (descEl) descEl.readOnly = !canWrite();
|
||||
if (enabledEl) enabledEl.disabled = !canWrite();
|
||||
dialogEl?.showModal();
|
||||
}
|
||||
|
||||
async function saveDialog() {
|
||||
if (!canWrite()) return;
|
||||
const name = nameEl?.value.trim() || "";
|
||||
if (!name) {
|
||||
alert(t("sounds.nameRequired"));
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
name,
|
||||
description: descEl?.value.trim() || "",
|
||||
enabled: enabledEl?.checked !== false,
|
||||
};
|
||||
try {
|
||||
if (store.editingId) {
|
||||
await apiJson(`/api/sounds/${encodeURIComponent(store.editingId)}`, {
|
||||
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 = store.sounds.find((s) => s.id === store.editingId);
|
||||
updateFileMeta(updated);
|
||||
if (!uploadInputEl?.files?.length) {
|
||||
dialogEl?.close();
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFile() {
|
||||
if (!canWrite() || !store.editingId) 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(store.sounds.find((s) => s.id === store.editingId));
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSound() {
|
||||
if (!canWrite() || !store.editingId) return;
|
||||
if (!confirm(t("sounds.deleteConfirm"))) return;
|
||||
try {
|
||||
await apiJson(`/api/sounds/${encodeURIComponent(store.editingId)}`, { method: "DELETE" });
|
||||
dialogEl?.close();
|
||||
store.editingId = null;
|
||||
await refreshSounds();
|
||||
renderList();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
createBtnEl?.addEventListener("click", () => {
|
||||
if (!canWrite()) return;
|
||||
openDialog(null);
|
||||
});
|
||||
formEl?.addEventListener("submit", (evt) => {
|
||||
evt.preventDefault();
|
||||
saveDialog();
|
||||
});
|
||||
el("soundEditCancelBtn")?.addEventListener("click", () => {
|
||||
stopPreview();
|
||||
dialogEl?.close();
|
||||
});
|
||||
dialogEl?.addEventListener("cancel", (evt) => {
|
||||
evt.preventDefault();
|
||||
stopPreview();
|
||||
dialogEl?.close();
|
||||
});
|
||||
uploadBtnEl?.addEventListener("click", () => uploadInputEl?.click());
|
||||
uploadInputEl?.addEventListener("change", () => {
|
||||
saveDialog().then(() => uploadFile());
|
||||
});
|
||||
playBtnEl?.addEventListener("click", () => {
|
||||
if (store.editingId) playSound(store.editingId);
|
||||
});
|
||||
deleteBtnEl?.addEventListener("click", () => deleteSound());
|
||||
}
|
||||
|
||||
async function onPageShow() {
|
||||
stopPreview();
|
||||
if (createBtnEl) createBtnEl.hidden = !canWrite();
|
||||
try {
|
||||
await refreshSounds();
|
||||
renderList();
|
||||
} catch (e) {
|
||||
if (emptyEl) {
|
||||
emptyEl.hidden = false;
|
||||
emptyEl.textContent = e.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onPageHide() {
|
||||
stopPreview();
|
||||
dialogEl?.close();
|
||||
}
|
||||
|
||||
function getSounds() {
|
||||
return JSON.parse(JSON.stringify(store.sounds));
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
|
||||
window.SoundsApp = {
|
||||
onPageShow,
|
||||
onPageHide,
|
||||
getSounds,
|
||||
refreshSounds,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user