Last active
December 2, 2025 20:25
-
-
Save minanagehsalalma/499bc2052ef6cab7e196ee524f723826 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (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); | |
| } | |
| })(); |
Author
With First Page As preview.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.