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>
This commit is contained in:
2026-06-03 21:38:31 +02:00
parent 6451f5774c
commit e3d1d9cc07
32 changed files with 1009 additions and 84 deletions

View File

@@ -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');

View File

@@ -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');
}
});
});

View File

@@ -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) {

View File

@@ -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');

View File

@@ -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 {