Skip to content

Instantly share code, notes, and snippets.

@minanagehsalalma
Last active December 2, 2025 20:25
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_v10_final';
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>
`);
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; }
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) {
const el = cardRegistry[id].el;
el.classList.remove('skeleton');
// --- DIRECT LINKING FIX ---
// We set the HREF to the final PDF url so standard clicks work natively
el.href = data.link;
el.target = "_blank";
const name = (data.title && data.title.length > 2 && !data.title.startsWith('Microsoft'))
? data.title
: data.filename;
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;
}
// --- 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.location.reload();
doc.getElementById('resetBtn').onclick = () => { localStorage.removeItem(STORAGE_KEY); driveWindow.location.reload(); };
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;
a.download = (data.title || data.filename).replace(/[^a-z0-9]/gi, '_') + '.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();
// --- KEY FIX: ID-BASED DEDUPLICATION ---
const allLinks = Array.from(document.querySelectorAll('a[href*="/mod/resource/view.php"]'));
const uniqueLinks = [];
const seenIds = new Set();
allLinks.forEach(link => {
// Extract ?id=XXXX
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 Immediately (Correct Count)
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 {
// Fetch wrapper page to find real PDF URL
const r = await fetch(moodleUrl);
const t = await r.text();
const docParser = new DOMParser().parseFromString(t, 'text/html');
const pdfAnchor = docParser.querySelector('a[href$=".pdf"]');
if(pdfAnchor) {
const directLink = pdfAnchor.href;
// Count & Metadata
const task = window.pdfjsLib.getDocument(directLink);
const pdf = await task.promise;
const meta = await pdf.getMetadata();
const itemData = {
pages: pdf.numPages,
title: meta.info?.Title || "",
filename: decodeURIComponent(directLink.split('?')[0].split('/').pop()),
link: directLink
};
cache[moodleUrl] = itemData;
saveCache(cache);
updateCard(i, itemData, false);
} else {
// Not a PDF? Remove skeleton.
cardRegistry[i].el.remove();
}
} catch(e) {
// Error? Leave skeleton or mark error
console.warn(e);
}
}
};
await Promise.all(Array(CONCURRENCY).fill(null).map(worker));
log(`Scan Complete! Loaded ${uniqueLinks.length} files.`);
} catch(e) {
log("Error: " + e.message);
}
})();
@minanagehsalalma
Copy link
Author

minanagehsalalma commented Dec 2, 2025

(async function() {
    // --- CONFIGURATION ---
    const STORAGE_KEY = 'moodle_drive_v11_thumbs';
    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>
    `);

    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; }

    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"; 

        const name = (data.title && data.title.length > 2 && !data.title.startsWith('Microsoft')) 
            ? data.title 
            : data.filename;

        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;
    }

    // --- 6. LOGIC ---
    // We only cache text metadata, NOT images (to prevent crash)
    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.location.reload();
    
    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;
                a.download = (data.title || data.filename).replace(/[^a-z0-9]/gi, '_') + '.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();
        
        // 1. DEDUPLICATE BY ID
        const allLinks = Array.from(document.querySelectorAll('a[href*="/mod/resource/view.php"]'));
        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...`);

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

        // 3. 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 {
                    // Fetch
                    const r = await fetch(moodleUrl);
                    const t = await r.text();
                    const docParser = new DOMParser().parseFromString(t, 'text/html');
                    const pdfAnchor = docParser.querySelector('a[href$=".pdf"]');

                    if(pdfAnchor) {
                        const directLink = pdfAnchor.href;
                        
                        const loadingTask = window.pdfjsLib.getDocument(directLink);
                        const pdf = await loadingTask.promise;
                        
                        // Metadata
                        const meta = await pdf.getMetadata();
                        const itemData = {
                            pages: pdf.numPages,
                            title: meta.info?.Title || "",
                            filename: decodeURIComponent(directLink.split('?')[0].split('/').pop()),
                            link: directLink
                        };

                        // --- GENERATE THUMBNAIL (The New Step) ---
                        const thumbBase64 = await generateThumbnail(pdf);

                        // Save text data to cache (not image)
                        cache[moodleUrl] = itemData; 
                        saveCache(cache);
                        
                        // Update UI with both
                        updateCard(i, itemData, thumbBase64);
                    } else {
                        cardRegistry[i].el.remove(); // Not a PDF
                    }
                } catch(e) { 
                    // console.warn(e); 
                }
            }
        };

        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