Last active
January 1, 2026 22:34
-
-
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_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); | |
| } | |
| })(); |
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.