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

With First Page As preview.

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