navidocs/thumbnail_implementation.js

186 lines
6.4 KiB
JavaScript

// ============================================================================
// THUMBNAIL GENERATION IMPLEMENTATION FOR NAVIDOCS DOCUMENTVIEW
// Agent 7 of 10 - Apple Preview-style Search with Page Thumbnails
// ============================================================================
// Add these state variables after line 356 (after searchStats computed property):
// Thumbnail cache and state for search results
const thumbnailCache = new Map() // pageNum -> dataURL
const thumbnailLoading = ref(new Set()) // Track which thumbnails are currently loading
// ============================================================================
// Add these functions after makeTocEntriesClickable() function (around line 837):
/**
* Generate thumbnail for a specific page
* @param {number} pageNum - Page number to generate thumbnail for
* @returns {Promise<string>} Data URL of the thumbnail image
*/
async function generateThumbnail(pageNum) {
// Check cache first
if (thumbnailCache.has(pageNum)) {
return thumbnailCache.get(pageNum)
}
// Check if already loading
if (thumbnailLoading.value.has(pageNum)) {
// Wait for the thumbnail to be generated
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (thumbnailCache.has(pageNum)) {
clearInterval(checkInterval)
resolve(thumbnailCache.get(pageNum))
}
}, 100)
})
}
// Mark as loading
thumbnailLoading.value.add(pageNum)
try {
if (!pdfDoc) {
throw new Error('PDF document not loaded')
}
const page = await pdfDoc.getPage(pageNum)
// Use small scale for thumbnail (0.2 = 20% of original size)
// This produces roughly 80x100px thumbnails for standard letter-sized pages
const viewport = page.getViewport({ scale: 0.2 })
// Create canvas for thumbnail rendering
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d', { alpha: false })
if (!context) {
throw new Error('Failed to get canvas context for thumbnail')
}
canvas.width = viewport.width
canvas.height = viewport.height
// Render page to canvas
await page.render({
canvasContext: context,
viewport: viewport
}).promise
// Convert canvas to data URL
const dataURL = canvas.toDataURL('image/png', 0.8) // 0.8 quality for smaller file size
// Cache the thumbnail
thumbnailCache.set(pageNum, dataURL)
return dataURL
} catch (err) {
console.error(`Failed to generate thumbnail for page ${pageNum}:`, err)
// Return a placeholder or empty data URL
return ''
} finally {
// Remove from loading set
thumbnailLoading.value.delete(pageNum)
}
}
/**
* Check if a thumbnail is currently being generated
* @param {number} pageNum - Page number to check
* @returns {boolean} True if thumbnail is loading
*/
function isThumbnailLoading(pageNum) {
return thumbnailLoading.value.has(pageNum)
}
/**
* Get thumbnail for a page, generating if needed
* @param {number} pageNum - Page number
* @returns {Promise<string>} Data URL of thumbnail
*/
async function getThumbnail(pageNum) {
return await generateThumbnail(pageNum)
}
/**
* Clear thumbnail cache (useful when document changes)
*/
function clearThumbnailCache() {
thumbnailCache.clear()
thumbnailLoading.value.clear()
}
// ============================================================================
// TEMPLATE USAGE EXAMPLE
// ============================================================================
// Add this to your template where search results are displayed:
/*
<template>
<!-- Jump List with Thumbnails -->
<div v-if="jumpListOpen && hitList.length > 0" class="search-results-sidebar">
<div class="grid gap-2 max-h-96 overflow-y-auto">
<button
v-for="(hit, idx) in hitList.slice(0, 10)"
:key="idx"
@click="jumpToHit(idx)"
class="search-result-item flex gap-3 p-2 bg-white/5 hover:bg-white/10 rounded transition-colors border border-white/10"
:class="{ 'ring-2 ring-pink-400': idx === currentHitIndex }"
>
<!-- Thumbnail -->
<div class="thumbnail-container flex-shrink-0">
<div
v-if="isThumbnailLoading(hit.page)"
class="w-20 h-25 bg-white/10 rounded flex items-center justify-center"
>
<div class="w-4 h-4 border-2 border-white/30 border-t-pink-400 rounded-full animate-spin"></div>
</div>
<img
v-else
:src="getThumbnail(hit.page)"
alt="`Page ${hit.page} thumbnail`"
class="w-20 h-auto rounded shadow-md"
loading="lazy"
/>
</div>
<!-- Match Info -->
<div class="flex-1 text-left">
<div class="flex items-center justify-between gap-2 mb-1">
<span class="text-white/70 text-xs font-mono">Match {{ idx + 1 }}</span>
<span class="text-white/50 text-xs">Page {{ hit.page }}</span>
</div>
<p class="text-white text-sm line-clamp-2">{{ hit.snippet }}</p>
</div>
</button>
</div>
</div>
</template>
*/
// ============================================================================
// USAGE NOTES
// ============================================================================
/*
1. Thumbnails are automatically cached after first generation
2. Multiple requests for the same page thumbnail will wait for the first to complete
3. Call clearThumbnailCache() when switching documents or on cleanup
4. Thumbnails are generated at 0.2 scale (20% of original size) for performance
5. The loading state is tracked in thumbnailLoading Set for UI feedback
6. getThumbnail() is the main function to call from templates (async)
7. isThumbnailLoading() checks if a thumbnail is currently being generated
INTEGRATION STEPS:
1. Add state variables (thumbnailCache, thumbnailLoading) around line 380
2. Add functions (generateThumbnail, isThumbnailLoading, getThumbnail, clearThumbnailCache) around line 837
3. Update template to show thumbnails in search results
4. Call clearThumbnailCache() in resetDocumentState() function
5. Make getThumbnail and isThumbnailLoading available in template (expose via return or export)
PERFORMANCE CONSIDERATIONS:
- Thumbnails are generated on-demand (lazy loading)
- Cache prevents regeneration for visited pages
- Small scale (0.2) keeps memory usage low
- PNG quality set to 0.8 for balance between size and quality
- Loading state prevents duplicate generation requests
*/