Admin : déroulant sommaire PDF par chapitre, transitions pages, URL GrateWizard
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user