fixed viewer and folder issues
This commit is contained in:
+57
-5
@@ -185,9 +185,56 @@ const fetchJson = async (url: string, token: string) => {
|
||||
};
|
||||
|
||||
const resolveShareLink = async (link: string, token: string) => {
|
||||
const encoded = `u!${b64Url(link)}`;
|
||||
const data = await fetchJson(`https://graph.microsoft.com/v1.0/shares/${encoded}/driveItem`, token);
|
||||
return { driveId: data.parentReference?.driveId as string, itemId: data.id as string };
|
||||
// Decode the link in case it comes pre-encoded from the database
|
||||
const decodedLink = decodeURIComponent(link);
|
||||
console.log('[resolveShareLink] Decoded link:', decodedLink);
|
||||
|
||||
// Check if this is a short sharing link (/:f:/ or /:b:/) or a direct document library URL
|
||||
if (decodedLink.includes('/:f:/') || decodedLink.includes('/:b:/')) {
|
||||
// Short sharing link - use shares API
|
||||
const encoded = `u!${b64Url(decodedLink)}`;
|
||||
console.log('[resolveShareLink] Using shares API, encoded:', encoded);
|
||||
const data = await fetchJson(`https://graph.microsoft.com/v1.0/shares/${encoded}/driveItem`, token);
|
||||
return { driveId: data.parentReference?.driveId as string, itemId: data.id as string };
|
||||
} else {
|
||||
// Direct document library URL - parse it and use drive API
|
||||
// Example: https://czflex.sharepoint.com/sites/Team/Shared Documents/Forms/AllItems.aspx?RootFolder=/sites/Team/Shared Documents/General/...
|
||||
const url = new URL(decodedLink);
|
||||
const pathMatch = url.hostname.match(/^([^.]+)\.sharepoint\.com$/);
|
||||
if (!pathMatch) throw new Error('Invalid SharePoint URL');
|
||||
|
||||
const hostname = pathMatch[1]; // e.g., 'czflex'
|
||||
const sitePath = url.pathname.split('/Shared')[0]; // e.g., /sites/Team
|
||||
const rootFolderParam = url.searchParams.get('RootFolder');
|
||||
if (!rootFolderParam) throw new Error('RootFolder parameter missing');
|
||||
|
||||
// Extract the folder path relative to the document library
|
||||
// RootFolder format: /sites/Team/Shared Documents/General/Operations [Server]/...
|
||||
// We need: General/Operations [Server]/...
|
||||
const folderPath = rootFolderParam.split('/Shared Documents/')[1];
|
||||
if (!folderPath) throw new Error('Could not parse folder path');
|
||||
|
||||
console.log('[resolveShareLink] Hostname:', hostname);
|
||||
console.log('[resolveShareLink] Site path:', sitePath);
|
||||
console.log('[resolveShareLink] Folder path:', folderPath);
|
||||
|
||||
// Get the site ID first using hostname:sitePath format
|
||||
const siteData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${hostname}.sharepoint.com:${sitePath}`, token);
|
||||
const siteId = siteData.id;
|
||||
|
||||
// Get the default document library drive
|
||||
const driveData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${siteId}/drive`, token);
|
||||
const driveId = driveData.id;
|
||||
|
||||
// Get the folder item by path
|
||||
const itemData = await fetchJson(
|
||||
`https://graph.microsoft.com/v1.0/drives/${driveId}/root:/${folderPath}`,
|
||||
token
|
||||
);
|
||||
|
||||
console.log('[resolveShareLink] Resolved to driveId:', driveId, 'itemId:', itemData.id);
|
||||
return { driveId, itemId: itemData.id };
|
||||
}
|
||||
};
|
||||
|
||||
const listChildrenRecursive = async (
|
||||
@@ -225,9 +272,12 @@ const filterByCategory = (items: DriveItem[], category?: string, pdfOnly: boolea
|
||||
if (i.folder) return false;
|
||||
const name = i.name.toLowerCase();
|
||||
const mimeType = i.file?.mimeType?.toLowerCase() || '';
|
||||
// Include PDFs and Word documents
|
||||
// Include PDFs, Word documents, and images
|
||||
return name.endsWith('.pdf') || mimeType.includes('pdf') ||
|
||||
name.endsWith('.doc') || name.endsWith('.docx') || mimeType.includes('word');
|
||||
name.endsWith('.doc') || name.endsWith('.docx') || mimeType.includes('word') ||
|
||||
name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') ||
|
||||
name.endsWith('.gif') || name.endsWith('.bmp') || name.endsWith('.tiff') ||
|
||||
name.endsWith('.webp') || mimeType.includes('image');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -274,6 +324,7 @@ app.get('/api/job-files', async (c) => {
|
||||
// Filter to show only PDFs by default (can be disabled with pdfOnly=false query param)
|
||||
const pdfOnly = c.req.query('pdfOnly') !== 'false';
|
||||
const filtered = filterByCategory(items, category, pdfOnly);
|
||||
console.log(`[job-files] Found ${items.length} total items, ${filtered.length} after filtering`);
|
||||
|
||||
const mapped = filtered.map((i) => ({
|
||||
id: i.id,
|
||||
@@ -291,6 +342,7 @@ app.get('/api/job-files', async (c) => {
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
console.error('[job-files] ERROR:', message);
|
||||
logLine('logs/error.log', `/api/job-files error: ${message}`);
|
||||
return c.json({ error: message }, status);
|
||||
}
|
||||
|
||||
+183
-26
@@ -67,7 +67,7 @@
|
||||
<div class="relative mb-2">
|
||||
<input id="searchBox" placeholder="Search jobs (just start typing or use voice)" class="w-full p-2.5 pr-12 rounded-lg border border-gray-300 text-lg placeholder:text-gray-400">
|
||||
<button id="voiceSearchBtn" class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full flex items-center justify-center hover:bg-gray-100 transition-colors" title="Voice search">
|
||||
<svg id="micIcon" class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg id="micIcon" class="w-5 h-5 text-gray-600 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path>
|
||||
</svg>
|
||||
<svg id="micRecordingIcon" class="hidden w-5 h-5 text-red-600 animate-pulse" fill="currentColor" viewBox="0 0 24 24">
|
||||
@@ -374,8 +374,17 @@
|
||||
// Load version from backend /version endpoint so UI matches package.json
|
||||
async function loadVersion(){
|
||||
if(!versionLabel) return;
|
||||
// Display "Redis" for the Redis implementation branch
|
||||
versionLabel.textContent = 'Redis';
|
||||
try{
|
||||
const res = await fetch('/version');
|
||||
if(!res.ok) {
|
||||
console.warn(`Version endpoint returned ${res.status}. Using default version.`);
|
||||
return;
|
||||
}
|
||||
const data = await res.json().catch(()=>null);
|
||||
if(data?.version) versionLabel.textContent = `v${data.version}`;
|
||||
}catch(err){
|
||||
console.debug('Version load failed (non-critical):', err.message);
|
||||
}
|
||||
}
|
||||
loadVersion();
|
||||
|
||||
@@ -478,7 +487,32 @@
|
||||
}
|
||||
|
||||
function applyFiltersAndRender(){
|
||||
const q = safeLower(searchBox.value||'');
|
||||
let q = safeLower(searchBox.value||'');
|
||||
|
||||
// Check for recency keywords
|
||||
const recencyKeywords = ['most recent', 'newest', 'latest', 'current', 'recent'];
|
||||
let sortByRecent = false;
|
||||
|
||||
// Check if query contains any recency keywords (check longer phrases first)
|
||||
for (const keyword of recencyKeywords) {
|
||||
if (q.includes(keyword)) {
|
||||
sortByRecent = true;
|
||||
// Remove the keyword from search query (use global replace)
|
||||
const regex = new RegExp(keyword, 'gi');
|
||||
q = q.replace(regex, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up multiple spaces and trim
|
||||
q = q.replace(/\s+/g, ' ').trim();
|
||||
|
||||
// Update the display to show cleaned query
|
||||
if (sortByRecent && searchBox.value !== q) {
|
||||
searchBox.value = q.split(' ').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join(' ');
|
||||
}
|
||||
|
||||
const divisionVals = Array.from(document.querySelectorAll('.filter-division:checked')).map(i=>i.value);
|
||||
const activeVals = Array.from(document.querySelectorAll('.filter-active:checked')).map(i=>i.value);
|
||||
const activeWanted = activeVals.map(v=>toBool(v)).filter(v=>v!==null);
|
||||
@@ -503,11 +537,31 @@
|
||||
return true;
|
||||
});
|
||||
|
||||
// Data already sorted from PocketBase, only re-sort if filters applied
|
||||
if(q || divisionVals.length || activeVals.length || statusVals.length || estimatorVals.length){
|
||||
// If recency keyword used, show only THE most recent match
|
||||
if(sortByRecent && visibleJobs.length > 0){
|
||||
console.log('Before sorting, visibleJobs count:', visibleJobs.length);
|
||||
|
||||
// Sort by Job_Number (higher number = newer job)
|
||||
visibleJobs.sort((a, b) => {
|
||||
const numA = jobNumberValue(a) || 0;
|
||||
const numB = jobNumberValue(b) || 0;
|
||||
return numB - numA; // Higher job number first (newest)
|
||||
});
|
||||
|
||||
console.log('After sorting, newest job:', visibleJobs[0]?.Job_Number);
|
||||
// Keep only the most recent
|
||||
visibleJobs = visibleJobs.slice(0, 1);
|
||||
console.log('✓ Showing only the most recent match');
|
||||
} else if(q || divisionVals.length || activeVals.length || statusVals.length || estimatorVals.length){
|
||||
sortJobsDescending(visibleJobs);
|
||||
}
|
||||
console.log('Filter summary', { q, divisionVals, activeWanted, statusVals, estimatorValsLower, total: jobsCache.length, visible: visibleJobs.length });
|
||||
|
||||
console.log('Filter summary', {
|
||||
originalSearch: searchBox.value,
|
||||
cleanedQ: q,
|
||||
sortByRecent,
|
||||
resultsCount: visibleJobs.length
|
||||
});
|
||||
renderCards(visibleJobs);
|
||||
}
|
||||
|
||||
@@ -600,9 +654,17 @@
|
||||
|
||||
let autoStopTimer = null;
|
||||
let silenceTimer = null;
|
||||
let lastResultIndex = 0;
|
||||
let baseSearchText = '';
|
||||
|
||||
recognition.onstart = () => {
|
||||
isListening = true;
|
||||
lastResultIndex = 0;
|
||||
baseSearchText = searchBox.value.trim();
|
||||
|
||||
// Change mic icon to filled maroon/red
|
||||
micIcon.style.fill = '#991b1b'; // Tailwind red-800
|
||||
micIcon.style.stroke = '#991b1b';
|
||||
|
||||
// Auto-stop after 10 seconds as a safety measure
|
||||
autoStopTimer = setTimeout(() => {
|
||||
@@ -619,17 +681,40 @@
|
||||
silenceTimer = null;
|
||||
}
|
||||
|
||||
const last = event.results.length - 1;
|
||||
const transcript = event.results[last][0].transcript;
|
||||
const isFinal = event.results[last].isFinal;
|
||||
// Process only new final results
|
||||
for (let i = lastResultIndex; i < event.results.length; i++) {
|
||||
if (event.results[i].isFinal) {
|
||||
let transcript = event.results[i][0].transcript.trim();
|
||||
|
||||
searchBox.value = transcript.trim();
|
||||
// Remove trailing punctuation (period, comma, etc.)
|
||||
transcript = transcript.replace(/[.,!?;:]+$/, '');
|
||||
|
||||
// Capitalize each word (title case)
|
||||
transcript = transcript.split(' ').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
||||
).join(' ');
|
||||
|
||||
if (transcript) {
|
||||
// Add space if base text exists and doesn't end with space
|
||||
if (baseSearchText && !baseSearchText.endsWith(' ')) {
|
||||
baseSearchText += ' ';
|
||||
}
|
||||
baseSearchText += transcript;
|
||||
}
|
||||
|
||||
lastResultIndex = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Update search box with accumulated text
|
||||
searchBox.value = baseSearchText;
|
||||
|
||||
// Trigger search with results
|
||||
applyFiltersAndRender();
|
||||
|
||||
// Stop after final result with short delay
|
||||
if (isFinal) {
|
||||
// Check if all results are final, if so prepare to stop
|
||||
const allFinal = Array.from(event.results).every(r => r.isFinal);
|
||||
if (allFinal && event.results.length > 0) {
|
||||
silenceTimer = setTimeout(() => {
|
||||
recognition.stop();
|
||||
}, 1500);
|
||||
@@ -663,6 +748,10 @@
|
||||
if (autoStopTimer) clearTimeout(autoStopTimer);
|
||||
if (silenceTimer) clearTimeout(silenceTimer);
|
||||
|
||||
// Reset mic icon to normal
|
||||
micIcon.style.fill = 'none';
|
||||
micIcon.style.stroke = '';
|
||||
|
||||
isListening = false;
|
||||
};
|
||||
|
||||
@@ -1310,7 +1399,11 @@
|
||||
window.openFileInViewer = openFileInViewer;
|
||||
|
||||
async function renderPdfPage() {
|
||||
const { doc, page, scale } = currentPdf;
|
||||
const { doc, page, scale, isImage } = currentPdf;
|
||||
if (isImage) {
|
||||
renderImageOnCanvas();
|
||||
return;
|
||||
}
|
||||
if (!doc) return;
|
||||
const pageObj = await doc.getPage(page);
|
||||
const viewport = pageObj.getViewport({ scale });
|
||||
@@ -1328,10 +1421,33 @@
|
||||
pdfZoomReset.textContent = `${Math.round(currentPdf.scale * 100)}%`;
|
||||
}
|
||||
|
||||
function renderImageOnCanvas() {
|
||||
const { scale, imageElement, imageWidth, imageHeight } = currentPdf;
|
||||
if (!imageElement) return;
|
||||
const canvas = pdfCanvas;
|
||||
const context = canvas.getContext('2d');
|
||||
const scaledWidth = imageWidth * scale;
|
||||
const scaledHeight = imageHeight * scale;
|
||||
canvas.width = scaledWidth;
|
||||
canvas.height = scaledHeight;
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.drawImage(imageElement, 0, 0, scaledWidth, scaledHeight);
|
||||
// re-apply pan transform after render
|
||||
pdfCanvas.style.transform = `translate(${pdfPan.x}px, ${pdfPan.y}px)`;
|
||||
pdfZoomLabel.textContent = `${Math.round(scale * 100)}%`;
|
||||
pdfZoomReset.textContent = `${Math.round(scale * 100)}%`;
|
||||
}
|
||||
|
||||
function showPdfViewer() {
|
||||
iframeViewer.classList.add('hidden');
|
||||
pdfViewer.classList.remove('hidden');
|
||||
iframeLoader.classList.add('hidden');
|
||||
// Ensure PDF controls are visible (might be hidden for images)
|
||||
pdfControls.style.display = '';
|
||||
// Show page navigation for PDFs
|
||||
pdfPrev.parentElement.style.display = '';
|
||||
pdfNext.parentElement.style.display = '';
|
||||
pdfPageNum.parentElement.style.display = '';
|
||||
}
|
||||
|
||||
pdfPrev.addEventListener('click', async () => {
|
||||
@@ -1347,18 +1463,18 @@
|
||||
await renderPdfPage();
|
||||
});
|
||||
pdfZoomIn.addEventListener('click', async () => {
|
||||
if (!currentPdf.doc) return;
|
||||
if (!currentPdf.doc && !currentPdf.isImage) return;
|
||||
currentPdf.scale = Math.min(currentPdf.scale + 0.1, MAX_PDF_SCALE);
|
||||
await renderPdfPage();
|
||||
});
|
||||
pdfZoomOut.addEventListener('click', async () => {
|
||||
if (!currentPdf.doc) return;
|
||||
if (!currentPdf.doc && !currentPdf.isImage) return;
|
||||
currentPdf.scale = Math.max(currentPdf.scale - 0.1, MIN_PDF_SCALE);
|
||||
await renderPdfPage();
|
||||
});
|
||||
pdfZoomReset.addEventListener('click', async () => {
|
||||
if (!currentPdf.doc) return;
|
||||
currentPdf.scale = DEFAULT_PDF_SCALE;
|
||||
if (!currentPdf.doc && !currentPdf.isImage) return;
|
||||
currentPdf.scale = currentPdf.isImage ? 1 : DEFAULT_PDF_SCALE;
|
||||
pdfPan = { x: 0, y: 0, dragging: false, startX: 0, startY: 0, baseX: 0, baseY: 0 };
|
||||
pdfCanvas.style.transform = 'translate(0px, 0px)';
|
||||
await renderPdfPage();
|
||||
@@ -1383,7 +1499,7 @@
|
||||
};
|
||||
|
||||
pdfCanvas.addEventListener('wheel', async (e) => {
|
||||
if (!currentPdf.doc) return;
|
||||
if (!currentPdf.doc && !currentPdf.isImage) return;
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY;
|
||||
const step = 0.1;
|
||||
@@ -1403,7 +1519,7 @@
|
||||
fileListContainer.classList.remove('hidden');
|
||||
});
|
||||
pdfCanvas.addEventListener('pointerdown', (e) => {
|
||||
if (!currentPdf.doc) return;
|
||||
if (!currentPdf.doc && !currentPdf.isImage) return;
|
||||
activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||
|
||||
if (activePointers.size === 2) {
|
||||
@@ -1423,7 +1539,7 @@
|
||||
});
|
||||
|
||||
pdfCanvas.addEventListener('pointermove', (e) => {
|
||||
if (!currentPdf.doc) return;
|
||||
if (!currentPdf.doc && !currentPdf.isImage) return;
|
||||
|
||||
if (activePointers.has(e.pointerId)) {
|
||||
activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||
@@ -1523,6 +1639,9 @@
|
||||
|
||||
// Word docs are converted to PDF, so treat them as PDFs
|
||||
const isPdf = isWordDoc || (contentType || '').toLowerCase().includes('pdf') || (name || '').toLowerCase().endsWith('.pdf');
|
||||
const isImage = (contentType || '').toLowerCase().includes('image') ||
|
||||
/\.(jpg|jpeg|png|gif|bmp|tiff|webp)$/i.test(name || '');
|
||||
|
||||
if (isPdf && window['pdfjsLib']) {
|
||||
const data = await blob.arrayBuffer();
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
||||
@@ -1533,8 +1652,39 @@
|
||||
pdfCanvas.style.transform = 'translate(0px, 0px)';
|
||||
await renderPdfPage();
|
||||
showPdfViewer();
|
||||
} else if (isImage) {
|
||||
// Show images in the PDF viewer area for consistency
|
||||
pdfViewer.classList.remove('hidden');
|
||||
iframeViewer.classList.add('hidden');
|
||||
iframeLoader.classList.add('hidden');
|
||||
|
||||
// Store original image for re-rendering at different scales
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
currentPdf = {
|
||||
doc: null,
|
||||
page: 1,
|
||||
pages: 1,
|
||||
scale: 1,
|
||||
isImage: true,
|
||||
imageElement: img,
|
||||
imageWidth: img.width,
|
||||
imageHeight: img.height
|
||||
};
|
||||
pdfPan = { x: 0, y: 0, dragging: false, startX: 0, startY: 0, baseX: 0, baseY: 0 };
|
||||
pdfCanvas.style.transform = 'translate(0px, 0px)';
|
||||
renderImageOnCanvas();
|
||||
|
||||
// Show PDF controls (zoom works for images too)
|
||||
pdfControls.style.display = '';
|
||||
// Hide page navigation for images
|
||||
pdfPrev.parentElement.style.display = 'none';
|
||||
pdfNext.parentElement.style.display = 'none';
|
||||
pdfPageNum.parentElement.style.display = 'none';
|
||||
};
|
||||
img.src = currentPreviewUrl;
|
||||
} else {
|
||||
// fallback to iframe blob view (images, etc.)
|
||||
// fallback to iframe blob view
|
||||
iframeViewer.src = 'about:blank';
|
||||
setTimeout(() => {
|
||||
iframeViewer.src = currentPreviewUrl;
|
||||
@@ -1554,13 +1704,16 @@
|
||||
const term = (fileListState.filter || '').toLowerCase().trim();
|
||||
const filtered = fileListState.items
|
||||
.filter((f) => {
|
||||
// Show PDF files and Word documents (which will be converted to PDF)
|
||||
// Show PDF files, Word documents (which will be converted to PDF), and images
|
||||
if (f.isFolder) return false;
|
||||
const name = f.name.toLowerCase();
|
||||
const contentType = (f.contentType || '').toLowerCase();
|
||||
const isPdfOrWord = name.endsWith('.pdf') || contentType.includes('pdf') ||
|
||||
name.endsWith('.doc') || name.endsWith('.docx') || contentType.includes('word');
|
||||
if (!isPdfOrWord) return false;
|
||||
const isImage = name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') ||
|
||||
name.endsWith('.gif') || name.endsWith('.bmp') || name.endsWith('.tiff') ||
|
||||
name.endsWith('.webp') || contentType.includes('image');
|
||||
if (!isPdfOrWord && !isImage) return false;
|
||||
// Apply search term filter if present
|
||||
return name.includes(term);
|
||||
});
|
||||
@@ -1665,6 +1818,7 @@
|
||||
if (cached) {
|
||||
console.log('Using cached files for job', job.Job_Number);
|
||||
fileListState.items = cached.items;
|
||||
iframeLoader.classList.add('hidden');
|
||||
renderFileGroups(link);
|
||||
return;
|
||||
}
|
||||
@@ -1683,7 +1837,7 @@
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 15000);
|
||||
const timeout = setTimeout(() => controller.abort(), 30000); // 30 seconds for large folders
|
||||
let resp = await fetch(`/api/job-files?link=${encodeURIComponent(link)}`, { headers, signal: controller.signal });
|
||||
clearTimeout(timeout);
|
||||
|
||||
@@ -1713,10 +1867,11 @@
|
||||
fileCache.set(link, { items, driveId: rootDriveId });
|
||||
fileListState.items = items;
|
||||
|
||||
console.log(`✓ Loaded ${items.length} files for job ${job.Job_Number}`);
|
||||
iframeLoader.classList.add('hidden');
|
||||
renderFileGroups(link);
|
||||
} catch (err) {
|
||||
console.error('job-files error', err);
|
||||
console.error('❌ job-files error for', job.Job_Number, err);
|
||||
const authExpired = err?.message === 'AUTH_EXPIRED' || String(err || '').includes('401');
|
||||
const timedOut = err?.name === 'AbortError';
|
||||
const friendly = authExpired
|
||||
@@ -1786,6 +1941,8 @@
|
||||
URL.revokeObjectURL(currentPreviewUrl);
|
||||
currentPreviewUrl = null;
|
||||
}
|
||||
// Clear file cache when closing to ensure fresh data on next open
|
||||
fileCache.clear();
|
||||
// Clear iframe src to stop any loading/video/audio and prevent stacking
|
||||
setTimeout(() => {
|
||||
iframeViewer.src = 'about:blank';
|
||||
|
||||
Reference in New Issue
Block a user