You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

347 lines
13 KiB

{% extends "base.html" %}
{% block content %}
<header class="topbar reveal">
<div>
<h1>Tableau de bord</h1>
<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>
<a href="{{ url_for('ui.logout') }}" class="ghost-link">Deconnexion</a>
</div>
</header>
<main class="dashboard-grid">
<section class="panel reveal delay-1">
<h2>Ajouter ou modifier un trigger</h2>
<form method="post" action="{{ url_for('ui.save_trigger') }}" class="form-grid" id="trigger-form">
<label>
Identifiant du trigger existant (optionnel)
<input type="text" name="original_id" id="original_id" placeholder="GPIO3" />
</label>
<label>
Type du trigger (obligatoire)
<input type="text" name="type" id="trigger_type" placeholder="GPIO3" required />
</label>
<label>
Nom
<input type="text" name="name" id="trigger_name" placeholder="Bouton entree" required />
</label>
<label>
Fichier audio
<input list="audio-files" name="music_file" id="music_file" placeholder="bell.mp3" required />
<datalist id="audio-files">
{% for f in audio_files %}
<option value="{{ f }}"></option>
{% endfor %}
</datalist>
</label>
<div class="time-row">
<label>
Debut (s)
<input type="number" step="0.1" min="0" name="start_seconds" id="start_seconds" value="0" />
</label>
<label>
Fin (s, optionnel)
<input type="number" step="0.1" min="0" name="end_seconds" id="end_seconds" />
</label>
</div>
<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>
</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 %}
</section>
<section class="panel reveal delay-3">
<h2>Stockage audio</h2>
<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>
<form method="post" action="{{ url_for('ui.upload_audio') }}" enctype="multipart/form-data" class="upload-row" id="upload-audio-form">
<input type="file" name="audio_file" id="audio_file_input" accept=".mp3,.wav,.ogg,.flac,.aac,.m4a" required />
<button type="submit">Televerser</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">Telecharger</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 }}" />
<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="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="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>
<p id="duplicate-name-message">Ce nom existe deja dans les fichiers enregistres.</p>
<div class="modal-actions">
<button type="button" class="ghost-link" id="duplicate-name-close">Fermer</button>
</div>
</div>
</div>
<script>
(function () {
const form = document.getElementById("trigger-form");
if (!form) return;
const originalIdInput = document.getElementById("original_id");
const typeInput = document.getElementById("trigger_type");
const nameInput = document.getElementById("trigger_name");
const musicInput = document.getElementById("music_file");
const startInput = document.getElementById("start_seconds");
const endInput = document.getElementById("end_seconds");
const rows = document.querySelectorAll("tr.trigger-row");
rows.forEach((row) => {
row.addEventListener("click", (event) => {
if (event.target.closest("form, button, a, input")) {
return;
}
if (originalIdInput) originalIdInput.value = row.dataset.triggerId || "";
if (typeInput) typeInput.value = row.dataset.triggerType || "";
if (nameInput) nameInput.value = row.dataset.triggerName || "";
if (musicInput) musicInput.value = row.dataset.triggerMusic || "";
if (startInput) startInput.value = row.dataset.triggerStart || "0";
if (endInput) endInput.value = row.dataset.triggerEnd || "";
typeInput?.focus();
form.scrollIntoView({ behavior: "smooth", block: "start" });
});
});
})();
(function () {
const player = document.getElementById("browser-audio-player");
const timeLabel = document.getElementById("audio-time");
const nowPlaying = document.getElementById("audio-now-playing");
if (!player || !timeLabel || !nowPlaying) return;
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(() => {
// Browsers may block autoplay in some contexts; user can press play manually.
});
nowPlaying.textContent = "Lecture: " + name;
updateTimeLabel();
});
});
})();
(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 %}