Browse Source
- Updated TriggerConfig and TriggerPatch models to include fade_in_seconds, fade_out_seconds, and normalize_audio fields. - Enhanced the save_trigger route to handle new fields and validate input. - Added a new audio storage page with upload, delete, and playback functionalities. - Implemented UI changes in dashboard and audio storage templates to support new features. - Introduced modals for audio playback and deletion confirmation.master
12 changed files with 868 additions and 102 deletions
Binary file not shown.
@ -0,0 +1,237 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<header class="topbar reveal"> |
||||||
|
<div> |
||||||
|
<h1>Stockage audio</h1> |
||||||
|
</div> |
||||||
|
<div class="topbar-actions"> |
||||||
|
<a href="{{ url_for('ui.dashboard') }}" class="ghost-link">Retour au tableau de bord</a> |
||||||
|
<a href="{{ url_for('ui.logout') }}" class="ghost-link">Déconnexion</a> |
||||||
|
</div> |
||||||
|
</header> |
||||||
|
|
||||||
|
<main class="dashboard-grid"> |
||||||
|
<section class="panel reveal delay-1" style="grid-column: span 12;"> |
||||||
|
<form method="post" action="{{ url_for('ui.upload_audio') }}" enctype="multipart/form-data" class="upload-row" id="upload-audio-form"> |
||||||
|
<input type="hidden" name="return_to" value="audio_storage" /> |
||||||
|
<input type="file" name="audio_file" id="audio_file_input" accept=".mp3,.wav,.ogg,.flac,.aac,.m4a" required /> |
||||||
|
<button type="submit">Téléverser</button> |
||||||
|
</form> |
||||||
|
|
||||||
|
{% if audio_files %} |
||||||
|
<ul class="audio-list"> |
||||||
|
{% for filename in audio_files %} |
||||||
|
<li> |
||||||
|
<span>{{ filename }}</span> |
||||||
|
<div class="audio-actions"> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
class="small-button js-play-browser" |
||||||
|
data-audio-url="{{ url_for('ui.stream_audio', filename=filename) }}" |
||||||
|
data-audio-name="{{ filename }}" |
||||||
|
> |
||||||
|
Lire |
||||||
|
</button> |
||||||
|
<a href="{{ url_for('ui.download_audio', filename=filename) }}" class="small-button">Télécharger</a> |
||||||
|
<form method="post" action="{{ url_for('ui.delete_audio') }}" class="inline-form js-delete-audio-form" data-filename="{{ filename }}"> |
||||||
|
<input type="hidden" name="filename" value="{{ filename }}" /> |
||||||
|
<input type="hidden" name="return_to" value="audio_storage" /> |
||||||
|
<button type="submit" class="small danger">Supprimer</button> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
{% endfor %} |
||||||
|
</ul> |
||||||
|
{% else %} |
||||||
|
<p class="muted">Aucun fichier audio dans le stockage.</p> |
||||||
|
{% endif %} |
||||||
|
</section> |
||||||
|
</main> |
||||||
|
|
||||||
|
<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 déjà utilisé</h3> |
||||||
|
<p id="duplicate-name-message">Ce nom existe déjà dans les fichiers enregistrés.</p> |
||||||
|
<div class="modal-actions"> |
||||||
|
<button type="button" class="ghost-link" id="duplicate-name-close">Fermer</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="modal-backdrop" id="delete-audio-modal" aria-hidden="true"> |
||||||
|
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="delete-audio-title"> |
||||||
|
<h3 id="delete-audio-title">Confirmer la suppression</h3> |
||||||
|
<p id="delete-audio-message">Voulez-vous vraiment supprimer ce fichier ?</p> |
||||||
|
<div class="modal-actions"> |
||||||
|
<button type="button" class="ghost-link" id="delete-audio-cancel">Annuler</button> |
||||||
|
<button type="button" class="danger" id="delete-audio-confirm">Supprimer</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="modal-backdrop" id="browser-audio-modal" aria-hidden="true"> |
||||||
|
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="browser-audio-title"> |
||||||
|
<h3 id="browser-audio-title">Lecture audio</h3> |
||||||
|
<div class="audio-player-card"> |
||||||
|
<p id="audio-now-playing" class="muted">Aucune lecture en cours.</p> |
||||||
|
<audio id="browser-audio-player" controls preload="metadata"></audio> |
||||||
|
<p class="audio-time" id="audio-time">Temps de lecture: 00:00 / 00:00</p> |
||||||
|
</div> |
||||||
|
<div class="modal-actions"> |
||||||
|
<button type="button" class="ghost-link" id="browser-audio-close">Fermer</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<script> |
||||||
|
(function () { |
||||||
|
const player = document.getElementById("browser-audio-player"); |
||||||
|
const timeLabel = document.getElementById("audio-time"); |
||||||
|
const nowPlaying = document.getElementById("audio-now-playing"); |
||||||
|
const modal = document.getElementById("browser-audio-modal"); |
||||||
|
const closeBtn = document.getElementById("browser-audio-close"); |
||||||
|
if (!player || !timeLabel || !nowPlaying || !modal || !closeBtn) return; |
||||||
|
|
||||||
|
const openModal = () => { |
||||||
|
modal.classList.add("is-open"); |
||||||
|
modal.setAttribute("aria-hidden", "false"); |
||||||
|
}; |
||||||
|
|
||||||
|
const closeModal = () => { |
||||||
|
player.pause(); |
||||||
|
modal.classList.remove("is-open"); |
||||||
|
modal.setAttribute("aria-hidden", "true"); |
||||||
|
}; |
||||||
|
|
||||||
|
const formatTime = (seconds) => { |
||||||
|
if (!Number.isFinite(seconds) || seconds < 0) return "00:00"; |
||||||
|
const total = Math.floor(seconds); |
||||||
|
const minutes = Math.floor(total / 60); |
||||||
|
const remain = total % 60; |
||||||
|
return String(minutes).padStart(2, "0") + ":" + String(remain).padStart(2, "0"); |
||||||
|
}; |
||||||
|
|
||||||
|
const updateTimeLabel = () => { |
||||||
|
const current = formatTime(player.currentTime || 0); |
||||||
|
const total = formatTime(player.duration || 0); |
||||||
|
timeLabel.textContent = "Temps de lecture: " + current + " / " + total; |
||||||
|
}; |
||||||
|
|
||||||
|
player.addEventListener("timeupdate", updateTimeLabel); |
||||||
|
player.addEventListener("loadedmetadata", updateTimeLabel); |
||||||
|
player.addEventListener("ended", updateTimeLabel); |
||||||
|
|
||||||
|
const playButtons = document.querySelectorAll(".js-play-browser"); |
||||||
|
playButtons.forEach((button) => { |
||||||
|
button.addEventListener("click", () => { |
||||||
|
const src = button.dataset.audioUrl; |
||||||
|
const name = button.dataset.audioName || "Fichier audio"; |
||||||
|
if (!src) return; |
||||||
|
|
||||||
|
if (player.src !== src) { |
||||||
|
player.src = src; |
||||||
|
} |
||||||
|
player.play().catch(() => {}); |
||||||
|
nowPlaying.textContent = "Lecture: " + name; |
||||||
|
updateTimeLabel(); |
||||||
|
openModal(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
closeBtn.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(); |
||||||
|
}); |
||||||
|
})(); |
||||||
|
|
||||||
|
(function () { |
||||||
|
const uploadForm = document.getElementById("upload-audio-form"); |
||||||
|
const fileInput = document.getElementById("audio_file_input"); |
||||||
|
const duplicateModal = document.getElementById("duplicate-name-modal"); |
||||||
|
const duplicateClose = document.getElementById("duplicate-name-close"); |
||||||
|
if (!uploadForm || !fileInput || !duplicateModal || !duplicateClose) return; |
||||||
|
|
||||||
|
const existing = new Set([ |
||||||
|
{% for filename in audio_files %} |
||||||
|
"{{ filename|lower|replace('\\', '\\\\')|replace('"', '\\"') }}", |
||||||
|
{% endfor %} |
||||||
|
]); |
||||||
|
|
||||||
|
const openDuplicateModal = () => { |
||||||
|
duplicateModal.classList.add("is-open"); |
||||||
|
duplicateModal.setAttribute("aria-hidden", "false"); |
||||||
|
duplicateClose.focus(); |
||||||
|
}; |
||||||
|
|
||||||
|
const closeDuplicateModal = () => { |
||||||
|
duplicateModal.classList.remove("is-open"); |
||||||
|
duplicateModal.setAttribute("aria-hidden", "true"); |
||||||
|
}; |
||||||
|
|
||||||
|
uploadForm.addEventListener("submit", (event) => { |
||||||
|
const file = fileInput.files && fileInput.files[0]; |
||||||
|
if (!file) return; |
||||||
|
|
||||||
|
if (existing.has(file.name.toLowerCase())) { |
||||||
|
event.preventDefault(); |
||||||
|
openDuplicateModal(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
duplicateClose.addEventListener("click", closeDuplicateModal); |
||||||
|
duplicateModal.addEventListener("click", (event) => { |
||||||
|
if (event.target === duplicateModal) closeDuplicateModal(); |
||||||
|
}); |
||||||
|
document.addEventListener("keydown", (event) => { |
||||||
|
if (event.key === "Escape" && duplicateModal.classList.contains("is-open")) closeDuplicateModal(); |
||||||
|
}); |
||||||
|
})(); |
||||||
|
|
||||||
|
(function () { |
||||||
|
const modal = document.getElementById("delete-audio-modal"); |
||||||
|
const message = document.getElementById("delete-audio-message"); |
||||||
|
const cancelBtn = document.getElementById("delete-audio-cancel"); |
||||||
|
const confirmBtn = document.getElementById("delete-audio-confirm"); |
||||||
|
if (!modal || !message || !cancelBtn || !confirmBtn) return; |
||||||
|
|
||||||
|
let pendingForm = null; |
||||||
|
|
||||||
|
const openModal = (form, filename) => { |
||||||
|
pendingForm = form; |
||||||
|
message.textContent = "Voulez-vous vraiment supprimer le fichier \"" + filename + "\" ?"; |
||||||
|
modal.classList.add("is-open"); |
||||||
|
modal.setAttribute("aria-hidden", "false"); |
||||||
|
confirmBtn.focus(); |
||||||
|
}; |
||||||
|
|
||||||
|
const closeModal = () => { |
||||||
|
modal.classList.remove("is-open"); |
||||||
|
modal.setAttribute("aria-hidden", "true"); |
||||||
|
pendingForm = null; |
||||||
|
}; |
||||||
|
|
||||||
|
document.querySelectorAll(".js-delete-audio-form").forEach((form) => { |
||||||
|
form.addEventListener("submit", (event) => { |
||||||
|
event.preventDefault(); |
||||||
|
const filename = form.dataset.filename || "ce fichier"; |
||||||
|
openModal(form, filename); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
cancelBtn.addEventListener("click", closeModal); |
||||||
|
modal.addEventListener("click", (event) => { |
||||||
|
if (event.target === modal) closeModal(); |
||||||
|
}); |
||||||
|
confirmBtn.addEventListener("click", () => { |
||||||
|
if (pendingForm) pendingForm.submit(); |
||||||
|
}); |
||||||
|
document.addEventListener("keydown", (event) => { |
||||||
|
if (event.key === "Escape" && modal.classList.contains("is-open")) closeModal(); |
||||||
|
}); |
||||||
|
})(); |
||||||
|
</script> |
||||||
|
{% endblock %} |
||||||
Loading…
Reference in new issue