Files
photoBooth/frontend/templates/public/gallery.html
T
admin 0b612e1302
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled
feat: impression depuis galerie publique (quota session, modal copies)
2026-07-17 17:52:52 +02:00

367 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Galerie photos — Photomaton LSDW{% endblock %}
{% block head %}
<style>
.pub-print-badge {
position: absolute; top: 6px; left: 6px;
background: rgba(30,160,70,.9); color: #fff;
border-radius: 12px; padding: .15rem .5rem;
font-size: .72rem; font-weight: 700;
pointer-events: none;
}
.photo-card { position: relative; }
/* ── Bouton imprimer sur la card ── */
.photo-card-actions { display: flex; gap: .35rem; flex-wrap: wrap; }
/* ── Bandeau quota ── */
#quota-bar {
display: none;
align-items: center;
gap: .6rem;
background: rgba(25,108,176,.12);
border: 1px solid rgba(25,108,176,.3);
border-radius: 8px;
padding: .45rem .85rem;
font-size: .88rem;
margin-bottom: .75rem;
}
#quota-bar.quota-warn {
background: rgba(224,123,0,.12);
border-color: rgba(224,123,0,.35);
}
#quota-bar.quota-empty {
background: rgba(192,0,0,.1);
border-color: rgba(192,0,0,.3);
}
/* ── Modal copies ── */
.modal-overlay {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,.65);
z-index: 2000;
align-items: center;
justify-content: center;
}
.modal-overlay.open { display: flex; }
.modal-box {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 14px;
padding: 1.5rem;
max-width: 340px;
width: 90%;
display: flex;
flex-direction: column;
gap: 1rem;
}
.modal-title { font-size: 1.05rem; font-weight: 700; }
.modal-preview { width: 100%; border-radius: 8px; object-fit: cover; max-height: 180px; }
.copies-row { display: flex; align-items: center; gap: .75rem; }
.copies-btn { background: var(--bg-muted); border: 1px solid var(--border); color: var(--text);
width: 2rem; height: 2rem; border-radius: 6px; cursor: pointer; font-size: 1.1rem;
display: flex; align-items: center; justify-content: center; }
.copies-val { font-size: 1.2rem; font-weight: 700; min-width: 1.5rem; text-align: center; }
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="flex items-center justify-between mb-2">
<h1 class="page-title" style="margin:0">📷 Vos photos</h1>
<div class="flex gap-1 items-center">
<span class="text-sm text-muted" id="photo-count">Chargement…</span>
<button class="btn btn-ghost btn-sm" onclick="loadPhotos(1)"></button>
</div>
</div>
<p class="text-sm text-muted mb-2">
Retrouvez vos photos ci-dessous. Scannez le QR code depuis la galerie du photomaton pour télécharger votre photo directement sur votre téléphone.
</p>
<!-- Bandeau quota -->
<div id="quota-bar">
<span id="quota-icon">🖨</span>
<span id="quota-text"></span>
</div>
<!-- Navigation pages -->
<div class="flex items-center gap-2 mb-2">
<button class="btn btn-ghost btn-sm" id="prev-btn" onclick="changePage(-1)" disabled>← Préc.</button>
<span class="text-sm text-muted">Page <span id="page-cur">1</span> / <span id="page-total">1</span></span>
<button class="btn btn-ghost btn-sm" id="next-btn" onclick="changePage(1)">Suiv. →</button>
</div>
<div class="photo-grid" id="photo-grid">
<div class="empty-state"><div class="icon"></div>Chargement…</div>
</div>
</div>
<!-- Lightbox -->
<div class="lightbox" id="lightbox" onclick="closeLightbox(event)">
<button class="lightbox-close">&times;</button>
<img id="lightbox-img" src="" alt="Photo">
<div class="lightbox-actions">
<a class="btn btn-primary" id="lb-download">⬇ Télécharger la photo</a>
<button class="btn btn-primary" id="lb-print" style="display:none" onclick="openPrintModal()">🖨 Imprimer</button>
</div>
<div class="text-xs text-muted">Connectez-vous au WiFi Photomaton pour télécharger</div>
</div>
<!-- Modal sélection copies -->
<div class="modal-overlay" id="print-modal" onclick="closePrintModal(event)">
<div class="modal-box" onclick="event.stopPropagation()">
<div class="modal-title">🖨 Demande d'impression</div>
<img id="pm-preview" src="" alt="" class="modal-preview">
<div id="pm-copies-row" class="copies-row" style="display:none">
<span style="flex:1;font-size:.9rem">Nombre de copies</span>
<button class="copies-btn" onclick="changeCopies(-1)"></button>
<span class="copies-val" id="pm-copies">1</span>
<button class="copies-btn" onclick="changeCopies(1)">+</button>
</div>
<p class="text-sm text-muted" id="pm-quota-info" style="margin:0"></p>
<div class="flex gap-1">
<button class="btn btn-primary" onclick="confirmPrint()" id="pm-confirm">🖨 Confirmer</button>
<button class="btn btn-ghost" onclick="closePrintModal()">Annuler</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// ── État ──────────────────────────────────────────────────────────────────────
let currentPage = 1;
let totalPages = 1;
let printCfg = { user_print_enabled: false, user_print_quota: 0,
user_print_max_copies: 1, remaining_quota: -1 };
let currentPrintPhotoId = null;
let currentPrintThumb = null;
let currentCopies = 1;
// ── Quota ─────────────────────────────────────────────────────────────────────
async function loadPrintStatus() {
try {
const s = await api('GET', '/api/print/user-print-status');
printCfg = s;
updateQuotaBar();
} catch(e) {}
}
function updateQuotaBar() {
const bar = document.getElementById('quota-bar');
const text = document.getElementById('quota-text');
const icon = document.getElementById('quota-icon');
if (!printCfg.user_print_enabled) { bar.style.display = 'none'; return; }
bar.style.display = 'flex';
bar.className = '';
if (printCfg.user_print_quota === 0) {
// Illimité — pas besoin d'afficher le bandeau
bar.style.display = 'none';
return;
}
const rem = printCfg.remaining_quota;
if (rem <= 0) {
bar.classList.add('quota-empty');
icon.textContent = '🚫';
text.textContent = `Vous avez utilisé votre quota d'impression (${printCfg.user_print_quota} max).`;
} else if (rem === 1) {
bar.classList.add('quota-warn');
icon.textContent = '⚠️';
text.textContent = `Dernière impression disponible pour cette session.`;
} else {
icon.textContent = '🖨';
text.textContent = `${rem} impression(s) restante(s) sur ${printCfg.user_print_quota}.`;
}
}
// ── Photos ────────────────────────────────────────────────────────────────────
async function loadPhotos(page = 1) {
try {
const data = await api('GET', `/api/gallery/photos?page=${page}&limit=24`);
const { photos, total, pages } = data;
currentPage = page;
totalPages = pages;
document.getElementById('photo-count').textContent = `${total} photo(s)`;
document.getElementById('page-cur').textContent = page;
document.getElementById('page-total').textContent = pages;
document.getElementById('prev-btn').disabled = page <= 1;
document.getElementById('next-btn').disabled = page >= pages;
const grid = document.getElementById('photo-grid');
if (!photos || !photos.length) {
grid.innerHTML = `
<div class="empty-state" style="grid-column:1/-1">
<div class="icon">📷</div>
Aucune photo pour l'instant — prenez la pose !
</div>`;
return;
}
const canPrint = printCfg.user_print_enabled &&
(printCfg.user_print_quota === 0 || printCfg.remaining_quota > 0);
grid.innerHTML = photos.map(p => {
const copies = p.print_done_copies || 0;
const printedBadge = copies > 0
? `<div class="pub-print-badge">🖨 ${copies}×</div>`
: '';
const printBtn = printCfg.user_print_enabled
? `<button class="btn btn-sm ${canPrint ? 'btn-ghost' : ''}"
style="${canPrint ? '' : 'opacity:.4;cursor:not-allowed'}"
onclick="event.stopPropagation();${canPrint ? `askPrint('${p.id}','${p.thumb_url}')` : 'showToast(\'Quota atteint\',\'error\')'}"
title="Imprimer cette photo">🖨</button>`
: '';
return `
<div class="photo-card" onclick="openLightbox('${p.full_url}','${p.download_url}','${p.id}','${p.thumb_url}')">
<img src="${p.thumb_url}" loading="lazy" alt="Photo" onerror="this.src='${p.full_url}'">
${printedBadge}
<div class="photo-card-actions">
<a class="btn btn-primary btn-sm" href="${p.download_url}" onclick="event.stopPropagation()">⬇ Télécharger</a>
${printBtn}
</div>
</div>`;
}).join('');
} catch(e) {
document.getElementById('photo-grid').innerHTML = `
<div class="empty-state" style="grid-column:1/-1">
<div class="icon">⚠️</div>
Impossible de charger les photos. Êtes-vous connecté au WiFi Photomaton ?
</div>`;
}
}
function changePage(delta) { loadPhotos(currentPage + delta); }
// ── Lightbox ──────────────────────────────────────────────────────────────────
function openLightbox(fullUrl, downloadUrl, photoId, thumbUrl) {
document.getElementById('lightbox-img').src = fullUrl;
document.getElementById('lb-download').href = downloadUrl;
const lbPrint = document.getElementById('lb-print');
if (printCfg.user_print_enabled) {
lbPrint.style.display = '';
const canPrint = printCfg.user_print_quota === 0 || printCfg.remaining_quota > 0;
lbPrint.disabled = !canPrint;
lbPrint.title = canPrint ? 'Demander une impression' : 'Quota atteint';
lbPrint.onclick = () => askPrint(photoId, thumbUrl);
} else {
lbPrint.style.display = 'none';
}
document.getElementById('lightbox').classList.add('open');
}
function closeLightbox(e) {
if (!e || e.target.id === 'lightbox' || e.target.classList.contains('lightbox-close')) {
document.getElementById('lightbox').classList.remove('open');
}
}
document.querySelector('.lightbox-close').addEventListener('click', closeLightbox);
document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeLightbox({}); closePrintModal(); } });
// ── Modal impression ──────────────────────────────────────────────────────────
function askPrint(photoId, thumbUrl) {
currentPrintPhotoId = photoId;
currentPrintThumb = thumbUrl;
currentCopies = 1;
document.getElementById('pm-preview').src = thumbUrl;
document.getElementById('pm-copies').textContent = '1';
// Afficher le sélecteur de copies seulement si max > 1
const copiesRow = document.getElementById('pm-copies-row');
copiesRow.style.display = printCfg.user_print_max_copies > 1 ? 'flex' : 'none';
// Info quota
const info = document.getElementById('pm-quota-info');
if (printCfg.user_print_quota > 0) {
const rem = printCfg.remaining_quota;
info.textContent = `Il vous reste ${rem} impression(s) pour cette session.`;
} else {
info.textContent = '';
}
document.getElementById('print-modal').classList.add('open');
}
function openPrintModal() { /* appelé depuis le bouton lightbox — déjà configuré via askPrint */ }
function closePrintModal(e) {
if (!e || e.target.id === 'print-modal') {
document.getElementById('print-modal').classList.remove('open');
}
}
function changeCopies(delta) {
currentCopies = Math.max(1, Math.min(printCfg.user_print_max_copies, currentCopies + delta));
document.getElementById('pm-copies').textContent = currentCopies;
}
async function confirmPrint() {
if (!currentPrintPhotoId) return;
const btn = document.getElementById('pm-confirm');
btn.disabled = true;
btn.textContent = '⏳ Envoi…';
try {
const r = await api('POST', '/api/print/request-gallery', {
photo_id: currentPrintPhotoId,
copies: currentCopies,
});
// Mettre à jour le quota local
printCfg.remaining_quota = r.remaining_quota;
printCfg.session_print_count = (printCfg.session_print_count || 0) + 1;
updateQuotaBar();
const modeMsg = r.mode === 'direct'
? 'L\'impression va démarrer.'
: 'Votre demande a bien été transmise à l\'admin.';
showToast(`✅ Demande envoyée ! ${modeMsg}`, 'success', 5000);
closePrintModal();
document.getElementById('lightbox').classList.remove('open');
// Recharger les photos pour mettre à jour le badge imprimé
setTimeout(() => loadPhotos(currentPage), 1500);
} catch(e) {
const msg = e.message || 'Erreur';
if (msg.includes('429')) {
showToast('🚫 Quota atteint — vous ne pouvez plus imprimer pour cette session.', 'error', 6000);
printCfg.remaining_quota = 0;
updateQuotaBar();
} else if (msg.includes('403')) {
showToast('L\'impression est désactivée.', 'error');
} else {
showToast('❌ Erreur : ' + msg, 'error');
}
closePrintModal();
} finally {
btn.disabled = false;
btn.textContent = '🖨 Confirmer';
}
}
// ── WebSocket ─────────────────────────────────────────────────────────────────
function onWsMessage(msg) {
if (msg.type === 'photobooth_event' && msg.event === 'finished') {
setTimeout(() => loadPhotos(1), 2000);
}
}
// ── Init ──────────────────────────────────────────────────────────────────────
(async () => {
await loadPrintStatus();
loadPhotos(1);
})();
setInterval(() => loadPhotos(currentPage), 30000);
</script>
{% endblock %}