Impression Mitsubishi K60 + cadres + interface admin distante
- Intégration selphy_print (backend CUPS dyesub Mitsubishi CP-K60DW-S) - Formats impression : 15x20, 10x15, 10x15 2-strips avec cadre et orientation portrait/paysage - Cadres d'impression par format (strip/10x15/15x20) : upload, sélection, aperçu live - Cadres démo générés : pellicule noir/vintage/couleurs + bordures classique/doré/rose gold - Écran de choix de cadre avant capture (clic direct, aperçu grand format) - Filtres réduits à 3 (Couleur, N&B, Sépia) et appliqués sur tous les strips - Port 80 via authbind, route /admin avec redirection auto depuis poste distant - Autologin LightDM corrigé (pam-autologin-service) - Bouton évacuer bourrage papier dans admin - Fix : _gp_lock manquant dans Camera.__init__ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BIN
frontend/assets/cadres/10x15/classique_blanc.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/assets/cadres/10x15/elegant_noir.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/assets/cadres/10x15/festif_dore.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
frontend/assets/cadres/10x15/rose_gold.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
frontend/assets/cadres/15x20/classique_blanc.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
frontend/assets/cadres/15x20/elegant_noir.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
frontend/assets/cadres/15x20/festif_dore.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
frontend/assets/cadres/15x20/rose_gold.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
frontend/assets/cadres/strip/classique_blanc.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
frontend/assets/cadres/strip/elegant_noir.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
frontend/assets/cadres/strip/festif_dore.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
frontend/assets/cadres/strip/pellicule_bleu.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
frontend/assets/cadres/strip/pellicule_noir.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
frontend/assets/cadres/strip/pellicule_rouge.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
frontend/assets/cadres/strip/pellicule_vert.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
frontend/assets/cadres/strip/pellicule_vintage.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
frontend/assets/cadres/strip/rose_gold.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
@@ -398,6 +398,7 @@ html, body {
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
max-height: 65vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preview-photo-container img {
|
||||
@@ -407,6 +408,17 @@ html, body {
|
||||
box-shadow: var(--ombre);
|
||||
}
|
||||
|
||||
.cadre-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
pointer-events: none;
|
||||
border-radius: var(--rayon);
|
||||
}
|
||||
|
||||
.barre-fonds {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
@@ -1120,6 +1132,165 @@ h3 {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* --- Cadres impression --- */
|
||||
.cadres-formats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1.2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.cadres-format-bloc {
|
||||
background: var(--fond);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.cadres-format-bloc h4 {
|
||||
margin: 0 0 0.8rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--texte-secondaire);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.cadres-liste {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.cadre-imp-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
padding: 0.5rem 0.7rem;
|
||||
background: var(--fond-secondaire, #1a1a2e);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cadre-imp-item input[type="radio"] {
|
||||
accent-color: var(--primaire);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cadre-imp-item .cadre-preview {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cadre-imp-item span {
|
||||
flex: 1;
|
||||
font-size: 0.85rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cadre-imp-item .btn-icone {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--danger);
|
||||
cursor: pointer;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cadre-imp-item .btn-icone:hover {
|
||||
background: rgba(244, 67, 54, 0.15);
|
||||
}
|
||||
|
||||
.cadres-upload {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cadres-upload .input-fichier {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* --- Fin cadres impression --- */
|
||||
|
||||
/* --- Choix cadre avant capture --- */
|
||||
#ecran-cadre-choix {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.grille-cadres-choix {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
max-height: 70vh;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.cadre-choix-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
border-radius: 14px;
|
||||
border: 4px solid transparent;
|
||||
background: transparent;
|
||||
transition: border-color 0.15s, transform 0.15s;
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.cadre-choix-item:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.cadre-choix-item img {
|
||||
width: 140px;
|
||||
height: 210px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
background: #111;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cadre-choix-item span {
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
color: var(--texte-secondaire);
|
||||
}
|
||||
|
||||
.cadre-choix-aucun .cadre-choix-vide {
|
||||
width: 140px;
|
||||
height: 210px;
|
||||
border-radius: 10px;
|
||||
border: 3px dashed rgba(255,255,255,0.2);
|
||||
background: rgba(255,255,255,0.04);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2.5rem;
|
||||
color: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.input-fichier {
|
||||
background: var(--fond);
|
||||
border: 2px dashed #444;
|
||||
|
||||
@@ -60,6 +60,18 @@
|
||||
<button class="btn-retour" onclick="allerA('accueil')">Retour</button>
|
||||
</section>
|
||||
|
||||
<!-- Choix de cadre avant capture -->
|
||||
<section id="ecran-cadre-choix" class="ecran">
|
||||
<h2>Choisissez un cadre</h2>
|
||||
<div id="grille-cadres-choix" class="grille-cadres-choix">
|
||||
<div class="cadre-choix-item cadre-choix-aucun" onclick="lancerSansCadre()">
|
||||
<div class="cadre-choix-vide"></div>
|
||||
<span>Sans cadre</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-retour" onclick="allerA('mode')">Retour</button>
|
||||
</section>
|
||||
|
||||
<!-- Compte a rebours + Capture -->
|
||||
<section id="ecran-capture" class="ecran">
|
||||
<div id="preview-live" class="preview-live">
|
||||
@@ -83,6 +95,7 @@
|
||||
<section id="ecran-preview" class="ecran">
|
||||
<div class="preview-photo-container">
|
||||
<img id="photo-resultat" src="" alt="Photo">
|
||||
<img id="cadre-overlay-preview" class="cadre-overlay cache" src="" alt="">
|
||||
</div>
|
||||
<div id="barre-filtres" class="barre-filtres">
|
||||
</div>
|
||||
@@ -358,11 +371,29 @@
|
||||
<select id="admin-imprimante"><option value="">Detection...</option></select>
|
||||
<button class="btn-secondaire btn-petit" onclick="rafraichirImprimantes()">Rafraichir</button>
|
||||
</div>
|
||||
<div class="champ">
|
||||
<label>Format papier</label>
|
||||
<select id="admin-format-papier">
|
||||
<option value="15x20">15×20 cm</option>
|
||||
<option value="10x15">10×15 cm</option>
|
||||
<option value="10x15-2up">10×15 cm — 2 strips</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="champ">
|
||||
<label>Orientation photo</label>
|
||||
<div class="radio-group">
|
||||
<label><input type="radio" name="admin-orientation" value="portrait"> Portrait</label>
|
||||
<label><input type="radio" name="admin-orientation" value="paysage"> Paysage</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="champ">
|
||||
<label>Nombre max d'exemplaires</label>
|
||||
<input type="number" id="admin-copies-max" min="1" max="20" value="5">
|
||||
</div>
|
||||
<button class="btn-action" onclick="sauvegarderMateriel()">Sauvegarder</button>
|
||||
<div class="champ" style="margin-top:1rem">
|
||||
<button class="btn-danger" onclick="evacuerBourrage()">⚠ Évacuer bourrage papier</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panneau Compteur -->
|
||||
@@ -435,8 +466,39 @@
|
||||
|
||||
<!-- Panneau Personnalisation (Cadres + Fonds + Animations) -->
|
||||
<div class="admin-panneau" id="panneau-personnalisation">
|
||||
<h3>Cadres / Overlays</h3>
|
||||
<p class="aide">Cochez les cadres a proposer aux utilisateurs.</p>
|
||||
|
||||
<h3>Cadres d'impression</h3>
|
||||
<p class="aide">Selectionnez un cadre PNG par format. Le cadre est applique sur chaque impression. Laissez vide pour imprimer sans cadre.</p>
|
||||
|
||||
<div class="cadres-formats">
|
||||
<div class="cadres-format-bloc" id="cadres-bloc-strip">
|
||||
<h4>Strip (5×15 cm)</h4>
|
||||
<div class="cadres-liste" id="cadres-liste-strip"></div>
|
||||
<div class="champ cadres-upload">
|
||||
<input type="file" id="input-cadre-strip" accept=".png" class="input-fichier">
|
||||
<button class="btn-secondaire btn-petit" onclick="uploaderCadreFormat('strip')">Importer</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cadres-format-bloc" id="cadres-bloc-10x15">
|
||||
<h4>10×15 cm</h4>
|
||||
<div class="cadres-liste" id="cadres-liste-10x15"></div>
|
||||
<div class="champ cadres-upload">
|
||||
<input type="file" id="input-cadre-10x15" accept=".png" class="input-fichier">
|
||||
<button class="btn-secondaire btn-petit" onclick="uploaderCadreFormat('10x15')">Importer</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cadres-format-bloc" id="cadres-bloc-15x20">
|
||||
<h4>15×20 cm</h4>
|
||||
<div class="cadres-liste" id="cadres-liste-15x20"></div>
|
||||
<div class="champ cadres-upload">
|
||||
<input type="file" id="input-cadre-15x20" accept=".png" class="input-fichier">
|
||||
<button class="btn-secondaire btn-petit" onclick="uploaderCadreFormat('15x20')">Importer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Cadres / Overlays photo</h3>
|
||||
<p class="aide">Cochez les cadres a proposer aux utilisateurs en live.</p>
|
||||
<div id="liste-cadres" class="liste-cadres">
|
||||
<p class="texte-secondaire">Aucun cadre importe</p>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ async function chargerAdmin() {
|
||||
chargerCompteur();
|
||||
chargerDestinations();
|
||||
chargerCadres();
|
||||
chargerCadresImpression();
|
||||
chargerAnimations();
|
||||
chargerFonctionnalites();
|
||||
chargerEmailAdmin();
|
||||
@@ -57,6 +58,10 @@ async function chargerMateriel() {
|
||||
// Imprimantes
|
||||
await rafraichirImprimantes();
|
||||
if (imp.imprimante) document.getElementById('admin-imprimante').value = imp.imprimante;
|
||||
if (imp.format) document.getElementById('admin-format-papier').value = imp.format;
|
||||
const fmt = (imp.format || '15x20').replace('-2up', '');
|
||||
const orient = (imp.orientations || {})[fmt] || 'portrait';
|
||||
document.querySelector(`input[name="admin-orientation"][value="${orient}"]`).checked = true;
|
||||
setValue('admin-copies-max', imp.copies_max || 5);
|
||||
}
|
||||
|
||||
@@ -78,6 +83,19 @@ async function reconnecterCamera() {
|
||||
chargerMateriel();
|
||||
}
|
||||
|
||||
function _getOrientations() {
|
||||
const fmt = (getValue('admin-format-papier') || '15x20').replace('-2up', '');
|
||||
const orient = document.querySelector('input[name="admin-orientation"]:checked')?.value || 'portrait';
|
||||
// On conserve les orientations des autres formats depuis la config courante
|
||||
const existing = (config.impression || {}).orientations || {};
|
||||
return { ...existing, [fmt]: orient };
|
||||
}
|
||||
|
||||
async function evacuerBourrage() {
|
||||
const r = await apiPost('/api/imprimante/evacuer', {});
|
||||
afficherStatut(r.message || 'Imprimante réactivée', r.succes ? 'succes' : 'erreur');
|
||||
}
|
||||
|
||||
async function sauvegarderMateriel() {
|
||||
await apiPost('/api/config', {
|
||||
camera: {
|
||||
@@ -86,7 +104,9 @@ async function sauvegarderMateriel() {
|
||||
},
|
||||
impression: {
|
||||
imprimante: getValue('admin-imprimante'),
|
||||
format: getValue('admin-format-papier'),
|
||||
copies_max: parseInt(getValue('admin-copies-max')) || 5,
|
||||
orientations: _getOrientations(),
|
||||
},
|
||||
});
|
||||
afficherStatut('Materiel sauvegarde', 'succes');
|
||||
@@ -208,7 +228,108 @@ async function sauvegarderDestinations() {
|
||||
afficherStatut('Destinations sauvegardees', 'succes');
|
||||
}
|
||||
|
||||
// === CADRES ===
|
||||
// === CADRES IMPRESSION (par format) ===
|
||||
|
||||
const FORMATS_IMPRESSION = ['strip', '10x15', '15x20'];
|
||||
|
||||
async function chargerCadresImpression() {
|
||||
for (const fmt of FORMATS_IMPRESSION) {
|
||||
await chargerCadresFormat(fmt);
|
||||
}
|
||||
}
|
||||
|
||||
async function chargerCadresFormat(fmt) {
|
||||
const data = await apiGet(`/api/cadres-impression/${fmt}`);
|
||||
const liste = document.getElementById(`cadres-liste-${fmt}`);
|
||||
liste.innerHTML = '';
|
||||
|
||||
// Option "Aucun cadre"
|
||||
const divAucun = _creerItemCadreImpression(fmt, null, data.actif);
|
||||
liste.appendChild(divAucun);
|
||||
|
||||
if (data.disponibles.length === 0) {
|
||||
const p = document.createElement('p');
|
||||
p.className = 'texte-secondaire';
|
||||
p.textContent = 'Aucun cadre importe';
|
||||
liste.appendChild(p);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const nom of data.disponibles) {
|
||||
const div = _creerItemCadreImpression(fmt, nom, data.actif);
|
||||
liste.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
function _creerItemCadreImpression(fmt, nom, actif) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'cadre-imp-item';
|
||||
|
||||
const rb = document.createElement('input');
|
||||
rb.type = 'radio';
|
||||
rb.name = `cadre-imp-${fmt}`;
|
||||
rb.value = nom || '';
|
||||
rb.checked = (actif === nom);
|
||||
rb.onchange = () => activerCadreFormat(fmt, nom);
|
||||
|
||||
if (nom) {
|
||||
const img = document.createElement('img');
|
||||
img.src = `/assets/cadres/${fmt}/${nom}`;
|
||||
img.alt = nom;
|
||||
img.className = 'cadre-preview';
|
||||
|
||||
const btnDel = document.createElement('button');
|
||||
btnDel.className = 'btn-danger btn-icone';
|
||||
btnDel.title = 'Supprimer';
|
||||
btnDel.textContent = '✕';
|
||||
btnDel.onclick = () => supprimerCadreFormat(fmt, nom);
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.textContent = nom.replace('.png', '');
|
||||
|
||||
div.appendChild(rb);
|
||||
div.appendChild(img);
|
||||
div.appendChild(span);
|
||||
div.appendChild(btnDel);
|
||||
} else {
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'Aucun cadre';
|
||||
div.appendChild(rb);
|
||||
div.appendChild(span);
|
||||
}
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
async function activerCadreFormat(fmt, nom) {
|
||||
await apiPost(`/api/cadres-impression/${fmt}/actif`, { nom: nom || null });
|
||||
afficherStatut(`Cadre ${fmt} mis a jour`, 'succes');
|
||||
}
|
||||
|
||||
async function supprimerCadreFormat(fmt, nom) {
|
||||
await fetch(`/api/cadres-impression/${fmt}/${encodeURIComponent(nom)}`, { method: 'DELETE' });
|
||||
await chargerCadresFormat(fmt);
|
||||
afficherStatut('Cadre supprime', 'succes');
|
||||
}
|
||||
|
||||
async function uploaderCadreFormat(fmt) {
|
||||
const input = document.getElementById(`input-cadre-${fmt}`);
|
||||
if (!input.files.length) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('fichier', input.files[0]);
|
||||
|
||||
const r = await fetch(`/api/upload/cadre/${fmt}`, { method: 'POST', body: formData });
|
||||
if (r.ok) {
|
||||
input.value = '';
|
||||
await chargerCadresFormat(fmt);
|
||||
afficherStatut(`Cadre ${fmt} importe`, 'succes');
|
||||
} else {
|
||||
afficherStatut('Erreur import cadre', 'erreur');
|
||||
}
|
||||
}
|
||||
|
||||
// === CADRES OVERLAYS PHOTO ===
|
||||
|
||||
async function chargerCadres() {
|
||||
const data = await apiGet('/api/cadres');
|
||||
|
||||
@@ -186,15 +186,49 @@ document.addEventListener('keydown', (e) => {
|
||||
|
||||
// --- Modes ---
|
||||
|
||||
let cadreChoisi = null; // cadre sélectionné par l'utilisateur avant capture
|
||||
|
||||
function setupModes() {
|
||||
document.querySelectorAll('#ecran-mode .btn-mode').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
btn.addEventListener('click', async () => {
|
||||
modeActuel = btn.dataset.mode;
|
||||
allerA('capture');
|
||||
cadreChoisi = null;
|
||||
const hasCadres = await chargerCadresChoix();
|
||||
allerA(hasCadres ? 'cadre-choix' : 'capture');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function chargerCadresChoix() {
|
||||
const cfg = await apiGet('/api/config');
|
||||
const fmt = (cfg.impression?.format || '15x20').replace('-2up', '');
|
||||
const data = await apiGet(`/api/cadres-impression/${fmt}`).catch(() => null);
|
||||
if (!data || data.disponibles.length === 0) return false;
|
||||
|
||||
const grille = document.getElementById('grille-cadres-choix');
|
||||
// Conserver "Sans cadre" en premier, supprimer les anciens cadres
|
||||
const aucun = grille.querySelector('.cadre-choix-aucun');
|
||||
grille.innerHTML = '';
|
||||
if (aucun) grille.appendChild(aucun);
|
||||
|
||||
for (const nom of data.disponibles) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'cadre-choix-item';
|
||||
div.innerHTML = `<img src="/assets/cadres/${fmt}/${nom}" alt="${nom}"><span>${nom.replace('.png','')}</span>`;
|
||||
div.onclick = () => {
|
||||
cadreChoisi = nom;
|
||||
allerA('capture');
|
||||
};
|
||||
grille.appendChild(div);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function lancerSansCadre() {
|
||||
cadreChoisi = null;
|
||||
allerA('capture');
|
||||
}
|
||||
|
||||
// --- Onglets admin ---
|
||||
|
||||
function setupOnglets() {
|
||||
@@ -269,9 +303,10 @@ wsOnMessage('config_maj', (msg) => {
|
||||
appliquerConfig();
|
||||
});
|
||||
|
||||
// Erreur camera : afficher overlay + rechargement auto apres 10s
|
||||
// Erreur camera : afficher overlay + rechargement auto apres 10s (kiosk seulement)
|
||||
let _erreurCameraTimer = null;
|
||||
wsOnMessage('camera_erreur', (msg) => {
|
||||
if (window.location.pathname === '/admin') return;
|
||||
document.getElementById('camera-erreur').classList.remove('cache');
|
||||
if (_erreurCameraTimer) clearTimeout(_erreurCameraTimer);
|
||||
_erreurCameraTimer = setTimeout(() => { location.reload(); }, 10000);
|
||||
@@ -398,4 +433,10 @@ async function wizardTerminer() {
|
||||
}
|
||||
|
||||
// Demarrage
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
init().then(() => {
|
||||
if (window.location.pathname === '/admin') {
|
||||
allerA('admin-choix');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -298,8 +298,9 @@ async function traiterCapture() {
|
||||
function afficherPreviewPhoto(chemin) {
|
||||
document.getElementById('photo-resultat').src = chemin;
|
||||
document.getElementById('photo-partage').src = chemin;
|
||||
afficherCadreOverlay(cadreChoisi);
|
||||
|
||||
if (modeActuel === 'simple' && config.fonctionnalites?.filtres) {
|
||||
if (config.fonctionnalites?.filtres) {
|
||||
chargerFiltres();
|
||||
}
|
||||
if (config.fonctionnalites?.overlays) {
|
||||
|
||||
48
frontend/js/effects.js
vendored
@@ -23,11 +23,18 @@ async function appliquerFiltreUI(filtre, btn) {
|
||||
btn.classList.add('actif');
|
||||
filtreActuel = filtre;
|
||||
|
||||
if (modeActuel === 'multi') {
|
||||
// En mode multi : ré-assembler le strip avec le filtre appliqué sur chaque photo
|
||||
await appliquerFiltreStrip(filtre);
|
||||
return;
|
||||
}
|
||||
|
||||
if (filtre === 'original') {
|
||||
const chemin = '/data/photos/' + photosSession[0];
|
||||
document.getElementById('photo-resultat').src = chemin;
|
||||
document.getElementById('photo-partage').src = chemin;
|
||||
photoFinale = photosSession[0];
|
||||
afficherCadreOverlay(cadreChoisi);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,9 +47,50 @@ async function appliquerFiltreUI(filtre, btn) {
|
||||
photoFinale = resultat.nom;
|
||||
document.getElementById('photo-resultat').src = '/data/exports/' + resultat.nom;
|
||||
document.getElementById('photo-partage').src = '/data/exports/' + resultat.nom;
|
||||
afficherCadreOverlay(cadreChoisi);
|
||||
}
|
||||
}
|
||||
|
||||
async function appliquerFiltreStrip(filtre) {
|
||||
let photosAAssembler = photosSession;
|
||||
let source = 'photos'; // les originaux sont dans data/photos/
|
||||
|
||||
if (filtre !== 'original') {
|
||||
const promises = photosSession.map(p => apiPost('/api/filtre', { photo: p, filtre }));
|
||||
const resultats = await Promise.all(promises);
|
||||
photosAAssembler = resultats.filter(r => r.nom).map(r => r.nom);
|
||||
source = 'exports'; // les filtrés sont dans data/exports/
|
||||
if (photosAAssembler.length === 0) return;
|
||||
}
|
||||
|
||||
const mode = config.multi_shot?.mode || 'strip';
|
||||
const endpoint = mode === 'strip' ? '/api/strip' : '/api/collage';
|
||||
const resultat = await apiPost(endpoint, { photos: photosAAssembler, source });
|
||||
|
||||
if (resultat.nom) {
|
||||
photoFinale = resultat.nom;
|
||||
if (resultat.impression) photoImpression = resultat.impression;
|
||||
const src = '/data/exports/' + resultat.nom;
|
||||
document.getElementById('photo-resultat').src = src;
|
||||
document.getElementById('photo-partage').src = src;
|
||||
afficherCadreOverlay(cadreChoisi);
|
||||
}
|
||||
}
|
||||
|
||||
function afficherCadreOverlay(nom) {
|
||||
const el = document.getElementById('cadre-overlay-preview');
|
||||
if (!el) return;
|
||||
if (!nom) {
|
||||
el.classList.add('cache');
|
||||
el.src = '';
|
||||
return;
|
||||
}
|
||||
const config_imp = config.impression || {};
|
||||
const fmt = (config_imp.format || '15x20').replace('-2up', '');
|
||||
el.src = `/assets/cadres/${fmt}/${nom}`;
|
||||
el.classList.remove('cache');
|
||||
}
|
||||
|
||||
async function chargerOverlays() {
|
||||
const overlays = await apiGet('/api/overlays');
|
||||
const barre = document.getElementById('barre-overlays');
|
||||
|
||||
@@ -25,7 +25,11 @@ async function lancerImpression() {
|
||||
// Pour les strips, imprimer la version 2 bandes sur 10x15
|
||||
const fichierImpression = photoImpression || photoFinale;
|
||||
afficherStatut(`Impression de ${nbExemplaires} exemplaire(s)...`, 'succes');
|
||||
const resultat = await apiPost('/api/imprimer', { photo: fichierImpression, copies: nbExemplaires });
|
||||
const resultat = await apiPost('/api/imprimer', {
|
||||
photo: fichierImpression,
|
||||
copies: nbExemplaires,
|
||||
cadre: cadreChoisi || undefined,
|
||||
});
|
||||
if (resultat.succes) {
|
||||
afficherStatut(`${nbExemplaires} exemplaire(s) envoye(s) a l'imprimante !`, 'succes');
|
||||
} else {
|
||||
|
||||