Skip to content

Instantly share code, notes, and snippets.

@minanagehsalalma
Last active January 1, 2026 22:34
Show Gist options
  • Select an option

  • Save minanagehsalalma/499bc2052ef6cab7e196ee524f723826 to your computer and use it in GitHub Desktop.

Select an option

Save minanagehsalalma/499bc2052ef6cab7e196ee524f723826 to your computer and use it in GitHub Desktop.
(async function() {
// --- CONFIGURATION ---
const STORAGE_KEY = 'moodle_drive_v13_accurate_nothumbs';
const CONCURRENCY = 5;
// --- 1. OPEN UI ---
const driveWindow = window.open("", "Course_Drive_Pro", "width=1280,height=900,scrollbars=yes");
if (!driveWindow) { alert("Please allow popups!"); return; }
driveWindow.document.write(`
<html>
<head>
<title>Course Drive</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
body { font-family: 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; margin: 0; user-select: none; }
/* HEADER */
.header { background: white; padding: 12px 24px; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 1000; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
.brand { display: flex; align-items: center; gap: 10px; font-size: 20px; color: #444; font-weight: 500; }
.toolbar { display: flex; gap: 8px; align-items: center; }
.divider { height: 24px; width: 1px; background: #ddd; margin: 0 8px; }
.btn { display: flex; align-items: center; gap: 6px; border: none; background: transparent; padding: 8px 14px; border-radius: 6px; font-weight: 500; cursor: pointer; color: #555; transition: 0.1s; }
.btn:hover { background: #eee; }
.btn-primary { background: #1a73e8; color: white; }
.btn-primary:hover { background: #1557b0; }
.btn-toggle { border: 1px solid #ddd; }
.btn-toggle.active { background: #e8f0fe; color: #1967d2; border-color: #1967d2; }
.btn-dl { background: #188038; color: white; }
.btn-dl:hover { background: #146c2e; }
.btn-dl:disabled { background: #ccc; cursor: default; opacity: 0.8; }
/* STATUS */
.status-bar { padding: 6px 24px; background: #333; color: white; font-size: 12px; display: flex; justify-content: space-between; }
/* GRID */
.container { padding: 24px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 20px; }
/* CARD */
.card {
background: white; border: 1px solid #ddd; border-radius: 8px;
display: flex; flex-direction: column; position: relative;
transition: all 0.2s; height: 220px; cursor: pointer; text-decoration: none; color: inherit;
}
.card:hover { transform: translateY(-3px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
/* Selection Mode */
.card.selected { border: 2px solid #1a73e8; background: #f0f7ff; }
.select-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 20; display: none; }
.card.select-mode .select-overlay { display: block; }
.check-icon { position: absolute; top: 8px; left: 8px; color: #1a73e8; display: none; z-index: 21; background: white; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
.card.selected .check-icon { display: block; }
/* Preview */
.preview { height: 120px; background: #f8f9fa; display: flex; align-items: center; justify-content: center; border-bottom: 1px solid #eee; }
.pdf-icon { font-size: 50px; color: #ea4335; }
/* Info */
.info { padding: 12px; display: flex; flex-direction: column; flex-grow: 1; }
.name {
font-size: 13px; font-weight: 600; color: #333; line-height: 1.4;
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
overflow: hidden; margin-bottom: auto;
}
.meta { font-size: 11px; color: #666; display: flex; justify-content: space-between; align-items: center; margin-top: 8px; }
.badge { padding: 2px 6px; border-radius: 4px; font-weight: 700; font-size: 10px; background: #e0e0e0; }
.badge.new { background: #d1e7dd; color: #0f5132; }
/* Skeleton */
.skeleton .name { background: #eee; color: transparent; border-radius: 4px; }
.skeleton .meta span { background: #eee; color: transparent; border-radius: 4px; }
.skeleton .pdf-icon { opacity: 0.2; }
</style>
</head>
<body>
<div class="header">
<div class="brand">
<span class="material-icons" style="color:#fbbc04">folder_copy</span>
<span>Course Drive</span>
</div>
<div class="toolbar">
<button id="modeBtn" class="btn-toggle">
<span class="material-icons">check_circle</span> Select Mode
</button>
<div class="divider"></div>
<button id="allBtn" class="btn" style="display:none">Select All</button>
<button id="dlBtn" class="btn-dl" disabled style="display:none">
<span class="material-icons">download</span> Download (<span id="count">0</span>)
</button>
<div class="divider"></div>
<button id="scanBtn" class="btn-primary"><span class="material-icons">sync</span> Rescan</button>
<button id="resetBtn" style="color:#d93025"><span class="material-icons">delete</span></button>
</div>
</div>
<div id="status" class="status-bar">Initializing...</div>
<div class="container"><div id="grid" class="grid"></div></div>
</body>
</html>
`);
// --- CRITICAL FIX: Close the document stream to ensure buttons are interactive ---
driveWindow.document.close();
driveWindow.focus();
const doc = driveWindow.document;
const grid = doc.getElementById('grid');
const statusDiv = doc.getElementById('status');
const modeBtn = doc.getElementById('modeBtn');
const dlBtn = doc.getElementById('dlBtn');
const allBtn = doc.getElementById('allBtn');
const countSpan = doc.getElementById('count');
// --- 2. STATE ---
let isSelectMode = false;
let selectedSet = new Set();
let cardRegistry = [];
// --- 3. UI FUNCTIONS ---
function log(msg) { statusDiv.textContent = msg; }
// Improved Filename Cleaning
function getCleanFilename(url) {
try {
// Remove query string first
const noQuery = url.split('?')[0];
// Get last segment
const lastSegment = noQuery.split('/').pop();
// Decode URI (handles %20 etc)
let clean = decodeURIComponent(lastSegment);
// Replace plus with space if decode didn't catch it
clean = clean.replace(/\+/g, ' ');
return clean;
} catch (e) {
return "document.pdf";
}
}
function toggleMode() {
isSelectMode = !isSelectMode;
if (isSelectMode) {
modeBtn.classList.add('active');
modeBtn.innerHTML = `<span class="material-icons">close</span> Exit Selection`;
dlBtn.style.display = 'flex';
allBtn.style.display = 'flex';
cardRegistry.forEach(c => c.el.classList.add('select-mode'));
} else {
modeBtn.classList.remove('active');
modeBtn.innerHTML = `<span class="material-icons">check_circle</span> Select Mode`;
dlBtn.style.display = 'none';
allBtn.style.display = 'none';
cardRegistry.forEach(c => {
c.el.classList.remove('select-mode');
c.el.classList.remove('selected');
});
selectedSet.clear();
updateCount();
}
}
function toggleSelect(id) {
const cardObj = cardRegistry[id];
if (selectedSet.has(id)) {
selectedSet.delete(id);
cardObj.el.classList.remove('selected');
} else {
selectedSet.add(id);
cardObj.el.classList.add('selected');
}
updateCount();
}
function updateCount() {
countSpan.textContent = selectedSet.size;
dlBtn.disabled = selectedSet.size === 0;
}
// --- 4. RENDERER ---
function createSkeleton(id) {
const el = doc.createElement('a');
el.className = 'card skeleton';
el.href = '#';
el.innerHTML = `
<div class="select-overlay"></div>
<span class="material-icons check-icon">check_circle</span>
<div class="preview"><span class="material-icons pdf-icon">picture_as_pdf</span></div>
<div class="info">
<div class="name">Loading file...</div>
<div class="meta"><span>...</span><span>...</span></div>
</div>
`;
// Handle Select Click
el.querySelector('.select-overlay').onclick = (e) => {
e.preventDefault(); e.stopPropagation();
toggleSelect(id);
};
// Handle Normal Click
el.onclick = (e) => {
if(isSelectMode) { e.preventDefault(); toggleSelect(id); }
};
grid.appendChild(el);
return el;
}
function updateCard(id, data, isCached) {
if (!cardRegistry[id]) return; // Safety check
const el = cardRegistry[id].el;
el.classList.remove('skeleton');
el.href = data.link;
el.target = "_blank";
// --- NAME LOGIC FIXED: Prefer URL Filename > Link Text > PDF Metadata ---
let name = data.filename;
// If filename is generic (view.php, content, etc), try fallback
if (!name || name.toLowerCase().includes('view.php') || name.toLowerCase().includes('pluginfile.php') || name === 'content') {
if (data.linkText && data.linkText.length > 2 && data.linkText !== 'File') {
name = data.linkText;
} else if (data.title && data.title.length > 2 && !data.title.startsWith('Microsoft')) {
name = data.title;
}
}
el.querySelector('.name').textContent = name;
el.querySelector('.name').title = name;
el.querySelector('.meta').innerHTML = `
<span>${data.pages} Pages</span>
<span class="badge ${isCached ? '' : 'new'}">${isCached ? 'CACHE' : 'NEW'}</span>
`;
cardRegistry[id].data = data;
// Ensure data has the display name for downloader
cardRegistry[id].data.displayName = name;
}
// --- 5. LOGIC ---
function getCache() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); } catch { return {}; } }
function saveCache(d) { localStorage.setItem(STORAGE_KEY, JSON.stringify(d)); }
async function initPdf() {
if (window.pdfjsLib) return;
log("Loading Engine...");
window.define = undefined; window.exports = undefined;
const s = document.createElement('script');
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
s.crossOrigin = "anonymous";
document.head.appendChild(s);
await new Promise(r => {
const i = setInterval(() => { if(window.pdfjsLib){ clearInterval(i); window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; r(); } }, 100);
});
}
// --- 6. EXECUTION ---
modeBtn.onclick = toggleMode;
doc.getElementById('scanBtn').onclick = () => {
driveWindow.close();
setTimeout(() => location.reload(), 100);
};
doc.getElementById('resetBtn').onclick = () => {
if(confirm('Clear all cached data and rescan?')) {
localStorage.removeItem(STORAGE_KEY);
driveWindow.close();
setTimeout(() => location.reload(), 100);
}
};
doc.getElementById('allBtn').onclick = () => {
const target = selectedSet.size !== cardRegistry.length;
selectedSet.clear();
cardRegistry.forEach((c, idx) => {
if(c.data && target) { selectedSet.add(idx); c.el.classList.add('selected'); }
else { c.el.classList.remove('selected'); }
});
updateCount();
};
doc.getElementById('dlBtn').onclick = async () => {
const ids = Array.from(selectedSet);
log(`Downloading ${ids.length} files...`);
for(let i=0; i<ids.length; i++) {
const data = cardRegistry[ids[i]].data;
if(!data) continue;
try {
const r = await fetch(data.link);
const b = await r.blob();
const u = URL.createObjectURL(b);
const a = doc.createElement('a');
a.href = u;
// Use the accurate display name for the downloaded file
a.download = (data.displayName || data.filename || "download").replace(/[^a-z0-9 .\-_]/gi, '_');
if (!a.download.toLowerCase().endsWith('.pdf')) a.download += '.pdf';
doc.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(u);
await new Promise(r => setTimeout(r, 500));
} catch(e) {}
}
log("Download Complete.");
};
try {
await initPdf();
// Target the main content region
const contentSelectors = [
'#region-main', // Moodle main content
'.course-content', // Course content area
'[role="main"]', // Main content role
'main', // HTML5 main element
'#page-content' // Alternative content ID
];
let contentArea = null;
for (const selector of contentSelectors) {
contentArea = document.querySelector(selector);
if (contentArea) break;
}
if (!contentArea) contentArea = document.body;
const allLinks = Array.from(contentArea.querySelectorAll('a[href*="/mod/resource/view.php"]'))
.filter(link => {
const excludeSelectors = ['nav', 'header', 'footer', '.navbar', '.nav-item',
'[role="navigation"]', '.block', '.sidebar'];
return !excludeSelectors.some(sel => link.closest(sel));
});
const uniqueLinks = [];
const seenIds = new Set();
allLinks.forEach(link => {
const match = link.href.match(/id=(\d+)/);
if (match) {
const id = match[1];
if (!seenIds.has(id)) {
seenIds.add(id);
uniqueLinks.push(link);
}
}
});
log(`Found ${uniqueLinks.length} unique files. Generating grid...`);
// Create Skeletons
uniqueLinks.forEach((link, idx) => {
const el = createSkeleton(idx);
cardRegistry.push({ id: idx, el: el, rawLink: link });
});
// Process Queue
const cache = getCache();
let index = 0;
const worker = async () => {
while(index < uniqueLinks.length) {
const i = index++;
const linkObj = uniqueLinks[i];
const moodleUrl = linkObj.href;
// 1. Try Cache
if(cache[moodleUrl]) {
updateCard(i, cache[moodleUrl], true);
continue;
}
// 2. Fetch
try {
const r = await fetch(moodleUrl);
// CHECK 1: Did we get a PDF directly? (Redirected)
let directLink = null;
const contentType = r.headers.get('content-type');
if (contentType && contentType.includes('application/pdf')) {
directLink = r.url;
} else {
// CHECK 2: Parse HTML wrapper
const t = await r.text();
const docParser = new DOMParser().parseFromString(t, 'text/html');
let pdfAnchor = docParser.querySelector('a[href$=".pdf"]') ||
docParser.querySelector('a[href*=".pdf"]') ||
docParser.querySelector('.resourceworkaround a') ||
docParser.querySelector('object[data$=".pdf"]');
if(pdfAnchor) {
directLink = pdfAnchor.href || pdfAnchor.getAttribute('data');
} else {
const embed = docParser.querySelector('embed[src$=".pdf"]') ||
docParser.querySelector('iframe[src$=".pdf"]');
if(embed) directLink = embed.src;
}
}
// --- CRITICAL FIX: If no PDF link found, DO NOT DELETE.
// Fallback to original URL.
if (!directLink) {
console.warn("Could not extract PDF path for:", moodleUrl, "Using fallback.");
directLink = moodleUrl;
}
// 3. Metadata Extraction
try {
// Only try to parse PDF metadata if it looks like a PDF URL
if (directLink.includes('.pdf') || directLink.includes('pluginfile.php')) {
const task = window.pdfjsLib.getDocument(directLink);
const pdf = await task.promise;
const meta = await pdf.getMetadata();
// CLEAN NAME EXTRACTION
const cleanName = getCleanFilename(directLink);
const itemData = {
pages: pdf.numPages,
title: meta.info?.Title, // Keep raw title
linkText: linkObj.innerText.trim(),
filename: cleanName,
link: directLink
};
cache[moodleUrl] = itemData;
saveCache(cache);
updateCard(i, itemData, false);
} else {
throw new Error("Not a standard PDF URL");
}
} catch(pdfErr) {
// Fallback display if PDF parsing fails
const cleanName = getCleanFilename(directLink);
const itemData = {
pages: "?",
title: "",
linkText: linkObj.innerText.trim(),
filename: cleanName !== "document.pdf" ? cleanName : "Resource",
link: directLink
};
updateCard(i, itemData, false);
}
} catch(e) {
console.error("Fetch error:", e);
// Even on error, don't delete. Show error state.
const el = cardRegistry[i].el;
el.querySelector('.name').textContent = 'Link Error';
el.querySelector('.meta').innerHTML = '<span style="color:red">Access Failed</span>';
el.href = moodleUrl;
}
}
};
await Promise.all(Array(CONCURRENCY).fill(null).map(worker));
log(`Scan Complete! Loaded ${uniqueLinks.length} items.`);
} catch(e) {
log("Error: " + e.message);
console.error(e);
}
})();
@minanagehsalalma
Copy link
Author

minanagehsalalma commented Dec 2, 2025

(async function() {
    // --- CONFIGURATION ---
    const STORAGE_KEY = 'moodle_drive_v12_thumbs_accurate';
    const CONCURRENCY = 4; // Lowered slightly to save CPU for image rendering

    // --- 1. UI SETUP ---
    const driveWindow = window.open("", "Course_Drive_Thumbs", "width=1280,height=900,scrollbars=yes");
    if (!driveWindow) { alert("Please allow popups!"); return; }

    driveWindow.document.write(`
        <html>
        <head>
            <title>Course Drive (Previews)</title>
            <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
            <style>
                body { font-family: 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; margin: 0; user-select: none; }
                
                /* HEADER */
                .header { background: white; padding: 12px 24px; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 1000; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
                .brand { display: flex; align-items: center; gap: 10px; font-size: 20px; color: #444; font-weight: 500; }
                .toolbar { display: flex; gap: 8px; align-items: center; }
                .divider { height: 24px; width: 1px; background: #ddd; margin: 0 8px; }
                
                button { display: flex; align-items: center; gap: 6px; padding: 8px 14px; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; transition: 0.1s; background: transparent; color: #555; }
                button:hover { background: #eee; }
                
                .btn-primary { background: #1a73e8; color: white; }
                .btn-primary:hover { background: #1557b0; }
                .btn-toggle.active { background: #e8f0fe; color: #1967d2; border-color: #1967d2; border: 1px solid #c2dbfe; }
                .btn-dl { background: #188038; color: white; }
                .btn-dl:hover { background: #146c2e; }
                .btn-dl:disabled { background: #ccc; cursor: default; opacity: 0.8; }

                /* GRID */
                .container { padding: 24px; }
                .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; }

                /* CARD */
                .card { 
                    background: white; border: 1px solid #ddd; border-radius: 8px; 
                    display: flex; flex-direction: column; position: relative; 
                    transition: all 0.2s; height: 240px; cursor: pointer; text-decoration: none; color: inherit; overflow: hidden;
                }
                .card:hover { transform: translateY(-3px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
                .card.selected { border: 2px solid #1a73e8; background: #f0f7ff; }
                
                /* THUMBNAIL AREA */
                .preview { 
                    height: 140px; background: #f1f3f4; border-bottom: 1px solid #eee; 
                    display: flex; align-items: center; justify-content: center; overflow: hidden; position: relative;
                }
                .pdf-icon { font-size: 50px; color: #ea4335; z-index: 1; }
                
                /* The Actual Image */
                .thumb-img { 
                    width: 100%; height: 100%; object-fit: cover; object-position: top; 
                    display: none; position: absolute; top: 0; left: 0; z-index: 2; opacity: 0; transition: opacity 0.3s;
                }
                .card.has-thumb .thumb-img { display: block; opacity: 1; }
                .card.has-thumb .pdf-icon { display: none; }

                /* SELECTION OVERLAYS */
                .select-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 20; display: none; }
                .card.select-mode .select-overlay { display: block; }
                .check-icon { position: absolute; top: 8px; left: 8px; color: #1a73e8; display: none; z-index: 21; background: white; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
                .card.selected .check-icon { display: block; }

                /* INFO */
                .info { padding: 12px; display: flex; flex-direction: column; flex-grow: 1; }
                .name { font-size: 13px; font-weight: 600; color: #333; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: auto; }
                .meta { font-size: 11px; color: #666; display: flex; justify-content: space-between; align-items: center; margin-top: 8px; }
                
                .badge { padding: 2px 6px; border-radius: 4px; font-weight: 700; font-size: 10px; background: #e0e0e0; }
                .badge.new { background: #d1e7dd; color: #0f5132; }

                /* LOADING STATE */
                .status-bar { padding: 6px 24px; background: #333; color: white; font-size: 12px; }
                .skeleton .name { background: #eee; color: transparent; border-radius: 4px; }
                .skeleton .meta span { background: #eee; color: transparent; border-radius: 4px; }
            </style>
        </head>
        <body>
            <div class="header">
                <div class="brand">
                    <span class="material-icons" style="color:#fbbc04">grid_view</span>
                    <span>Course Drive</span>
                </div>
                <div class="toolbar">
                    <button id="modeBtn" class="btn-toggle">
                        <span class="material-icons">check_circle</span> Select
                    </button>
                    <div class="divider"></div>
                    <button id="allBtn" style="display:none">All</button>
                    <button id="dlBtn" class="btn-dl" disabled style="display:none">
                        <span class="material-icons">download</span> (<span id="count">0</span>)
                    </button>
                    <div class="divider"></div>
                    <button id="scanBtn" class="btn-primary"><span class="material-icons">sync</span> Rescan</button>
                </div>
            </div>
            <div id="status" class="status-bar">Initializing...</div>
            <div class="container"><div id="grid" class="grid"></div></div>
        </body>
        </html>
    `);

    // --- CRITICAL FIX 1: Close document stream ---
    driveWindow.document.close();
    driveWindow.focus();

    const doc = driveWindow.document;
    const grid = doc.getElementById('grid');
    const statusDiv = doc.getElementById('status');
    const modeBtn = doc.getElementById('modeBtn');
    const dlBtn = doc.getElementById('dlBtn');
    const allBtn = doc.getElementById('allBtn');
    const countSpan = doc.getElementById('count');

    // --- 2. STATE ---
    let isSelectMode = false;
    let selectedSet = new Set();
    let cardRegistry = [];

    // --- 3. HELPER FUNCTIONS ---
    function log(msg) { statusDiv.textContent = msg; }
    
    // Improved Filename Cleaning
    function getCleanFilename(url) {
        try {
            // Remove query string first
            const noQuery = url.split('?')[0];
            // Get last segment
            const lastSegment = noQuery.split('/').pop();
            // Decode URI (handles %20 etc)
            let clean = decodeURIComponent(lastSegment);
            // Replace plus with space if decode didn't catch it
            clean = clean.replace(/\+/g, ' ');
            return clean;
        } catch (e) {
            return "document.pdf";
        }
    }

    function toggleMode() {
        isSelectMode = !isSelectMode;
        if (isSelectMode) {
            modeBtn.classList.add('active');
            dlBtn.style.display = 'flex';
            allBtn.style.display = 'flex';
            cardRegistry.forEach(c => c.el.classList.add('select-mode'));
        } else {
            modeBtn.classList.remove('active');
            dlBtn.style.display = 'none';
            allBtn.style.display = 'none';
            cardRegistry.forEach(c => {
                c.el.classList.remove('select-mode');
                c.el.classList.remove('selected');
            });
            selectedSet.clear();
            updateCount();
        }
    }

    function toggleSelect(id) {
        const cardObj = cardRegistry[id];
        if (selectedSet.has(id)) {
            selectedSet.delete(id);
            cardObj.el.classList.remove('selected');
        } else {
            selectedSet.add(id);
            cardObj.el.classList.add('selected');
        }
        updateCount();
    }

    function updateCount() {
        countSpan.textContent = selectedSet.size;
        dlBtn.disabled = selectedSet.size === 0;
    }

    // --- 4. THUMBNAIL GENERATOR ---
    async function generateThumbnail(pdf) {
        try {
            const page = await pdf.getPage(1);
            // Scale: 0.6 is good balance between quality and speed
            const viewport = page.getViewport({ scale: 0.6 });
            
            const canvas = document.createElement('canvas');
            const context = canvas.getContext('2d');
            canvas.height = viewport.height;
            canvas.width = viewport.width;

            await page.render({ canvasContext: context, viewport: viewport }).promise;
            
            // Return base64 image
            return canvas.toDataURL('image/jpeg', 0.8);
        } catch (e) {
            console.error("Thumb error", e);
            return null;
        }
    }

    // --- 5. RENDERER ---
    function createSkeleton(id) {
        const el = doc.createElement('a');
        el.className = 'card skeleton';
        el.href = '#'; 
        el.innerHTML = `
            <div class="select-overlay"></div>
            <span class="material-icons check-icon">check_circle</span>
            <div class="preview">
                <span class="material-icons pdf-icon">picture_as_pdf</span>
                <img class="thumb-img" alt="">
            </div>
            <div class="info">
                <div class="name">Loading file...</div>
                <div class="meta"><span>...</span><span>...</span></div>
            </div>
        `;
        
        el.querySelector('.select-overlay').onclick = (e) => {
            e.preventDefault(); e.stopPropagation();
            toggleSelect(id);
        };
        el.onclick = (e) => {
            if(isSelectMode) { e.preventDefault(); toggleSelect(id); }
        };

        grid.appendChild(el);
        return el;
    }

    function updateCard(id, data, thumbData) {
        const el = cardRegistry[id].el;
        el.classList.remove('skeleton');
        el.href = data.link; 
        el.target = "_blank"; 

        // --- NAME LOGIC FIXED: Prefer URL Filename > Link Text > PDF Metadata ---
        let name = data.filename;
        
        // If filename is generic (view.php, content, etc), try fallback
        if (!name || name.toLowerCase().includes('view.php') || name.toLowerCase().includes('pluginfile.php') || name === 'content') {
            if (data.linkText && data.linkText.length > 2 && data.linkText !== 'File') {
                name = data.linkText;
            } else if (data.pdfTitle && data.pdfTitle.length > 2 && !data.pdfTitle.startsWith('Microsoft')) {
                name = data.pdfTitle;
            }
        }

        el.querySelector('.name').textContent = name;
        el.querySelector('.name').title = name;
        el.querySelector('.meta').innerHTML = `
            <span>${data.pages} Pages</span>
            <span class="badge new">READY</span>
        `;
        
        // APPLY THUMBNAIL
        if (thumbData) {
            const img = el.querySelector('.thumb-img');
            img.src = thumbData;
            el.classList.add('has-thumb');
        }
        
        cardRegistry[id].data = data;
        // Ensure data has the display name for downloader
        cardRegistry[id].data.displayName = name; 
    }

    // --- 6. LOGIC ---
    function getCache() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); } catch { return {}; } }
    function saveCache(d) { localStorage.setItem(STORAGE_KEY, JSON.stringify(d)); }

    async function initPdf() {
        if (window.pdfjsLib) return;
        log("Loading Engine...");
        window.define = undefined; window.exports = undefined; 
        const s = document.createElement('script');
        s.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
        s.crossOrigin = "anonymous";
        document.head.appendChild(s);
        await new Promise(r => {
            const i = setInterval(() => { if(window.pdfjsLib){ clearInterval(i); window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; r(); } }, 100);
        });
    }

    // --- 7. EXECUTION ---
    modeBtn.onclick = toggleMode;
    doc.getElementById('scanBtn').onclick = () => { driveWindow.close(); setTimeout(() => location.reload(), 100); };
    
    doc.getElementById('allBtn').onclick = () => {
        const target = selectedSet.size !== cardRegistry.length;
        selectedSet.clear();
        cardRegistry.forEach((c, idx) => {
            if(c.data && target) { selectedSet.add(idx); c.el.classList.add('selected'); }
            else { c.el.classList.remove('selected'); }
        });
        updateCount();
    };

    doc.getElementById('dlBtn').onclick = async () => {
        const ids = Array.from(selectedSet);
        log(`Downloading ${ids.length} files...`);
        for(let i=0; i<ids.length; i++) {
            const data = cardRegistry[ids[i]].data;
            if(!data) continue;
            try {
                const r = await fetch(data.link);
                const b = await r.blob();
                const u = URL.createObjectURL(b);
                const a = doc.createElement('a');
                a.href = u;
                // Use the accurate display name for the downloaded file
                a.download = (data.displayName || data.filename || "download").replace(/[^a-z0-9 .\-_]/gi, '_');
                if (!a.download.toLowerCase().endsWith('.pdf')) a.download += '.pdf';
                
                doc.body.appendChild(a);
                a.click();
                a.remove();
                URL.revokeObjectURL(u);
                await new Promise(r => setTimeout(r, 500));
            } catch(e) {}
        }
        log("Download Complete.");
    };

    try {
        await initPdf();
        
        // --- CRITICAL FIX 2: Better Content Selection ---
        const contentSelectors = [
            '#region-main', '.course-content', '[role="main"]', 'main', '#page-content'
        ];
        
        let contentArea = null;
        for (const selector of contentSelectors) {
            contentArea = document.querySelector(selector);
            if (contentArea) break;
        }
        if (!contentArea) contentArea = document.body;
        
        const allLinks = Array.from(contentArea.querySelectorAll('a[href*="/mod/resource/view.php"]'))
            .filter(link => {
                const excludeSelectors = ['nav', 'header', 'footer', '.navbar', '.nav-item', 
                                         '[role="navigation"]', '.block', '.sidebar'];
                return !excludeSelectors.some(sel => link.closest(sel));
            });

        const uniqueLinks = [];
        const seenIds = new Set();
        
        allLinks.forEach(link => {
            const match = link.href.match(/id=(\d+)/);
            if (match) {
                const id = match[1];
                if (!seenIds.has(id)) {
                    seenIds.add(id);
                    uniqueLinks.push(link);
                }
            }
        });

        log(`Generating previews for ${uniqueLinks.length} files...`);

        // CREATE SKELETONS
        uniqueLinks.forEach((link, idx) => {
            const el = createSkeleton(idx);
            cardRegistry.push({ id: idx, el: el, rawLink: link });
        });

        // PROCESS WITH THUMBNAILS
        const cache = getCache();
        let index = 0;

        const worker = async () => {
            while(index < uniqueLinks.length) {
                const i = index++;
                const linkObj = uniqueLinks[i];
                const moodleUrl = linkObj.href;

                // Try Cache? We need to re-verify filenames often, so let's skip cache 
                // for the filename part but use it for page counts if needed.
                // For simplicity/robustness, we re-fetch headers.

                try {
                    const r = await fetch(moodleUrl);
                    
                    // --- CRITICAL FIX 3: Detect Redirects & Headers ---
                    let directLink = null;
                    const contentType = r.headers.get('content-type');
                    
                    if (contentType && contentType.includes('application/pdf')) {
                        directLink = r.url;
                    } else {
                        const t = await r.text();
                        const docParser = new DOMParser().parseFromString(t, 'text/html');
                        
                        // Robust Selectors
                        let pdfAnchor = docParser.querySelector('a[href$=".pdf"]') ||
                                       docParser.querySelector('a[href*=".pdf"]') ||
                                       docParser.querySelector('.resourceworkaround a') ||
                                       docParser.querySelector('object[data$=".pdf"]');
                        
                        if(pdfAnchor) {
                            directLink = pdfAnchor.href || pdfAnchor.getAttribute('data');
                        } else {
                            const embed = docParser.querySelector('embed[src$=".pdf"]') || 
                                         docParser.querySelector('iframe[src$=".pdf"]');
                            if(embed) directLink = embed.src;
                        }
                    }

                    // --- CRITICAL FIX 4: NO DELETION Fallback ---
                    if (!directLink) {
                        console.warn("No PDF link found for", moodleUrl);
                        directLink = moodleUrl; // Fallback to wrapper
                    }

                    // Load PDF & Generate Thumb
                    try {
                        if (directLink.includes('.pdf') || directLink.includes('pluginfile.php')) {
                            const loadingTask = window.pdfjsLib.getDocument(directLink);
                            const pdf = await loadingTask.promise;
                            
                            const meta = await pdf.getMetadata();
                            
                            // CLEAN NAME EXTRACTION
                            const cleanName = getCleanFilename(directLink);
                            
                            const itemData = {
                                pages: pdf.numPages,
                                pdfTitle: meta.info?.Title,
                                linkText: linkObj.innerText.trim(),
                                filename: cleanName,
                                link: directLink
                            };

                            // Generate Thumb
                            const thumbBase64 = await generateThumbnail(pdf);

                            // Cache Text Data
                            cache[moodleUrl] = itemData; 
                            saveCache(cache);
                            
                            updateCard(i, itemData, thumbBase64);
                        } else {
                            throw new Error("Not a standard PDF URL");
                        }
                    } catch (pdfErr) {
                        // Fallback: Show card without thumbnail, DO NOT DELETE
                        const cleanName = getCleanFilename(directLink);
                        const itemData = {
                            pages: "?",
                            pdfTitle: "",
                            linkText: linkObj.innerText.trim(),
                            filename: cleanName !== "document.pdf" ? cleanName : "Resource",
                            link: directLink
                        };
                        updateCard(i, itemData, null);
                    }
                } catch(e) { 
                    console.error("Fetch error", e);
                    // Fallback error state
                    const itemData = {
                        pages: "Err",
                        pdfTitle: "Error",
                        linkText: "Error Loading",
                        filename: "error.txt",
                        link: moodleUrl
                    };
                    updateCard(i, itemData, null);
                }
            }
        };

        await Promise.all(Array(CONCURRENCY).fill(null).map(worker));
        log(`Drive Ready! Loaded ${uniqueLinks.length} files.`);

    } catch(e) {
        log("Error: " + e.message);
    }
})();

@minanagehsalalma
Copy link
Author

With First Page As preview.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment