@ -66,6 +66,10 @@
@@ -66,6 +66,10 @@
< input type = "number" step = "0.1" min = "0" name = "fade_out_seconds" id = "fade_out_seconds" value = "0" / >
< / label >
< / div >
< label >
Nombre de répétitions
< input type = "number" step = "1" min = "0" name = "repeat_count" id = "repeat_count" value = "0" / >
< / label >
< label class = "normalize-row" >
< input type = "checkbox" name = "normalize_audio" id = "normalize_audio" / >
Normaliser le niveau sonore
@ -93,6 +97,7 @@
@@ -93,6 +97,7 @@
< th > Volume< / th >
< th > Fade in< / th >
< th > Fade out< / th >
< th > Répétitions< / th >
< th > Normalisation< / th >
< th > Actions< / th >
< / tr >
@ -110,6 +115,7 @@
@@ -110,6 +115,7 @@
data-trigger-volume="{{ trigger.get('volume', 80) }}"
data-trigger-fade-in="{{ trigger.get('fade_in_seconds', 0.0) }}"
data-trigger-fade-out="{{ trigger.get('fade_out_seconds', 0.0) }}"
data-trigger-repeat-count="{{ trigger.get('repeat_count', 0) }}"
data-trigger-normalize="{{ 1 if trigger.get('normalize_audio', false) else 0 }}"
title="Cliquer pour charger ce trigger dans le formulaire"
>
@ -121,12 +127,14 @@
@@ -121,12 +127,14 @@
< td > {{ trigger.get('volume', 80) }}< / td >
< td > {{ trigger.get('fade_in_seconds', 0.0) }}< / td >
< td > {{ trigger.get('fade_out_seconds', 0.0) }}< / td >
< td > {{ trigger.get('repeat_count', 0) }}< / td >
< td > {{ 'Oui' if trigger.get('normalize_audio', false) else 'Non' }}< / td >
< td >
< button
type="button"
class="small js-play-trigger"
data-trigger-id="{{ trigger_id }}"
data-repeat-count="{{ trigger.get('repeat_count', 0) }}"
data-play-url="{{ url_for('ui.play_trigger') }}"
>Lancer< / button >
< button
@ -315,6 +323,7 @@
@@ -315,6 +323,7 @@
const endVal = (t.end_seconds != null & & t.end_seconds !== "") ? t.end_seconds : "";
const fadeInVal = t.fade_in_seconds != null ? t.fade_in_seconds : 0;
const fadeOutVal = t.fade_out_seconds != null ? t.fade_out_seconds : 0;
const repeatCountVal = t.repeat_count != null ? t.repeat_count : 0;
const normalizeVal = !!t.normalize_audio;
let row = tbody.querySelector(`tr[data-trigger-id="${CSS.escape(triggerId)}"]`);
@ -338,6 +347,7 @@
@@ -338,6 +347,7 @@
set("trigger_volume", row.dataset.triggerVolume || "80");
set("fade_in_seconds", row.dataset.triggerFadeIn || "0");
set("fade_out_seconds", row.dataset.triggerFadeOut || "0");
set("repeat_count", row.dataset.triggerRepeatCount || "0");
const normalize = document.getElementById("normalize_audio");
if (normalize) normalize.checked = (row.dataset.triggerNormalize === "1");
document.getElementById("trigger_type")?.focus();
@ -354,6 +364,7 @@
@@ -354,6 +364,7 @@
row.dataset.triggerVolume = t.volume != null ? t.volume : 80;
row.dataset.triggerFadeIn = fadeInVal;
row.dataset.triggerFadeOut = fadeOutVal;
row.dataset.triggerRepeatCount = repeatCountVal;
row.dataset.triggerNormalize = normalizeVal ? "1" : "0";
row.innerHTML = `
@ -365,10 +376,12 @@
@@ -365,10 +376,12 @@
< td > ${t.volume != null ? t.volume : 80}< / td >
< td > ${fadeInVal}< / td >
< td > ${fadeOutVal}< / td >
< td > ${repeatCountVal}< / td >
< td > ${normalizeVal ? "Oui" : "Non"}< / td >
< td >
< button type = "button" class = "small js-play-trigger"
data-trigger-id="${triggerId}"
data-repeat-count="${repeatCountVal}"
data-play-url="${playUrl}">Lancer< / button >
< button type = "button" class = "small danger js-delete-trigger-btn"
data-trigger-id="${triggerId}">Supprimer< / button >
@ -387,7 +400,9 @@
@@ -387,7 +400,9 @@
if (!ok) return;
playBtn.disabled = true;
const body = new URLSearchParams({ trigger_id: tid });
const parsedRepeatCount = Number.parseInt(playBtn.dataset.repeatCount || row.dataset.triggerRepeatCount || "0", 10);
const safeRepeatCount = Number.isFinite(parsedRepeatCount) & & parsedRepeatCount >= 0 ? String(parsedRepeatCount) : "0";
const body = new URLSearchParams({ trigger_id: tid, repeat_count: safeRepeatCount });
fetch(url, {
method: "POST",
headers: { "X-Requested-With": "fetch", "Content-Type": "application/x-www-form-urlencoded" },
@ -445,7 +460,9 @@
@@ -445,7 +460,9 @@
if (!ok) return;
btn.disabled = true;
const body = new URLSearchParams({ trigger_id: triggerId });
const parsedRepeatCount = Number.parseInt(btn.dataset.repeatCount || row?.dataset.triggerRepeatCount || "0", 10);
const safeRepeatCount = Number.isFinite(parsedRepeatCount) & & parsedRepeatCount >= 0 ? String(parsedRepeatCount) : "0";
const body = new URLSearchParams({ trigger_id: triggerId, repeat_count: safeRepeatCount });
fetch(url, {
method: "POST",
headers: { "X-Requested-With": "fetch", "Content-Type": "application/x-www-form-urlencoded" },
@ -483,6 +500,7 @@
@@ -483,6 +500,7 @@
const volumeDisplay = document.getElementById("trigger_volume_display");
const fadeInInput = document.getElementById("fade_in_seconds");
const fadeOutInput = document.getElementById("fade_out_seconds");
const repeatCountInput = document.getElementById("repeat_count");
const normalizeInput = document.getElementById("normalize_audio");
if (volumeInput & & volumeDisplay) {
volumeInput.addEventListener("input", () => { volumeDisplay.textContent = volumeInput.value; });
@ -535,6 +553,7 @@
@@ -535,6 +553,7 @@
}
if (fadeInInput) fadeInInput.value = row.dataset.triggerFadeIn || "0";
if (fadeOutInput) fadeOutInput.value = row.dataset.triggerFadeOut || "0";
if (repeatCountInput) repeatCountInput.value = row.dataset.triggerRepeatCount || "0";
if (normalizeInput) normalizeInput.checked = (row.dataset.triggerNormalize === "1");
typeInput?.focus();
@ -551,6 +570,21 @@
@@ -551,6 +570,21 @@
const closeBtn = document.getElementById("browser-audio-close");
if (!player || !timeLabel || !nowPlaying || !modal || !closeBtn) return;
let previewName = "Fichier audio";
let repeatsRemaining = 0;
let totalPlays = 1;
let currentPlay = 1;
const parseRepeatCount = (rawValue) => {
const parsed = Number.parseInt(String(rawValue ?? ""), 10);
if (!Number.isFinite(parsed) || parsed < 0 ) return 0 ;
return parsed;
};
const updateNowPlaying = () => {
nowPlaying.textContent = "Lecture: " + previewName + " (" + currentPlay + "/" + totalPlays + ")";
};
const openModal = () => {
modal.classList.add("is-open");
modal.setAttribute("aria-hidden", "false");
@ -558,6 +592,9 @@
@@ -558,6 +592,9 @@
const closeModal = () => {
player.pause();
repeatsRemaining = 0;
totalPlays = 1;
currentPlay = 1;
modal.classList.remove("is-open");
modal.setAttribute("aria-hidden", "true");
};
@ -578,7 +615,18 @@
@@ -578,7 +615,18 @@
player.addEventListener("timeupdate", updateTimeLabel);
player.addEventListener("loadedmetadata", updateTimeLabel);
player.addEventListener("ended", updateTimeLabel);
player.addEventListener("ended", () => {
if (repeatsRemaining < = 0) {
updateTimeLabel();
return;
}
repeatsRemaining -= 1;
currentPlay += 1;
player.currentTime = 0;
player.play().catch(() => {});
updateNowPlaying();
updateTimeLabel();
});
const playButtons = document.querySelectorAll(".js-play-browser");
playButtons.forEach((button) => { button.addEventListener("click", () => {
@ -586,13 +634,25 @@
@@ -586,13 +634,25 @@
const name = button.dataset.audioName || "Fichier audio";
if (!src) return;
const triggerRepeatInput = document.getElementById("repeat_count");
const repeatCount = button.dataset.repeatCount != null
? parseRepeatCount(button.dataset.repeatCount)
: parseRepeatCount(triggerRepeatInput?.value);
previewName = name;
repeatsRemaining = repeatCount;
totalPlays = repeatCount + 1;
currentPlay = 1;
if (player.src !== src) {
player.src = src;
} else {
player.currentTime = 0;
}
player.play().catch(() => {
// Browsers may block autoplay in some contexts; user can press play manually.
});
nowPlaying.textContent = "Lecture: " + name;
updateNowPlaying() ;
updateTimeLabel();
openModal();
});
@ -795,8 +855,10 @@
@@ -795,8 +855,10 @@
let previewContext = null;
let previewSource = null;
let previewSessionId = 0;
const stopPreview = () => {
previewSessionId += 1;
if (previewSource) {
try {
previewSource.stop();
@ -842,6 +904,7 @@
@@ -842,6 +904,7 @@
const volumeInput = document.getElementById("trigger_volume");
const fadeInInput = document.getElementById("fade_in_seconds");
const fadeOutInput = document.getElementById("fade_out_seconds");
const repeatCountInput = document.getElementById("repeat_count");
const normalizeInput = document.getElementById("normalize_audio");
const startSeconds = asNonNegative(startInput, 0);
@ -850,6 +913,7 @@
@@ -850,6 +913,7 @@
const volume = asNonNegative(volumeInput, 80);
const fadeInSeconds = asNonNegative(fadeInInput, 0);
const fadeOutSeconds = asNonNegative(fadeOutInput, 0);
const repeatCount = Math.max(0, Math.floor(asNonNegative(repeatCountInput, 0)));
const normalizeAudio = !!normalizeInput?.checked;
btn.disabled = true;
@ -894,21 +958,25 @@
@@ -894,21 +958,25 @@
}
const playDuration = clampedEnd - startSeconds;
const normalizeGain = normalizeAudio ? computeNormalizeGain(audioBuffer, startSeconds, playDuration) : 1;
const baseGain = Math.max(0, Math.min(1, volume / 100)) * normalizeGain;
const fadeIn = Math.min(Math.max(0, fadeInSeconds), playDuration);
const fadeOut = Math.min(Math.max(0, fadeOutSeconds), playDuration);
const currentSession = previewSessionId;
const totalPlays = repeatCount + 1;
const startPreviewPlay = (playIndex) => {
if (currentSession !== previewSessionId) return;
const source = previewContext.createBufferSource();
source.buffer = audioBuffer;
const gainNode = previewContext.createGain();
source.connect(gainNode);
gainNode.connect(previewContext.destination);
const normalizeGain = normalizeAudio ? computeNormalizeGain(audioBuffer, startSeconds, playDuration) : 1;
const baseGain = Math.max(0, Math.min(1, volume / 100)) * normalizeGain;
const now = previewContext.currentTime;
const startAt = now + 0.03;
const endAt = startAt + playDuration;
const fadeIn = Math.min(Math.max(0, fadeInSeconds), playDuration);
const fadeOut = Math.min(Math.max(0, fadeOutSeconds), playDuration);
const fadeOutStart = Math.max(startAt + fadeIn, endAt - fadeOut);
gainNode.gain.cancelScheduledValues(now);
@ -929,6 +997,10 @@
@@ -929,6 +997,10 @@
if (previewSource === source) {
previewSource = null;
}
if (currentSession !== previewSessionId) return;
if (playIndex < totalPlays ) {
startPreviewPlay(playIndex + 1);
}
};
previewSource = source;
@ -936,8 +1008,12 @@
@@ -936,8 +1008,12 @@
if (nowPlaying) {
nowPlaying.textContent = "Prévisualisation: " + filename +
" (" + startSeconds.toFixed(1) + "s -> " + clampedEnd.toFixed(1) + "s)";
" (" + startSeconds.toFixed(1) + "s -> " + clampedEnd.toFixed(1) + "s)" +
" [" + playIndex + "/" + totalPlays + "]";
}
};
startPreviewPlay(1);
player?.closest("section")?.scrollIntoView({ behavior: "smooth", block: "nearest" });
})
.catch((err) => {