Admin : déroulant sommaire PDF par chapitre, transitions pages, URL GrateWizard
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

- Ajout API /api/admin/pdf-outline (extraction sommaire PDF côté serveur via pdfjs-dist)
- Déroulant <select> dans chaque ligne de chapitre admin avec les 61 titres/sous-titres du PDF
- Sauvegarde des associations chapitre→page PDF via config YAML
- Transition douce (fondu 1s/1.2s) pour le changement de pages dans le viewer PDF
- Correction des numéros de pages réels dans chapterPages (extraits du sommaire PDF)
- URL GrateWizard prod → gratewizard.axiom-team.fr

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-02-28 22:29:54 +01:00
parent 8a38c86794
commit 1af00cc64c
6 changed files with 187 additions and 23 deletions

View File

@@ -151,12 +151,31 @@ html, body {
align-items: center;
justify-content: center;
max-height: 100%;
position: relative;
}
.page-slot.empty {
visibility: hidden;
}
/* Page turn transition */
.page-slot canvas {
transition: opacity 1s ease-out, transform 1s ease-out;
}
.page-slot canvas.entering {
opacity: 0;
transform: translateX(var(--enter-dir, 20px));
transition: none;
}
.page-slot canvas.leaving {
opacity: 0;
transform: translateX(var(--leave-dir, -20px));
position: absolute;
transition: opacity 1.2s ease-in, transform 1.2s ease-in;
}
/* Navigation bar */
.nav-bar {
display: flex;
@@ -281,6 +300,7 @@ let currentSpread = 0; // index into spreads[]
let spreads = []; // [[1], [2,3], [4,5], ...] — page 1 alone (cover), then pairs
let pageCanvasCache = new Map();
let outlinePageMap = []; // [{item, pageNum}] for highlighting
let isAnimating = false;
function buildSpreads(numPages) {
spreads = [];
@@ -327,33 +347,74 @@ async function renderPageCanvas(pageNum) {
return canvas;
}
async function showSpread(index) {
async function showSpread(index, animate = true) {
if (index < 0 || index >= spreads.length) return;
if (isAnimating) return;
const prevIndex = currentSpread;
const direction = index > prevIndex ? 1 : -1; // 1 = forward, -1 = back
currentSpread = index;
const pages = spreads[index];
const slotLeft = document.getElementById('slotLeft');
const slotRight = document.getElementById('slotRight');
// Collect old canvases for fade-out
const oldCanvases = [...slotLeft.querySelectorAll('canvas'), ...slotRight.querySelectorAll('canvas')];
const shouldAnimate = animate && oldCanvases.length > 0;
if (shouldAnimate) {
isAnimating = true;
// Fade out old canvases with directional slide
oldCanvases.forEach(c => {
c.style.setProperty('--leave-dir', `${-direction * 20}px`);
c.classList.add('leaving');
});
// Wait for fade-out to mostly complete
await new Promise(r => setTimeout(r, 500));
}
slotLeft.innerHTML = '';
slotRight.innerHTML = '';
slotLeft.className = 'page-slot';
slotRight.className = 'page-slot';
if (pages.length === 1) {
// Single page (cover or last odd page) — centered
const canvas = await renderPageCanvas(pages[0]);
if (shouldAnimate) {
canvas.style.setProperty('--enter-dir', `${direction * 20}px`);
canvas.classList.add('entering');
}
slotLeft.appendChild(canvas);
slotRight.className = 'page-slot empty';
if (shouldAnimate) {
// Double rAF ensures the browser has painted the initial state before transitioning
requestAnimationFrame(() => requestAnimationFrame(() => canvas.classList.remove('entering')));
}
} else {
const [left, right] = await Promise.all([
renderPageCanvas(pages[0]),
renderPageCanvas(pages[1]),
]);
if (shouldAnimate) {
left.style.setProperty('--enter-dir', `${direction * 20}px`);
right.style.setProperty('--enter-dir', `${direction * 20}px`);
left.classList.add('entering');
right.classList.add('entering');
}
slotLeft.appendChild(left);
slotRight.appendChild(right);
if (shouldAnimate) {
requestAnimationFrame(() => requestAnimationFrame(() => {
left.classList.remove('entering');
right.classList.remove('entering');
}));
}
}
if (shouldAnimate) setTimeout(() => { isAnimating = false; }, 1100);
else isAnimating = false;
updateNav();
highlightOutline();
}
@@ -405,7 +466,7 @@ window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
pageCanvasCache.clear();
showSpread(currentSpread);
showSpread(currentSpread, false);
}, 200);
});
@@ -505,7 +566,7 @@ async function init() {
document.getElementById('loadingMsg').remove();
const startSpread = getSpreadForPage(targetPage);
await showSpread(startSpread >= 0 ? startSpread : 0);
await showSpread(startSpread >= 0 ? startSpread : 0, false);
}
init().catch(err => {
@@ -538,7 +599,7 @@ window.toggleSidebar = function() {
// Recalc on sidebar toggle
setTimeout(() => {
pageCanvasCache.clear();
showSpread(currentSpread);
showSpread(currentSpread, false);
}, 300);
};
</script>