@jaymanyoo/pdf-book-viewer
Version:
A React-compatible library for displaying PDFs as realistic flip books with 3D page turning animations
292 lines (250 loc) • 7.76 kB
text/typescript
import { useState, useCallback, useRef, useEffect } from 'react';
// Simple interfaces for the hook
interface PDFBookViewerState {
isLoading: boolean;
isReady: boolean;
currentPage: number;
totalPages: number;
error: string | null;
}
interface PDFBookViewerMethods {
load: (source: string | File) => Promise<void>;
nextPage: () => void;
prevPage: () => void;
goToPage: (page: number) => void;
destroy: () => void;
getCurrentPage: () => number;
getTotalPages: () => number;
}
// Hook for managing PDF viewer state
export const usePDFViewerState = () => {
const [state, setState] = useState<PDFBookViewerState>({
isLoading: false,
isReady: false,
currentPage: 0,
totalPages: 0,
error: null
});
const setLoading = useCallback((loading: boolean) => {
setState((prev: PDFBookViewerState) => ({ ...prev, isLoading: loading, error: loading ? null : prev.error }));
}, []);
const setReady = useCallback((ready: boolean) => {
setState((prev: PDFBookViewerState) => ({ ...prev, isReady: ready }));
}, []);
const setCurrentPage = useCallback((page: number) => {
setState((prev: PDFBookViewerState) => ({ ...prev, currentPage: page }));
}, []);
const setTotalPages = useCallback((total: number) => {
setState((prev: PDFBookViewerState) => ({ ...prev, totalPages: total }));
}, []);
const setError = useCallback((error: string | null) => {
setState((prev: PDFBookViewerState) => ({ ...prev, error, isLoading: false }));
}, []);
const resetState = useCallback(() => {
setState({
isLoading: false,
isReady: false,
currentPage: 0,
totalPages: 0,
error: null
});
}, []);
return {
state,
setLoading,
setReady,
setCurrentPage,
setTotalPages,
setError,
resetState
};
};
// Hook for PDF navigation
export const usePDFNavigation = (viewerRef: React.RefObject<PDFBookViewerMethods>) => {
const [canGoNext, setCanGoNext] = useState(false);
const [canGoPrev, setCanGoPrev] = useState(false);
const updateNavigation = useCallback(() => {
if (viewerRef.current) {
const currentPage = viewerRef.current.getCurrentPage();
const totalPages = viewerRef.current.getTotalPages();
setCanGoPrev(currentPage > 0);
setCanGoNext(currentPage < totalPages - 1);
}
}, [viewerRef]);
const goNext = useCallback(() => {
if (canGoNext) {
viewerRef.current?.nextPage();
updateNavigation();
}
}, [canGoNext, viewerRef, updateNavigation]);
const goPrev = useCallback(() => {
if (canGoPrev) {
viewerRef.current?.prevPage();
updateNavigation();
}
}, [canGoPrev, viewerRef, updateNavigation]);
const goToPage = useCallback((page: number) => {
viewerRef.current?.goToPage(page);
updateNavigation();
}, [viewerRef, updateNavigation]);
return {
canGoNext,
canGoPrev,
goNext,
goPrev,
goToPage,
updateNavigation
};
};
// Hook for keyboard shortcuts
export const usePDFKeyboardShortcuts = (
enabled: boolean,
onNext: () => void,
onPrev: () => void,
onEscape?: () => void
) => {
useEffect(() => {
if (!enabled) return;
const handleKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowRight':
case ' ': // Spacebar
event.preventDefault();
onNext();
break;
case 'ArrowLeft':
event.preventDefault();
onPrev();
break;
case 'Escape':
if (onEscape) {
event.preventDefault();
onEscape();
}
break;
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [enabled, onNext, onPrev, onEscape]);
};
// Hook for responsive sizing
export const usePDFResponsiveSize = (
containerRef: React.RefObject<HTMLElement>,
aspectRatio: number = 0.75
) => {
const [dimensions, setDimensions] = useState({ width: 400, height: 600 });
useEffect(() => {
const updateSize = () => {
if (containerRef.current) {
const containerWidth = containerRef.current.offsetWidth;
const maxWidth = Math.min(containerWidth * 0.9, 800);
const width = Math.max(maxWidth, 300);
const height = width * (1 / aspectRatio);
setDimensions({ width: width / 2, height }); // Divide by 2 for PageFlip
}
};
updateSize();
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
}, [containerRef, aspectRatio]);
return dimensions;
};
// Hook for PDF loading with progress
export const usePDFLoader = () => {
const [loadingProgress, setLoadingProgress] = useState(0);
const [loadingStep, setLoadingStep] = useState<string>('');
const updateProgress = useCallback((progress: number, step: string = '') => {
setLoadingProgress(Math.max(0, Math.min(100, progress)));
setLoadingStep(step);
}, []);
const resetProgress = useCallback(() => {
setLoadingProgress(0);
setLoadingStep('');
}, []);
return {
loadingProgress,
loadingStep,
updateProgress,
resetProgress
};
};
// Hook for PDF page analysis
export const usePDFPageAnalysis = () => {
const [pageTypes, setPageTypes] = useState<Array<'portrait' | 'landscape'>>([]);
const [splitPages, setSplitPages] = useState<number[]>([]);
const analyzePages = useCallback((viewer: PDFBookViewerMethods) => {
// This would need to be implemented based on the internal viewer structure
// For now, this is a placeholder for future functionality
console.log('Page analysis would be implemented here');
}, []);
return {
pageTypes,
splitPages,
analyzePages
};
};
// Hook for PDF bookmarks/table of contents
export const usePDFBookmarks = () => {
const [bookmarks, setBookmarks] = useState<Array<{
title: string;
page: number;
level: number;
}>>([]);
const addBookmark = useCallback((title: string, page: number, level: number = 0) => {
setBookmarks(prev => [...prev, { title, page, level }].sort((a, b) => a.page - b.page));
}, []);
const removeBookmark = useCallback((page: number) => {
setBookmarks(prev => prev.filter(bookmark => bookmark.page !== page));
}, []);
const goToBookmark = useCallback((
bookmark: { page: number },
viewer: PDFBookViewerMethods
) => {
viewer.goToPage(bookmark.page);
}, []);
return {
bookmarks,
addBookmark,
removeBookmark,
goToBookmark
};
};
// Hook for PDF search functionality
export const usePDFSearch = () => {
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<Array<{
page: number;
text: string;
position: { x: number; y: number };
}>>([]);
const [currentSearchIndex, setCurrentSearchIndex] = useState(-1);
const search = useCallback((query: string) => {
setSearchQuery(query);
// Search implementation would go here
// This is a placeholder for future PDF text search functionality
console.log('Search functionality would be implemented here');
}, []);
const nextResult = useCallback(() => {
if (searchResults.length > 0) {
setCurrentSearchIndex(prev =>
prev < searchResults.length - 1 ? prev + 1 : 0
);
}
}, [searchResults.length]);
const prevResult = useCallback(() => {
if (searchResults.length > 0) {
setCurrentSearchIndex(prev =>
prev > 0 ? prev - 1 : searchResults.length - 1
);
}
}, [searchResults.length]);
return {
searchQuery,
searchResults,
currentSearchIndex,
search,
nextResult,
prevResult
};
};