Spaces:
Running
Running
| // PDF.js Configuration | |
| pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; | |
| // Theme Management | |
| const themeToggle = document.getElementById('themeToggle'); | |
| const sunIcon = document.getElementById('sunIcon'); | |
| const moonIcon = document.getElementById('moonIcon'); | |
| // Initialize theme from localStorage or default to dark | |
| const currentTheme = localStorage.getItem('theme') || 'dark'; | |
| if (currentTheme === 'light') { | |
| document.documentElement.setAttribute('data-theme', 'light'); | |
| sunIcon.style.display = 'none'; | |
| moonIcon.style.display = 'block'; | |
| } | |
| // Toggle theme function | |
| function toggleTheme() { | |
| const currentTheme = document.documentElement.getAttribute('data-theme'); | |
| const newTheme = currentTheme === 'light' ? 'dark' : 'light'; | |
| document.documentElement.setAttribute('data-theme', newTheme); | |
| localStorage.setItem('theme', newTheme); | |
| // Toggle icons | |
| if (newTheme === 'light') { | |
| sunIcon.style.display = 'none'; | |
| moonIcon.style.display = 'block'; | |
| } else { | |
| sunIcon.style.display = 'block'; | |
| moonIcon.style.display = 'none'; | |
| } | |
| } | |
| // State Management | |
| const state = { | |
| pdfDoc: null, | |
| pageNum: 1, | |
| pageRendering: false, | |
| pageNumPending: null, | |
| scale: 1.5, | |
| canvas: document.getElementById('pdfCanvas'), | |
| ctx: null | |
| }; | |
| // Initialize | |
| state.ctx = state.canvas.getContext('2d'); | |
| // DOM Elements | |
| const elements = { | |
| loadingOverlay: document.getElementById('loadingOverlay'), | |
| pageNum: document.getElementById('pageNum'), | |
| pageCount: document.getElementById('pageCount'), | |
| currentPageDisplay: document.getElementById('currentPageDisplay'), | |
| totalPagesDisplay: document.getElementById('totalPagesDisplay'), | |
| zoomLevel: document.getElementById('zoomLevel'), | |
| prevPageBtn: document.getElementById('prevPageBtn'), | |
| nextPageBtn: document.getElementById('nextPageBtn'), | |
| zoomInBtn: document.getElementById('zoomInBtn'), | |
| zoomOutBtn: document.getElementById('zoomOutBtn'), | |
| fitWidthBtn: document.getElementById('fitWidthBtn'), | |
| pageInput: document.getElementById('pageInput'), | |
| goToPageBtn: document.getElementById('goToPageBtn'), | |
| sidebar: document.getElementById('sidebar'), | |
| sidebarToggle: document.getElementById('sidebarToggle'), | |
| closeSidebarBtn: document.getElementById('closeSidebarBtn'), | |
| searchBtn: document.getElementById('searchBtn'), | |
| searchPanel: document.getElementById('searchPanel'), | |
| closeSearchBtn: document.getElementById('closeSearchBtn'), | |
| fullscreenBtn: document.getElementById('fullscreenBtn'), | |
| canvasContainer: document.getElementById('canvasContainer') | |
| }; | |
| // Render Page | |
| function renderPage(num) { | |
| state.pageRendering = true; | |
| state.pdfDoc.getPage(num).then(page => { | |
| const viewport = page.getViewport({ scale: state.scale }); | |
| state.canvas.height = viewport.height; | |
| state.canvas.width = viewport.width; | |
| const renderContext = { | |
| canvasContext: state.ctx, | |
| viewport: viewport | |
| }; | |
| const renderTask = page.render(renderContext); | |
| renderTask.promise.then(() => { | |
| state.pageRendering = false; | |
| if (state.pageNumPending !== null) { | |
| renderPage(state.pageNumPending); | |
| state.pageNumPending = null; | |
| } | |
| // Update UI | |
| updatePageDisplay(); | |
| // Hide loading overlay on first render | |
| if (num === 1) { | |
| elements.loadingOverlay.classList.add('hidden'); | |
| } | |
| }); | |
| }); | |
| // Update page display immediately | |
| elements.pageNum.textContent = num; | |
| elements.currentPageDisplay.textContent = num; | |
| } | |
| // Queue Page Render | |
| function queueRenderPage(num) { | |
| if (state.pageRendering) { | |
| state.pageNumPending = num; | |
| } else { | |
| renderPage(num); | |
| } | |
| } | |
| // Update Page Display | |
| function updatePageDisplay() { | |
| elements.zoomLevel.textContent = Math.round(state.scale * 100) + '%'; | |
| updateThumbnailSelection(); | |
| } | |
| // Navigate to Previous Page | |
| function onPrevPage() { | |
| if (state.pageNum <= 1) { | |
| return; | |
| } | |
| state.pageNum--; | |
| queueRenderPage(state.pageNum); | |
| } | |
| // Navigate to Next Page | |
| function onNextPage() { | |
| if (state.pageNum >= state.pdfDoc.numPages) { | |
| return; | |
| } | |
| state.pageNum++; | |
| queueRenderPage(state.pageNum); | |
| } | |
| // Zoom In | |
| function zoomIn() { | |
| if (state.scale < 3) { | |
| state.scale += 0.25; | |
| queueRenderPage(state.pageNum); | |
| } | |
| } | |
| // Zoom Out | |
| function zoomOut() { | |
| if (state.scale > 0.5) { | |
| state.scale -= 0.25; | |
| queueRenderPage(state.pageNum); | |
| } | |
| } | |
| // Fit to Width | |
| function fitToWidth() { | |
| const containerWidth = elements.canvasContainer.clientWidth - 40; | |
| state.pdfDoc.getPage(state.pageNum).then(page => { | |
| const viewport = page.getViewport({ scale: 1 }); | |
| state.scale = containerWidth / viewport.width; | |
| queueRenderPage(state.pageNum); | |
| }); | |
| } | |
| // Go to Page | |
| function goToPage() { | |
| const pageNumber = parseInt(elements.pageInput.value); | |
| if (pageNumber >= 1 && pageNumber <= state.pdfDoc.numPages) { | |
| state.pageNum = pageNumber; | |
| queueRenderPage(state.pageNum); | |
| elements.pageInput.value = ''; | |
| } | |
| } | |
| // Toggle Sidebar | |
| function toggleSidebar() { | |
| elements.sidebar.classList.toggle('hidden'); | |
| } | |
| // Toggle Search | |
| function toggleSearch() { | |
| elements.searchPanel.classList.toggle('active'); | |
| if (elements.searchPanel.classList.contains('active')) { | |
| document.getElementById('searchInput').focus(); | |
| } | |
| } | |
| // Toggle Fullscreen | |
| function toggleFullscreen() { | |
| if (!document.fullscreenElement) { | |
| document.documentElement.requestFullscreen(); | |
| } else { | |
| if (document.exitFullscreen) { | |
| document.exitFullscreen(); | |
| } | |
| } | |
| } | |
| // Event Listeners | |
| themeToggle.addEventListener('click', toggleTheme); | |
| elements.prevPageBtn.addEventListener('click', onPrevPage); | |
| elements.nextPageBtn.addEventListener('click', onNextPage); | |
| elements.zoomInBtn.addEventListener('click', zoomIn); | |
| elements.zoomOutBtn.addEventListener('click', zoomOut); | |
| elements.fitWidthBtn.addEventListener('click', fitToWidth); | |
| elements.goToPageBtn.addEventListener('click', goToPage); | |
| elements.sidebarToggle.addEventListener('click', toggleSidebar); | |
| elements.closeSidebarBtn.addEventListener('click', toggleSidebar); | |
| elements.searchBtn.addEventListener('click', toggleSearch); | |
| elements.closeSearchBtn.addEventListener('click', toggleSearch); | |
| elements.fullscreenBtn.addEventListener('click', toggleFullscreen); | |
| // Enter key for page input | |
| elements.pageInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| goToPage(); | |
| } | |
| }); | |
| // Keyboard Shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| // Arrow keys for navigation | |
| if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { | |
| e.preventDefault(); | |
| onPrevPage(); | |
| } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { | |
| e.preventDefault(); | |
| onNextPage(); | |
| } | |
| // Zoom shortcuts | |
| if (e.key === '+' || e.key === '=') { | |
| e.preventDefault(); | |
| zoomIn(); | |
| } else if (e.key === '-') { | |
| e.preventDefault(); | |
| zoomOut(); | |
| } | |
| // Search shortcut | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'f') { | |
| e.preventDefault(); | |
| toggleSearch(); | |
| } | |
| // Fullscreen shortcut | |
| if (e.key === 'f' && !e.ctrlKey && !e.metaKey) { | |
| const activeElement = document.activeElement; | |
| if (activeElement.tagName !== 'INPUT') { | |
| toggleFullscreen(); | |
| } | |
| } | |
| }); | |
| // Search Functionality | |
| let searchMatches = []; | |
| let currentMatch = 0; | |
| document.getElementById('searchInput').addEventListener('input', async (e) => { | |
| const query = e.target.value.toLowerCase(); | |
| const results = document.getElementById('searchResults'); | |
| if (query.length < 2) { | |
| results.textContent = ''; | |
| searchMatches = []; | |
| return; | |
| } | |
| searchMatches = []; | |
| // Search through all pages | |
| for (let i = 1; i <= state.pdfDoc.numPages; i++) { | |
| const page = await state.pdfDoc.getPage(i); | |
| const textContent = await page.getTextContent(); | |
| const text = textContent.items.map(item => item.str).join(' ').toLowerCase(); | |
| if (text.includes(query)) { | |
| searchMatches.push(i); | |
| } | |
| } | |
| if (searchMatches.length > 0) { | |
| results.textContent = `Found ${searchMatches.length} matches`; | |
| currentMatch = 0; | |
| } else { | |
| results.textContent = 'No matches found'; | |
| } | |
| }); | |
| document.getElementById('searchNextBtn').addEventListener('click', () => { | |
| if (searchMatches.length > 0) { | |
| currentMatch = (currentMatch + 1) % searchMatches.length; | |
| state.pageNum = searchMatches[currentMatch]; | |
| queueRenderPage(state.pageNum); | |
| document.getElementById('searchResults').textContent = | |
| `Match ${currentMatch + 1} of ${searchMatches.length} (Page ${searchMatches[currentMatch]})`; | |
| } | |
| }); | |
| document.getElementById('searchPrevBtn').addEventListener('click', () => { | |
| if (searchMatches.length > 0) { | |
| currentMatch = (currentMatch - 1 + searchMatches.length) % searchMatches.length; | |
| state.pageNum = searchMatches[currentMatch]; | |
| queueRenderPage(state.pageNum); | |
| document.getElementById('searchResults').textContent = | |
| `Match ${currentMatch + 1} of ${searchMatches.length} (Page ${searchMatches[currentMatch]})`; | |
| } | |
| }); | |
| // Mouse Wheel Zoom | |
| elements.canvasContainer.addEventListener('wheel', (e) => { | |
| if (e.ctrlKey) { | |
| e.preventDefault(); | |
| if (e.deltaY < 0) { | |
| zoomIn(); | |
| } else { | |
| zoomOut(); | |
| } | |
| } | |
| }); | |
| // Load PDF | |
| const url = 'Machine-Learning-Systems.pdf'; | |
| pdfjsLib.getDocument(url).promise.then(pdfDoc => { | |
| state.pdfDoc = pdfDoc; | |
| elements.pageCount.textContent = pdfDoc.numPages; | |
| elements.totalPagesDisplay.textContent = pdfDoc.numPages; | |
| elements.pageInput.max = pdfDoc.numPages; | |
| // Render the first page | |
| renderPage(state.pageNum); | |
| // Generate thumbnails | |
| generateThumbnails(); | |
| }).catch(error => { | |
| console.error('Error loading PDF:', error); | |
| elements.loadingOverlay.innerHTML = ` | |
| <div style="text-align: center;"> | |
| <h2 style="color: var(--color-accent-primary); margin-bottom: 1rem;">Error Loading PDF</h2> | |
| <p style="color: var(--color-text-secondary);">Please make sure the PDF file is in the same directory as this HTML file.</p> | |
| <p style="color: var(--color-text-muted); margin-top: 1rem; font-size: 0.875rem;">Looking for: ${url}</p> | |
| </div> | |
| `; | |
| }); | |
| // Generate Thumbnails | |
| let currentThumbnailCount = 20; | |
| const thumbnailsPerLoad = 20; | |
| async function generateThumbnails(startFrom = 1, count = 20) { | |
| const thumbnailsContainer = document.getElementById('thumbnails'); | |
| if (startFrom === 1) { | |
| thumbnailsContainer.innerHTML = '<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--color-text-secondary);">Pages</h3>'; | |
| } | |
| const endAt = Math.min(startFrom + count - 1, state.pdfDoc.numPages); | |
| for (let i = startFrom; i <= endAt; i++) { | |
| const page = await state.pdfDoc.getPage(i); | |
| const viewport = page.getViewport({ scale: 0.2 }); | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.height = viewport.height; | |
| canvas.width = viewport.width; | |
| await page.render({ | |
| canvasContext: ctx, | |
| viewport: viewport | |
| }).promise; | |
| const thumbnailDiv = document.createElement('div'); | |
| thumbnailDiv.className = 'thumbnail-item'; | |
| thumbnailDiv.dataset.page = i; | |
| // Create wrapper and label | |
| const wrapper = document.createElement('div'); | |
| wrapper.className = 'thumbnail-canvas-wrapper'; | |
| wrapper.appendChild(canvas); // Append the actual canvas object with drawing | |
| const label = document.createElement('div'); | |
| label.className = 'thumbnail-label'; | |
| label.textContent = `Page ${i}`; | |
| thumbnailDiv.appendChild(wrapper); | |
| thumbnailDiv.appendChild(label); | |
| thumbnailDiv.addEventListener('click', () => { | |
| state.pageNum = i; | |
| queueRenderPage(i); | |
| // Highlight selected thumbnail | |
| updateThumbnailSelection(); // Modified this line | |
| }); | |
| thumbnailsContainer.appendChild(thumbnailDiv); | |
| } | |
| // Remove existing "Show More" button if present | |
| const existingShowMore = thumbnailsContainer.querySelector('.show-more-btn'); | |
| if (existingShowMore) { | |
| existingShowMore.remove(); | |
| } | |
| // Add "Show More" button if there are more pages | |
| if (endAt < state.pdfDoc.numPages) { | |
| const showMoreDiv = document.createElement('div'); | |
| showMoreDiv.className = 'show-more-btn'; | |
| showMoreDiv.innerHTML = ` | |
| <button onclick="loadMoreThumbnails()"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M12 5V19M5 12L12 19L19 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| Show More (${state.pdfDoc.numPages - endAt} remaining) | |
| </button> | |
| `; | |
| thumbnailsContainer.appendChild(showMoreDiv); | |
| } | |
| // Highlight the current page | |
| updateThumbnailSelection(); | |
| } | |
| function loadMoreThumbnails() { | |
| const nextStart = currentThumbnailCount + 1; | |
| currentThumbnailCount += thumbnailsPerLoad; | |
| generateThumbnails(nextStart, thumbnailsPerLoad); | |
| } | |
| function updateThumbnailSelection() { | |
| document.querySelectorAll('.thumbnail-item').forEach(t => { | |
| if (parseInt(t.dataset.page) === state.pageNum) { | |
| t.classList.add('active'); | |
| // Scroll to the active thumbnail if it's not in view | |
| t.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| } else { | |
| t.classList.remove('active'); | |
| } | |
| }); | |
| } | |
| // Responsive adjustments | |
| window.addEventListener('resize', () => { | |
| if (window.innerWidth <= 768 && !elements.sidebar.classList.contains('hidden')) { | |
| elements.sidebar.classList.add('hidden'); | |
| } | |
| }); | |
| console.log('PDF Viewer initialized successfully!'); | |