@ -7,9 +7,7 @@
@@ -7,9 +7,7 @@
< p class = "muted" > Backend: {{ backend_url }} | Utilisateur: {{ username }}< / p >
< / div >
< div class = "topbar-actions" >
< form method = "post" action = "{{ url_for('ui.stop_audio') }}" >
< button type = "submit" class = "danger" > Arreter l'audio< / button >
< / form >
< button type = "button" class = "danger" id = "btn-stop-audio" > Arreter l'audio< / button >
< a href = "{{ url_for('ui.logout') }}" class = "ghost-link" > Deconnexion< / a >
< / div >
< / header >
@ -49,62 +47,73 @@
@@ -49,62 +47,73 @@
< input type = "number" step = "0.1" min = "0" name = "end_seconds" id = "end_seconds" / >
< / label >
< / div >
< label class = "volume-row" >
< span > Volume : < span id = "trigger_volume_display" > 80< / span > %< / span >
< input type = "range" min = "0" max = "100" name = "volume" id = "trigger_volume" value = "80" / >
< / label >
< button type = "submit" > Enregistrer le trigger< / button >
< / form >
< / section >
< section class = "panel reveal delay-2" >
< h2 > Liste des triggers< / h2 >
{% if triggers %}
< div class = "table-wrap" >
< table >
< thead >
< tr >
< th > ID< / th >
< th > Nom< / th >
< th > Type< / th >
< th > Audio< / th >
< th > Debut< / th >
< th > Fin< / th >
< th > Actions< / th >
< p class = "muted" id = "no-triggers-msg" { % if triggers % } hidden { % endif % } > Aucun trigger configure.< / p >
< div class = "table-wrap" { % if not triggers % } hidden { % endif % } id = "triggers-table-wrap" >
< table
id="triggers-table"
data-play-url="{{ url_for('ui.play_trigger') }}"
data-delete-url="{{ url_for('ui.delete_trigger') }}"
>
< thead >
< tr >
< th > ID< / th >
< th > Nom< / th >
< th > Type< / th >
< th > Audio< / th >
< th > Debut< / th >
< th > Fin< / th >
< th > Volume< / th >
< th > Actions< / th >
< / tr >
< / thead >
< tbody id = "triggers-tbody" >
{% for trigger_id, trigger in triggers.items() %}
< tr
class="trigger-row"
data-trigger-id="{{ trigger_id }}"
data-trigger-type="{{ trigger.get('type', '') }}"
data-trigger-name="{{ trigger.get('name', '') }}"
data-trigger-music="{{ trigger.get('music_file', '') }}"
data-trigger-start="{{ trigger.get('start_seconds', 0) }}"
data-trigger-end="{{ '' if trigger.get('end_seconds') is none else trigger.get('end_seconds') }}"
data-trigger-volume="{{ trigger.get('volume', 80) }}"
title="Cliquer pour charger ce trigger dans le formulaire"
>
< td > {{ trigger_id }}< / td >
< td > {{ trigger.get('name', '') }}< / td >
< td > {{ trigger.get('type', '') }}< / td >
< td > {{ trigger.get('music_file', '') }}< / td >
< td > {{ trigger.get('start_seconds', 0) }}< / td >
< td > {{ trigger.get('end_seconds', '') }}< / td >
< td > {{ trigger.get('volume', 80) }}< / td >
< td >
< button
type="button"
class="small js-play-trigger"
data-trigger-id="{{ trigger_id }}"
data-play-url="{{ url_for('ui.play_trigger') }}"
>Lancer< / button >
< button
type="button"
class="small danger js-delete-trigger-btn"
data-trigger-id="{{ trigger_id }}"
>Supprimer< / button >
< / td >
< / tr >
< / thead >
< tbody >
{% for trigger_id, trigger in triggers.items() %}
< tr
class="trigger-row"
data-trigger-id="{{ trigger_id }}"
data-trigger-type="{{ trigger.get('type', '') }}"
data-trigger-name="{{ trigger.get('name', '') }}"
data-trigger-music="{{ trigger.get('music_file', '') }}"
data-trigger-start="{{ trigger.get('start_seconds', 0) }}"
data-trigger-end="{{ '' if trigger.get('end_seconds') is none else trigger.get('end_seconds') }}"
title="Cliquer pour charger ce trigger dans le formulaire"
>
< td > {{ trigger_id }}< / td >
< td > {{ trigger.get('name', '') }}< / td >
< td > {{ trigger.get('type', '') }}< / td >
< td > {{ trigger.get('music_file', '') }}< / td >
< td > {{ trigger.get('start_seconds', 0) }}< / td >
< td > {{ trigger.get('end_seconds', '') }}< / td >
< td >
< form method = "post" action = "{{ url_for('ui.play_trigger') }}" class = "inline-form" >
< input type = "hidden" name = "trigger_id" value = "{{ trigger_id }}" / >
< button type = "submit" class = "small" > Lancer< / button >
< / form >
< form method = "post" action = "{{ url_for('ui.delete_trigger') }}" class = "inline-form" onsubmit = "return confirm('Supprimer le trigger {{ trigger_id }} ?')" >
< input type = "hidden" name = "trigger_id" value = "{{ trigger_id }}" / >
< button type = "submit" class = "small danger" > Supprimer< / button >
< / form >
< / td >
< / tr >
{% endfor %}
< / tbody >
< / table >
< / div >
{% else %}
< p class = "muted" > Aucun trigger configure.< / p >
{% endif %}
{% endfor %}
< / tbody >
< / table >
< / div >
< / section >
< section class = "panel reveal delay-3" >
@ -160,6 +169,17 @@
@@ -160,6 +169,17 @@
< / div >
< / div >
< div class = "modal-backdrop" id = "delete-trigger-modal" aria-hidden = "true" >
< div class = "modal-card" role = "dialog" aria-modal = "true" aria-labelledby = "delete-trigger-title" >
< h3 id = "delete-trigger-title" > Confirmer la suppression< / h3 >
< p id = "delete-trigger-message" > Voulez-vous vraiment supprimer ce trigger ?< / p >
< div class = "modal-actions" >
< button type = "button" class = "ghost-link" id = "delete-trigger-cancel" > Annuler< / button >
< button type = "button" class = "danger" id = "delete-trigger-confirm" > Supprimer< / button >
< / div >
< / div >
< / div >
< div class = "modal-backdrop" id = "duplicate-name-modal" aria-hidden = "true" >
< div class = "modal-card" role = "dialog" aria-modal = "true" aria-labelledby = "duplicate-name-title" >
< h3 id = "duplicate-name-title" > Nom deja utilise< / h3 >
@ -171,6 +191,180 @@
@@ -171,6 +191,180 @@
< / div >
< script >
function showFlash(message, category) {
let stack = document.querySelector(".flash-stack");
if (!stack) {
stack = document.createElement("div");
stack.className = "flash-stack";
document.body.prepend(stack);
}
const flash = document.createElement("div");
flash.className = "flash flash-" + category;
flash.textContent = message;
stack.appendChild(flash);
window.setTimeout(() => {
flash.classList.add("is-hiding");
window.setTimeout(() => {
flash.remove();
if (!stack.querySelector(".flash")) stack.remove();
}, 360);
}, 3600);
}
function upsertTriggerRow(data) {
const tbody = document.getElementById("triggers-tbody");
const tableWrap = document.getElementById("triggers-table-wrap");
const noMsg = document.getElementById("no-triggers-msg");
const table = document.getElementById("triggers-table");
if (!tbody || !table) return;
const triggerId = data.trigger_id;
const originalId = data.original_id || triggerId;
const t = data.trigger || {};
const playUrl = table.dataset.playUrl || "";
const deleteUrl = table.dataset.deleteUrl || "";
// Supprimer l'ancienne ligne si renommage
if (originalId & & originalId !== triggerId) {
const oldRow = tbody.querySelector(`tr[data-trigger-id="${CSS.escape(originalId)}"]`);
if (oldRow) oldRow.remove();
}
const endVal = (t.end_seconds != null & & t.end_seconds !== "") ? t.end_seconds : "";
let row = tbody.querySelector(`tr[data-trigger-id="${CSS.escape(triggerId)}"]`);
if (!row) {
row = document.createElement("tr");
row.className = "trigger-row";
row.title = "Cliquer pour charger ce trigger dans le formulaire";
tbody.appendChild(row);
// Attacher le listener de clic (meme logique que pour les lignes existantes)
row.addEventListener("click", (event) => {
if (event.target.closest("form, button, a, input")) return;
const f = document.getElementById("trigger-form");
if (!f) return;
const set = (id, val) => { const el = document.getElementById(id); if (el) el.value = val; };
set("original_id", row.dataset.triggerId || "");
set("trigger_type", row.dataset.triggerType || "");
set("trigger_name", row.dataset.triggerName || "");
set("music_file", row.dataset.triggerMusic || "");
set("start_seconds", row.dataset.triggerStart || "0");
set("end_seconds", row.dataset.triggerEnd || "");
set("trigger_volume", row.dataset.triggerVolume || "80");
document.getElementById("trigger_type")?.focus();
f.scrollIntoView({ behavior: "smooth", block: "start" });
});
}
row.dataset.triggerId = triggerId;
row.dataset.triggerType = t.type || "";
row.dataset.triggerName = t.name || "";
row.dataset.triggerMusic = t.music_file || "";
row.dataset.triggerStart = t.start_seconds != null ? t.start_seconds : "0";
row.dataset.triggerEnd = endVal;
row.dataset.triggerVolume = t.volume != null ? t.volume : 80;
row.innerHTML = `
< td > ${triggerId}< / td >
< td > ${t.name || ""}< / td >
< td > ${t.type || ""}< / td >
< td > ${t.music_file || ""}< / td >
< td > ${t.start_seconds != null ? t.start_seconds : 0}< / td >
< td > ${endVal}< / td >
< td > ${t.volume != null ? t.volume : 80}< / td >
< td >
< button type = "button" class = "small js-play-trigger"
data-trigger-id="${triggerId}"
data-play-url="${playUrl}">Lancer< / button >
< button type = "button" class = "small danger js-delete-trigger-btn"
data-trigger-id="${triggerId}">Supprimer< / button >
< / td > `;
// Rebrancher le listener du bouton Lancer
const playBtn = row.querySelector(".js-play-trigger");
if (playBtn) {
playBtn.addEventListener("click", () => {
const tid = playBtn.dataset.triggerId;
const url = playBtn.dataset.playUrl;
if (!tid || !url) return;
playBtn.disabled = true;
const body = new URLSearchParams({ trigger_id: tid });
fetch(url, {
method: "POST",
headers: { "X-Requested-With": "fetch", "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
})
.then((res) => res.json())
.then((d) => { showFlash(d.ok ? (d.message || "Trigger demarre.") : (d.error || "Echec."), d.ok ? "success" : "error"); })
.catch(() => { showFlash("Erreur reseau lors du lancement.", "error"); })
.finally(() => { playBtn.disabled = false; });
});
}
// Afficher la table si elle etait cachee
if (tableWrap) tableWrap.hidden = false;
if (noMsg) noMsg.hidden = true;
}
(function () {
const btn = document.getElementById("btn-stop-audio");
if (!btn) return;
btn.addEventListener("click", () => {
btn.disabled = true;
fetch("{{ url_for('ui.stop_audio') }}", {
method: "POST",
headers: { "X-Requested-With": "fetch" },
})
.then((res) => res.json())
.then((data) => {
if (data.ok) {
showFlash(data.message || "Audio arrete.", "info");
} else {
showFlash(data.error || "Echec de l'arret audio.", "error");
}
})
.catch(() => {
showFlash("Erreur reseau lors de l'arret audio.", "error");
})
.finally(() => {
btn.disabled = false;
});
});
})();
(function () {
document.querySelectorAll(".js-play-trigger").forEach((btn) => {
btn.addEventListener("click", () => {
const triggerId = btn.dataset.triggerId;
const url = btn.dataset.playUrl;
if (!triggerId || !url) return;
btn.disabled = true;
const body = new URLSearchParams({ trigger_id: triggerId });
fetch(url, {
method: "POST",
headers: { "X-Requested-With": "fetch", "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
})
.then((res) => res.json())
.then((data) => {
if (data.ok) {
showFlash(data.message || "Trigger demarre.", "success");
} else {
showFlash(data.error || "Echec du lancement.", "error");
}
})
.catch(() => {
showFlash("Erreur reseau lors du lancement.", "error");
})
.finally(() => {
btn.disabled = false;
});
});
});
})();
(function () {
const form = document.getElementById("trigger-form");
if (!form) return;
@ -181,6 +375,39 @@
@@ -181,6 +375,39 @@
const musicInput = document.getElementById("music_file");
const startInput = document.getElementById("start_seconds");
const endInput = document.getElementById("end_seconds");
const volumeInput = document.getElementById("trigger_volume");
const volumeDisplay = document.getElementById("trigger_volume_display");
if (volumeInput & & volumeDisplay) {
volumeInput.addEventListener("input", () => { volumeDisplay.textContent = volumeInput.value; });
}
form.addEventListener("submit", (event) => {
event.preventDefault();
const submitBtn = form.querySelector("button[type=submit]");
if (submitBtn) submitBtn.disabled = true;
const body = new URLSearchParams(new FormData(form));
fetch(form.action, {
method: "POST",
headers: { "X-Requested-With": "fetch", "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
})
.then((res) => res.json())
.then((data) => {
if (data.ok) {
showFlash(data.message || "Trigger enregistre.", "success");
upsertTriggerRow(data);
} else {
showFlash(data.error || "Echec de l'enregistrement.", "error");
}
})
.catch(() => {
showFlash("Erreur reseau lors de l'enregistrement.", "error");
})
.finally(() => {
if (submitBtn) submitBtn.disabled = false;
});
});
const rows = document.querySelectorAll("tr.trigger-row");
rows.forEach((row) => {
@ -195,6 +422,10 @@
@@ -195,6 +422,10 @@
if (musicInput) musicInput.value = row.dataset.triggerMusic || "";
if (startInput) startInput.value = row.dataset.triggerStart || "0";
if (endInput) endInput.value = row.dataset.triggerEnd || "";
if (volumeInput) {
volumeInput.value = row.dataset.triggerVolume || "80";
if (volumeDisplay) volumeDisplay.textContent = volumeInput.value;
}
typeInput?.focus();
form.scrollIntoView({ behavior: "smooth", block: "start" });
@ -227,8 +458,7 @@
@@ -227,8 +458,7 @@
player.addEventListener("ended", updateTimeLabel);
const playButtons = document.querySelectorAll(".js-play-browser");
playButtons.forEach((button) => {
button.addEventListener("click", () => {
playButtons.forEach((button) => { button.addEventListener("click", () => {
const src = button.dataset.audioUrl;
const name = button.dataset.audioName || "Fichier audio";
if (!src) return;
@ -343,5 +573,80 @@
@@ -343,5 +573,80 @@
}
});
})();
(function () {
const modal = document.getElementById("delete-trigger-modal");
const message = document.getElementById("delete-trigger-message");
const cancelBtn = document.getElementById("delete-trigger-cancel");
const confirmBtn = document.getElementById("delete-trigger-confirm");
const tbody = document.getElementById("triggers-tbody");
const tableWrap = document.getElementById("triggers-table-wrap");
const noMsg = document.getElementById("no-triggers-msg");
const deleteUrl = document.getElementById("triggers-table")?.dataset.deleteUrl || "";
if (!modal || !message || !cancelBtn || !confirmBtn || !tbody) return;
let pendingTriggerId = null;
const openModal = (triggerId) => {
pendingTriggerId = triggerId;
message.textContent = `Voulez-vous vraiment supprimer le trigger "${triggerId}" ?`;
modal.classList.add("is-open");
modal.setAttribute("aria-hidden", "false");
confirmBtn.focus();
};
const closeModal = () => {
modal.classList.remove("is-open");
modal.setAttribute("aria-hidden", "true");
pendingTriggerId = null;
};
tbody.addEventListener("click", (event) => {
const btn = event.target.closest(".js-delete-trigger-btn");
if (!btn) return;
openModal(btn.dataset.triggerId);
});
cancelBtn.addEventListener("click", closeModal);
modal.addEventListener("click", (event) => {
if (event.target === modal) closeModal();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" & & modal.classList.contains("is-open")) closeModal();
});
confirmBtn.addEventListener("click", () => {
if (!pendingTriggerId) return;
const triggerId = pendingTriggerId;
closeModal();
confirmBtn.disabled = true;
const body = new URLSearchParams({ trigger_id: triggerId });
fetch(deleteUrl, {
method: "POST",
headers: { "X-Requested-With": "fetch", "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
})
.then((res) => res.json())
.then((data) => {
if (data.ok) {
const row = tbody.querySelector(`tr[data-trigger-id="${CSS.escape(triggerId)}"]`);
if (row) row.remove();
if (!tbody.querySelector("tr")) {
if (tableWrap) tableWrap.hidden = true;
if (noMsg) noMsg.hidden = false;
}
showFlash(data.message || "Trigger supprime.", "success");
} else {
showFlash(data.error || "Echec de la suppression.", "error");
}
})
.catch(() => {
showFlash("Erreur reseau lors de la suppression.", "error");
})
.finally(() => {
confirmBtn.disabled = false;
});
});
})();
< / script >
{% endblock %}