diff --git a/components/PublicGrid.tsx b/components/PublicGrid.tsx index 11b75e5..fe660f2 100644 --- a/components/PublicGrid.tsx +++ b/components/PublicGrid.tsx @@ -422,83 +422,31 @@ function FullscreenViewer({ } function FlipBook({ pages, onClose }: { pages: string[]; onClose: () => void }) { - const containerRef = useRef(null); - const flipRef = useRef(null); - const [currentPage, setCurrentPage] = useState(0); - const [pageCount, setPageCount] = useState(pages.length); - - useEffect(() => { - if (!containerRef.current || pages.length === 0) return; - let cancelled = false; - - // page-flip's destroy() removes its own block from the DOM. Give it a child - // div we own so React's managed container stays intact across StrictMode cycles. - const block = document.createElement('div'); - block.style.width = '100%'; - block.style.height = '100%'; - containerRef.current.appendChild(block); - - // HTML mode: each page is a div with an using object-fit:contain so - // mixed aspect ratios get letterboxed with white margins (like a real book - // page) instead of being stretched to fill the slot. - const pageElements = pages.map((url) => { - const page = document.createElement('div'); - page.style.cssText = 'background:#ffffff;width:100%;height:100%;overflow:hidden;display:flex;align-items:center;justify-content:center;'; - const img = document.createElement('img'); - img.src = url; - img.draggable = false; - img.style.cssText = 'max-width:100%;max-height:100%;object-fit:contain;display:block;pointer-events:none;user-select:none;'; - page.appendChild(img); - return page; - }); - - import('@/lib/page-flip').then(({ PageFlip }) => { - if (cancelled) return; - const flip = new PageFlip(block, { - width: 550, - height: 733, - size: 'stretch', - minWidth: 315, - maxWidth: 1400, - minHeight: 420, - maxHeight: 900, - drawShadow: true, - flippingTime: 800, - usePortrait: true, - maxShadowOpacity: 0.5, - showCover: false, - mobileScrollSupport: false, - useMouseEvents: true, - swipeDistance: 30, - showPageCorners: true, - disableFlipByClick: false, - }); - - flip.on('flip', (e) => { - const newPage = typeof e.data === 'number' ? e.data : 0; - setCurrentPage(newPage); - }); - flip.on('init', () => { - setPageCount(flip.getPageCount()); - }); - - flip.loadFromHTML(pageElements); - flipRef.current = flip; - }).catch(() => {}); - - return () => { - cancelled = true; - if (flipRef.current) { - try { flipRef.current.destroy(); } catch {} - flipRef.current = null; - } - try { block.remove(); } catch {} - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const goNext = useCallback(() => flipRef.current?.flipNext(), []); - const goPrev = useCallback(() => flipRef.current?.flipPrev(), []); + const [spread, setSpread] = useState(0); + const [flipping, setFlipping] = useState<'forward' | 'backward' | null>(null); + const dragStartX = useRef(null); + + const totalSpreads = Math.max(1, Math.ceil(pages.length / 2)); + const getPage = (i: number) => (i >= 0 && i < pages.length ? pages[i] : ''); + + const leftIdx = spread * 2; + const rightIdx = spread * 2 + 1; + const nextLeftIdx = (spread + 1) * 2; + const nextRightIdx = (spread + 1) * 2 + 1; + const prevLeftIdx = (spread - 1) * 2; + const prevRightIdx = (spread - 1) * 2 + 1; + + const goNext = useCallback(() => { + if (flipping !== null || spread >= totalSpreads - 1) return; + setFlipping('forward'); + window.setTimeout(() => { setSpread(s => s + 1); setFlipping(null); }, 700); + }, [flipping, spread, totalSpreads]); + + const goPrev = useCallback(() => { + if (flipping !== null || spread <= 0) return; + setFlipping('backward'); + window.setTimeout(() => { setSpread(s => s - 1); setFlipping(null); }, 700); + }, [flipping, spread]); useEffect(() => { const onKey = (e: KeyboardEvent) => { @@ -510,8 +458,39 @@ function FlipBook({ pages, onClose }: { pages: string[]; onClose: () => void }) return () => window.removeEventListener('keydown', onKey); }, [onClose, goNext, goPrev]); + const onPointerDown = (e: React.PointerEvent) => { dragStartX.current = e.clientX; }; + const onPointerUp = (e: React.PointerEvent) => { + if (dragStartX.current === null) return; + const dx = e.clientX - dragStartX.current; + if (Math.abs(dx) > 50) (dx < 0 ? goNext() : goPrev()); + dragStartX.current = null; + }; + const onPointerCancel = () => { dragStartX.current = null; }; + + const visibleLeft = flipping === 'backward' ? getPage(prevLeftIdx) : getPage(leftIdx); + const visibleRight = flipping === 'forward' ? getPage(nextRightIdx) : getPage(rightIdx); + + const currentPageNum = Math.min(leftIdx + 1, pages.length); + const lastPageNum = Math.min(rightIdx + 1, pages.length); + const indicatorLabel = currentPageNum === lastPageNum + ? `${currentPageNum} / ${pages.length}` + : `${currentPageNum}-${lastPageNum} / ${pages.length}`; + + const pageImgClass = 'w-full h-full object-contain'; + return ( -
+
+