VolariGlobal/MachineLearning
0
1// PDF.js Configuration
2pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
3
4// Theme Management
5const themeToggle = document.getElementById('themeToggle');
6const sunIcon = document.getElementById('sunIcon');
7const moonIcon = document.getElementById('moonIcon');
8
9// Initialize theme from localStorage or default to dark
10const currentTheme = localStorage.getItem('theme') || 'dark';
11if (currentTheme === 'light') {
12 document.documentElement.setAttribute('data-theme', 'light');
13 sunIcon.style.display = 'none';
14 moonIcon.style.display = 'block';
15}
16
17// Toggle theme function
18function toggleTheme() {
19 const currentTheme = document.documentElement.getAttribute('data-theme');
20 const newTheme = currentTheme === 'light' ? 'dark' : 'light';
21
22 document.documentElement.setAttribute('data-theme', newTheme);
23 localStorage.setItem('theme', newTheme);
24
25 // Toggle icons
26 if (newTheme === 'light') {
27 sunIcon.style.display = 'none';
28 moonIcon.style.display = 'block';
29 } else {
30 sunIcon.style.display = 'block';
31 moonIcon.style.display = 'none';
32 }
33}
34
35// State Management
36const state = {
37 pdfDoc: null,
38 pageNum: 1,
39 pageRendering: false,
40 pageNumPending: null,
41 scale: 1.5,
42 canvas: document.getElementById('pdfCanvas'),
43 ctx: null
44};
45
46// Initialize
47state.ctx = state.canvas.getContext('2d');
48
49// DOM Elements
50const elements = {
51 loadingOverlay: document.getElementById('loadingOverlay'),
52 pageNum: document.getElementById('pageNum'),
53 pageCount: document.getElementById('pageCount'),
54 currentPageDisplay: document.getElementById('currentPageDisplay'),
55 totalPagesDisplay: document.getElementById('totalPagesDisplay'),
56 zoomLevel: document.getElementById('zoomLevel'),
57 prevPageBtn: document.getElementById('prevPageBtn'),
58 nextPageBtn: document.getElementById('nextPageBtn'),
59 zoomInBtn: document.getElementById('zoomInBtn'),
60 zoomOutBtn: document.getElementById('zoomOutBtn'),
61 fitWidthBtn: document.getElementById('fitWidthBtn'),
62 pageInput: document.getElementById('pageInput'),
63 goToPageBtn: document.getElementById('goToPageBtn'),
64 sidebar: document.getElementById('sidebar'),
65 sidebarToggle: document.getElementById('sidebarToggle'),
66 closeSidebarBtn: document.getElementById('closeSidebarBtn'),
67 searchBtn: document.getElementById('searchBtn'),
68 searchPanel: document.getElementById('searchPanel'),
69 closeSearchBtn: document.getElementById('closeSearchBtn'),
70 fullscreenBtn: document.getElementById('fullscreenBtn'),
71 canvasContainer: document.getElementById('canvasContainer')
72};
73
74// Render Page
75function renderPage(num) {
76 state.pageRendering = true;
77
78 state.pdfDoc.getPage(num).then(page => {
79 const viewport = page.getViewport({ scale: state.scale });
80 state.canvas.height = viewport.height;
81 state.canvas.width = viewport.width;
82
83 const renderContext = {
84 canvasContext: state.ctx,
85 viewport: viewport
86 };
87
88 const renderTask = page.render(renderContext);
89
90 renderTask.promise.then(() => {
91 state.pageRendering = false;
92
93 if (state.pageNumPending !== null) {
94 renderPage(state.pageNumPending);
95 state.pageNumPending = null;
96 }
97
98 // Update UI
99 updatePageDisplay();
100
101 // Hide loading overlay on first render
102 if (num === 1) {
103 elements.loadingOverlay.classList.add('hidden');
104 }
105 });
106 });
107
108 // Update page display immediately
109 elements.pageNum.textContent = num;
110 elements.currentPageDisplay.textContent = num;
111}
112
113// Queue Page Render
114function queueRenderPage(num) {
115 if (state.pageRendering) {
116 state.pageNumPending = num;
117 } else {
118 renderPage(num);
119 }
120}
121
122// Update Page Display
123function updatePageDisplay() {
124 elements.zoomLevel.textContent = Math.round(state.scale * 100) + '%';
125 updateThumbnailSelection();
126}
127
128// Navigate to Previous Page
129function onPrevPage() {
130 if (state.pageNum <= 1) {
131 return;
132 }
133 state.pageNum--;
134 queueRenderPage(state.pageNum);
135}
136
137// Navigate to Next Page
138function onNextPage() {
139 if (state.pageNum >= state.pdfDoc.numPages) {
140 return;
141 }
142 state.pageNum++;
143 queueRenderPage(state.pageNum);
144}
145
146// Zoom In
147function zoomIn() {
148 if (state.scale < 3) {
149 state.scale += 0.25;
150 queueRenderPage(state.pageNum);
151 }
152}
153
154// Zoom Out
155function zoomOut() {
156 if (state.scale > 0.5) {
157 state.scale -= 0.25;
158 queueRenderPage(state.pageNum);
159 }
160}
161
162// Fit to Width
163function fitToWidth() {
164 const containerWidth = elements.canvasContainer.clientWidth - 40;
165 state.pdfDoc.getPage(state.pageNum).then(page => {
166 const viewport = page.getViewport({ scale: 1 });
167 state.scale = containerWidth / viewport.width;
168 queueRenderPage(state.pageNum);
169 });
170}
171
172// Go to Page
173function goToPage() {
174 const pageNumber = parseInt(elements.pageInput.value);
175 if (pageNumber >= 1 && pageNumber <= state.pdfDoc.numPages) {
176 state.pageNum = pageNumber;
177 queueRenderPage(state.pageNum);
178 elements.pageInput.value = '';
179 }
180}
181
182// Toggle Sidebar
183function toggleSidebar() {
184 elements.sidebar.classList.toggle('hidden');
185}
186
187// Toggle Search
188function toggleSearch() {
189 elements.searchPanel.classList.toggle('active');
190 if (elements.searchPanel.classList.contains('active')) {
191 document.getElementById('searchInput').focus();
192 }
193}
194
195// Toggle Fullscreen
196function toggleFullscreen() {
197 if (!document.fullscreenElement) {
198 document.documentElement.requestFullscreen();
199 } else {
200 if (document.exitFullscreen) {
201 document.exitFullscreen();
202 }
203 }
204}
205
206// Event Listeners
207themeToggle.addEventListener('click', toggleTheme);
208elements.prevPageBtn.addEventListener('click', onPrevPage);
209elements.nextPageBtn.addEventListener('click', onNextPage);
210elements.zoomInBtn.addEventListener('click', zoomIn);
211elements.zoomOutBtn.addEventListener('click', zoomOut);
212elements.fitWidthBtn.addEventListener('click', fitToWidth);
213elements.goToPageBtn.addEventListener('click', goToPage);
214elements.sidebarToggle.addEventListener('click', toggleSidebar);
215elements.closeSidebarBtn.addEventListener('click', toggleSidebar);
216elements.searchBtn.addEventListener('click', toggleSearch);
217elements.closeSearchBtn.addEventListener('click', toggleSearch);
218elements.fullscreenBtn.addEventListener('click', toggleFullscreen);
219
220// Enter key for page input
221elements.pageInput.addEventListener('keypress', (e) => {
222 if (e.key === 'Enter') {
223 goToPage();
224 }
225});
226
227// Keyboard Shortcuts
228document.addEventListener('keydown', (e) => {
229 // Arrow keys for navigation
230 if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
231 e.preventDefault();
232 onPrevPage();
233 } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
234 e.preventDefault();
235 onNextPage();
236 }
237
238 // Zoom shortcuts
239 if (e.key === '+' || e.key === '=') {
240 e.preventDefault();
241 zoomIn();
242 } else if (e.key === '-') {
243 e.preventDefault();
244 zoomOut();
245 }
246
247 // Search shortcut
248 if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
249 e.preventDefault();
250 toggleSearch();
251 }
252
253 // Fullscreen shortcut
254 if (e.key === 'f' && !e.ctrlKey && !e.metaKey) {
255 const activeElement = document.activeElement;
256 if (activeElement.tagName !== 'INPUT') {
257 toggleFullscreen();
258 }
259 }
260});
261
262// Search Functionality
263let searchMatches = [];
264let currentMatch = 0;
265
266document.getElementById('searchInput').addEventListener('input', async (e) => {
267 const query = e.target.value.toLowerCase();
268 const results = document.getElementById('searchResults');
269
270 if (query.length < 2) {
271 results.textContent = '';
272 searchMatches = [];
273 return;
274 }
275
276 searchMatches = [];
277
278 // Search through all pages
279 for (let i = 1; i <= state.pdfDoc.numPages; i++) {
280 const page = await state.pdfDoc.getPage(i);
281 const textContent = await page.getTextContent();
282 const text = textContent.items.map(item => item.str).join(' ').toLowerCase();
283
284 if (text.includes(query)) {
285 searchMatches.push(i);
286 }
287 }
288
289 if (searchMatches.length > 0) {
290 results.textContent = `Found ${searchMatches.length} matches`;
291 currentMatch = 0;
292 } else {
293 results.textContent = 'No matches found';
294 }
295});
296
297document.getElementById('searchNextBtn').addEventListener('click', () => {
298 if (searchMatches.length > 0) {
299 currentMatch = (currentMatch + 1) % searchMatches.length;
300 state.pageNum = searchMatches[currentMatch];
301 queueRenderPage(state.pageNum);
302 document.getElementById('searchResults').textContent =
303 `Match ${currentMatch + 1} of ${searchMatches.length} (Page ${searchMatches[currentMatch]})`;
304 }
305});
306
307document.getElementById('searchPrevBtn').addEventListener('click', () => {
308 if (searchMatches.length > 0) {
309 currentMatch = (currentMatch - 1 + searchMatches.length) % searchMatches.length;
310 state.pageNum = searchMatches[currentMatch];
311 queueRenderPage(state.pageNum);
312 document.getElementById('searchResults').textContent =
313 `Match ${currentMatch + 1} of ${searchMatches.length} (Page ${searchMatches[currentMatch]})`;
314 }
315});
316
317// Mouse Wheel Zoom
318elements.canvasContainer.addEventListener('wheel', (e) => {
319 if (e.ctrlKey) {
320 e.preventDefault();
321 if (e.deltaY < 0) {
322 zoomIn();
323 } else {
324 zoomOut();
325 }
326 }
327});
328
329// Load PDF
330const url = 'Machine-Learning-Systems.pdf';
331
332pdfjsLib.getDocument(url).promise.then(pdfDoc => {
333 state.pdfDoc = pdfDoc;
334 elements.pageCount.textContent = pdfDoc.numPages;
335 elements.totalPagesDisplay.textContent = pdfDoc.numPages;
336 elements.pageInput.max = pdfDoc.numPages;
337
338 // Render the first page
339 renderPage(state.pageNum);
340
341 // Generate thumbnails
342 generateThumbnails();
343}).catch(error => {
344 console.error('Error loading PDF:', error);
345 elements.loadingOverlay.innerHTML = `
346 <div style="text-align: center;">
347 <h2 style="color: var(--color-accent-primary); margin-bottom: 1rem;">Error Loading PDF</h2>
348 <p style="color: var(--color-text-secondary);">Please make sure the PDF file is in the same directory as this HTML file.</p>
349 <p style="color: var(--color-text-muted); margin-top: 1rem; font-size: 0.875rem;">Looking for: ${url}</p>
350 </div>
351 `;
352});
353
354// Generate Thumbnails
355let currentThumbnailCount = 20;
356const thumbnailsPerLoad = 20;
357
358async function generateThumbnails(startFrom = 1, count = 20) {
359 const thumbnailsContainer = document.getElementById('thumbnails');
360
361 if (startFrom === 1) {
362 thumbnailsContainer.innerHTML = '<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--color-text-secondary);">Pages</h3>';
363 }
364
365 const endAt = Math.min(startFrom + count - 1, state.pdfDoc.numPages);
366
367 for (let i = startFrom; i <= endAt; i++) {
368 const page = await state.pdfDoc.getPage(i);
369 const viewport = page.getViewport({ scale: 0.2 });
370
371 const canvas = document.createElement('canvas');
372 const ctx = canvas.getContext('2d');
373 canvas.height = viewport.height;
374 canvas.width = viewport.width;
375
376 await page.render({
377 canvasContext: ctx,
378 viewport: viewport
379 }).promise;
380
381 const thumbnailDiv = document.createElement('div');
382 thumbnailDiv.className = 'thumbnail-item';
383 thumbnailDiv.dataset.page = i;
384
385 // Create wrapper and label
386 const wrapper = document.createElement('div');
387 wrapper.className = 'thumbnail-canvas-wrapper';
388 wrapper.appendChild(canvas); // Append the actual canvas object with drawing
389
390 const label = document.createElement('div');
391 label.className = 'thumbnail-label';
392 label.textContent = `Page ${i}`;
393
394 thumbnailDiv.appendChild(wrapper);
395 thumbnailDiv.appendChild(label);
396
397 thumbnailDiv.addEventListener('click', () => {
398 state.pageNum = i;
399 queueRenderPage(i);
400 // Highlight selected thumbnail
401 updateThumbnailSelection(); // Modified this line
402 });
403
404 thumbnailsContainer.appendChild(thumbnailDiv);
405 }
406
407 // Remove existing "Show More" button if present
408 const existingShowMore = thumbnailsContainer.querySelector('.show-more-btn');
409 if (existingShowMore) {
410 existingShowMore.remove();
411 }
412
413 // Add "Show More" button if there are more pages
414 if (endAt < state.pdfDoc.numPages) {
415 const showMoreDiv = document.createElement('div');
416 showMoreDiv.className = 'show-more-btn';
417 showMoreDiv.innerHTML = `
418 <button onclick="loadMoreThumbnails()">
419 <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
420 <path d="M12 5V19M5 12L12 19L19 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
421 </svg>
422 Show More (${state.pdfDoc.numPages - endAt} remaining)
423 </button>
424 `;
425 thumbnailsContainer.appendChild(showMoreDiv);
426 }
427
428 // Highlight the current page
429 updateThumbnailSelection();
430}
431
432function loadMoreThumbnails() {
433 const nextStart = currentThumbnailCount + 1;
434 currentThumbnailCount += thumbnailsPerLoad;
435 generateThumbnails(nextStart, thumbnailsPerLoad);
436}
437
438function updateThumbnailSelection() {
439 document.querySelectorAll('.thumbnail-item').forEach(t => {
440 if (parseInt(t.dataset.page) === state.pageNum) {
441 t.classList.add('active');
442 // Scroll to the active thumbnail if it's not in view
443 t.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
444 } else {
445 t.classList.remove('active');
446 }
447 });
448}
449
450// Responsive adjustments
451window.addEventListener('resize', () => {
452 if (window.innerWidth <= 768 && !elements.sidebar.classList.contains('hidden')) {
453 elements.sidebar.classList.add('hidden');
454 }
455});
456
457console.log('PDF Viewer initialized successfully!');
458 