642 lines
25 KiB
HTML
642 lines
25 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Actions — JH Photomaton{% endblock %}
|
|
|
|
{% block head %}
|
|
<style>
|
|
/* ── Cards actions ──────────────────────────────────────────────────── */
|
|
.action-card {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
margin-bottom: .6rem;
|
|
transition: box-shadow .15s, opacity .15s;
|
|
cursor: default;
|
|
}
|
|
.action-card.drag-over {
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 2px var(--primary);
|
|
}
|
|
.action-card.dragging { opacity: .35; }
|
|
|
|
.ac-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .6rem;
|
|
padding: .75rem 1rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
.drag-handle {
|
|
cursor: grab;
|
|
font-size: 1.2rem;
|
|
color: var(--text-muted);
|
|
user-select: none;
|
|
flex-shrink: 0;
|
|
letter-spacing: .05em;
|
|
}
|
|
.drag-handle:active { cursor: grabbing; }
|
|
.ac-idx {
|
|
font-size: .7rem;
|
|
background: var(--bg-muted);
|
|
color: var(--text-muted);
|
|
border-radius: 4px;
|
|
padding: .1rem .4rem;
|
|
flex-shrink: 0;
|
|
}
|
|
.ac-name {
|
|
font-weight: 600;
|
|
min-width: 120px;
|
|
}
|
|
.ac-chips {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: .35rem;
|
|
flex: 1;
|
|
}
|
|
.ac-chip {
|
|
font-size: .72rem;
|
|
background: var(--bg-muted);
|
|
border-radius: 10px;
|
|
padding: .1rem .5rem;
|
|
color: var(--text-muted);
|
|
white-space: nowrap;
|
|
}
|
|
.ac-chip.hl { background: var(--primary-muted, rgba(99,102,241,.15)); color: var(--primary); }
|
|
.ac-btns { display: flex; gap: .25rem; flex-shrink: 0; }
|
|
|
|
/* ── Edit form ──────────────────────────────────────────────────────── */
|
|
.ac-edit {
|
|
border-top: 1px solid var(--border);
|
|
padding: 1rem;
|
|
}
|
|
.edit-section { margin-bottom: 1rem; }
|
|
.edit-section-title {
|
|
font-size: .75rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: .07em;
|
|
color: var(--text-muted);
|
|
margin-bottom: .5rem;
|
|
padding-bottom: .25rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.grid-3 {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
gap: .75rem;
|
|
}
|
|
|
|
/* ── Presets ────────────────────────────────────────────────────────── */
|
|
.preset-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .75rem;
|
|
padding: .6rem .9rem;
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
margin-bottom: .4rem;
|
|
}
|
|
.preset-info { flex: 1; }
|
|
.preset-name { font-weight: 600; }
|
|
.preset-meta { font-size: .75rem; color: var(--text-muted); }
|
|
|
|
/* ── Reorder bar ────────────────────────────────────────────────────── */
|
|
#reorder-bar {
|
|
display: none;
|
|
background: rgba(245,158,11,.1);
|
|
border: 1px solid rgba(245,158,11,.4);
|
|
border-radius: var(--radius);
|
|
padding: .5rem .75rem;
|
|
font-size: .85rem;
|
|
margin-bottom: .75rem;
|
|
align-items: center;
|
|
gap: .75rem;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container">
|
|
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h1 class="page-title" style="margin:0">Gestion des actions</h1>
|
|
<div class="flex gap-1">
|
|
<button class="btn btn-ghost btn-sm" id="restart-btn" onclick="restartPhotobooth()" style="display:none">
|
|
🔄 Redémarrer photobooth-app
|
|
</button>
|
|
<button class="btn btn-primary" onclick="newAction()">+ Nouvelle action</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Actions ──────────────────────────────────────────────────── -->
|
|
<div class="section">
|
|
<div class="card-title">Actions image photobooth-app <span class="text-muted text-sm" id="action-count"></span></div>
|
|
|
|
<!-- Barre "ordre modifié" -->
|
|
<div id="reorder-bar">
|
|
<span>↕ Ordre modifié</span>
|
|
<button class="btn btn-primary btn-sm" onclick="saveOrder()">💾 Sauvegarder l'ordre</button>
|
|
<button class="btn btn-ghost btn-sm" onclick="loadAll()">✕ Annuler</button>
|
|
</div>
|
|
|
|
<div id="actions-list">
|
|
<div class="empty-state"><div class="icon">⏳</div>Chargement…</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Mapping bouton ───────────────────────────────────────────── -->
|
|
<div class="section">
|
|
<div class="card-title">Mapping bouton → actions</div>
|
|
<div class="card">
|
|
<p class="text-sm text-muted mb-2">Associe chaque nombre de clics à une action.</p>
|
|
<div id="mapping-table">
|
|
<div class="empty-state"><div class="icon">⏳</div></div>
|
|
</div>
|
|
<button class="btn btn-primary mt-2" onclick="saveMapping()">💾 Sauvegarder le mapping</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Presets ──────────────────────────────────────────────────── -->
|
|
<div class="section">
|
|
<div class="card-title">Presets d'actions</div>
|
|
<div class="card">
|
|
<p class="text-sm text-muted mb-2">Sauvegarder la configuration actuelle pour la restaurer plus tard (ex : mariage, baptême, anniversaire…).</p>
|
|
<div class="flex gap-1 mb-2" style="flex-wrap:wrap">
|
|
<input type="text" class="form-control" id="preset-name" placeholder="Nom du preset…" style="max-width:220px">
|
|
<input type="text" class="form-control" id="preset-desc" placeholder="Description (optionnel)" style="max-width:280px">
|
|
<button class="btn btn-primary" onclick="savePreset()">💾 Sauvegarder le preset</button>
|
|
</div>
|
|
<div id="presets-list">
|
|
<div class="empty-state text-sm">Aucun preset sauvegardé</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
// ── État global ───────────────────────────────────────────────────────────────
|
|
let actions = [];
|
|
let assets = { frames: [], backgrounds: [] };
|
|
let mapping = {};
|
|
let orderChanged = false;
|
|
|
|
const FILTERS = [
|
|
['original', 'Original'],
|
|
['FilterPilgram2.earlybird', 'Earlybird'],
|
|
['FilterPilgram2.reyes', 'Reyes'],
|
|
['FilterPilgram2.moon', 'Moon'],
|
|
['FilterPilgram2.gingham', 'Gingham'],
|
|
['FilterPilgram2.mayfair', 'Mayfair'],
|
|
['FilterPilgram2.nashville', 'Nashville'],
|
|
['FilterPilgram2.inkwell', 'Inkwell (N&B)'],
|
|
];
|
|
|
|
// ── Chargement ────────────────────────────────────────────────────────────────
|
|
async function loadAll() {
|
|
orderChanged = false;
|
|
document.getElementById('reorder-bar').style.display = 'none';
|
|
try {
|
|
const [actData, assetData] = await Promise.all([
|
|
api('GET', '/api/actions/photobooth'),
|
|
api('GET', '/api/actions/assets'),
|
|
]);
|
|
actions = actData.actions || [];
|
|
mapping = actData.button_mapping || {};
|
|
assets = assetData;
|
|
renderActions();
|
|
renderMapping();
|
|
loadPresets();
|
|
} catch(e) {
|
|
document.getElementById('actions-list').innerHTML =
|
|
`<div class="empty-state"><div class="icon">⚠️</div>Erreur chargement: ${e.message}</div>`;
|
|
}
|
|
}
|
|
|
|
// ── Rendu des actions ─────────────────────────────────────────────────────────
|
|
function renderActions() {
|
|
const el = document.getElementById('actions-list');
|
|
document.getElementById('action-count').textContent = `(${actions.length})`;
|
|
if (!actions.length) {
|
|
el.innerHTML = '<div class="empty-state"><div class="icon">⚙️</div>Aucune action</div>';
|
|
return;
|
|
}
|
|
el.innerHTML = actions.map((a, i) => cardHTML(a, i)).join('');
|
|
attachDragListeners();
|
|
}
|
|
|
|
function cardHTML(action, idx) {
|
|
const name = esc(action.name || 'Sans nom');
|
|
const proc = action.processing || {};
|
|
const jc = action.jobcontrol || {};
|
|
const chips = [
|
|
`<span class="ac-chip">⏱ ${jc.countdown_capture ?? '?'}s</span>`,
|
|
];
|
|
if (proc.img_frame_enable && proc.img_frame_file)
|
|
chips.push(`<span class="ac-chip">🖼 ${esc(proc.img_frame_file.split('/').pop())}</span>`);
|
|
if (proc.img_background_enable && proc.img_background_file)
|
|
chips.push(`<span class="ac-chip">🌄 ${esc(proc.img_background_file.split('/').pop())}</span>`);
|
|
if (proc.fill_background_enable)
|
|
chips.push(`<span class="ac-chip" style="background:${esc(proc.fill_background_color||'#eee')};color:#000">⬛ fond couleur</span>`);
|
|
if (proc.remove_background)
|
|
chips.push('<span class="ac-chip hl">Remove BG</span>');
|
|
if (proc.image_filter && proc.image_filter !== 'original')
|
|
chips.push(`<span class="ac-chip hl">🎨 ${esc(proc.image_filter.split('.').pop())}</span>`);
|
|
|
|
return `
|
|
<div class="action-card" draggable="true" data-idx="${idx}" id="ac-${idx}">
|
|
<div class="ac-header">
|
|
<span class="drag-handle" title="Glisser pour réordonner">⠿⠿</span>
|
|
<span class="ac-idx">#${idx}</span>
|
|
<span class="ac-name">${name}</span>
|
|
<div class="ac-chips">${chips.join('')}</div>
|
|
<div class="ac-btns">
|
|
<button class="btn btn-ghost btn-xs" onclick="toggleEdit(${idx})" title="Modifier">✏</button>
|
|
<button class="btn btn-ghost btn-xs" onclick="cloneAction(${idx})" title="Dupliquer">⧉</button>
|
|
<button class="btn btn-ghost btn-xs" onclick="triggerAction(${idx})" title="Déclencher">▶</button>
|
|
<button class="btn btn-danger btn-xs" onclick="deleteAction(${idx})" title="Supprimer">✕</button>
|
|
</div>
|
|
</div>
|
|
<div class="ac-edit" id="edit-${idx}" style="display:none"></div>
|
|
</div>`;
|
|
}
|
|
|
|
// ── Formulaire d'édition (rendu lazy au premier toggle) ───────────────────────
|
|
function editFormHTML(action, idx) {
|
|
const proc = action.processing || {};
|
|
const jc = action.jobcontrol || {};
|
|
|
|
const frameSel = assetSelect(`ef-frame-sel-${idx}`, proc.img_frame_file || '', assets.frames, '— aucun cadre —', `ef-frame-${idx}`);
|
|
const bgSel = assetSelect(`ef-bg-sel-${idx}`, proc.img_background_file || '', assets.backgrounds, '— aucun fond —', `ef-bg-${idx}`);
|
|
const filterOpts = FILTERS.map(([v, l]) =>
|
|
`<option value="${esc(v)}" ${(proc.image_filter||'original')===v?'selected':''}>${esc(l)}</option>`).join('');
|
|
|
|
return `
|
|
<div class="edit-section">
|
|
<div class="edit-section-title">Général</div>
|
|
<div class="grid-3">
|
|
<div class="form-group">
|
|
<label class="form-label">Nom de l'action</label>
|
|
<input type="text" class="form-control" id="ef-name-${idx}" value="${esc(action.name||'')}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Countdown (secondes)</label>
|
|
<input type="number" class="form-control" id="ef-countdown-${idx}"
|
|
value="${jc.countdown_capture??5}" min="1" max="60" step="0.5">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="edit-section">
|
|
<div class="edit-section-title">Cadre</div>
|
|
<div class="grid-3">
|
|
<div class="form-group">
|
|
<label class="form-label flex items-center gap-1">
|
|
<input type="checkbox" id="ef-frame-ena-${idx}" ${proc.img_frame_enable?'checked':''}> Activer le cadre
|
|
</label>
|
|
${frameSel}
|
|
<input type="text" class="form-control mt-1" id="ef-frame-${idx}"
|
|
value="${esc(proc.img_frame_file||'')}" placeholder="userdata/…/cadre.png" style="font-size:.8rem">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="edit-section">
|
|
<div class="edit-section-title">Fond</div>
|
|
<div class="grid-3">
|
|
<div class="form-group">
|
|
<label class="form-label flex items-center gap-1">
|
|
<input type="checkbox" id="ef-bg-ena-${idx}" ${proc.img_background_enable?'checked':''}> Activer le fond image
|
|
</label>
|
|
${bgSel}
|
|
<input type="text" class="form-control mt-1" id="ef-bg-${idx}"
|
|
value="${esc(proc.img_background_file||'')}" placeholder="userdata/…/fond.jpg" style="font-size:.8rem">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label flex items-center gap-1">
|
|
<input type="checkbox" id="ef-fill-ena-${idx}" ${proc.fill_background_enable?'checked':''}> Activer le fond couleur
|
|
</label>
|
|
<input type="color" class="form-control mt-1" id="ef-fill-color-${idx}"
|
|
value="${esc(proc.fill_background_color||'#ededed')}" style="height:36px;max-width:80px">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="edit-section">
|
|
<div class="edit-section-title">Traitement image</div>
|
|
<div class="grid-3">
|
|
<div class="form-group">
|
|
<label class="form-label flex items-center gap-1">
|
|
<input type="checkbox" id="ef-rmbg-${idx}" ${proc.remove_background?'checked':''}>
|
|
Remove Background (MODNet)
|
|
</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Filtre</label>
|
|
<select class="form-control" id="ef-filter-${idx}">${filterOpts}</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-1">
|
|
<button class="btn btn-primary btn-sm" onclick="saveAction(${idx})">💾 Sauvegarder</button>
|
|
<button class="btn btn-ghost btn-sm" onclick="toggleEdit(${idx})">Annuler</button>
|
|
</div>`;
|
|
}
|
|
|
|
function assetSelect(selId, current, options, placeholder, textId) {
|
|
const opts = options.map(f => {
|
|
const label = f.replace(/^userdata\//, '');
|
|
return `<option value="${esc(f)}" ${f===current?'selected':''}>${esc(label)}</option>`;
|
|
}).join('');
|
|
return `<select class="form-control" id="${selId}"
|
|
onchange="document.getElementById('${textId}').value=this.value">
|
|
<option value="">${esc(placeholder)}</option>${opts}
|
|
</select>`;
|
|
}
|
|
|
|
// ── Toggle édition ────────────────────────────────────────────────────────────
|
|
function toggleEdit(idx) {
|
|
const el = document.getElementById(`edit-${idx}`);
|
|
if (el.style.display === 'none') {
|
|
if (!el.dataset.rendered) {
|
|
el.innerHTML = editFormHTML(actions[idx], idx);
|
|
el.dataset.rendered = '1';
|
|
}
|
|
el.style.display = 'block';
|
|
} else {
|
|
el.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// ── Sauvegarde action ─────────────────────────────────────────────────────────
|
|
async function saveAction(idx) {
|
|
const realIdx = actions[idx]._index ?? idx;
|
|
const updates = {
|
|
name: document.getElementById(`ef-name-${idx}`).value,
|
|
countdown_capture: parseFloat(document.getElementById(`ef-countdown-${idx}`).value),
|
|
img_frame_enable: document.getElementById(`ef-frame-ena-${idx}`).checked,
|
|
img_frame_file: document.getElementById(`ef-frame-${idx}`).value || null,
|
|
img_background_enable: document.getElementById(`ef-bg-ena-${idx}`).checked,
|
|
img_background_file: document.getElementById(`ef-bg-${idx}`).value || null,
|
|
fill_background_enable: document.getElementById(`ef-fill-ena-${idx}`).checked,
|
|
fill_background_color: document.getElementById(`ef-fill-color-${idx}`).value,
|
|
remove_background: document.getElementById(`ef-rmbg-${idx}`).checked,
|
|
image_filter: document.getElementById(`ef-filter-${idx}`).value,
|
|
};
|
|
try {
|
|
await api('PUT', `/api/actions/photobooth/${realIdx}`, updates);
|
|
showToast('✅ Action sauvegardée', 'success');
|
|
showRestartBtn();
|
|
await loadAll();
|
|
} catch(e) {
|
|
showToast(`❌ Erreur: ${e.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// ── Clone / Delete / Trigger ──────────────────────────────────────────────────
|
|
async function cloneAction(idx) {
|
|
const realIdx = actions[idx]._index ?? idx;
|
|
try {
|
|
await api('POST', `/api/actions/photobooth/${realIdx}/clone`);
|
|
showToast('⧉ Action dupliquée', 'success');
|
|
showRestartBtn();
|
|
await loadAll();
|
|
} catch(e) { showToast(`❌ ${e.message}`, 'error'); }
|
|
}
|
|
|
|
async function deleteAction(idx) {
|
|
const name = actions[idx].name || `Action ${idx}`;
|
|
if (!confirm(`Supprimer « ${name} » ?\nCette action sera définitivement retirée de photobooth-app.`)) return;
|
|
const realIdx = actions[idx]._index ?? idx;
|
|
try {
|
|
await api('DELETE', `/api/actions/photobooth/${realIdx}`);
|
|
showToast(`🗑 « ${name} » supprimée`, 'info');
|
|
showRestartBtn();
|
|
await loadAll();
|
|
} catch(e) { showToast(`❌ ${e.message}`, 'error'); }
|
|
}
|
|
|
|
async function newAction() {
|
|
const name = prompt('Nom de la nouvelle action :', 'Nouvelle action');
|
|
if (name === null) return;
|
|
try {
|
|
await api('POST', '/api/actions/photobooth', { name: name.trim() || 'Nouvelle action' });
|
|
showToast('✅ Action créée', 'success');
|
|
showRestartBtn();
|
|
await loadAll();
|
|
} catch(e) { showToast(`❌ ${e.message}`, 'error'); }
|
|
}
|
|
|
|
async function triggerAction(idx) {
|
|
const realIdx = actions[idx]._index ?? idx;
|
|
const name = actions[idx].name || `Action ${idx}`;
|
|
if (!confirm(`Déclencher « ${name} » (prise de photo) ?`)) return;
|
|
try {
|
|
await api('POST', `/api/actions/trigger/${realIdx}`);
|
|
showToast(`▶ « ${name} » déclenchée`, 'info');
|
|
} catch(e) { showToast(`❌ ${e.message}`, 'error'); }
|
|
}
|
|
|
|
// ── Drag & Drop ───────────────────────────────────────────────────────────────
|
|
let dragSrcIdx = null;
|
|
|
|
function attachDragListeners() {
|
|
document.querySelectorAll('.action-card').forEach(card => {
|
|
const idx = parseInt(card.dataset.idx);
|
|
card.addEventListener('dragstart', e => {
|
|
dragSrcIdx = idx;
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
setTimeout(() => card.classList.add('dragging'), 0);
|
|
});
|
|
card.addEventListener('dragend', () => {
|
|
card.classList.remove('dragging');
|
|
document.querySelectorAll('.action-card').forEach(c => c.classList.remove('drag-over'));
|
|
});
|
|
card.addEventListener('dragover', e => {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
});
|
|
card.addEventListener('dragenter', e => {
|
|
e.preventDefault();
|
|
if (parseInt(card.dataset.idx) !== dragSrcIdx) {
|
|
document.querySelectorAll('.action-card').forEach(c => c.classList.remove('drag-over'));
|
|
card.classList.add('drag-over');
|
|
}
|
|
});
|
|
card.addEventListener('dragleave', e => {
|
|
if (!card.contains(e.relatedTarget)) card.classList.remove('drag-over');
|
|
});
|
|
card.addEventListener('drop', e => {
|
|
e.preventDefault();
|
|
const targetIdx = parseInt(card.dataset.idx);
|
|
if (dragSrcIdx === null || dragSrcIdx === targetIdx) return;
|
|
// Réordonne en mémoire
|
|
const [moved] = actions.splice(dragSrcIdx, 1);
|
|
actions.splice(targetIdx, 0, moved);
|
|
dragSrcIdx = null;
|
|
orderChanged = true;
|
|
renderActions();
|
|
document.getElementById('reorder-bar').style.display = 'flex';
|
|
});
|
|
});
|
|
}
|
|
|
|
async function saveOrder() {
|
|
// _index contient l'index original côté serveur
|
|
const order = actions.map(a => a._index);
|
|
try {
|
|
await api('PUT', '/api/actions/photobooth/reorder', order);
|
|
showToast('✅ Ordre sauvegardé', 'success');
|
|
showRestartBtn();
|
|
await loadAll();
|
|
} catch(e) { showToast(`❌ ${e.message}`, 'error'); }
|
|
}
|
|
|
|
// ── Mapping bouton ────────────────────────────────────────────────────────────
|
|
function renderMapping() {
|
|
const el = document.getElementById('mapping-table');
|
|
const names = actions.map(a => a.name || '?');
|
|
let html = '<table class="table"><thead><tr><th>Clics</th><th>Label</th><th>Action</th><th></th></tr></thead><tbody>';
|
|
for (let n = 1; n <= 4; n++) {
|
|
const m = mapping[n] || mapping[String(n)] || {};
|
|
const selectedIdx = m.photobooth_index ?? 0;
|
|
const opts = actions.map((a, i) =>
|
|
`<option value="${i}" ${i===selectedIdx?'selected':''}>#${i} — ${esc(a.name||'?')}</option>`
|
|
).join('');
|
|
html += `<tr>
|
|
<td class="font-bold">${n} clic${n>1?'s':''}</td>
|
|
<td><input type="text" id="ml-${n}" class="form-control" style="max-width:180px" value="${esc(m.label||'')}" placeholder="Label…"></td>
|
|
<td><select id="mi-${n}" class="form-control" style="max-width:280px">${opts}</select></td>
|
|
<td><button class="btn btn-ghost btn-xs" onclick="testMapping(${n})">▶ Test</button></td>
|
|
</tr>`;
|
|
}
|
|
html += '</tbody></table>';
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
async function saveMapping() {
|
|
const m = {};
|
|
for (let n = 1; n <= 4; n++) {
|
|
m[n] = {
|
|
label: document.getElementById(`ml-${n}`).value,
|
|
photobooth_index: parseInt(document.getElementById(`mi-${n}`).value),
|
|
};
|
|
}
|
|
try {
|
|
await api('PUT', '/api/actions/mapping', m);
|
|
showToast('✅ Mapping sauvegardé', 'success');
|
|
mapping = m;
|
|
} catch(e) { showToast(`❌ ${e.message}`, 'error'); }
|
|
}
|
|
|
|
async function testMapping(n) {
|
|
const idx = parseInt(document.getElementById(`mi-${n}`).value);
|
|
const realIdx = actions[idx]?._index ?? idx;
|
|
if (!confirm(`Déclencher action #${realIdx} (test ${n} clic) ?`)) return;
|
|
try {
|
|
await api('POST', `/api/actions/trigger/${realIdx}`);
|
|
showToast(`▶ Test ${n} clic → action #${realIdx}`, 'info');
|
|
} catch(e) { showToast(`❌ ${e.message}`, 'error'); }
|
|
}
|
|
|
|
// ── Presets ───────────────────────────────────────────────────────────────────
|
|
async function loadPresets() {
|
|
try {
|
|
const data = await api('GET', '/api/actions/presets');
|
|
renderPresets(data.presets || []);
|
|
} catch(e) {}
|
|
}
|
|
|
|
function renderPresets(presets) {
|
|
const el = document.getElementById('presets-list');
|
|
if (!presets.length) {
|
|
el.innerHTML = '<p class="text-sm text-muted">Aucun preset sauvegardé.</p>';
|
|
return;
|
|
}
|
|
el.innerHTML = presets.map(p => `
|
|
<div class="preset-card">
|
|
<div class="preset-info">
|
|
<div class="preset-name">${esc(p.label || p.name)}</div>
|
|
<div class="preset-meta">
|
|
${p.action_count} action(s)
|
|
${p.description ? ' · ' + esc(p.description) : ''}
|
|
${p.saved_at ? ' · ' + esc(p.saved_at.replace('T',' ')) : ''}
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-1">
|
|
<button class="btn btn-primary btn-sm" onclick="restorePreset('${esc(p.name)}', '${esc(p.label||p.name)}')">↩ Restaurer</button>
|
|
<button class="btn btn-danger btn-xs" onclick="deletePreset('${esc(p.name)}', '${esc(p.label||p.name)}')">✕</button>
|
|
</div>
|
|
</div>`).join('');
|
|
}
|
|
|
|
async function savePreset() {
|
|
const name = document.getElementById('preset-name').value.trim();
|
|
const desc = document.getElementById('preset-desc').value.trim();
|
|
if (!name) { showToast('Saisissez un nom de preset', 'error'); return; }
|
|
try {
|
|
const r = await api('POST', '/api/actions/presets', { name, description: desc });
|
|
showToast(`✅ Preset « ${name} » sauvegardé (${r.action_count} actions)`, 'success');
|
|
document.getElementById('preset-name').value = '';
|
|
document.getElementById('preset-desc').value = '';
|
|
loadPresets();
|
|
} catch(e) { showToast(`❌ ${e.message}`, 'error'); }
|
|
}
|
|
|
|
async function restorePreset(name, label) {
|
|
if (!confirm(`Restaurer le preset « ${label} » ?\nLes actions actuelles seront remplacées.`)) return;
|
|
try {
|
|
const r = await api('POST', `/api/actions/presets/${encodeURIComponent(name)}/restore`);
|
|
showToast(`✅ Preset « ${label} » restauré (${r.action_count} actions)`, 'success');
|
|
showRestartBtn();
|
|
await loadAll();
|
|
} catch(e) { showToast(`❌ ${e.message}`, 'error'); }
|
|
}
|
|
|
|
async function deletePreset(name, label) {
|
|
if (!confirm(`Supprimer le preset « ${label} » ?`)) return;
|
|
try {
|
|
await api('DELETE', `/api/actions/presets/${encodeURIComponent(name)}`);
|
|
showToast(`🗑 Preset « ${label} » supprimé`, 'info');
|
|
loadPresets();
|
|
} catch(e) { showToast(`❌ ${e.message}`, 'error'); }
|
|
}
|
|
|
|
// ── Utilitaires ───────────────────────────────────────────────────────────────
|
|
function esc(s) {
|
|
if (s === null || s === undefined) return '';
|
|
return String(s)
|
|
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
.replace(/"/g,'"').replace(/'/g,''');
|
|
}
|
|
|
|
// ── Restart photobooth-app ────────────────────────────────────────────────────
|
|
function showRestartBtn() {
|
|
document.getElementById('restart-btn').style.display = '';
|
|
}
|
|
|
|
async function restartPhotobooth() {
|
|
const btn = document.getElementById('restart-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = '⏳ Redémarrage…';
|
|
try {
|
|
await api('POST', '/api/system/restart-photobooth');
|
|
showToast('🔄 photobooth-app redémarre — patientez ~10s', 'info', 8000);
|
|
btn.style.display = 'none';
|
|
} catch(e) {
|
|
showToast('❌ Erreur redémarrage: ' + e.message, 'error');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = '🔄 Redémarrer photobooth-app';
|
|
}
|
|
}
|
|
|
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
|
loadAll();
|
|
</script>
|
|
{% endblock %}
|